{
  "jsonrpc": "2.0",
  "id": "",
  "result": {
    "genesis": {
      "genesis_time": "2026-03-20T16:20:00Z",
      "chain_id": "test12",
      "consensus_params": {
        "Block": {
          "MaxTxBytes": "1000000",
          "MaxDataBytes": "2000000",
          "MaxBlockBytes": "0",
          "MaxGas": "3000000000",
          "TimeIotaMS": "100"
        },
        "Validator": {
          "PubKeyTypeURLs": [
            "/tm.PubKeyEd25519",
            "/tm.PubKeySecp256k1"
          ]
        }
      },
      "validators": [
        {
          "address": "g1qtxq5t3acclx03cznwusddg5mv4pfm2knjcmsg",
          "pub_key": {
            "@type": "/tm.PubKeySecp256k1",
            "value": "AqI7TOuAOJZrzDjc79bJFGSTVJa7ICC15Za+VOoEth7t"
          },
          "power": "1",
          "name": "aeddi-2"
        },
        {
          "address": "g15atj32de45nqgm68298aua8ayy4aujwyewegvd",
          "pub_key": {
            "@type": "/tm.PubKeySecp256k1",
            "value": "A4sTE4u06uJFE+ONDnu2LqNmKEvIRGkYg3cu62HuFdVy"
          },
          "power": "1",
          "name": "gfanton-2"
        },
        {
          "address": "g18kp360plxkmh3yal3juzffjuqa0ugys0963a80",
          "pub_key": {
            "@type": "/tm.PubKeySecp256k1",
            "value": "A02zuKeqWo2VQZ4q/Qj4Yr/EcqfNktuRbN4AakDwxhML"
          },
          "power": "1",
          "name": "gnocore-val-01"
        },
        {
          "address": "g1z9eedz4qfru6ggdsyj7yn85s5ewvdr5gr39c7r",
          "pub_key": {
            "@type": "/tm.PubKeyEd25519",
            "value": "HjMqSmSFCuLeHV4G/zySpdy+yucuhD5E8bKw0YYod+w="
          },
          "power": "1",
          "name": "samourai-crew-1"
        }
      ],
      "app_hash": null,
      "app_state": {
        "@type": "/gno.GenesisState",
        "balances": [
          "g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l=58699307ugnot",
          "g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7=13823004ugnot",
          "g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da=15748205ugnot",
          "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5=2246503ugnot",
          "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5=9223372036854775807ugnot",
          "g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l=12897402ugnot",
          "g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen=1712703ugnot",
          "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p=117612844ugnot",
          "g1manfred47kzduec920z88wfr64ylksmdcedlf5=11483610ugnot"
        ],
        "txs": [
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n\nPackage `ufmt` provides utility functions for formatting strings, similarly to the Go package `fmt`, of which only a subset is currently supported.\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "GONRLrcYBwUPj+LbcTqK08rIASUk8U1gmGNhiJ70MRhUzRIq2N5gW/D7MpSu0BzqYTaxyKribV5UXLf7WWlNvw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "diff",
                    "path": "gno.land/p/onbloc/diff",
                    "files": [
                      {
                        "name": "diff.gno",
                        "body": "// The diff package implements the Myers diff algorithm to compute the edit distance\n// and generate a minimal edit script between two strings.\n//\n// Edit distance, also known as Levenshtein distance, is a measure of the similarity\n// between two strings. It is defined as the minimum number of single-character edits (insertions,\n// deletions, or substitutions) required to change one string into the other.\npackage diff\n\nimport (\n\t\"strings\"\n)\n\n// EditType represents the type of edit operation in a diff.\ntype EditType uint8\n\nconst (\n\t// EditKeep indicates that a character is unchanged in both strings.\n\tEditKeep EditType = iota\n\n\t// EditInsert indicates that a character was inserted in the new string.\n\tEditInsert\n\n\t// EditDelete indicates that a character was deleted from the old string.\n\tEditDelete\n)\n\n// Edit represent a single edit operation in a diff.\ntype Edit struct {\n\t// Type is the kind of edit operation.\n\tType EditType\n\n\t// Char is the character involved in the edit operation.\n\tChar rune\n}\n\n// MyersDiff computes the difference between two strings using Myers' diff algorithm.\n// It returns a slice of Edit operations that transform the old string into the new string.\n// This implementation finds the shortest edit script (SES) that represents the minimal\n// set of operations to transform one string into the other.\n//\n// The function handles both ASCII and non-ASCII characters correctly.\n//\n// Time complexity: O((N+M)D), where N and M are the lengths of the input strings,\n// and D is the size of the minimum edit script.\n//\n// Space complexity: O((N+M)D)\n//\n// In the worst case, where the strings are completely different, D can be as large as N+M,\n// leading to a time and space complexity of O((N+M)^2). However, for strings with many\n// common substrings, the performance is much better, often closer to O(N+M).\n//\n// Parameters:\n//   - old: the original string.\n//   - new: the modified string.\n//\n// Returns:\n//   - A slice of Edit operations representing the minimum difference between the two strings.\nfunc MyersDiff(old, new string) []Edit {\n\toldRunes, newRunes := []rune(old), []rune(new)\n\tn, m := len(oldRunes), len(newRunes)\n\n\tif n == 0 \u0026\u0026 m == 0 {\n\t\treturn []Edit{}\n\t}\n\n\t// old is empty\n\tif n == 0 {\n\t\tedits := make([]Edit, m)\n\t\tfor i, r := range newRunes {\n\t\t\tedits[i] = Edit{Type: EditInsert, Char: r}\n\t\t}\n\t\treturn edits\n\t}\n\n\tif m == 0 {\n\t\tedits := make([]Edit, n)\n\t\tfor i, r := range oldRunes {\n\t\t\tedits[i] = Edit{Type: EditDelete, Char: r}\n\t\t}\n\t\treturn edits\n\t}\n\n\tmax := n + m\n\tv := make([]int, 2*max+1)\n\tvar trace [][]int\nsearch:\n\tfor d := 0; d \u003c= max; d++ {\n\t\t// iterate through diagonals\n\t\tfor k := -d; k \u003c= d; k += 2 {\n\t\t\tvar x int\n\t\t\tif k == -d || (k != d \u0026\u0026 v[max+k-1] \u003c v[max+k+1]) {\n\t\t\t\tx = v[max+k+1] // move down\n\t\t\t} else {\n\t\t\t\tx = v[max+k-1] + 1 // move right\n\t\t\t}\n\t\t\ty := x - k\n\n\t\t\t// extend the path as far as possible with matching characters\n\t\t\tfor x \u003c n \u0026\u0026 y \u003c m \u0026\u0026 oldRunes[x] == newRunes[y] {\n\t\t\t\tx++\n\t\t\t\ty++\n\t\t\t}\n\n\t\t\tv[max+k] = x\n\n\t\t\t// check if we've reached the end of both strings\n\t\t\tif x == n \u0026\u0026 y == m {\n\t\t\t\ttrace = append(trace, append([]int(nil), v...))\n\t\t\t\tbreak search\n\t\t\t}\n\t\t}\n\t\ttrace = append(trace, append([]int(nil), v...))\n\t}\n\n\t// backtrack to construct the edit script\n\tedits := make([]Edit, 0, n+m)\n\tx, y := n, m\n\tfor d := len(trace) - 1; d \u003e= 0; d-- {\n\t\tvPrev := trace[d]\n\t\tk := x - y\n\t\tvar prevK int\n\t\tif k == -d || (k != d \u0026\u0026 vPrev[max+k-1] \u003c vPrev[max+k+1]) {\n\t\t\tprevK = k + 1\n\t\t} else {\n\t\t\tprevK = k - 1\n\t\t}\n\t\tprevX := vPrev[max+prevK]\n\t\tprevY := prevX - prevK\n\n\t\t// add keep edits for matching characters\n\t\tfor x \u003e prevX \u0026\u0026 y \u003e prevY {\n\t\t\tif x \u003e 0 \u0026\u0026 y \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditKeep, Char: oldRunes[x-1]}}, edits...)\n\t\t\t}\n\t\t\tx--\n\t\t\ty--\n\t\t}\n\t\tif y \u003e prevY {\n\t\t\tif y \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditInsert, Char: newRunes[y-1]}}, edits...)\n\t\t\t}\n\t\t\ty--\n\t\t} else if x \u003e prevX {\n\t\t\tif x \u003e 0 {\n\t\t\t\tedits = append([]Edit{{Type: EditDelete, Char: oldRunes[x-1]}}, edits...)\n\t\t\t}\n\t\t\tx--\n\t\t}\n\t}\n\n\treturn edits\n}\n\n// Format converts a slice of Edit operations into a human-readable string representation.\n// It groups consecutive edits of the same type and formats them as follows:\n//   - Unchanged characters are left as-is\n//   - Inserted characters are wrapped in [+...]\n//   - Deleted characters are wrapped in [-...]\n//\n// This function is useful for visualizing the differences between two strings\n// in a compact and intuitive format.\n//\n// Parameters:\n//   - edits: A slice of Edit operations, typically produced by MyersDiff\n//\n// Returns:\n//   - A formatted string representing the diff\n//\n// Example output:\n//\n//\tFor the diff between \"abcd\" and \"acbd\", the output might be:\n//\t\"a[-b]c[+b]d\"\n//\n// Note:\n//\n//\tThe function assumes that the input slice of edits is in the correct order.\n//\tAn empty input slice will result in an empty string.\nfunc Format(edits []Edit) string {\n\tif len(edits) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar (\n\t\tresult       strings.Builder\n\t\tcurrentType  EditType\n\t\tcurrentChars strings.Builder\n\t)\n\n\tflushCurrent := func() {\n\t\tif currentChars.Len() \u003e 0 {\n\t\t\tswitch currentType {\n\t\t\tcase EditKeep:\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\tcase EditInsert:\n\t\t\t\tresult.WriteString(\"[+\")\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\t\tresult.WriteByte(']')\n\t\t\tcase EditDelete:\n\t\t\t\tresult.WriteString(\"[-\")\n\t\t\t\tresult.WriteString(currentChars.String())\n\t\t\t\tresult.WriteByte(']')\n\t\t\t}\n\t\t\tcurrentChars.Reset()\n\t\t}\n\t}\n\n\tfor _, edit := range edits {\n\t\tif edit.Type != currentType {\n\t\t\tflushCurrent()\n\t\t\tcurrentType = edit.Type\n\t\t}\n\t\tcurrentChars.WriteRune(edit.Char)\n\t}\n\tflushCurrent()\n\n\treturn result.String()\n}\n"
                      },
                      {
                        "name": "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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "AyT/muMf+skQzwoj832cdIQDtcpUbT7IqwGeKivpyvkSW9n05Z0le/3P8Y5n431OWBrOgSfkZNFKk0TmkytRpw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "nestedpkg",
                    "path": "gno.land/p/demo/nestedpkg",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/demo/nestedpkg\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "nestedpkg.gno",
                        "body": "// Package nestedpkg provides helpers for package-path based access control.\n// It is useful for upgrade patterns relying on namespaces.\npackage nestedpkg\n\n// To test this from a realm and have runtime.CurrentRealm/PreviousRealm work correctly,\n// this file is tested from gno.land/r/tests/vm/nestedpkg_test.gno\n// XXX: move test to ths directory once we support testing a package and\n// specifying values for both PreviousRealm and CurrentRealm.\n\nimport (\n\t\"chain/runtime\"\n\t\"strings\"\n)\n\n// IsCallerSubPath checks if the caller realm is located in a subfolder of the current realm.\nfunc IsCallerSubPath() bool {\n\tvar (\n\t\tcur  = runtime.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = runtime.PreviousRealm().PkgPath() + \"/\"\n\t)\n\treturn strings.HasPrefix(prev, cur)\n}\n\n// AssertCallerIsSubPath panics if IsCallerSubPath returns false.\nfunc AssertCallerIsSubPath() {\n\tvar (\n\t\tcur  = runtime.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = runtime.PreviousRealm().PkgPath() + \"/\"\n\t)\n\tif !strings.HasPrefix(prev, cur) {\n\t\tpanic(\"call restricted to nested packages. current realm is \" + cur + \", previous realm is \" + prev)\n\t}\n}\n\n// IsCallerParentPath checks if the caller realm is located in a parent location of the current realm.\nfunc IsCallerParentPath() bool {\n\tvar (\n\t\tcur  = runtime.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = runtime.PreviousRealm().PkgPath() + \"/\"\n\t)\n\treturn strings.HasPrefix(cur, prev)\n}\n\n// AssertCallerIsParentPath panics if IsCallerParentPath returns false.\nfunc AssertCallerIsParentPath() {\n\tvar (\n\t\tcur  = runtime.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = runtime.PreviousRealm().PkgPath() + \"/\"\n\t)\n\tif !strings.HasPrefix(cur, prev) {\n\t\tpanic(\"call restricted to parent packages. current realm is \" + cur + \", previous realm is \" + prev)\n\t}\n}\n\n// IsSameNamespace checks if the caller realm and the current realm are in the same namespace.\nfunc IsSameNamespace() bool {\n\tvar (\n\t\tcur  = nsFromPath(runtime.CurrentRealm().PkgPath()) + \"/\"\n\t\tprev = nsFromPath(runtime.PreviousRealm().PkgPath()) + \"/\"\n\t)\n\treturn cur == prev\n}\n\n// AssertIsSameNamespace panics if IsSameNamespace returns false.\nfunc AssertIsSameNamespace() {\n\tvar (\n\t\tcur  = nsFromPath(runtime.CurrentRealm().PkgPath()) + \"/\"\n\t\tprev = nsFromPath(runtime.PreviousRealm().PkgPath()) + \"/\"\n\t)\n\tif cur != prev {\n\t\tpanic(\"call restricted to packages from the same namespace. current realm is \" + cur + \", previous realm is \" + prev)\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "Rc8ItVvcIpEhfgMAZpEneiKc0ehwCL7qe1UAg0FTurRvBMCzJEeLXCJ3NyboU/tJII/5G0a3jGhtUrZHOvb3Zw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n\nPackage `testutils` provides testing utilities for Gno packages and realms, including test address generation and cryptographic helpers.\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// For testing std.CallerAt().\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "ojTMe8vckfCaIzOGrP5BfThbNfwU5uQ/ypUBCLl10WEIdQOcHUy1tk83J/48aXKNOpK/GG24meWY1146XA2mVw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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 \"chain/runtime\"\n\nfunc GetCurrentRealm(cur realm) runtime.Realm {\n\treturn runtime.CurrentRealm()\n}\n\nfunc GetPreviousRealm(cur realm) runtime.Realm {\n\treturn runtime.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 runtime.PreviousRealm().IsUser()\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "Zi12iK4gOqPKqMiA9YY/b+9Uu7FMqP21mUp0euF7GPZ4zNRys7L3IxigB5DMv/JzwGMGVeDZ7lwbTn7DmW/Bzg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "tests",
                    "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 tests\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 tests\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 tests\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNestedPkg(t *testing.T) {\n\t// direct child\n\tcur := \"gno.land/r/tests/vm/foo\"\n\ttesting.SetRealm(testing.NewCodeRealm(cur))\n\tif !IsCallerSubPath(cross) {\n\t\tt.Errorf(cur + \" should be a sub path\")\n\t}\n\tif IsCallerParentPath(cross) {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif !HasCallerSameNamespace(cross) {\n\t\tt.Errorf(cur + \" should be from the same namespace\")\n\t}\n\n\t// grand-grand-child\n\tcur = \"gno.land/r/tests/vm/foo/bar/baz\"\n\ttesting.SetRealm(testing.NewCodeRealm(cur))\n\tif !IsCallerSubPath(cross) {\n\t\tt.Errorf(cur + \" should be a sub path\")\n\t}\n\tif IsCallerParentPath(cross) {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif !HasCallerSameNamespace(cross) {\n\t\tt.Errorf(cur + \" 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\tcur = \"gno.land/r/test\" // without the 's' at the end\n\ttesting.SetRealm(testing.NewCodeRealm(cur))\n\tif IsCallerSubPath(cross) {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath(cross) {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace(cross) {\n\t\tt.Errorf(cur + \" should not be from the same namespace\")\n\t}\n\n\t// fake parent (prefix)\n\tcur = \"gno.land/r/dem\"\n\ttesting.SetRealm(testing.NewCodeRealm(cur))\n\tif IsCallerSubPath(cross) {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath(cross) {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace(cross) {\n\t\tt.Errorf(cur + \" should not be from the same namespace\")\n\t}\n\n\t// different namespace\n\tcur = \"gno.land/r/foo\"\n\ttesting.SetRealm(testing.NewCodeRealm(cur))\n\tif IsCallerSubPath(cross) {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath(cross) {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace(cross) {\n\t\tt.Errorf(cur + \" should not be from the same namespace\")\n\t}\n}\n"
                      },
                      {
                        "name": "realm_compositelit.gno",
                        "body": "package tests\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 tests\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 tests\n\nimport (\n\t\"chain/runtime\"\n\n\t\"gno.land/p/demo/nestedpkg\"\n\trsubtests \"gno.land/r/tests/vm/subtests\"\n)\n\nvar counter int\n\nfunc IncCounter(cur realm) {\n\tcounter++\n}\n\nfunc Counter(cur realm) int {\n\treturn counter\n}\n\nfunc CurrentRealmPath(cur realm) string {\n\treturn runtime.CurrentRealm().PkgPath()\n}\n\nvar initOriginCaller = runtime.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 !runtime.PreviousRealm().IsCode()\n\treturn runtime.PreviousRealm().IsUser()\n}\n\nfunc CallSubtestsAssertOriginCall(cur realm) {\n\trsubtests.CallAssertOriginCall(cross)\n}\n\nfunc CallSubtestsIsOriginCall(cur realm) bool {\n\treturn rsubtests.CallIsOriginCall(cross)\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\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 runtime.PreviousRealm()\n}\n\nfunc GetRSubtestsPreviousRealm(cur realm) runtime.Realm {\n\treturn rsubtests.GetPreviousRealm(cross)\n}\n\nfunc Exec(fn func()) {\n\t// no realm switching.\n\tfn()\n}\n\nfunc ExecSwitch(cur realm, fn func()) {\n\tfn()\n}\n\nfunc IsCallerSubPath(cur realm) bool {\n\treturn nestedpkg.IsCallerSubPath()\n}\n\nfunc IsCallerParentPath(cur realm) bool {\n\treturn nestedpkg.IsCallerParentPath()\n}\n\nfunc HasCallerSameNamespace(cur realm) bool {\n\treturn nestedpkg.IsSameNamespace()\n}\n"
                      },
                      {
                        "name": "tests_test.gno",
                        "body": "package tests_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(t *testing.T) {\n\t// CallAssertOriginCall(): no panic\n\tcaller := testutils.TestAddress(\"caller\")\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\ttests.CallAssertOriginCall(cross)\n\tif !tests.CallIsOriginCall(cross) {\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) {\n\t\t\tt.Errorf(\"expected IsOriginCall=false but got true\")\n\t\t}\n\t\ttests.CallAssertOriginCall(cross) // \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) {\n\t\t\tt.Errorf(\"expected IsOriginCall=false but got true\")\n\t\t}\n\t\ttests.CallSubtestsAssertOriginCall(cross)\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(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).Address(); addr != firstRealm {\n\t\tprintln(tests.GetPreviousRealm(cross))\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).Address(); addr != rTestsAddr {\n\t\tt.Errorf(\"want GetRSubtestsPreviousRealm().Address==%s, got %s\", rTestsAddr, addr)\n\t}\n}\n"
                      },
                      {
                        "name": "z0_filetest.gno",
                        "body": "package main\n\nimport (\n\ttests \"gno.land/r/tests/vm\"\n)\n\nfunc main() {\n\tprintln(\"tests.CallIsOriginCall:\", tests.CallIsOriginCall(cross))\n\ttests.CallAssertOriginCall(cross)\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))\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)\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": "package main\n\nimport (\n\ttests \"gno.land/r/tests/vm\"\n)\n\nfunc main() {\n\tprintln(tests.Counter(cross))\n\ttests.IncCounter(cross)\n\tprintln(tests.Counter(cross))\n}\n\n// Output:\n// 0\n// 1\n"
                      },
                      {
                        "name": "z2_filetest.gno",
                        "body": "package main\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() {\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).Address())\n\tprintln(\"tests.GetRSubtestsPreviousRealm().Address(): \", tests.GetRSubtestsPreviousRealm(cross).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\npackage tests\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\nfunc main() {\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 := tests.GetPreviousRealm(cross).Address(); addr != eoa {\n\t\tprintln(\"want tests.GetPreviousRealm().Address ==\", eoa, \"got\", addr)\n\t}\n\t// When 2 or more realms in the frames, it is also different\n\tif addr := tests.GetRSubtestsPreviousRealm(cross).Address(); addr != rTestsAddr {\n\t\tprintln(\"want GetRSubtestsPreviousRealm().Address ==\", rTestsAddr, \"got\", addr)\n\t}\n\tprintln(\"Done.\")\n}\n\n// Output:\n// Done.\n"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "51ML5zy0LGkvnVE4zJr+z0vmuQBL8psT5xRVRUxB4ShK+i1pHcUlqTRb9s4tHpSs5gFDHC6NJdIxZOrNzgIp4A=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n\nPackage `uassert` provides assertion helpers for testing Gno packages and realms.\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) (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)\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\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/onbloc/diff\"\n)\n\n// NoError asserts that a function returned no error (i.e. `nil`).\nfunc NoError(t TestingT, err error, msgs ...string) bool {\n\tt.Helper()\n\tif err != nil {\n\t\treturn fail(t, msgs, \"unexpected error: %s\", err.Error())\n\t}\n\treturn true\n}\n\n// Error asserts that a function returned an error (i.e. not `nil`).\nfunc Error(t TestingT, err error, msgs ...string) bool {\n\tt.Helper()\n\tif err == nil {\n\t\treturn fail(t, msgs, \"an error is expected but got nil\")\n\t}\n\treturn true\n}\n\n// ErrorContains asserts that a function returned an error (i.e. not `nil`)\n// and that the error contains the specified substring.\nfunc ErrorContains(t TestingT, err error, contains string, msgs ...string) bool {\n\tt.Helper()\n\n\tif !Error(t, err, msgs...) {\n\t\treturn false\n\t}\n\n\tactual := err.Error()\n\tif !strings.Contains(actual, contains) {\n\t\treturn fail(t, msgs, \"error %q does not contain %q\", actual, contains)\n\t}\n\n\treturn true\n}\n\n// True asserts that the specified value is true.\nfunc True(t TestingT, value bool, msgs ...string) bool {\n\tt.Helper()\n\tif !value {\n\t\treturn fail(t, msgs, \"should be true\")\n\t}\n\treturn true\n}\n\n// False asserts that the specified value is false.\nfunc False(t TestingT, value bool, msgs ...string) bool {\n\tt.Helper()\n\tif value {\n\t\treturn fail(t, msgs, \"should be false\")\n\t}\n\treturn true\n}\n\n// ErrorIs asserts the given error matches the target error\nfunc ErrorIs(t TestingT, err, target error, msgs ...string) bool {\n\tt.Helper()\n\n\tif err == nil || target == nil {\n\t\treturn err == target\n\t}\n\n\t// XXX: if errors.Is(err, target) return true\n\n\tif err.Error() != target.Error() {\n\t\treturn fail(t, msgs, \"error mismatch, expected %s, got %s\", target.Error(), err.Error())\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// NOTE: This relies on gno's `revive` mechanism to catch aborts.\nfunc AbortsWithMessage(t TestingT, 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) })\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.\nfunc AbortsContains(t TestingT, 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) })\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.\nfunc NotAborts(t TestingT, 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) })\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).\nfunc PanicsWithMessage(t TestingT, msg string, f any, msgs ...string) bool {\n\tt.Helper()\n\n\tdidPanic, panicValue := checkDidPanic(f)\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.\nfunc PanicsContains(t TestingT, substr string, f any, msgs ...string) bool {\n\tt.Helper()\n\n\tdidPanic, panicValue := checkDidPanic(f)\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).\nfunc NotPanics(t TestingT, 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)\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\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(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.PanicsWithMessage(mockT, \"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, \"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, \"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, \"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(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.PanicsContains(mockT, \"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, \"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, \"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(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.AbortsWithMessage(mockT, \"abort message\", func() {\n\t\ttests.ExecSwitch(cross, 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, \"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, \"at the disco\", func() {\n\t\ttests.ExecSwitch(cross, 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, \"non-abort panic\", func() {\n\t\tuassert.AbortsWithMessage(mockT, \"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, \"doesn't matter\", func() {\n\t\ttests.ExecSwitch(cross, 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(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !uassert.AbortsContains(mockT, \"abort\", func() {\n\t\ttests.ExecSwitch(cross, 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, \"notfound\", func() {\n\t\ttests.ExecSwitch(cross, 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, \"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(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tif !uassert.NotPanics(mockT, 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, func() {\n\t\ttests.ExecSwitch(cross, 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(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tif !uassert.NotPanics(mockT, 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, 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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "vU3urZrmDrubJ695FHEG9Ciq5mk7D3samlmT64E9u0U9hU1vkFUIuOoi1ZGJDJ3wMDfvLmfhnmxpoDDVMZNjVA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n# AVL Tree Package\n\nThe `avl` package provides a gas-efficient AVL tree implementation for storing key-value data in Gno realms.\n\n## Basic Usage\n\n```go\npackage myrealm\n\nimport \"gno.land/p/nt/avl/v0\"\n\n// This AVL tree will be persisted after transaction calls\nvar tree *avl.Tree\n\nfunc Set(key string, value int) {\n\t// tree.Set takes in a string key, and a value that can be of any type\n\ttree.Set(key, value)\n}\n\nfunc Get(key string) int {\n\t// tree.Get returns the value at given key in its raw form,\n\t// and a bool to signify the existence of the key-value pair\n\trawValue, exists := tree.Get(key)\n\tif !exists {\n\t\tpanic(\"value at given key does not exist\")\n\t}\n\n\t// rawValue needs to be converted into the proper type before returning it\n\treturn rawValue.(int)\n}\n```\n\n## Storage Architecture: AVL Tree vs Map\n\nIn Gno, the choice between `avl.Tree` and `map` is fundamentally about how data is persisted in storage.\n\n**Maps** are stored as a single, monolithic object. When you access *any* value in a map, Gno must load the *entire* map into memory. For a map with 1,000 entries, accessing one value means loading all 1,000 entries.\n\n**AVL trees** store each node as a separate object. When you access a value, Gno only loads the nodes along the search path (typically log2(n) nodes). For a tree with 1,000 entries, accessing one value loads ~10 nodes; but a tree with 1,000,000 entries only needs to load ~20 nodes.\n\n## Storage Comparison Example\n\nConsider a realm with 1,000 key-value pairs. Here's what happens when you access a single value:\n\n**Map storage:**\n\n```\nObject :4 = map{\n  (\"0\" string):(\"123\" string),\n  (\"1\" string):(\"123\" string),\n  ...\n  (\"999\" string):(\"123\" string)\n}\n```\n- Accessing `map[\"100\"]` loads object `:4` (contains **all 1,000 pairs**)\n- Gas cost is proportional to total map size (1,000 entries)\n- **1 object fetch, but massive data load**\n\n**AVL tree storage:**\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- Accessing `tree.Get(\"100\")` loads ~10 objects (the search path)\n- Gas cost is proportional to log2(n) ≈ 10 nodes\n- **10 object fetches, each containing only a single node**\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/) - Howl detailed analysis\n- [Berty's AVL scalability report](https://github.com/gnolang/hackerspace/issues/67) - Real-world testing with up to 20M entries\n- [Wikipedia - AVL tree](https://en.wikipedia.org/wiki/AVL_tree) - Algorithm details and balancing\n- [Effective Gno](https://docs.gno.land/resources/effective-gno#prefer-avltree-over-map-for-scalable-storage) - High-level usage guidance\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 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\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 node.height == 0 {\n\t\tif index == 0 {\n\t\t\treturn node.key, node.value\n\t\t} else {\n\t\t\tpanic(\"GetByIndex asked for invalid index\")\n\t\t}\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// 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 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) (value any, exists bool)\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 and a boolean indicating whether the key exists.\nfunc (tree *Tree) Get(key string) (value any, exists bool) {\n\t_, value, exists = tree.node.Get(key)\n\treturn\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, exists := tree.Get(\"key1\")\n\tif !exists || value != \"value1\" {\n\t\tt.Error(\"Expected Get to return value1 and true\")\n\t}\n\n\t_, exists = tree.Get(\"key2\")\n\tif exists {\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"
                      },
                      {
                        "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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "HxEDBrihdrqYVOroMHk/wLebbOSAPanpYOv+vK8HB2xH7K/y7WVZO0mkKoTHbLRMK6ik93cMpr+zlz3pvFQm+A=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n\nPackage `urequire` provides test assertion functions that immediately fail the test on error, complementing the `uassert` package.\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\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\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\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\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\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\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.\nfunc AbortsWithMessage(t uassert.TestingT, msg string, f any, msgs ...string) {\n\tt.Helper()\n\tif uassert.AbortsWithMessage(t, msg, 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.\nfunc NotAborts(t uassert.TestingT, f any, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotPanics(t, 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).\nfunc PanicsWithMessage(t uassert.TestingT, msg string, f any, msgs ...string) {\n\tt.Helper()\n\tif uassert.PanicsWithMessage(t, msg, 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).\nfunc NotPanics(t uassert.TestingT, f any, msgs ...string) {\n\tt.Helper()\n\tif uassert.NotPanics(t, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\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\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\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\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"
                      },
                      {
                        "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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "08qNW2Eobi2EV/Db30pLPs5lf9MjneRaextR5YRegy9T3lhsgaWG7pf6J7GQAhxu7UNAOsSkeP33UG/eWEUm0g=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "grc20",
                    "path": "gno.land/p/demo/tokens/grc20",
                    "files": [
                      {
                        "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/demo/tokens/grc20\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "mock.gno",
                        "body": "package grc20\n\n// XXX: func Mock(t *Token)\n"
                      },
                      {
                        "name": "tellers.gno",
                        "body": "package grc20\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n)\n\n// CallerTeller returns a GRC20 compatible teller that checks the PreviousRealm\n// caller for each call. It's usually safe to expose it publicly to let users\n// manipulate their tokens directly, or for realms to use their allowance.\nfunc (tok *Token) CallerTeller() Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func() address {\n\t\t\tcaller := runtime.PreviousRealm().Address()\n\t\t\treturn caller\n\t\t},\n\t\tToken: tok,\n\t}\n}\n\n// ReadonlyTeller is a GRC20 compatible teller that panics for any write operation.\nfunc (tok *Token) ReadonlyTeller() Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\n\treturn \u0026fnTeller{\n\t\taccountFn: nil,\n\t\tToken:     tok,\n\t}\n}\n\n// RealmTeller returns a GRC20 compatible teller that will store the\n// caller realm permanently. Calling anything through this teller will\n// result in allowance or balance changes for the realm that initialized the teller.\n// The initializer of this teller should usually never share the resulting Teller from\n// this method except maybe for advanced delegation flows such as a DAO treasury\n// management.\n// WARN: Should be initialized within a crossing function\n// This way the the realm that created the teller will match CurrentRealm\nfunc (tok *Token) RealmTeller() Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\n\tcaller := runtime.CurrentRealm().Address()\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func() 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// WARN: Should be initialized within a crossing function\n// This way the realm that created the teller will match CurrentRealm\nfunc (tok *Token) RealmSubTeller(slug string) Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\n\tcaller := runtime.CurrentRealm().Address()\n\taccount := accountSlugAddr(caller, slug)\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func() 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() address {\n\t\t\treturn addr\n\t\t},\n\t\tToken: ledger.token,\n\t}\n}\n\n// generic tellers methods.\n//\n\nfunc (ft *fnTeller) Transfer(to address, amount int64) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\tcaller := ft.accountFn()\n\treturn ft.Token.ledger.Transfer(caller, to, amount)\n}\n\nfunc (ft *fnTeller) Approve(spender address, amount int64) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\tcaller := ft.accountFn()\n\treturn ft.Token.ledger.Approve(caller, spender, amount)\n}\n\nfunc (ft *fnTeller) TransferFrom(owner, to address, amount int64) error {\n\tif ft.accountFn == nil {\n\t\treturn ErrReadonly\n\t}\n\tspender := ft.accountFn()\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(t *testing.T) {\n\ttok, _ := NewToken(\"Dummy\", \"DUMMY\", 4)\n\tteller := tok.CallerTeller()\n\turequire.False(t, tok == nil)\n\tvar _ Teller = teller\n}\n\nfunc TestTeller(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 := NewToken(\"Dummy\", \"DUMMY\", 6)\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(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcarl := testutils.TestAddress(\"carl\")\n\n\ttoken, ledger := NewToken(\"Dummy\", \"DUMMY\", 6)\n\tteller := token.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\ttellerThrough := func(action func()) {\n\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/realm_exposing_the_teller\"))\n\t\taction()\n\t}\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\ttellerThrough(func() { urequire.NoError(t, teller.Approve(bob, 600)) })\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(600, 0, 0, 0, 0, 0)\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\ttellerThrough(func() { urequire.Error(t, teller.TransferFrom(alice, carl, 700)) })\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(600, 0, 0, 0, 0, 0)\n\ttellerThrough(func() { urequire.NoError(t, teller.TransferFrom(alice, carl, 400)) })\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\"chain/runtime\"\n\t\"math\"\n\t\"math/overflow\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// NewToken creates a new Token.\n// It returns a pointer to the Token and a pointer to the Ledger.\n// Expected usage: Token, admin := NewToken(\"Dummy\", \"DUMMY\", 4)\nfunc NewToken(name, symbol string, decimals int) (*Token, *PrivateLedger) {\n\tif name == \"\" {\n\t\tpanic(\"name should not be empty\")\n\t}\n\tif symbol == \"\" {\n\t\tpanic(\"symbol should not be empty\")\n\t}\n\t// XXX additional checks (length, characters, limits, etc)\n\n\tledger := \u0026PrivateLedger{}\n\ttoken := \u0026Token{\n\t\tname:      name,\n\t\tsymbol:    symbol,\n\t\tdecimals:  decimals,\n\t\torigRealm: runtime.CurrentRealm().PkgPath(),\n\t\tledger:    ledger,\n\t}\n\tledger.token = token\n\treturn token, ledger\n}\n\n// GetName returns the name of the token.\nfunc (tok Token) GetName() string { return tok.name }\n\n// GetSymbol returns the symbol of the token.\nfunc (tok Token) GetSymbol() string { return tok.symbol }\n\n// GetDecimals returns the number of decimals used to get the token's precision.\nfunc (tok Token) GetDecimals() int { return tok.decimals }\n\n// TotalSupply returns the total supply of the token.\nfunc (tok Token) TotalSupply() int64 { return tok.ledger.totalSupply }\n\n// KnownAccounts returns the number of known accounts in the bank.\nfunc (tok Token) KnownAccounts() int { return tok.ledger.balances.Size() }\n\n// ID returns the Identifier of the token.\n// It is composed of the original realm and the provided symbol.\nfunc (tok *Token) ID() string {\n\treturn tok.origRealm + \".\" + tok.symbol\n}\n\n// HasAddr checks if the specified address is a known account in the bank.\nfunc (tok Token) HasAddr(addr address) bool {\n\treturn tok.ledger.hasAddr(addr)\n}\n\n// BalanceOf returns the balance of the specified address.\nfunc (tok Token) BalanceOf(addr address) int64 {\n\treturn tok.ledger.balanceOf(addr)\n}\n\n// Allowance returns the allowance of the specified owner and spender.\nfunc (tok Token) Allowance(owner, spender address) int64 {\n\treturn tok.ledger.allowance(owner, spender)\n}\n\nfunc (tok Token) RenderHome() string {\n\tstr := \"\"\n\tstr += ufmt.Sprintf(\"# %s ($%s)\\n\\n\", tok.name, tok.symbol)\n\tstr += ufmt.Sprintf(\"* **Decimals**: %d\\n\", tok.decimals)\n\tstr += ufmt.Sprintf(\"* **Total supply**: %d\\n\", tok.ledger.totalSupply)\n\tstr += ufmt.Sprintf(\"* **Known accounts**: %d\\n\", tok.KnownAccounts())\n\treturn str\n}\n\n// SpendAllowance decreases the allowance of the specified owner and spender.\nfunc (led *PrivateLedger) SpendAllowance(owner, spender address, amount int64) error {\n\tif !owner.IsValid() || !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\t// do nothing\n\tif amount == 0 {\n\t\treturn nil\n\t}\n\n\tcurrentAllowance := led.allowance(owner, spender)\n\tif currentAllowance \u003c amount {\n\t\treturn ErrInsufficientAllowance\n\t}\n\n\tkey := allowanceKey(owner, spender)\n\tnewAllowance := overflow.Sub64p(currentAllowance, amount)\n\n\tif newAllowance == 0 {\n\t\tled.allowances.Remove(key)\n\t} else {\n\t\tled.allowances.Set(key, newAllowance)\n\t}\n\n\treturn nil\n}\n\n// Transfer transfers tokens from the specified from address to the specified to address.\nfunc (led *PrivateLedger) Transfer(from, to address, amount int64) error {\n\tif !from.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\n\tvar (\n\t\ttoBalance   = led.balanceOf(to)\n\t\tfromBalance = led.balanceOf(from)\n\t)\n\n\tif fromBalance \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\tvar (\n\t\tnewToBalance   = overflow.Add64p(toBalance, amount)\n\t\tnewFromBalance = overflow.Sub64p(fromBalance, amount)\n\t)\n\n\tled.balances.Set(string(to), newToBalance)\n\n\tif newFromBalance == 0 {\n\t\tled.balances.Remove(string(from))\n\t} else {\n\t\tled.balances.Set(string(from), newFromBalance)\n\t}\n\n\tchain.Emit(\n\t\tTransferEvent,\n\t\t\"token\", led.token.ID(),\n\t\t\"from\", from.String(),\n\t\t\"to\", to.String(),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\n// TransferFrom transfers tokens from the specified owner to the specified to address.\n// It first checks if the owner has sufficient balance and then decreases the allowance.\nfunc (led *PrivateLedger) TransferFrom(owner, spender, to address, amount int64) error {\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\n\tif !owner.IsValid() || !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif led.balanceOf(owner) \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\t// The check above guarantees that Transfer will succeed, ensuring\n\t// atomicity for the subsequent operations.\n\tif err := led.SpendAllowance(owner, spender, amount); err != nil {\n\t\treturn err\n\t}\n\n\tif err := led.Transfer(owner, to, amount); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Approve sets the allowance of the specified owner and spender.\nfunc (led *PrivateLedger) Approve(owner, spender address, amount int64) error {\n\tif !owner.IsValid() || !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\n\tled.allowances.Set(allowanceKey(owner, spender), amount)\n\n\tchain.Emit(\n\t\tApprovalEvent,\n\t\t\"token\", led.token.ID(),\n\t\t\"owner\", string(owner),\n\t\t\"spender\", string(spender),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\n// Mint increases the total supply of the token and adds the specified amount to the specified address.\nfunc (led *PrivateLedger) Mint(addr address, amount int64) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\n\t// limit amount to MaxInt64 - totalSupply\n\tif amount \u003e overflow.Sub64p(math.MaxInt64, led.totalSupply) {\n\t\treturn ErrMintOverflow\n\t}\n\n\tled.totalSupply += amount\n\tcurrentBalance := led.balanceOf(addr)\n\tnewBalance := overflow.Add64p(currentBalance, amount)\n\n\tled.balances.Set(string(addr), newBalance)\n\n\tchain.Emit(\n\t\tTransferEvent,\n\t\t\"token\", led.token.ID(),\n\t\t\"from\", \"\",\n\t\t\"to\", string(addr),\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\n// Burn decreases the total supply of the token and subtracts the specified amount from the specified address.\nfunc (led *PrivateLedger) Burn(addr address, amount int64) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif amount \u003c 0 {\n\t\treturn ErrInvalidAmount\n\t}\n\n\tcurrentBalance := led.balanceOf(addr)\n\tif currentBalance \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\tled.totalSupply = overflow.Sub64p(led.totalSupply, amount)\n\tnewBalance := overflow.Sub64p(currentBalance, amount)\n\n\tif newBalance == 0 {\n\t\tled.balances.Remove(string(addr))\n\t} else {\n\t\tled.balances.Set(string(addr), newBalance)\n\t}\n\n\tchain.Emit(\n\t\tTransferEvent,\n\t\t\"token\", led.token.ID(),\n\t\t\"from\", string(addr),\n\t\t\"to\", \"\",\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\n// hasAddr checks if the specified address is a known account in the ledger.\nfunc (led PrivateLedger) hasAddr(addr address) bool {\n\treturn led.balances.Has(addr.String())\n}\n\n// balanceOf returns the balance of the specified address.\nfunc (led PrivateLedger) balanceOf(addr address) int64 {\n\tbalance, found := led.balances.Get(addr.String())\n\tif !found {\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, found := led.allowances.Get(allowanceKey(owner, spender))\n\tif !found {\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_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/ufmt/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestTestImpl(t *testing.T) {\n\tbank, _ := NewToken(\"Dummy\", \"DUMMY\", 4)\n\turequire.False(t, bank == nil, \"dummy should not be nil\")\n}\n\nfunc TestToken(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 := NewToken(\"Dummy\", \"DUMMY\", 6)\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(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\ttok, adm := NewToken(\"Dummy\", \"DUMMY\", 6)\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(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 := NewToken(\"Test\", \"TEST\", 6)\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// 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(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\ttok, adm := NewToken(\"Dummy\", \"DUMMY\", 6)\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 err == nil || err.Error() != tt.expectedError.Error() {\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.\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`.\n\t//\n\t// Returns an error if the operation failed.\n\tTransfer(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(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(from, to address, amount int64) error\n}\n\n// Token represents a fungible token with a name, symbol, and a certain number\n// of decimal places. It maintains a ledger for tracking balances and allowances\n// 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// 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// Original realm of the token (e.g., \"gno.land/r/demo/foo20\").\n\torigRealm string\n\t// Pointer to the PrivateLedger that manages balances and allowances.\n\tledger *PrivateLedger\n}\n\n// PrivateLedger is a struct that holds the balances and allowances for the\n// token. It provides administrative functions for minting, burning,\n// transferring tokens, and managing allowances.\n//\n// The PrivateLedger is not safe to expose publicly, as it contains sensitive\n// information regarding token balances and allowances, and allows direct,\n// unrestricted access to all administrative functions.\ntype PrivateLedger struct {\n\t// Total supply of the token managed by this ledger.\n\ttotalSupply int64\n\t// chain.Address -\u003e int64\n\tbalances avl.Tree\n\t// owner.(chain.Address)+\":\"+spender.(chain.Address)) -\u003e int64\n\tallowances avl.Tree\n\t// Pointer to the associated Token struct\n\ttoken *Token\n}\n\nvar (\n\tErrInsufficientBalance   = errors.New(\"insufficient balance\")\n\tErrInsufficientAllowance = errors.New(\"insufficient allowance\")\n\tErrInvalidAddress        = errors.New(\"invalid address\")\n\tErrCannotTransferToSelf  = errors.New(\"cannot send transfer to self\")\n\tErrReadonly              = errors.New(\"banker is readonly\")\n\tErrRestrictedTokenOwner  = errors.New(\"restricted to bank owner\")\n\tErrMintOverflow          = errors.New(\"mint overflow\")\n\tErrInvalidAmount         = errors.New(\"invalid amount\")\n)\n\nconst (\n\tMintEvent     = \"Mint\"\n\tBurnEvent     = \"Burn\"\n\tTransferEvent = \"Transfer\"\n\tApprovalEvent = \"Approval\"\n)\n\ntype fnTeller struct {\n\taccountFn func() address\n\t*Token\n}\n\nvar _ Teller = (*fnTeller)(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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "XwYjMrbviHbiDrVj4dD4FcxwjGD1arFdXYYDVMmQoZUfJPk1vj0lZLfSLB15h+bwecPZ+XYZKk6eEdRDOdUt3g=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
                  "package": {
                    "name": "md",
                    "path": "gno.land/p/moul/md",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/moul/md\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"
                      },
                      {
                        "name": "md.gno",
                        "body": "// Package md provides helper functions for generating Markdown content programmatically.\n//\n// It includes utilities for text formatting, creating lists, blockquotes, code blocks,\n// links, images, and more.\n//\n// Highlights:\n// - Supports basic Markdown syntax such as bold, italic, strikethrough, headers, and lists.\n// - Manages multiline support in lists (e.g., bullet, ordered, and todo lists).\n// - Includes advanced helpers like inline images with links and nested list prefixes.\n//\n// For a comprehensive example of how to use these helpers, see:\n// https://gno.land/r/docs/moul_md\npackage md\n\nimport (\n\t\"strconv\"\n\t\"strings\"\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 a blockquote for markdown.\n// Example: Blockquote(\"foo\\nbar\") =\u003e \"\u003e foo\\n\u003e bar\\n\"\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(\"\u003e \" + line + \"\\n\")\n\t}\n\treturn sb.String()\n}\n\n// InlineCode returns inline code for markdown.\n// Example: InlineCode(\"foo\") =\u003e \"`foo`\"\nfunc InlineCode(code string) string {\n\treturn \"`\" + strings.ReplaceAll(code, \"`\", \"\\\\`\") + \"`\"\n}\n\n// CodeBlock creates a markdown code block.\n// Example: CodeBlock(\"foo\") =\u003e \"```\\nfoo\\n```\"\nfunc CodeBlock(content string) string {\n\treturn \"```\\n\" + strings.ReplaceAll(content, \"```\", \"\\\\```\") + \"\\n```\"\n}\n\n// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting.\n// Example: LanguageCodeBlock(\"go\", \"foo\") =\u003e \"```go\\nfoo\\n```\"\nfunc LanguageCodeBlock(language, content string) string {\n\treturn \"```\" + language + \"\\n\" + strings.ReplaceAll(content, \"```\", \"\\\\```\") + \"\\n```\"\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)\"\nfunc Link(text, url string) string {\n\treturn \"[\" + EscapeText(text) + \"](\" + url + \")\"\n}\n\n// UserLink returns a user profile link for markdown.\n// For usernames, it adds @ prefix to the display text.\n// Example: UserLink(\"moul\") =\u003e \"[@moul](/u/moul)\"\n// Example: UserLink(\"g1blah\") =\u003e \"[g1blah](/u/g1blah)\"\nfunc UserLink(user string) string {\n\tif strings.HasPrefix(user, \"g1\") {\n\t\treturn \"[\" + EscapeText(user) + \"](/u/\" + user + \")\"\n\t}\n\treturn \"[@\" + EscapeText(user) + \"](/u/\" + user + \")\"\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)\"\nfunc InlineImageWithLink(altText, imageUrl, linkUrl string) string {\n\treturn \"[\" + Image(altText, imageUrl) + \"](\" + linkUrl + \")\"\n}\n\n// Image returns an image for markdown.\n// Example: Image(\"foo\", \"http://example.com\") =\u003e \"![foo](http://example.com)\"\nfunc Image(altText, url string) string {\n\treturn \"![\" + EscapeText(altText) + \"](\" + url + \")\"\n}\n\n// Footnote returns a footnote for markdown.\n// Example: Footnote(\"foo\", \"bar\") =\u003e \"[foo]: bar\"\nfunc Footnote(reference, text string) string {\n\treturn \"[\" + EscapeText(reference) + \"]: \" + text\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\nfunc CollapsibleSection(title, content string) string {\n\treturn \"\u003cdetails\u003e\u003csummary\u003e\" + EscapeText(title) + \"\u003c/summary\u003e\\n\\n\" + content + \"\\n\u003c/details\u003e\\n\"\n}\n\n// EscapeText escapes special Markdown characters in regular text where needed.\nfunc EscapeText(text string) string {\n\treplacer := strings.NewReplacer(\n\t\t`*`, `\\*`,\n\t\t`_`, `\\_`,\n\t\t`[`, `\\[`,\n\t\t`]`, `\\]`,\n\t\t`(`, `\\(`,\n\t\t`)`, `\\)`,\n\t\t`~`, `\\~`,\n\t\t`\u003e`, `\\\u003e`,\n\t\t`|`, `\\|`,\n\t\t`-`, `\\-`,\n\t\t`+`, `\\+`,\n\t\t\".\", `\\.`,\n\t\t\"!\", `\\!`,\n\t\t\"`\", \"\\\\`\",\n\t)\n\treturn replacer.Replace(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.\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(\"|||\\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.\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\"\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\t\t{\"LanguageCodeBlock\", func() string { return md.LanguageCodeBlock(\"go\", \"foo\") }, \"```go\\nfoo\\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 { return md.InlineImageWithLink(\"alt\", \"image-url\", \"link-url\") }, \"[![alt](image-url)](link-url)\"},\n\t\t{\"Footnote\", func() string { return md.Footnote(\"foo\", \"bar\") }, \"[foo]: bar\"},\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{\"address\", \"g1blah\", \"[g1blah](/u/g1blah)\"},\n\t\t{\"username with special chars\", \"user_name\", \"[@user\\\\_name](/u/user_name)\"},\n\t\t{\"address with numbers\", \"g1abc123\", \"[g1abc123](/u/g1abc123)\"},\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|||\n\n|||\n\n|||\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|||\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|||\nColumn 2\n|||\n\n|||\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|||\nB\n|||\nC\n|||\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|||\nB\n|||\nC\n|||\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|||\n2\n|||\n3\n|||\n4\n|||\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|||\n2\n|||\n3\n|||\n4\n|||\n5\n|||\n\n|||\n\n|||\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|||\nB\n|||\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|||\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|||\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|||\nB\n\u003c/gno-columns\u003e\n\u003cgno-columns\u003e\nC\n|||\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|||\n\n|||\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"
                      },
                      {
                        "name": "z1_filetest.gno",
                        "body": "package main\n\nimport \"gno.land/p/moul/md\"\n\nfunc main() {\n\tprintln(md.H1(\"Header 1\"))\n\tprintln(md.H2(\"Header 2\"))\n\tprintln(md.H3(\"Header 3\"))\n\tprintln(md.H4(\"Header 4\"))\n\tprintln(md.H5(\"Header 5\"))\n\tprintln(md.H6(\"Header 6\"))\n\tprintln(md.Bold(\"bold\"))\n\tprintln(md.Italic(\"italic\"))\n\tprintln(md.Strikethrough(\"strikethrough\"))\n\tprintln(md.BulletList([]string{\n\t\t\"Item 1\",\n\t\t\"Item 2\\nMore details for item 2\",\n\t}))\n\tprintln(md.OrderedList([]string{\"Step 1\", \"Step 2\"}))\n\tprintln(md.TodoList([]string{\"Task 1\", \"Task 2\\nSubtask 2\"}, []bool{true, false}))\n\tprintln(md.Nested(md.BulletList([]string{\"Parent Item\", md.OrderedList([]string{\"Child 1\", \"Child 2\"})}), \"  \"))\n\tprintln(md.Blockquote(\"This is a blockquote\\nSpanning multiple lines\"))\n\tprintln(md.InlineCode(\"inline `code`\"))\n\tprintln(md.CodeBlock(\"line1\\nline2\"))\n\tprintln(md.LanguageCodeBlock(\"go\", \"func main() {\\nprintln(\\\"Hello, world!\\\")\\n}\"))\n\tprintln(md.HorizontalRule())\n\tprintln(md.Link(\"Gno\", \"http://gno.land\"))\n\tprintln(md.Image(\"Alt Text\", \"http://example.com/image.png\"))\n\tprintln(md.InlineImageWithLink(\"Alt Text\", \"http://example.com/image.png\", \"http://example.com\"))\n\tprintln(md.Footnote(\"ref\", \"This is a footnote\"))\n\tprintln(md.Paragraph(\"This is a paragraph.\"))\n\n\tprintln(\"4 columns in one gno-columns tag:\")\n\tprintln(md.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(md.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(md.ColumnsN([]string{\n\t\t\"Column1\\ncontent1\",\n\t\t\"Column2\\ncontent2\",\n\t}, 4, true))\n\n}\n\n// Output:\n// # Header 1\n//\n// ## Header 2\n//\n// ### Header 3\n//\n// #### Header 4\n//\n// ##### Header 5\n//\n// ###### Header 6\n//\n// **bold**\n// *italic*\n// ~~strikethrough~~\n// - Item 1\n// - Item 2\n//   More details for item 2\n//\n// 1. Step 1\n// 2. Step 2\n//\n// - [x] Task 1\n// - [ ] Task 2\n//   Subtask 2\n//\n//   - Parent Item\n//   - 1. Child 1\n//     2. Child 2\n//\n//\n// \u003e This is a blockquote\n// \u003e Spanning multiple lines\n//\n// `inline \\`code\\``\n// ```\n// line1\n// line2\n// ```\n// ```go\n// func main() {\n// println(\"Hello, world!\")\n// }\n// ```\n// ---\n//\n// [Gno](http://gno.land)\n// ![Alt Text](http://example.com/image.png)\n// [![Alt Text](http://example.com/image.png)](http://example.com)\n// [ref]: This is a footnote\n// This is a paragraph.\n//\n//\n// 4 columns in one gno-columns tag:\n// \u003cgno-columns\u003e\n// Column1\n// content1\n// |||\n// Column2\n// content2\n// |||\n// Column3\n// content3\n// |||\n// Column4\n// content4\n// \u003c/gno-columns\u003e\n//\n// 3 cols per row without padding:\n// \u003cgno-columns\u003e\n// Row1Column1\n// content1\n// |||\n// Row1Column2\n// content2\n// |||\n// Row1Column3\n// content3\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// Row2Column1\n// content1\n// |||\n// Row2Column2\n// content2\n// |||\n// Row2Column3\n// content3\n// \u003c/gno-columns\u003e\n// \u003cgno-columns\u003e\n// Row3Column1\n// content1\n// |||\n// Row3Column2\n// content2\n// |||\n// Row3Column3\n// content3\n// \u003c/gno-columns\u003e\n//\n// 2 padded to 4:\n// \u003cgno-columns\u003e\n// Column1\n// content1\n// |||\n// Column2\n// content2\n// |||\n//\n// |||\n//\n// \u003c/gno-columns\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "W6dN4Dnmqfshx3F8OSAlrgtTXtSstaAsx1sCREKd07cX1SYQOcuv6uX1zNI/HdQbsZGUNSF+IHtxD1vB7HDEmg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "rotree",
                    "path": "gno.land/p/nt/avl/v0/rotree",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/nt/avl/v0/rotree\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "rotree.gno",
                        "body": "// Package rotree provides a read-only wrapper for avl.Tree with safe value transformation.\n//\n// It is useful when you want to expose a read-only view of a tree while ensuring that\n// the sensitive data cannot be modified.\n//\n// Example:\n//\n//\t// Define a user structure with sensitive data\n//\ttype User struct {\n//\t\tName     string\n//\t\tBalance  int\n//\t\tInternal string // sensitive field\n//\t}\n//\n//\t// Create and populate the original tree\n//\tprivateTree := avl.NewTree()\n//\tprivateTree.Set(\"alice\", \u0026User{\n//\t\tName:     \"Alice\",\n//\t\tBalance:  100,\n//\t\tInternal: \"sensitive\",\n//\t})\n//\n//\t// Create a safe transformation function that copies the struct\n//\t// while excluding sensitive data\n//\tmakeEntrySafeFn := func(v any) any {\n//\t\tu := v.(*User)\n//\t\treturn \u0026User{\n//\t\t\tName:     u.Name,\n//\t\t\tBalance:  u.Balance,\n//\t\t\tInternal: \"\", // omit sensitive data\n//\t\t}\n//\t}\n//\n//\t// Create a read-only view of the tree\n//\tPublicTree := rotree.Wrap(tree, makeEntrySafeFn)\n//\n//\t// Safely access the data\n//\tvalue, _ := roTree.Get(\"alice\")\n//\tuser := value.(*User)\n//\t// user.Name == \"Alice\"\n//\t// user.Balance == 100\n//\t// user.Internal == \"\" (sensitive data is filtered)\npackage rotree\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// Wrap creates a new ReadOnlyTree from an existing avl.Tree and a safety transformation function.\n// If makeEntrySafeFn is nil, values will be returned as-is without transformation.\n//\n// makeEntrySafeFn is a function that transforms a tree entry into a safe version that can be exposed to external users.\n// This function should be implemented based on the specific safety requirements of your use case:\n//\n//  1. No-op transformation: For primitive types (int, string, etc.) or already safe objects,\n//     simply pass nil as the makeEntrySafeFn to return values as-is.\n//\n//  2. Defensive copying: For mutable types like slices or maps, you should create a deep copy\n//     to prevent modification of the original data.\n//     Example: func(v any) any { return append([]int{}, v.([]int)...) }\n//\n//  3. Read-only wrapper: Return a read-only version of the object that implements\n//     a limited interface.\n//     Example: func(v any) any { return NewReadOnlyObject(v) }\n//\n//  4. DAO transformation: Transform the object into a data access object that\n//     controls how the underlying data can be accessed.\n//     Example: func(v any) any { return NewDAO(v) }\n//\n// The function ensures that the returned object is safe to expose to untrusted code,\n// preventing unauthorized modifications to the original data structure.\nfunc Wrap(tree *avl.Tree, makeEntrySafeFn func(any) any) *ReadOnlyTree {\n\treturn \u0026ReadOnlyTree{\n\t\ttree:            tree,\n\t\tmakeEntrySafeFn: makeEntrySafeFn,\n\t}\n}\n\n// ReadOnlyTree wraps an avl.Tree and provides read-only access.\ntype ReadOnlyTree struct {\n\ttree            *avl.Tree\n\tmakeEntrySafeFn func(any) any\n}\n\n// IReadOnlyTree defines the read-only operations available on a tree.\ntype IReadOnlyTree interface {\n\tSize() int\n\tHas(key string) bool\n\tGet(key string) (any, bool)\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.\nfunc (roTree *ReadOnlyTree) Get(key string) (any, bool) {\n\tvalue, exists := roTree.tree.Get(key)\n\tif !exists {\n\t\treturn nil, false\n\t}\n\treturn roTree.getSafeValue(value), true\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, exists := roTree.Get(\"alice\")\n\t\tif !exists {\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\texists   bool\n\t}{\n\t\t{\"ExistingKey1\", \"key1\", \"value1_readonly\", true},\n\t\t{\"ExistingKey2\", \"key2\", \"value2_readonly\", true},\n\t\t{\"NonExistingKey\", \"key4\", nil, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvalue, exists := roTree.Get(tt.key)\n\t\t\tif exists != tt.exists || value != tt.expected {\n\t\t\t\tt.Errorf(\"For key %s, expected %v (exists: %v), got %v (exists: %v)\", tt.key, tt.expected, tt.exists, value, exists)\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, exists := roTree.Get(tt.key)\n\t\t\tif !exists {\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, exists := roTree.Get(\"test\")\n\tif !exists {\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "id24tWEeeeY0veO1hHK42qTi3GKADBh5eL4mnrpUxMt8fIAH7nLVcF9yHf8pPix4ctw662cYpdwF89JU7St4gw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n# cford32\n\n```\npackage cford32 // import \"gno.land/p/nt/cford32/v0\"\n\nPackage cford32 implements a base32-like encoding/decoding package, with the\nencoding scheme specified by Douglas Crockford.\n\nFrom 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\n    symbols.\n  - Be error resistant. Entering the symbols must not require keyboarding\n    gymnastics.\n  - Be pronounceable. Humans should be able to accurately transmit the symbols\n    to other humans using a telephone.\n\nThis is slightly different from a simple difference in encoding table from\nthe Go's stdlib `encoding/base32`, as when decoding the characters i I l L are\nparsed as 1, and o O is parsed as 0.\n\nThis package additionally provides ways to encode uint64's efficiently, as well\nas efficient encoding to a lowercase variation of the encoding. The encodings\nnever use paddings.\n\n# Uint64 Encoding\n\nAside from lower/uppercase encoding, there is a compact encoding, allowing to\nencode all values in [0,2^34), and the full encoding, allowing all values in\n[0,2^64). The compact encoding uses 7 characters, and the full encoding uses 13\ncharacters. Both are parsed unambiguously by the Uint64 decoder.\n\nThe compact encodings have the first character between ['0','f'], while the\nfull encoding's first character ranges between ['g','z']. Practically, in your\nusage of the package, you should consider which one to use and stick with it,\nwhile considering that the compact encoding, once it reaches 2^34, automatically\nswitches to the full encoding. The properties of the generated strings are still\nmaintained: for instance, any two encoded uint64s x,y consistently generated\nwith the compact encoding, if the numeric value is x \u003c y, will also be x \u003c y in\nlexical ordering. However, values [0,2^34) have a \"double encoding\", which if\nmixed together lose the lexical ordering property.\n\nThe Uint64 encoding is most useful for generating string versions of Uint64 IDs.\nPractically, it allows you to retain sleek and compact IDs for your application\nfor the first 2^34 (\u003e17 billion) entities, while seamlessly rolling over to the\nfull encoding should you exceed that. You are encouraged to use it unless you\nhave a requirement or preferences for IDs consistently being always the same\nsize.\n\nTo use the cford32 encoding for IDs, you may want to consider using package\ngno.land/p/nt/seqid/v0.\n\n[specified by Douglas Crockford]: https://www.crockford.com/base32.html\n\nfunc AppendCompact(id uint64, b []byte) []byte\nfunc AppendDecode(dst, src []byte) ([]byte, error)\nfunc AppendEncode(dst, src []byte) []byte\nfunc AppendEncodeLower(dst, src []byte) []byte\nfunc Decode(dst, src []byte) (n int, err error)\nfunc DecodeString(s string) ([]byte, error)\nfunc DecodedLen(n int) int\nfunc Encode(dst, src []byte)\nfunc EncodeLower(dst, src []byte)\nfunc EncodeToString(src []byte) string\nfunc EncodeToStringLower(src []byte) string\nfunc EncodedLen(n int) int\nfunc NewDecoder(r io.Reader) io.Reader\nfunc NewEncoder(w io.Writer) io.WriteCloser\nfunc NewEncoderLower(w io.Writer) io.WriteCloser\nfunc PutCompact(id uint64) []byte\nfunc PutUint64(id uint64) [13]byte\nfunc PutUint64Lower(id uint64) [13]byte\nfunc Uint64(b []byte) (uint64, error)\ntype CorruptInputError int64\n```\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "9Mum2bYC9zCW1Vp6DcPNVU4ltTzGyW2phYVRiiqj+qkdJ5UqhUSiNecJNdgWnW5zXYmDZUe9whqEWz0mbTqpBg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n# seqid\n\n```\npackage seqid // import \"gno.land/p/nt/seqid/v0\"\n\nPackage seqid provides a simple way to have sequential IDs which will be ordered\ncorrectly when inserted in an AVL tree.\n\nSample usage:\n\n    var id seqid.ID\n    var users avl.Tree\n\n    func NewUser() {\n    \tusers.Set(id.Next().Binary(), \u0026User{ ... })\n    }\n\nTYPES\n\ntype ID uint64\n    An ID is a simple sequential ID generator.\n\nfunc FromBinary(b string) (ID, bool)\n    FromBinary creates a new ID from the given string.\n\nfunc (i ID) Binary() string\n    Binary returns a big-endian binary representation of the ID, suitable to be\n    used as an AVL key.\n\nfunc (i *ID) Next() ID\n    Next advances the ID i. It will panic if increasing ID would overflow.\n\nfunc (i *ID) TryNext() (ID, bool)\n    TryNext increases i by 1 and returns its value. It returns true if\n    successful, or false if the increment would result in an overflow.\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 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(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(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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "C2CE59cO8h18FiGM5tUmqdAFriAGLIwf2Sjc8GVmaF8aCEkWOSizmcSl/jgwz9lcUyGOyHM5BCagspSv4l09sA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "pager",
                    "path": "gno.land/p/nt/avl/v0/pager",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/nt/avl/v0/pager\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "pager.gno",
                        "body": "package pager\n\nimport (\n\t\"math\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/avl/v0/rotree\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Pager is a struct that holds the AVL tree and pagination parameters.\ntype Pager struct {\n\tTree            rotree.IReadOnlyTree\n\tPageQueryParam  string\n\tSizeQueryParam  string\n\tDefaultPageSize int\n\tReversed        bool\n}\n\n// Page represents a single page of results.\ntype Page struct {\n\tItems      []Item\n\tPageNumber int\n\tPageSize   int\n\tTotalItems int\n\tTotalPages int\n\tHasPrev    bool\n\tHasNext    bool\n\tPager      *Pager // Reference to the parent Pager\n}\n\n// Item represents a key-value pair in the AVL tree.\ntype Item struct {\n\tKey   string\n\tValue any\n}\n\n// NewPager creates a new Pager with default values.\nfunc NewPager(tree rotree.IReadOnlyTree, defaultPageSize int, reversed bool) *Pager {\n\treturn \u0026Pager{\n\t\tTree:            tree,\n\t\tPageQueryParam:  \"page\",\n\t\tSizeQueryParam:  \"size\",\n\t\tDefaultPageSize: defaultPageSize,\n\t\tReversed:        reversed,\n\t}\n}\n\n// GetPage retrieves a page of results from the AVL tree.\nfunc (p *Pager) GetPage(pageNumber int) *Page {\n\treturn p.GetPageWithSize(pageNumber, p.DefaultPageSize)\n}\n\nfunc (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page {\n\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/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/v0\"\n\t\"gno.land/p/nt/avl/v0/pager\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "CaTPhQRUUkitj2fbMFrZYTma/96MlbGbIR8mXBqRzxRx6E6eQIHejoonuzkEc+xw3IyMrgScdaYbAVSG8dLZMg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n\nPackage `mux` provides a simple routing and rendering library for handling dynamic path-based requests in Gno contracts, similar to `http.ServeMux` in Go.\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\ntype Handler struct {\n\tPattern string\n\tFn      HandlerFunc\n}\n\ntype HandlerFunc func(*ResponseWriter, *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\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// 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// 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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "kvFr4PKFez9b81/zX9jEIw9armorQDP3YVZlKi/mM89RZFcADQSFjrLhSh+z6CVn+Z4mo2wTKsiJU0AiysefcQ=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n\nPackage `ownable` provides an ownership pattern for Gno realms, allowing contracts to restrict access to privileged operations to a designated owner.\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 (\n\t\"chain\"\n\t\"chain/runtime\"\n)\n\nconst OwnershipTransferEvent = \"OwnershipTransfer\"\n\n// Ownable is meant to be used as a top-level object to make your contract ownable OR\n// being embedded in a Gno object to manage per-object ownership.\n// Ownable is safe to export as a top-level object.\n//\n// The auth mode (current-realm vs previous-realm) is decided at construction time:\n//   - New() and NewWithAddress() use current-realm auth.\n//   - NewWithOrigin() and NewWithAddressByPrevious() use previous-realm auth.\n//\n// All ownership operations (TransferOwnership, DropOwnership, Owned, AssertOwned)\n// automatically use the configured auth mode.\ntype Ownable struct {\n\towner    address\n\tprevious bool // if true, ownership is checked against the previous realm\n}\n\n// New creates an Ownable owned by the current realm, using current-realm auth.\nfunc New() *Ownable {\n\treturn \u0026Ownable{\n\t\towner: runtime.CurrentRealm().Address(),\n\t}\n}\n\n// NewWithOrigin creates an Ownable owned by the origin caller (the user who\n// initiated the transaction). Must be called from init() where\n// PreviousRealm() is the origin caller. Uses previous-realm auth.\nfunc NewWithOrigin() *Ownable {\n\torigin := runtime.OriginCaller()\n\tprevious := runtime.PreviousRealm()\n\tif origin != previous.Address() {\n\t\tpanic(\"NewWithOrigin() should be called from init() where std.PreviousRealm() is origin\")\n\t}\n\treturn \u0026Ownable{\n\t\towner:    origin,\n\t\tprevious: true,\n\t}\n}\n\n// NewWithAddress creates an Ownable with the given address as owner,\n// using current-realm auth.\nfunc NewWithAddress(addr address) *Ownable {\n\treturn \u0026Ownable{\n\t\towner: addr,\n\t}\n}\n\n// NewWithAddressByPrevious creates an Ownable with the given address as owner,\n// using previous-realm auth.\nfunc NewWithAddressByPrevious(addr address) *Ownable {\n\treturn \u0026Ownable{\n\t\towner:    addr,\n\t\tprevious: true,\n\t}\n}\n\n// Owned returns true if the caller is the owner, according to the configured auth mode.\nfunc (o *Ownable) Owned() bool {\n\tif o == nil {\n\t\treturn false\n\t}\n\tif o.previous {\n\t\treturn runtime.PreviousRealm().Address() == o.owner\n\t}\n\treturn runtime.CurrentRealm().Address() == o.owner\n}\n\n// AssertOwned panics if the caller is not the owner, according to the configured auth mode.\nfunc (o *Ownable) AssertOwned() {\n\tif !o.Owned() {\n\t\tpanic(ErrUnauthorized)\n\t}\n}\n\n// TransferOwnership transfers ownership of the Ownable to a new address.\n// Uses the configured auth mode to verify the caller.\nfunc (o *Ownable) TransferOwnership(newOwner address) error {\n\tif !o.Owned() {\n\t\treturn ErrUnauthorized\n\t}\n\n\tif !newOwner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\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\n\treturn nil\n}\n\n// DropOwnership removes the owner, effectively disabling any owner-related actions.\n// Uses the configured auth mode to verify the caller.\nfunc (o *Ownable) DropOwnership() error {\n\tif !o.Owned() {\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 from Ownable.\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\"chain/runtime\"\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 TestNew(t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/test/test\"))\n\tcurrent := runtime.CurrentRealm().Address()\n\n\to := New()\n\tgot := o.Owner()\n\tuassert.Equal(t, got, current)\n}\n\nfunc TestNewWithOriginPanic(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tuassert.PanicsWithMessage(t, \"frame not found: cannot seek beyond origin caller override\", func() {\n\t\tNewWithOrigin()\n\t})\n}\n\nfunc TestNewWithOrigin(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\t// This is the only way to test crosses from a p package for now.\n\t\to := NewWithOrigin()\n\t\tgot := o.Owner()\n\t\tuassert.Equal(t, got, alice)\n\t})\n}\n\nfunc TestNewWithAddress(t *testing.T) {\n\to := NewWithAddress(alice)\n\n\tgot := o.Owner()\n\tuassert.Equal(t, got, alice)\n}\n\nfunc TestNewWithAddressByPrevious(t *testing.T) {\n\to := NewWithAddressByPrevious(alice)\n\n\tgot := o.Owner()\n\tuassert.Equal(t, got, alice)\n}\n\nfunc TestTransferOwnership(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\to := New()\n\terr := o.TransferOwnership(bob)\n\turequire.NoError(t, err)\n\n\tgot := o.Owner()\n\tuassert.Equal(t, got, bob)\n}\n\nfunc TestTransferOwnershipByPrevious(t *testing.T) {\n\tvar o *Ownable\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\to = NewWithOrigin()\n\t})\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\terr := o.TransferOwnership(bob)\n\t\turequire.NoError(t, err)\n\t})\n\n\tgot := o.Owner()\n\tuassert.Equal(t, got, bob)\n}\n\nfunc TestTransferOwnershipUnauthorized(t *testing.T) {\n\tvar o *Ownable\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\to = NewWithOrigin() // owned by alice, previous-mode\n\t})\n\n\t// Bob cannot transfer ownership.\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\tuassert.ErrorContains(t, o.TransferOwnership(bob), ErrUnauthorized.Error())\n\t})\n}\n\nfunc TestDropOwnership(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\to := New()\n\n\terr := o.DropOwnership()\n\turequire.NoError(t, err, \"DropOwnership failed\")\n\n\towner := o.Owner()\n\tuassert.Empty(t, owner, \"owner should be empty\")\n}\n\nfunc TestDropOwnershipByPrevious(t *testing.T) {\n\tvar o *Ownable\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\to = NewWithOrigin()\n\t})\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\terr := o.DropOwnership()\n\t\turequire.NoError(t, err, \"DropOwnership failed\")\n\t})\n\n\towner := o.Owner()\n\tuassert.Empty(t, owner, \"owner should be empty\")\n}\n\nfunc TestDropOwnershipUnauthorized(t *testing.T) {\n\tvar o *Ownable\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\to = NewWithOrigin() // owned by alice\n\t})\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\tuassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error())\n\t})\n}\n\nfunc TestOwned(t *testing.T) {\n\t// Current-mode.\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := New()\n\tuassert.True(t, o.Owned())\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tuassert.False(t, o.Owned())\n\n\t// Previous-mode.\n\tvar po *Ownable\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\tpo = NewWithOrigin()\n\t})\n\n\t// Previous-mode Owned() requires a cross-realm context (PreviousRealm() is only\n\t// meaningful when called from a crossing function).\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\tuassert.True(t, po.Owned())\n\t})\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\tuassert.False(t, po.Owned())\n\t})\n}\n\nfunc TestAssertOwned(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\to := New()\n\n\t// Should not panic.\n\to.AssertOwned()\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tuassert.PanicsWithMessage(t, ErrUnauthorized.Error(), func() {\n\t\to.AssertOwned()\n\t})\n}\n\nfunc TestErrInvalidAddress(t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/test/test\"))\n\n\to := New()\n\terr := o.TransferOwnership(\"\")\n\tuassert.ErrorContains(t, err, ErrInvalidAddress.Error())\n\n\terr = o.TransferOwnership(\"10000000001000000000100000000010000000001000000000\")\n\tuassert.ErrorContains(t, err, ErrInvalidAddress.Error())\n}\n\nfunc TestNilReceiver(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.Owned()\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.AssertOwned()\n}\n\nfunc crossThrough(rlm runtime.Realm, cr func()) {\n\ttesting.SetRealm(rlm)\n\tcr()\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "4gPefPfzMRu1+9Uw78OLXDKtGeMAGqnN59d480e+XwhWD+5KbWDg+HqYl8cseATijLAxKCXxdJeT0a96+skI9w=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n\nPackage `fqname` provides utilities for handling fully qualified identifiers in Gno, typically a package path followed by a dot and a symbol name.\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\treturn \"[\" + pkgPath + \"](\" + pkgLink + \").\" + slug\n\t\t}\n\n\t\treturn \"[\" + pkgPath + \"](\" + pkgLink + \")\"\n\t}\n\n\tif slug != \"\" {\n\t\treturn pkgPath + \".\" + slug\n\t}\n\n\treturn pkgPath\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "R30YfPUBwupmXwHR07GJkITB4RUAyBQjJuBD/2GvaNoC1O/aGVKZlDlu0TmJ8e9kGwkxakNOiLtJqAw8Sb0d8w=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "grc20reg",
                    "path": "gno.land/r/demo/defi/grc20reg",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/demo/defi/grc20reg\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "grc20reg.gno",
                        "body": "package grc20reg\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/avl/v0/rotree\"\n\t\"gno.land/p/nt/fqname/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar registry = avl.NewTree() // rlmPath[.slug] -\u003e *Token (slug is optional)\nfunc Register(cur realm, token *grc20.Token, slug string) {\n\trlmPath := runtime.PreviousRealm().PkgPath()\n\tkey := fqname.Construct(rlmPath, slug)\n\tregistry.Set(key, token)\n\tchain.Emit(\n\t\tregisterEvent,\n\t\t\"pkgpath\", rlmPath,\n\t\t\"slug\", slug,\n\t)\n}\n\nfunc Get(key string) *grc20.Token {\n\ttoken, ok := registry.Get(key)\n\tif !ok {\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\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, slug := fqname.Parse(key)\n\t\t\trlmLink := fqname.RenderLink(rlmPath, slug)\n\t\t\tinfoLink := \"/r/demo/grc20reg:\" + key\n\t\t\ts += ufmt.Sprintf(\"- **%s** - %s - [info](%s)\\n\", token.GetName(), rlmLink, infoLink)\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, slug := fqname.Parse(key)\n\t\trlmLink := fqname.RenderLink(rlmPath, slug)\n\t\ts := ufmt.Sprintf(\"# %s\\n\", token.GetName())\n\t\ts += ufmt.Sprintf(\"- symbol: **%s**\\n\", token.GetSymbol())\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 registerEvent = \"register\"\n\nfunc GetRegistry() *rotree.ReadOnlyTree {\n\treturn rotree.Wrap(registry, nil)\n}\n"
                      },
                      {
                        "name": "grc20reg_test.gno",
                        "body": "package grc20reg\n\nimport (\n\t\"chain/runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestRegistry(t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/foo\"))\n\trealmAddr := runtime.CurrentRealm().PkgPath()\n\ttoken, ledger := grc20.NewToken(\"TestToken\", \"TST\", 4)\n\tledger.Mint(runtime.CurrentRealm().Address(), 1234567)\n\t// register\n\tRegister(cross, token, \"\")\n\tregToken := Get(realmAddr)\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) - [info](/r/demo/grc20reg:gno.land/r/demo/foo)\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\t// register with a slug\n\tRegister(cross, token, \"mySlug\")\n\tregToken = Get(realmAddr + \".mySlug\")\n\turequire.True(t, regToken != nil, \"expected to find a token\") // fixme: use urequire.NotNil\n\turequire.Equal(t, regToken.GetSymbol(), \"TST\")\n\n\t// override\n\tRegister(cross, token, \"\")\n\tregToken = Get(realmAddr + \"\")\n\turequire.True(t, regToken != nil, \"expected to find a token\") // fixme: use urequire.NotNil\n\turequire.Equal(t, regToken.GetSymbol(), \"TST\")\n\n\tgot = Render(\"\")\n\turequire.True(t, strings.Contains(got, `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo) - [info](/r/demo/grc20reg:gno.land/r/demo/foo)`))\n\turequire.True(t, strings.Contains(got, `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo).mySlug - [info](/r/demo/grc20reg:gno.land/r/demo/foo.mySlug)`))\n\n\texpected = `# TestToken\n- symbol: **TST**\n- realm: [gno.land/r/demo/foo](/r/demo/foo).mySlug\n- decimals: 4\n- total supply: 1234567\n`\n\tgot = Render(\"gno.land/r/demo/foo.mySlug\")\n\turequire.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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "X6RsG4zLAYKaVNUWIIrDpgthbzmgylAjh7wWabZDSfkEyVAMyfUGADoAsUf9bWZf+IkIOM8l99NDwfYJpiaQ5Q=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "foo20",
                    "path": "gno.land/r/demo/defi/grc20factory",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/demo/defi/grc20factory\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "grc20factory.gno",
                        "body": "package foo20\n\nimport (\n\t\"chain/runtime\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/avl/v0\"\n\tp \"gno.land/p/nt/avl/v0/pager\"\n\t\"gno.land/p/nt/avl/v0/rotree\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/demo/defi/grc20reg\"\n)\n\nvar (\n\tinstances avl.Tree // symbol -\u003e *instance\n\tpager     = p.NewPager(rotree.Wrap(\u0026instances, nil), 20, false)\n)\n\ntype instance struct {\n\ttoken  *grc20.Token\n\tledger *grc20.PrivateLedger\n\tadmin  *ownable.Ownable\n\tfaucet int64 // per-request amount. disabled if 0.\n}\n\nfunc New(cur realm, name, symbol string, decimals int, initialMint, faucet int64) {\n\tcaller := runtime.PreviousRealm().Address()\n\tNewWithAdmin(cur, name, symbol, decimals, initialMint, faucet, caller)\n}\n\nfunc NewWithAdmin(cur realm, name, symbol string, decimals int, initialMint, faucet int64, admin address) {\n\texists := instances.Has(symbol)\n\tif exists {\n\t\tpanic(\"token already exists\")\n\t}\n\n\ttoken, ledger := grc20.NewToken(name, symbol, decimals)\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.NewWithAddressByPrevious(admin),\n\t\tfaucet: faucet,\n\t}\n\tinstances.Set(symbol, \u0026inst)\n\n\tgrc20reg.Register(cross, token, symbol)\n}\n\nfunc Bank(symbol string) *grc20.Token {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token\n}\n\nfunc TotalSupply(symbol string) int64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().TotalSupply()\n}\n\nfunc HasAddr(symbol string, owner address) bool {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.HasAddr(owner)\n}\n\nfunc BalanceOf(symbol string, owner address) int64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().BalanceOf(owner)\n}\n\nfunc Allowance(symbol string, owner, spender address) int64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().Allowance(owner, spender)\n}\n\nfunc Transfer(cur realm, symbol string, to address, amount int64) {\n\tinst := mustGetInstance(symbol)\n\tcaller := runtime.PreviousRealm().Address()\n\tteller := inst.ledger.ImpersonateTeller(caller)\n\tcheckErr(teller.Transfer(to, amount))\n}\n\nfunc Approve(cur realm, symbol string, spender address, amount int64) {\n\tinst := mustGetInstance(symbol)\n\tcaller := runtime.PreviousRealm().Address()\n\tteller := inst.ledger.ImpersonateTeller(caller)\n\tcheckErr(teller.Approve(spender, amount))\n}\n\nfunc TransferFrom(cur realm, symbol string, from, to address, amount int64) {\n\tinst := mustGetInstance(symbol)\n\tcaller := runtime.PreviousRealm().Address()\n\tteller := inst.ledger.ImpersonateTeller(caller)\n\tcheckErr(teller.TransferFrom(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 := runtime.PreviousRealm().Address()\n\tcheckErr(inst.ledger.Mint(caller, inst.faucet))\n}\n\nfunc Mint(cur realm, symbol string, to address, amount int64) {\n\tinst := mustGetInstance(symbol)\n\tinst.admin.AssertOwned()\n\tcheckErr(inst.ledger.Mint(to, amount))\n}\n\nfunc Burn(cur realm, symbol string, from address, amount int64) {\n\tinst := mustGetInstance(symbol)\n\tinst.admin.AssertOwned()\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())\n}\n\nfunc TransferInstanceOwnership(cur realm, symbol string, newOwner address) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.admin.TransferOwnership(newOwner))\n}\n\nfunc ListTokens(pageNumber, pageSize int) []*grc20.Token {\n\tpage := pager.GetPageWithSize(pageNumber, pageSize)\n\n\ttokens := make([]*grc20.Token, len(page.Items))\n\tfor i := range page.Items {\n\t\ttokens[i] = page.Items[i].Value.(*instance).token\n\t}\n\n\treturn tokens\n}\n\nfunc Render(path string) string {\n\trouter := mux.NewRouter()\n\trouter.HandleFunc(\"\", renderHome)\n\trouter.HandleFunc(\"{symbol}\", renderToken)\n\trouter.HandleFunc(\"{symbol}/balance/{address}\", renderBalance)\n\treturn router.Render(path)\n}\n\nfunc renderHome(res *mux.ResponseWriter, req *mux.Request) {\n\tout := md.H1(ufmt.Sprintf(\"GRC20 Tokens (%d)\", instances.Size()))\n\n\t// Get the current page of tokens based on the request path.\n\tpage := pager.MustGetPageByPath(req.RawPath)\n\n\t// Render the list of tokens.\n\tfor _, item := range page.Items {\n\t\ttoken := item.Value.(*instance).token\n\t\tout += md.BulletItem(\n\t\t\tmd.Link(\n\t\t\t\tufmt.Sprintf(\"%s ($%s)\", token.GetName(), token.GetSymbol()),\n\t\t\t\tufmt.Sprintf(\"/r/demo/grc20factory:%s\", token.GetSymbol()),\n\t\t\t),\n\t\t)\n\t}\n\tout += \"\\n\"\n\n\t// Add the page picker.\n\tout += md.Paragraph(page.Picker(req.Path))\n\n\tres.Write(out)\n}\n\nfunc renderToken(res *mux.ResponseWriter, req *mux.Request) {\n\t// Get the token symbol from the request.\n\tsymbol := req.GetVar(\"symbol\")\n\tinst := mustGetInstance(symbol)\n\n\t// Render the token details.\n\tout := inst.token.RenderHome()\n\tout += md.BulletItem(\n\t\tufmt.Sprintf(\"%s: %s\", md.Bold(\"Admin\"), inst.admin.Owner()),\n\t)\n\n\tres.Write(out)\n}\n\nfunc renderBalance(res *mux.ResponseWriter, req *mux.Request) {\n\tvar (\n\t\tsymbol = req.GetVar(\"symbol\")\n\t\taddr   = req.GetVar(\"address\")\n\t)\n\n\t// Get the balance of the specified address for the token.\n\tinst := mustGetInstance(symbol)\n\tbalance := inst.token.CallerTeller().BalanceOf(address(addr))\n\n\t// Render the balance information.\n\tout := md.Paragraph(\n\t\tufmt.Sprintf(\"%s balance: %d\", md.Bold(addr), balance),\n\t)\n\n\tres.Write(out)\n}\n\nfunc mustGetInstance(symbol string) *instance {\n\tt, exists := instances.Get(symbol)\n\tif !exists {\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 foo20\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 TestReadOnlyPublicMethods(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 int64\n\t\tfn      func() int64\n\t}\n\n\tcheckBalances := func(step string, totSup, balAdm, balBob, allowAdmBob, balCarl int64) {\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", totSup, func() int64 { return TotalSupply(\"FOO\") }},\n\t\t\t{\"BalanceOf(admin)\", balAdm, func() int64 { return BalanceOf(\"FOO\", admin) }},\n\t\t\t{\"BalanceOf(bob)\", balBob, func() int64 { return BalanceOf(\"FOO\", bob) }},\n\t\t\t{\"Allowance(admin, bob)\", allowAdmBob, func() int64 { return Allowance(\"FOO\", admin, bob) }},\n\t\t\t{\"BalanceOf(carl)\", balCarl, func() int64 { 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\tuassert.Equal(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, \"Foo\", \"FOO\", 3, 1_111_111_000, 5_555, admin)\n\tNewWithAdmin(cross, \"Bar\", \"BAR\", 3, 2_222_000, 6_666, admin)\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, \"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, \"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, \"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, \"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"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "hZCAtrtP8yjkDsjNCCSViUlRHwKpqCtoNFzvz6/kuPhjPseKAFZfKqedhEGuszyl5yO2bQ8ONuho5NI24xhfzg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "panictoerr",
                    "path": "gno.land/p/aeddi/panictoerr",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/aeddi/panictoerr\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "panictoerr.gno",
                        "body": "package panictoerr\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// PanicToError executes a function that might panic and, if it does,\n// recovers the panic and converts it to an error.\nfunc PanicToError(mightPanic func()) (err error) {\n\t// Catch any panic that might occur and convert it to an error.\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = anyToError(r)\n\t\t}\n\t}()\n\n\t// Execute the function that might panic.\n\tmightPanic()\n\n\treturn nil\n}\n\n// AbortToError executes a function that might abort, and if it does,\n// revives the abort and converts it to an error.\nfunc AbortToError(mightAbort func()) error {\n\t// Catch any abort that might occur and convert it to an error.\n\tif r := revive(mightAbort); r != nil {\n\t\treturn anyToError(r)\n\t}\n\n\treturn nil\n}\n\n// PanicAbortToError executes a function that might either panic or abort,\n// and if it does, it recovers the panic or revives the abort and converts\n// it to an error.\nfunc PanicAbortToError(mightPanicOrAbort func()) error {\n\tvar panicErr error\n\n\t// Catch any panic or abort that might occur and convert it to an error.\n\tif abortErr := AbortToError(func() {\n\t\tpanicErr = PanicToError(mightPanicOrAbort)\n\t}); abortErr != nil {\n\t\treturn abortErr\n\t}\n\n\treturn panicErr\n}\n\n// anyToError converts any value to an error.\nfunc anyToError(v any) error {\n\tswitch v := v.(type) {\n\tcase string:\n\t\treturn errors.New(v)\n\tcase error:\n\t\treturn v\n\tdefault:\n\t\treturn errors.New(ufmt.Sprint(v))\n\t}\n}\n"
                      },
                      {
                        "name": "panictoerr_test.gno",
                        "body": "package panictoerr_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\tpte \"gno.land/p/aeddi/panictoerr\"\n\t\"gno.land/p/nt/uassert/v0\"\n\tgrc20 \"gno.land/r/demo/defi/grc20factory\"\n)\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(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 (not crossing).\n\tpanicking := func() {\n\t\tgrc20.Bank(\"unknown\")\n\t}\n\n\t// panicking function should panic.\n\tuassert.PanicsWithMessage(t, message, panicking)\n\n\t// panicking function should panic when wrapped in AbortToError.\n\tuassert.PanicsWithMessage(t, message, func() { pte.AbortToError(panicking) })\n\n\t// panicking function should not panic when wrapped in PanicToError.\n\tuassert.NotPanics(\n\t\tt,\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,\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, \"unknown\")\n\t}\n\n\t// aborting function should abort.\n\tuassert.AbortsWithMessage(t, message, aborting)\n\n\t// aborting function should abort when wrapped in PanicToError.\n\tuassert.AbortsWithMessage(t, message, func() { pte.PanicToError(aborting) })\n\n\t// aborting function should not abort when wrapped in AbortToError.\n\tuassert.NotAborts(\n\t\tt,\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,\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "dhgT/Y0V9psf+Z7Z5ISCfAtRu5hEFe1p1AqnFp3Cz1snSgQYDhxpVfv2jN+nFLA38O5nP28QNLDoLzjz/FrCnQ=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "blog",
                    "path": "gno.land/p/demo/blog",
                    "files": [
                      {
                        "name": "blog.gno",
                        "body": "package blog\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Blog struct {\n\tTitle             string\n\tPrefix            string   // i.e. r/gnoland/blog:\n\tPosts             avl.Tree // slug -\u003e *Post\n\tPostsPublished    avl.Tree // published-date -\u003e *Post\n\tPostsAlphabetical avl.Tree // title -\u003e *Post\n\tNoBreadcrumb      bool\n}\n\nfunc (b Blog) RenderLastPostsWidget(limit int) string {\n\tif b.PostsPublished.Size() == 0 {\n\t\treturn \"No posts.\"\n\t}\n\n\toutput := \"\"\n\ti := 0\n\tb.PostsPublished.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tp := value.(*Post)\n\t\toutput += ufmt.Sprintf(\"- [%s](%s)\\n\", p.Title, p.URL())\n\t\ti++\n\t\treturn i \u003e= limit\n\t})\n\treturn output\n}\n\nfunc (b Blog) RenderHome(res *mux.ResponseWriter, _ *mux.Request) {\n\tif !b.NoBreadcrumb {\n\t\tres.Write(breadcrumb([]string{b.Title}))\n\t}\n\n\tif b.Posts.Size() == 0 {\n\t\tres.Write(\"No posts.\")\n\t\treturn\n\t}\n\n\tconst maxCol = 3\n\tvar rowItems []string\n\n\tb.PostsPublished.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tpost := value.(*Post)\n\t\trowItems = append(rowItems, post.RenderListItem())\n\n\t\tif len(rowItems) == maxCol {\n\t\t\tres.Write(\"\u003cgno-columns\u003e\" + strings.Join(rowItems, \"|||\") + \"\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|||\\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, found := b.Posts.Get(slug)\n\tif !found {\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 _, found := b.Posts.Get(slug); found {\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 _, found := b.PostsAlphabetical.Get(trimmedTitleKey); found {\n\t\t\treturn ErrPostTitleExists\n\t\t}\n\t\t// Cannot have two posts with *exact* same timestamp\n\t\tif _, found := b.PostsPublished.Get(pubDateKey); found {\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, exists := b.Posts.Get(slug)\n\tif !exists {\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, found := b.Posts.Get(slug)\n\tif !found {\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/demo/blog\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "util.gno",
                        "body": "package blog\n\nimport (\n\t\"strings\"\n\t\"time\"\n)\n\nfunc breadcrumb(parts []string) string {\n\treturn \"# \" + strings.Join(parts, \" / \") + \"\\n\\n\"\n}\n\nfunc getTitleKey(title string) string {\n\treturn strings.ReplaceAll(title, \" \", \"\")\n}\n\nfunc getPublishedKey(t time.Time) string {\n\treturn t.Format(time.RFC3339)\n}\n"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "fQyspcGu778HNw2soX6VIPxa85uSntEcWEcpHJT76Pcbf8nWfgKYZPTAvC1EtpHBInVqbeV6eisNLoJ8Dv1+tA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "svg",
                    "path": "gno.land/p/demo/svg",
                    "files": [
                      {
                        "name": "doc.gno",
                        "body": "/*\nPackage svg is a minimalist and extensible SVG generation library for Gno.\n\nIt provides a structured way to create and compose SVG elements such as rectangles, circles, text, paths, and more. The package is designed to be modular and developer-friendly, enabling optional attributes and method chaining for ease of use.\n\nEach SVG element embeds a BaseAttrs struct, which supports common SVG attributes like `id`, `class`, `style`, `fill`, `stroke`, and `transform`.\n\nCanvas objects represent the root SVG container and support global dimensions, viewBox configuration, embedded styles, and element composition.\n\nExample:\n\n\timport \"gno.land/p/demo/svg\"\n\n\tfunc Foo() string {\n\t\tcanvas := svg.NewCanvas(200, 200).WithViewBox(0, 0, 200, 200)\n\t\tcanvas.AddStyle(\".my-rect\", \"stroke:black;stroke-width:2\")\n\t\tcanvas.Append(\n\t\t\tsvg.NewRectangle(60, 40, 100, 50, \"red\").WithClass(\"my-rect\"),\n\t\t\tsvg.NewCircle(50, 80, 40, \"blue\"),\n\t\t\t\u0026svg.Path{D: `M 10,30\n\t\t\tA 20,20 0,0,1 50,30\n\t\t\t\tA 20,20 0,0,1  90,30\n\t\t\t\tQ 90,60 50,90\n\t\t\t\tQ 10,60 10,30 z`, Fill: \"magenta\"},\n\t\t\tsvg.NewText(20, 50, \"Hello SVG\", \"black\"),\n\t\t)\n\t\tmysvg := canvas.Base64()\n\t}\n*/\npackage svg // import \"gno.land/p/demo/svg\"\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/demo/svg\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "svg.gno",
                        "body": "package svg\n\nimport (\n\t\"encoding/base64\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\ntype Canvas struct {\n\tWidth, Height int\n\tViewBox       string\n\tElems         []Elem\n\tStyle         *avl.Tree\n}\n\ntype Elem interface{ String() string }\n\nfunc NewCanvas(width, height int) *Canvas {\n\treturn \u0026Canvas{\n\t\tWidth:  width,\n\t\tHeight: height,\n\t\tStyle:  nil,\n\t}\n}\n\nfunc (c *Canvas) AddStyle(key, value string) *Canvas {\n\tif c.Style == nil {\n\t\tc.Style = avl.NewTree()\n\t}\n\tc.Style.Set(key, value)\n\treturn c\n}\n\nfunc (c *Canvas) WithViewBox(x, y, width, height int) *Canvas {\n\tc.ViewBox = ufmt.Sprintf(\"%d %d %d %d\", x, y, width, height)\n\treturn c\n}\n\n// Render renders your canvas\nfunc (c Canvas) Render(alt string) string {\n\tbase64SVG := base64.StdEncoding.EncodeToString([]byte(c.String()))\n\treturn ufmt.Sprintf(\"![%s](data:image/svg+xml;base64,%s)\", alt, base64SVG)\n}\n\nfunc (c Canvas) String() string {\n\tout := \"\"\n\tout += ufmt.Sprintf(`\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\" viewBox=\"%s\"\u003e`, c.Width, c.Height, c.ViewBox)\n\tif c.Style != nil {\n\t\tout += \"\u003cstyle\u003e\"\n\t\tc.Style.Iterate(\"\", \"\", func(k string, val interface{}) bool {\n\t\t\tv := val.(string)\n\t\t\tout += ufmt.Sprintf(\"%s{%s}\", k, v)\n\t\t\treturn false\n\t\t})\n\t\tout += \"\u003c/style\u003e\"\n\t}\n\tfor _, elem := range c.Elems {\n\t\tout += elem.String()\n\t}\n\tout += \"\u003c/svg\u003e\"\n\treturn out\n}\n\nfunc (c Canvas) Base64() string {\n\tout := c.String()\n\treturn base64.StdEncoding.EncodeToString([]byte(out))\n}\n\nfunc (c *Canvas) Append(elem ...Elem) {\n\tc.Elems = append(c.Elems, elem...)\n}\n\ntype BaseAttrs struct {\n\tID          string\n\tClass       string\n\tStyle       string\n\tStroke      string\n\tStrokeWidth string\n\tOpacity     string\n\tTransform   string\n\tVisibility  string\n}\n\nfunc (b BaseAttrs) String() string {\n\tvar elems []string\n\n\tif b.ID != \"\" {\n\t\telems = append(elems, `id=\"`+b.ID+`\"`)\n\t}\n\tif b.Class != \"\" {\n\t\telems = append(elems, `class=\"`+b.Class+`\"`)\n\t}\n\tif b.Style != \"\" {\n\t\telems = append(elems, `style=\"`+b.Style+`\"`)\n\t}\n\tif b.Stroke != \"\" {\n\t\telems = append(elems, `stroke=\"`+b.Stroke+`\"`)\n\t}\n\tif b.StrokeWidth != \"\" {\n\t\telems = append(elems, `stroke-width=\"`+b.StrokeWidth+`\"`)\n\t}\n\tif b.Opacity != \"\" {\n\t\telems = append(elems, `opacity=\"`+b.Opacity+`\"`)\n\t}\n\tif b.Transform != \"\" {\n\t\telems = append(elems, `transform=\"`+b.Transform+`\"`)\n\t}\n\tif b.Visibility != \"\" {\n\t\telems = append(elems, `visibility=\"`+b.Visibility+`\"`)\n\t}\n\tif len(elems) == 0 {\n\t\treturn \"\"\n\t}\n\treturn strings.Join(elems, \" \")\n}\n\ntype Circle struct {\n\tCX   int // center X\n\tCY   int // center Y\n\tR    int // radius\n\tFill string\n\tAttr BaseAttrs\n}\n\nfunc (c Circle) String() string {\n\treturn ufmt.Sprintf(`\u003ccircle cx=\"%d\" cy=\"%d\" r=\"%d\" fill=\"%s\" %s/\u003e`, c.CX, c.CY, c.R, c.Fill, c.Attr.String())\n}\n\nfunc NewCircle(cx, cy, r int, fill string) *Circle {\n\treturn \u0026Circle{\n\t\tCX:   cx,\n\t\tCY:   cy,\n\t\tR:    r,\n\t\tFill: fill,\n\t}\n}\n\nfunc (c *Circle) WithClass(class string) *Circle {\n\tc.Attr.Class = class\n\treturn c\n}\n\ntype Ellipse struct {\n\tCX   int // center X\n\tCY   int // center Y\n\tRX   int // radius X\n\tRY   int // radius Y\n\tFill string\n\tAttr BaseAttrs\n}\n\nfunc (e Ellipse) String() string {\n\treturn ufmt.Sprintf(`\u003cellipse cx=\"%d\" cy=\"%d\" rx=\"%d\" ry=\"%d\" fill=\"%s\" %s/\u003e`, e.CX, e.CY, e.RX, e.RY, e.Fill, e.Attr.String())\n}\n\nfunc NewEllipse(cx, cy int, fill string) *Ellipse {\n\treturn \u0026Ellipse{\n\t\tCX:   cx,\n\t\tCY:   cy,\n\t\tFill: fill,\n\t}\n}\n\nfunc (e *Ellipse) WithClass(class string) *Ellipse {\n\te.Attr.Class = class\n\treturn e\n}\n\ntype Rectangle struct {\n\tX, Y, Width, Height int\n\tRX, RY              int // corner radiuses\n\tFill                string\n\tAttr                BaseAttrs\n}\n\nfunc (r Rectangle) String() string {\n\treturn ufmt.Sprintf(`\u003crect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" rx=\"%d\" ry=\"%d\" fill=\"%s\" %s/\u003e`, r.X, r.Y, r.Width, r.Height, r.RX, r.RY, r.Fill, r.Attr.String())\n}\n\nfunc NewRectangle(x, y, width, height int, fill string) *Rectangle {\n\treturn \u0026Rectangle{\n\t\tX:      x,\n\t\tY:      y,\n\t\tWidth:  width,\n\t\tHeight: height,\n\t\tFill:   fill,\n\t}\n}\n\nfunc (r *Rectangle) WithClass(class string) *Rectangle {\n\tr.Attr.Class = class\n\treturn r\n}\n\ntype Path struct {\n\tD    string\n\tFill string\n\tAttr BaseAttrs\n}\n\nfunc (p Path) String() string {\n\treturn ufmt.Sprintf(`\u003cpath d=\"%s\" fill=\"%s\" %s/\u003e`, p.D, p.Fill, p.Attr.String())\n}\n\nfunc NewPath(d, fill string) *Path {\n\treturn \u0026Path{\n\t\tD:    d,\n\t\tFill: fill,\n\t}\n}\n\nfunc (p *Path) WithClass(class string) *Path {\n\tp.Attr.Class = class\n\treturn p\n}\n\ntype Polygon struct { // closed shape\n\tPoints string\n\tFill   string\n\tAttr   BaseAttrs\n}\n\nfunc (p Polygon) String() string {\n\treturn ufmt.Sprintf(`\u003cpolygon points=\"%s\" fill=\"%s\" %s/\u003e`, p.Points, p.Fill, p.Attr.String())\n}\n\nfunc NewPolygon(points, fill string) *Polygon {\n\treturn \u0026Polygon{\n\t\tPoints: points,\n\t\tFill:   fill,\n\t}\n}\n\nfunc (p *Polygon) WithClass(class string) *Polygon {\n\tp.Attr.Class = class\n\treturn p\n}\n\ntype Polyline struct { // polygon but not necessarily closed\n\tPoints string\n\tFill   string\n\tAttr   BaseAttrs\n}\n\nfunc (p Polyline) String() string {\n\treturn ufmt.Sprintf(`\u003cpolyline points=\"%s\" fill=\"%s\" %s/\u003e`, p.Points, p.Fill, p.Attr.String())\n}\n\nfunc NewPolyline(points, fill string) *Polyline {\n\treturn \u0026Polyline{\n\t\tPoints: points,\n\t\tFill:   fill,\n\t}\n}\n\nfunc (p *Polyline) WithClass(class string) *Polyline {\n\tp.Attr.Class = class\n\treturn p\n}\n\ntype Text struct {\n\tX, Y       int\n\tDX, DY     int // shift text pos horizontally/ vertically\n\tRotate     string\n\tText, Fill string\n\tAttr       BaseAttrs\n}\n\nfunc (c Text) String() string {\n\treturn ufmt.Sprintf(`\u003ctext x=\"%d\" y=\"%d\" dx=\"%d\" dy=\"%d\" rotate=\"%s\" fill=\"%s\" %s\u003e%s\u003c/text\u003e`, c.X, c.Y, c.DX, c.DY, c.Rotate, c.Fill, c.Attr.String(), c.Text)\n}\n\nfunc NewText(x, y int, text, fill string) *Text {\n\treturn \u0026Text{\n\t\tX:    x,\n\t\tY:    y,\n\t\tText: text,\n\t\tFill: fill,\n\t}\n}\n\nfunc (c *Text) WithClass(class string) *Text {\n\tc.Attr.Class = class\n\treturn c\n}\n\ntype Group struct {\n\tElems []Elem\n\tFill  string\n\tAttr  BaseAttrs\n}\n\nfunc (g Group) String() string {\n\tout := \"\"\n\tfor _, e := range g.Elems {\n\t\tout += e.String()\n\t}\n\treturn ufmt.Sprintf(`\u003cg fill=\"%s\" %s\u003e%s\u003c/g\u003e`, g.Fill, g.Attr.String(), out)\n}\n\nfunc NewGroup(fill string) *Group {\n\treturn \u0026Group{\n\t\tFill: fill,\n\t}\n}\n\nfunc (g *Group) Append(elem ...Elem) {\n\tg.Elems = append(g.Elems, elem...)\n}\n\nfunc (g *Group) WithClass(class string) *Group {\n\tg.Attr.Class = class\n\treturn g\n}\n"
                      },
                      {
                        "name": "z0_filetest.gno",
                        "body": "// PKGPATH: gno.land/p/demo/svg_test\npackage svg_test\n\nimport \"gno.land/p/demo/svg\"\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/p/demo/svg_test\npackage svg_test\n\nimport \"gno.land/p/demo/svg\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "HJB29j1aWD7ZVY6iKytz4GGUw4/fi7UCO35A809npSsp50e90p2V13gELkO0v/8QfDnANdgB6L+6+ruTI1ngow=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "boards",
                    "path": "gno.land/p/gnoland/boards",
                    "files": [
                      {
                        "name": "board.gno",
                        "body": "package boards\n\nimport \"time\"\n\n// Board defines a type for boards.\ntype Board struct {\n\t// ID is the unique identifier of the board.\n\tID ID\n\n\t// Name is the current name of the board.\n\tName string\n\n\t// Aliases contains a list of alternative names for the board.\n\tAliases []string\n\n\t// Readonly indicates that the board is readonly.\n\tReadonly bool\n\n\t// Threads contains board threads.\n\tThreads PostStorage\n\n\t// ThreadsSequence generates sequential ID for new threads.\n\tThreadsSequence IdentifierGenerator\n\n\t// Permissions enables support for permissioned boards.\n\t// This type of boards allows managing members with roles and permissions.\n\t// It also enables the implementation of permissioned execution of board related features.\n\tPermissions Permissions\n\n\t// Creator is the account address that created the board.\n\tCreator address\n\n\t// Meta allows storing board metadata.\n\tMeta any\n\n\t// CreatedAt is the board's creation time.\n\tCreatedAt time.Time\n\n\t// UpdatedAt is the board's update time.\n\tUpdatedAt time.Time\n}\n\n// New creates a new basic non permissioned board.\nfunc New(id ID) *Board {\n\treturn \u0026Board{\n\t\tID:              id,\n\t\tThreads:         NewPostStorage(),\n\t\tThreadsSequence: NewIdentifierGenerator(),\n\t\tCreatedAt:       time.Now(),\n\t}\n}\n\n// SetID sets board ID value.\nfunc (board *Board) SetID(v ID) {\n\tboard.ID = v\n}\n\n// SetName sets name value.\nfunc (board *Board) SetName(v string) {\n\tboard.Name = v\n}\n\n// SetAliases sets board name aliases.\nfunc (board *Board) SetAliases(v []string) {\n\tboard.Aliases = v\n}\n\n// SetReadonly sets readonly value.\nfunc (board *Board) SetReadonly(v bool) {\n\tboard.Readonly = v\n}\n\n// SetThreadStorage sets the storage where board threads are stored.\nfunc (board *Board) SetThreadStorage(v PostStorage) {\n\tboard.Threads = v\n}\n\n// SetThreadsSequence sets the sequential thread ID generator.\nfunc (board *Board) SetThreadsSequence(v IdentifierGenerator) {\n\tboard.ThreadsSequence = v\n}\n\n// SetPermissions sets permissions value.\nfunc (board *Board) SetPermissions(v Permissions) {\n\tboard.Permissions = v\n}\n\n// SetCreator sets the address of the account that created the board.\nfunc (board *Board) SetCreator(v address) {\n\tboard.Creator = v\n}\n\n// SetCreatedAt sets the time when board was created.\nfunc (board *Board) SetCreatedAt(v time.Time) {\n\tboard.CreatedAt = v\n}\n\n// SetUpdatedAt sets the time when a board value was updated.\nfunc (board *Board) SetUpdatedAt(v time.Time) {\n\tboard.UpdatedAt = v\n}\n\n// SetMeta sets board metadata.\nfunc (board *Board) SetMeta(v any) {\n\tboard.Meta = v\n}\n"
                      },
                      {
                        "name": "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\"\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/avl/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{avl.NewTree()}\n}\n\ntype flagStorage struct {\n\tflags *avl.Tree // 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\"\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: \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"})\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\tflags := []boards.Flag{\n\t\t{\n\t\t\tUser:   \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\",\n\t\t\tReason: \"a\",\n\t\t},\n\t\t{\n\t\t\tUser:   \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\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\"\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\"\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": "permissions.gno",
                        "body": "package boards\n\ntype (\n\t// Permission defines the type for permissions.\n\tPermission string\n\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"
                      },
                      {
                        "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/avl/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{avl.NewTree()}\n}\n\ntype postStorage struct {\n\tposts *avl.Tree // 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, found := s.posts.Get(k)\n\tif !found {\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\"\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\"\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\"\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/avl/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{avl.NewTree()}\n}\n\ntype repostStorage struct {\n\treposts *avl.Tree // 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, found := s.reposts.Get(boardID.Key())\n\tif !found {\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\"\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/avl/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:   avl.NewTree(),\n\t\tbyName: avl.NewTree(),\n\t}\n}\n\ntype storage struct {\n\tbyID   *avl.Tree // string(Board.ID) -\u003e *Board\n\tbyName *avl.Tree // 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, found := s.byID.Get(key)\n\tif !found {\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, found := s.byName.Get(key)\n\tif !found {\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, found := s.byName.Get(key)\n\t\tif found \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\"\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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "j83/L5y65I01gqU92OJQsmGTV/Uj1zE5QzU2b+30ZRlXL9ojeNlDhFfYy+QCw9ta6SNU7wF+1JUIynZ7cgRSLw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
                  "package": {
                    "name": "addrset",
                    "path": "gno.land/p/moul/addrset",
                    "files": [
                      {
                        "name": "addrset.gno",
                        "body": "// Package addrset provides a specialized set data structure for managing unique Gno addresses.\n//\n// It is built on top of an AVL tree for efficient operations and maintains addresses in sorted order.\n// This package is particularly useful when you need to:\n//   - Track a collection of unique addresses (e.g., for whitelists, participants, etc.)\n//   - Efficiently check address membership\n//   - Support pagination when displaying addresses\n//\n// Example usage:\n//\n//\timport (\n//\t    \"std\"\n//\t    \"gno.land/p/moul/addrset\"\n//\t)\n//\n//\tfunc MyHandler() {\n//\t    // Create a new address set\n//\t    var set addrset.Set\n//\n//\t    // Add some addresses\n//\t    addr1 := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n//\t    addr2 := address(\"g1sss5g0rkqr88k4u648yd5d3l9t4d8vvqwszqth\")\n//\n//\t    set.Add(addr1)  // returns true (newly added)\n//\t    set.Add(addr2)  // returns true (newly added)\n//\t    set.Add(addr1)  // returns false (already exists)\n//\n//\t    // Check membership\n//\t    if set.Has(addr1) {\n//\t        // addr1 is in the set\n//\t    }\n//\n//\t    // Get size\n//\t    size := set.Size()  // returns 2\n//\n//\t    // Iterate with pagination (10 items per page, starting at offset 0)\n//\t    set.IterateByOffset(0, 10, func(addr address) bool {\n//\t        // Process addr\n//\t        return false  // continue iteration\n//\t    })\n//\n//\t    // Remove an address\n//\t    set.Remove(addr1)  // returns true (was present)\n//\t    set.Remove(addr1)  // returns false (not present)\n//\t}\npackage addrset\n\nimport \"gno.land/p/nt/avl/v0\"\n\ntype Set struct {\n\ttree avl.Tree\n}\n\n// Add inserts an address into the set.\n// Returns true if the address was newly added, false if it already existed.\nfunc (s *Set) Add(addr address) bool {\n\treturn !s.tree.Set(string(addr), nil)\n}\n\n// Remove deletes an address from the set.\n// Returns true if the address was found and removed, false if it didn't exist.\nfunc (s *Set) Remove(addr address) bool {\n\t_, removed := s.tree.Remove(string(addr))\n\treturn removed\n}\n\n// Has checks if an address exists in the set.\nfunc (s *Set) Has(addr address) bool {\n\treturn s.tree.Has(string(addr))\n}\n\n// Size returns the number of addresses in the set.\nfunc (s *Set) Size() int {\n\treturn s.tree.Size()\n}\n\n// IterateByOffset walks through addresses starting at the given offset.\n// The callback should return true to stop iteration.\nfunc (s *Set) IterateByOffset(offset int, count int, cb func(addr address) bool) {\n\ts.tree.IterateByOffset(offset, count, func(key string, _ any) bool {\n\t\treturn cb(address(key))\n\t})\n}\n\n// ReverseIterateByOffset walks through addresses in reverse order starting at the given offset.\n// The callback should return true to stop iteration.\nfunc (s *Set) ReverseIterateByOffset(offset int, count int, cb func(addr address) bool) {\n\ts.tree.ReverseIterateByOffset(offset, count, func(key string, _ any) bool {\n\t\treturn cb(address(key))\n\t})\n}\n\n// Tree returns the underlying AVL tree for advanced usage.\nfunc (s *Set) Tree() avl.ITree {\n\treturn \u0026s.tree\n}\n"
                      },
                      {
                        "name": "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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "CssvMkEBAyDDAKFfB9wF8mbJVdByGXm9nBKCP++ichxVvH4sK4g/JZ9Ev3l7M+orvLCNXZhxCo7zzpFOqlGLIg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "list",
                    "path": "gno.land/p/nt/avl/v0/list",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/nt/avl/v0/list\"\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/v0\"\n\t\"gno.land/p/nt/avl/v0/rotree\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// IList defines the interface for list operations\ntype IList interface {\n\tLen() int\n\tAppend(values ...any)\n\tGet(index int) any\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\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "fdF0q603HX0q6Qv7RMhP2uoYGryJL7fj1ps9DI4Z9KQMuOh9aeUVu6DU8daxLKznpyGfLEmp8vsSnWQFLHCDpA=="
                }
              ],
              "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\n# CommonDAO Package\n\nCommonDAO is a general-purpose package that provides support to implement\ncustom Decentralized Autonomous Organizations (DAO) on Gno.land.\n\nIt offers a minimal and flexible framework for building DAOs, with customizable\noptions that adapt across multiple use cases.\n\n## Core Types\n\nPackage contains some core types which are important in any DAO implementation,\nthese are **CommonDAO**, **ProposalDefinition**, **Proposal** and **Vote**.\n\n### 1. CommonDAO Type\n\nCommonDAO type is the main type used to define DAOs, allowing standalone DAO\ncreation or hierarchical tree based ones.\n\nDuring creation, it accepts many optional arguments some of which are handy\ndepending on the DAO type. For example, standalone DAOs might use IDs, a name\nand description to uniquely identify individual DAOs; Hierarchical ones might\nchoose to use slugs instead of IDs, or even a mix of both.\n\n#### DAO Creation Examples\n\nStandalone DAO:\n\n```go\nimport \"gno.land/p/nt/commondao/v0\"\n\ndao := commondao.New(\n    commondao.WithID(1),\n    commondao.WithName(\"MyDAO\"),\n    commondao.WithDescription(\"An example DAO\"),\n    commondao.WithMember(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n    commondao.WithMember(\"g1hy6zry03hg5d8le9s2w4fxme6236hkgd928dun\"),\n)\n```\n\nHierarchical DAO:\n\n```go\nimport \"gno.land/p/nt/commondao/v0\"\n\ndao := commondao.New(\n    commondao.WithSlug(\"parent\"),\n    commondao.WithName(\"ParentDAO\"),\n    commondao.WithMember(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"),\n)\n\nsubDAO := commondao.New(\n    commondao.WithSlug(\"child\"),\n    commondao.WithName(\"ChildDAO\"),\n    commondao.WithParent(dao),\n)\n```\n\n### 2. ProposalDefinition Type\n\nProposal definitions are the way proposal types are implemented in `commondao`.\nDefinitions are required when creating a new proposal because they define the\nbehavior of the proposal.\n\nGenerally speaking, proposals can be divided in two types, one are the\n*general* (a.k.a. *text proposals*), and the other are the *executable* ones.\nThe difference is that *executable* ones modify the blockchain state when they\nare executed after they have been approved, while *general* ones don't, they\nare usually used to signal or measure sentiment, for example regarding a\nrelevant issue.\n\nCreating a new proposal type requires implementing the following interface:\n\n```go\ntype ProposalDefinition interface {\n    // Title returns proposal title.\n    Title() string\n\n    // Body returns proposal's body.\n    // It usually contains description or values that are specific to\n    // the proposal, like a description of the proposal's motivation\n    // or the list of values that would be applied when the proposal\n    // is approved.\n    Body() string\n\n    // VotingPeriod returns the period where votes are allowed after\n    // proposal creation. It's used to calculate the voting deadline\n    // from the proposal's creationd date.\n    VotingPeriod() time.Duration\n\n    // Tally counts the number of votes and verifies if proposal passes.\n    // It receives a voting context containing a readonly record with the votes\n    // that has been submitted for the proposal and also the list of DAO members.\n    Tally(VotingContext) (passes bool, _ error)\n}\n```\n\nThis minimal interface is the one required for *general proposal types*. Here\nthe most important method is the `Tally()` one. It's used to check whether a\nproposal passes or not.\n\nWithin `Tally()` votes can be counted using different rules depending on the\nproposal type, some proposal types might decide if there is consensus by using\nsuper majority while others might decide using plurality for example, or even\njust counting that a minimum number of certain positive votes have been\nsubmitted to approve a proposal.\n\nCommonDAO provides a couple of helpers for this, to cover some cases:\n- `SelectChoiceByAbsoluteMajority()`\n- `SelectChoiceBySuperMajority()` (using a 2/3s threshold)\n- `SelectChoiceByPlurality()`\n\n#### 2.1. Executable Proposals\n\nProposal definitions have optional features that could be implemented to extend\nthe proposal type behaviour. One of those is required to enable execution\nsupport.\n\nA proposal can be executable implementing the **Executable** interface as part\nof the new proposal definition:\n\n```go\ntype Executable interface {\n    // Executor returns a function to execute the proposal.\n    Executor() func(realm) error\n}\n```\n\nThe crossing function returned by the `Executor()` method is where the realm\nchanges are made once the proposal is executed.\n\nOther features can be enabled by implementing the **Validable** interface and\nthe **CustomizableVoteChoices** one, as a way to separate pre-execution\nvalidation and to support proposal voting choices different than the default\nones (YES, NO and ABSTAIN).\n\n### 3. Proposal Type\n\nProposals are key for governance, they are the main mechanic that allows DAO\nmembers to engage on governance.\n\nThey are usually not created directly but though **CommonDAO** instances, by\ncalling the `CommonDAO.Propose()` or `CommonDAO.MustPropose()` methods. Though,\nalternatively, proposals could be added to CommonDAO's active proposals storage\nusing `CommonDAO.ActiveProposals().Add()`.\n\n```go\nimport (\n    \"gno.land/p/nt/commondao/v0\"\n    \"gno.land/r/example/mydao\"\n)\n\ndao := commondao.New()\ncreator := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\npropDef := mydao.NewGeneralProposalDefinition(\"Title\", \"Description\")\nproposal := dao.MustPropose(creator, propDef)\n```\n\n#### 3.1. Voting on Proposals\n\nThe preferred way to submit a vote, once a proposal is created, is by calling\nthe `CommonDAO.Vote()` method because it performs sanity checks before a vote\nis considered valid; Alternatively votes can be directly added without sanity\nchecks to the proposal's voting record by calling\n`Proposal.VotingRecord().AddVote()`.\n\n#### 3.2. Voting Record\n\nEach proposal keeps track of their submitted votes within an internal voting\nrecord. CommonDAO package defines it as a **VotingRecord** type.\n\nThe voting record of a proposal can be getted by calling its\n`Proposal.VotingRecord()` method.\n\nRight now proposals have a single voting record but the plan is to support\nmultiple voting records per proposal as an optional feature, which could be\nused in cases where a proposal must track votes in multiple independent\nrecords, for example in cases where a proposal could be promoted to a different\nDAO with a different set of members.\n\n#### 4. Vote Type\n\nVote type defines the structure to store information for individual proposal\nvotes. Apart from the normally mandatory `Address` and voting `Choice` fields,\nthere are two optional fields that can be useful in different use cases; These\nfields are `Reason` which can store a string with the reason for the vote, and\n`Context` which can be used to store generic values related to the vote, for\nexample vote weight information.\n\nIt's *very important* to be careful when using the `Context` field, in case\nreferences/pointers are assigned to it because they could potentially be\naccessed anywhere, which could lead to unwanted indirect modifications.\n\nVote type is defined as:\n\n```go\ntype Vote struct {\n    // Address is the address of the user that this vote belons to.\n    Address address\n\n    // Choice contains the voted choice.\n    Choice VoteChoice\n\n    // Reason contains an optional reason for the vote.\n    Reason string\n\n    // Context can store any custom voting values related to the vote.\n    Context any\n}\n```\n\n## Secondary Types\n\nThere are other types which can be handy for some implementations which might\nrequire to store DAO members or proposals in a custom location, or that might\nneed member grouping support.\n\n### 1. MemberStorage and ProposalStorage Types\n\nThese two types allows storing and iterating DAO members and proposals. They\nsupport DAO implementations that might require storing either members or\nproposals in an external realm other than the DAO realm.\n\nCommonDAO package provides implementations that use AVL trees under the hood\nfor storage and lookup.\n\nCustom implementations are supported though the **MemberStorage** and\n**ProposalStorage** interfaces:\n\n```go\ntype MemberStorage interface {\n\t// Size returns the number of members in the storage.\n\tSize() int\n\n\t// Has checks if a member exists in the storage.\n\tHas(address) bool\n\n\t// Add adds a member to the storage.\n\tAdd(address) bool\n\n\t// Remove removes a member from the storage.\n\tRemove(address) bool\n\n\t// Grouping returns member groups when supported.\n\tGrouping() MemberGrouping\n\n\t// IterateByOffset iterates members starting at the given offset.\n\tIterateByOffset(offset, count int, fn func(address) bool)\n}\n\ntype ProposalStorage interface {\n\t// Has checks if a proposal exists.\n\tHas(id uint64) bool\n\n\t// Get returns a proposal or nil when proposal doesn't exist.\n\tGet(id uint64) *Proposal\n\n\t// Add adds a proposal to the storage.\n\tAdd(*Proposal)\n\n\t// Remove removes a proposal from the storage.\n\tRemove(id uint64)\n\n\t// Size returns the number of proposals that the storage contains.\n\tSize() int\n\n\t// Iterate iterates proposals.\n\tIterate(offset, count int, reverse bool, fn func(*Proposal) bool) bool\n}\n```\n\n### 2. MemberGrouping and MemberGroup Types\n\nMembers grouping is an optional feature that provides support for DAO members\ngrouping.\n\nGrouping can be useful for DAOs that require grouping users by roles or tiers\nfor example.\n\nThe **MemberGrouping** type is a collection of member groups, while the\n**MemberGroup** is a group of members with metadata.\n\n#### Grouping by Role Example\n\n```go\nimport \"gno.land/p/nt/commondao/v0\"\n\nstorage := commondao.NewMemberStorageWithGrouping()\n\n// Add a member that doesn't belong to any group\nstorage.Add(\"g1...a\")\n\n// Create a member group for owners\nowners, err := storage.Grouping().Add(\"owners\")\nif err != nil {\n  panic(err)\n}\n\n// Add a member to the owners group\nowners.Members().Add(\"g1...b\")\n\n// Add voting power to owners group metadata\nowners.SetMeta(3)\n\n// Create a member group for moderators\nmoderators, err := storage.Grouping().Add(\"moderators\")\nif err != nil {\n  panic(err)\n}\n\n// Add voting power to moderators group metadata\nmoderators.SetMeta(1)\n\n// Add members to the moderators group\nmoderators.Members().Add(\"g1...c\")\nmoderators.Members().Add(\"g1...d\")\n```\n"
                      },
                      {
                        "name": "commondao.gno",
                        "body": "package commondao\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0/list\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// PathSeparator is the separator character used in DAO paths.\nconst PathSeparator = \"/\"\n\nvar (\n\tErrExecutionNotAllowed  = errors.New(\"proposal must pass before execution\")\n\tErrInvalidVoteChoice    = errors.New(\"invalid vote choice\")\n\tErrNotMember            = errors.New(\"account is not a member of the DAO\")\n\tErrOverflow             = errors.New(\"next ID overflows uint64\")\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.\ntype CommonDAO struct {\n\tid                         uint64\n\tslug                       string\n\tname                       string\n\tdescription                string\n\tparent                     *CommonDAO\n\tchildren                   list.IList\n\tmembers                    MemberStorage\n\tgenID                      seqid.ID\n\tactiveProposals            ProposalStorage\n\tfinishedProposals          ProposalStorage\n\tdeleted                    bool // Soft delete\n\tdisableVotingDeadlineCheck bool\n}\n\n// New creates a new common DAO.\nfunc New(options ...Option) *CommonDAO {\n\tdao := \u0026CommonDAO{\n\t\tchildren:          \u0026list.List{},\n\t\tmembers:           NewMemberStorage(),\n\t\tactiveProposals:   NewProposalStorage(),\n\t\tfinishedProposals: NewProposalStorage(),\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// Slug returns DAO's URL slug.\nfunc (dao CommonDAO) Slug() string {\n\treturn dao.slug\n}\n\n// Name returns DAO's name.\nfunc (dao CommonDAO) Name() string {\n\treturn dao.name\n}\n\n// Description returns DAO's description.\nfunc (dao CommonDAO) Description() string {\n\treturn dao.description\n}\n\n// Path returns the full path to the DAO.\n// Paths are normally used when working with hierarchical\n// DAOs and is created by concatenating DAO slugs.\nfunc (dao CommonDAO) Path() string {\n\t// NOTE: Path could be a value but there might be use cases where dynamic path is useful (?)\n\tparent := dao.Parent()\n\tif parent != nil {\n\t\tprefix := parent.Path()\n\t\tif prefix != \"\" {\n\t\t\treturn prefix + PathSeparator + dao.slug\n\t\t}\n\t}\n\treturn dao.slug\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// Children returns a list with the direct DAO children.\n// Each item in the list is a reference to a CommonDAO instance.\nfunc (dao CommonDAO) Children() list.IList {\n\treturn dao.children\n}\n\n// TopParent returns the topmost parent DAO.\n// The top parent is the root of the DAO tree.\nfunc (dao *CommonDAO) TopParent() *CommonDAO {\n\tparent := dao.Parent()\n\tif parent != nil {\n\t\treturn parent.TopParent()\n\t}\n\treturn dao\n}\n\n// Members returns the list of DAO members.\nfunc (dao CommonDAO) Members() MemberStorage {\n\treturn dao.members\n}\n\n// ActiveProposals returns active DAO proposals.\nfunc (dao CommonDAO) ActiveProposals() ProposalStorage {\n\treturn dao.activeProposals\n}\n\n// FinishedProposalsi returns finished DAO proposals.\nfunc (dao CommonDAO) FinishedProposals() ProposalStorage {\n\treturn dao.finishedProposals\n}\n\n// IsDeleted returns true when DAO has been soft deleted.\nfunc (dao CommonDAO) IsDeleted() bool {\n\treturn dao.deleted\n}\n\n// SetDeleted changes DAO's soft delete flag.\nfunc (dao *CommonDAO) SetDeleted(deleted bool) {\n\tdao.deleted = deleted\n}\n\n// Propose creates a new DAO proposal.\nfunc (dao *CommonDAO) Propose(creator address, d ProposalDefinition) (*Proposal, error) {\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\tdao.activeProposals.Add(p)\n\treturn p, nil\n}\n\n// MustPropose creates a new DAO proposal or panics on error.\nfunc (dao *CommonDAO) MustPropose(creator address, d ProposalDefinition) *Proposal {\n\tp, err := dao.Propose(creator, d)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn p\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 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.VotingRecord().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// By default votes are only allowed to members of the DAO when the proposal is active,\n// and within the voting period. No votes are allowed once the voting deadline passes.\n// DAO deadline checks can optionally be disabled using the `DisableVotingDeadlineCheck` option.\nfunc (dao *CommonDAO) Vote(member address, proposalID uint64, c VoteChoice, reason string) error {\n\tif !dao.Members().Has(member) {\n\t\treturn ErrNotMember\n\t}\n\n\tp := dao.activeProposals.Get(proposalID)\n\tif p == nil {\n\t\treturn ErrProposalNotFound\n\t}\n\n\tif !dao.disableVotingDeadlineCheck \u0026\u0026 p.HasVotingDeadlinePassed() {\n\t\treturn ErrVotingDeadlinePassed\n\t}\n\n\tif !p.IsVoteChoiceValid(c) {\n\t\treturn ErrInvalidVoteChoice\n\t}\n\n\tp.record.AddVote(Vote{\n\t\tAddress: member,\n\t\tChoice:  c,\n\t\tReason:  reason,\n\t})\n\treturn nil\n}\n\n// Execute executes a proposal.\n//\n// By default active proposals can only be executed after their voting deadline passes.\n// DAO deadline checks can optionally be disabled using the `DisableVotingDeadlineCheck` option.\nfunc (dao *CommonDAO) Execute(proposalID uint64) error {\n\tp := dao.activeProposals.Get(proposalID)\n\tif p == nil {\n\t\treturn ErrProposalNotFound\n\t}\n\n\t// Proposal must be active or have passed to be executed\n\tif p.status != StatusActive \u0026\u0026 p.status != StatusPassed {\n\t\treturn ErrExecutionNotAllowed\n\t}\n\n\t// Execution must be done after voting deadline\n\tif !dao.disableVotingDeadlineCheck \u0026\u0026 !p.HasVotingDeadlinePassed() {\n\t\treturn ErrVotingDeadlineNotMet\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 proposal before execution\n\terr := p.Validate()\n\n\t// Tally votes and update proposal status to \"passed\" or \"rejected\"\n\tif err == nil {\n\t\terr = p.Tally(dao.Members())\n\t\tif err == nil \u0026\u0026 p.Status() == StatusRejected {\n\t\t\t// Don't try to execute proposal if it's been rejected\n\t\t\treturn nil\n\t\t}\n\t}\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(cross)\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}\n\n\t// Whichever the outcome of the validation, tallying\n\t// and execution consider the proposal finished.\n\tdao.activeProposals.Remove(p.id)\n\tdao.finishedProposals.Add(p)\n\treturn nil\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// 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// WithSlug assigns a URL slug to the DAO.\nfunc WithSlug(slug string) Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.slug = slug\n\t}\n}\n\n// WithParent assigns a parent DAO.\nfunc WithParent(p *CommonDAO) Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.parent = p\n\t}\n}\n\n// WithChildren assigns one or more direct child SubDAOs to the DAO.\nfunc WithChildren(children ...*CommonDAO) Option {\n\treturn func(dao *CommonDAO) {\n\t\tfor _, subDAO := range children {\n\t\t\tdao.children.Append(subDAO)\n\t\t}\n\t}\n}\n\n// WithMember assigns a member to the DAO.\nfunc WithMember(addr address) Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.members.Add(addr)\n\t}\n}\n\n// WithMemberStorage assigns a custom member storage to the DAO.\n// An empty member storage is used by default if the specified storage is nil.\nfunc WithMemberStorage(s MemberStorage) Option {\n\treturn func(dao *CommonDAO) {\n\t\tif s == nil {\n\t\t\ts = NewMemberStorage()\n\t\t}\n\t\tdao.members = s\n\t}\n}\n\n// WithActiveProposalStorage assigns a custom storage for active proposals.\n// A default empty proposal storage is used when the custopm storage is nil.\n// Custom storage implementations can be used to store proposals in a different location.\nfunc WithActiveProposalStorage(s ProposalStorage) Option {\n\treturn func(dao *CommonDAO) {\n\t\tif s == nil {\n\t\t\ts = NewProposalStorage()\n\t\t}\n\t\tdao.activeProposals = s\n\t}\n}\n\n// WithFinishedProposalStorage assigns a custom storage for finished proposals.\n// A default empty proposal storage is used when the custopm storage is nil.\n// Custom storage implementations can be used to store proposals in a different location.\nfunc WithFinishedProposalStorage(s ProposalStorage) Option {\n\treturn func(dao *CommonDAO) {\n\t\tif s == nil {\n\t\t\ts = NewProposalStorage()\n\t\t}\n\t\tdao.finishedProposals = s\n\t}\n}\n\n// DisableVotingDeadlineCheck disables voting deadline check when voting or executing proposals.\n// By default CommonDAO checks that the proposal voting deadline has not been met when a new vote\n// is submitted, before registering the vote, and on proposal execution it also checks that voting\n// deadline has been met before executing a proposal.\n//\n// Disabling these checks can be useful in different use cases, moving the responsibility to check\n// the deadline to the commondao package caller. One example where this could be useful would be\n// in case where 100% or a required number of members of a DAO vote on a proposal and reach consensus\n// before the deadline is met, otherwise proposal would have to wait until deadline to be executed.\nfunc DisableVotingDeadlineCheck() Option {\n\treturn func(dao *CommonDAO) {\n\t\tdao.disableVotingDeadlineCheck = true\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\nfunc TestNew(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\tparent  *commondao.CommonDAO\n\t\tmembers []address\n\t}{\n\t\t{\n\t\t\tname:    \"with parent\",\n\t\t\tparent:  commondao.New(),\n\t\t\tmembers: []address{\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},\n\t\t},\n\t\t{\n\t\t\tname:    \"without parent\",\n\t\t\tmembers: []address{\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple members\",\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no members\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmembersCount := len(tc.members)\n\t\t\toptions := []commondao.Option{commondao.WithParent(tc.parent)}\n\t\t\tfor _, m := range tc.members {\n\t\t\t\toptions = append(options, commondao.WithMember(m))\n\t\t\t}\n\n\t\t\tdao := commondao.New(options...)\n\n\t\t\tif tc.parent == nil {\n\t\t\t\tuassert.Equal(t, nil, dao.Parent())\n\t\t\t} else {\n\t\t\t\tuassert.NotEqual(t, nil, dao.Parent())\n\t\t\t}\n\n\t\t\tuassert.False(t, dao.IsDeleted(), \"expect DAO not to be soft deleted by default\")\n\t\t\turequire.Equal(t, membersCount, dao.Members().Size(), \"dao members\")\n\n\t\t\tvar i int\n\t\t\tdao.Members().IterateByOffset(0, membersCount, func(addr address) bool {\n\t\t\t\tuassert.Equal(t, tc.members[i], addr)\n\t\t\t\ti++\n\t\t\t\treturn false\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestCommonDAOMembersAdd(t *testing.T) {\n\tmember := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tdao := commondao.New(commondao.WithMember(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\"))\n\n\tadded := dao.Members().Add(member)\n\turequire.True(t, added)\n\n\tuassert.Equal(t, 2, dao.Members().Size())\n\tuassert.True(t, dao.Members().Has(member))\n\n\tadded = dao.Members().Add(member)\n\turequire.False(t, added)\n}\n\nfunc TestCommonDAOMembersRemove(t *testing.T) {\n\tmember := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tdao := commondao.New(commondao.WithMember(member))\n\n\tremoved := dao.Members().Remove(member)\n\turequire.True(t, removed)\n\n\tremoved = dao.Members().Remove(member)\n\turequire.False(t, removed)\n}\n\nfunc TestCommonDAOMembersHas(t *testing.T) {\n\tcases := []struct {\n\t\tname   string\n\t\tmember address\n\t\tdao    *commondao.CommonDAO\n\t\twant   bool\n\t}{\n\t\t{\n\t\t\tname:   \"member\",\n\t\t\tmember: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\tdao:    commondao.New(commondao.WithMember(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")),\n\t\t\twant:   true,\n\t\t},\n\t\t{\n\t\t\tname:   \"not a dao member\",\n\t\t\tmember: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\tdao:    commondao.New(commondao.WithMember(\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\")),\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.dao.Members().Has(tc.member)\n\t\t\tuassert.Equal(t, got, tc.want)\n\t\t})\n\t}\n}\n\nfunc TestCommonDAOPropose(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\tsetup   func() *commondao.CommonDAO\n\t\tcreator address\n\t\tdef     commondao.ProposalDefinition\n\t\terr     error\n\t}{\n\t\t{\n\t\t\tname:    \"success\",\n\t\t\tsetup:   func() *commondao.CommonDAO { return commondao.New() },\n\t\t\tcreator: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\tdef:     testPropDef{},\n\t\t},\n\t\t{\n\t\t\tname:  \"nil definition\",\n\t\t\tsetup: func() *commondao.CommonDAO { return commondao.New() },\n\t\t\terr:   commondao.ErrProposalDefinitionRequired,\n\t\t},\n\t\t{\n\t\t\tname:  \"invalid creator address\",\n\t\t\tsetup: func() *commondao.CommonDAO { return commondao.New() },\n\t\t\tdef:   testPropDef{},\n\t\t\terr:   commondao.ErrInvalidCreatorAddress,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdao := tc.setup()\n\n\t\t\tp, err := dao.Propose(tc.creator, tc.def)\n\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\n\t\t\tfound := dao.ActiveProposals().Has(p.ID())\n\t\t\turequire.True(t, found, \"proposal not found\")\n\t\t\tuassert.Equal(t, p.Creator(), tc.creator)\n\t\t})\n\t}\n}\n\nfunc TestCommonDAOWithdraw(t *testing.T) {\n\tcases := []struct {\n\t\tname       string\n\t\tproposalID uint64\n\t\tsetup      func() *commondao.CommonDAO\n\t\terr        error\n\t}{\n\t\t{\n\t\t\tname:       \"success\",\n\t\t\tproposalID: 1,\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tmember := address(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")\n\t\t\t\tdao := commondao.New(commondao.WithMember(member))\n\t\t\t\tdao.Propose(member, testPropDef{votingPeriod: time.Hour})\n\t\t\t\treturn dao\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"proposal not found\",\n\t\t\tproposalID: 404,\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\treturn commondao.New(commondao.WithMember(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\"))\n\t\t\t},\n\t\t\terr: commondao.ErrProposalNotFound,\n\t\t},\n\t\t{\n\t\t\tname:       \"withdrawal not allowed\",\n\t\t\tproposalID: 1,\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tmember := address(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")\n\t\t\t\tdao := commondao.New(commondao.WithMember(member))\n\t\t\t\tp, _ := dao.Propose(member, testPropDef{votingPeriod: time.Hour})\n\t\t\t\tdao.Vote(member, p.ID(), commondao.ChoiceYes, \"\")\n\t\t\t\treturn dao\n\t\t\t},\n\t\t\terr: commondao.ErrWithdrawalNotAllowed,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdao := tc.setup()\n\n\t\t\terr := dao.Withdraw(tc.proposalID)\n\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})\n\t}\n}\n\nfunc TestCommonDAOVote(t *testing.T) {\n\tcases := []struct {\n\t\tname       string\n\t\tsetup      func() *commondao.CommonDAO\n\t\tmember     address\n\t\tchoice     commondao.VoteChoice\n\t\tproposalID uint64\n\t\terr        error\n\t}{\n\t\t{\n\t\t\tname: \"success\",\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tmember := address(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")\n\t\t\t\tdao := commondao.New(commondao.WithMember(member))\n\t\t\t\tdao.Propose(member, testPropDef{votingPeriod: time.Hour})\n\t\t\t\treturn dao\n\t\t\t},\n\t\t\tmember:     \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\tchoice:     commondao.ChoiceYes,\n\t\t\tproposalID: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"success with custom vote choice\",\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tmember := address(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")\n\t\t\t\tdao := commondao.New(commondao.WithMember(member))\n\t\t\t\tdao.Propose(member, testPropDef{\n\t\t\t\t\tvotingPeriod: time.Hour,\n\t\t\t\t\tvoteChoices:  []commondao.VoteChoice{\"FOO\", \"BAR\"},\n\t\t\t\t})\n\t\t\t\treturn dao\n\t\t\t},\n\t\t\tmember:     \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\tchoice:     commondao.VoteChoice(\"BAR\"),\n\t\t\tproposalID: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"success with deadline check disabled\",\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tmember := address(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")\n\t\t\t\tdao := commondao.New(\n\t\t\t\t\tcommondao.WithMember(member),\n\t\t\t\t\tcommondao.DisableVotingDeadlineCheck(),\n\t\t\t\t)\n\t\t\t\tdao.Propose(member, testPropDef{})\n\t\t\t\treturn dao\n\t\t\t},\n\t\t\tmember:     \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\tchoice:     commondao.ChoiceYes,\n\t\t\tproposalID: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid vote choice\",\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tmember := address(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")\n\t\t\t\tdao := commondao.New(commondao.WithMember(member))\n\t\t\t\tdao.Propose(member, testPropDef{votingPeriod: time.Hour})\n\t\t\t\treturn dao\n\t\t\t},\n\t\t\tmember:     \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\tchoice:     commondao.VoteChoice(\"invalid\"),\n\t\t\tproposalID: 1,\n\t\t\terr:        commondao.ErrInvalidVoteChoice,\n\t\t},\n\t\t{\n\t\t\tname:   \"not a member\",\n\t\t\tsetup:  func() *commondao.CommonDAO { return commondao.New() },\n\t\t\tmember: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\tchoice: commondao.ChoiceAbstain,\n\t\t\terr:    commondao.ErrNotMember,\n\t\t},\n\t\t{\n\t\t\tname: \"proposal not found\",\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\treturn commondao.New(commondao.WithMember(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\"))\n\t\t\t},\n\t\t\tmember:     \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\tchoice:     commondao.ChoiceAbstain,\n\t\t\tproposalID: 42,\n\t\t\terr:        commondao.ErrProposalNotFound,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdao := tc.setup()\n\n\t\t\terr := dao.Vote(tc.member, tc.proposalID, tc.choice, \"\")\n\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\n\t\t\tp := dao.ActiveProposals().Get(tc.proposalID)\n\t\t\turequire.NotEqual(t, nil, p, \"proposal not found\")\n\n\t\t\trecord := p.VotingRecord()\n\t\t\tuassert.True(t, record.HasVoted(tc.member))\n\t\t\tuassert.Equal(t, record.VoteCount(tc.choice), 1)\n\t\t})\n\t}\n}\n\nfunc TestCommonDAOExecute(t *testing.T) {\n\terrTest := errors.New(\"test\")\n\tmember := address(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")\n\tcases := []struct {\n\t\tname         string\n\t\tsetup        func() *commondao.CommonDAO\n\t\tproposalID   uint64\n\t\tstatus       commondao.ProposalStatus\n\t\tstatusReason string\n\t\terr          error\n\t}{\n\t\t{\n\t\t\tname: \"success\",\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tdao := commondao.New(commondao.WithMember(member))\n\t\t\t\tdao.Propose(member, testPropDef{tallyResult: true}) // Non crossing definition\n\t\t\t\treturn dao\n\t\t\t},\n\t\t\tstatus:     commondao.StatusExecuted,\n\t\t\tproposalID: 1,\n\t\t},\n\t\t{\n\t\t\tname:       \"proposal not found\",\n\t\t\tsetup:      func() *commondao.CommonDAO { return commondao.New() },\n\t\t\tproposalID: 1,\n\t\t\terr:        commondao.ErrProposalNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"execution not allowed\",\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tdao := commondao.New(commondao.WithMember(member))\n\t\t\t\tp, _ := dao.Propose(member, testPropDef{tallyResult: false})\n\t\t\t\tp.Tally(dao.Members())\n\t\t\t\treturn dao\n\t\t\t},\n\t\t\tproposalID: 1,\n\t\t\terr:        commondao.ErrExecutionNotAllowed,\n\t\t},\n\t\t{\n\t\t\tname: \"voting deadline not met\",\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tdao := commondao.New(commondao.WithMember(member))\n\t\t\t\tdao.Propose(member, testPropDef{votingPeriod: time.Minute * 5})\n\t\t\t\treturn dao\n\t\t\t},\n\t\t\tproposalID: 1,\n\t\t\terr:        commondao.ErrVotingDeadlineNotMet,\n\t\t},\n\t\t{\n\t\t\tname: \"validation error\",\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tdao := commondao.New(commondao.WithMember(member))\n\t\t\t\tdao.Propose(member, testPropDef{\n\t\t\t\t\tvalidationErr: errTest,\n\t\t\t\t\ttallyResult:   true,\n\t\t\t\t})\n\t\t\t\treturn dao\n\t\t\t},\n\t\t\tproposalID:   1,\n\t\t\tstatus:       commondao.StatusFailed,\n\t\t\tstatusReason: errTest.Error(),\n\t\t},\n\t\t{\n\t\t\tname: \"tally error\",\n\t\t\tsetup: func() *commondao.CommonDAO {\n\t\t\t\tdao := commondao.New(commondao.WithMember(member))\n\t\t\t\tdao.Propose(member, testPropDef{tallyErr: errTest})\n\t\t\t\treturn dao\n\t\t\t},\n\t\t\tproposalID:   1,\n\t\t\tstatus:       commondao.StatusFailed,\n\t\t\tstatusReason: errTest.Error(),\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdao := tc.setup()\n\n\t\t\terr := dao.Execute(tc.proposalID)\n\n\t\t\tif tc.err != nil {\n\t\t\t\turequire.Error(t, err, \"expected an error\")\n\t\t\t\turequire.ErrorIs(t, err, tc.err, \"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\tfound := dao.ActiveProposals().Has(tc.proposalID)\n\t\t\turequire.False(t, found, \"proposal should not be active\")\n\n\t\t\tp := dao.FinishedProposals().Get(tc.proposalID)\n\t\t\turequire.NotEqual(t, nil, p, \"proposal must be found\")\n\t\t\tuassert.Equal(t, string(p.Status()), string(tc.status), \"status must match\")\n\t\t\tuassert.Equal(t, string(p.StatusReason()), string(tc.statusReason), \"status reason must match\")\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 provides support to implement custom Decentralized Autonomous Organizations (DAO).\n// It aims to be minimal and flexible, allowing the implementation of multiple DAO use cases,\n// like standalone or hierarchical tree based DAOs.\npackage commondao\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/nt/commondao/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"\n"
                      },
                      {
                        "name": "member_group.gno",
                        "body": "package commondao\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\n// MemberGroup defines an interface for a group of members.\ntype MemberGroup interface {\n\t// Name returns the name of the group.\n\tName() string\n\n\t// Members returns the members that belong to the group.\n\tMembers() MemberStorage\n\n\t// SetMeta sets any metadata relevant to the group.\n\t// Metadata can be used to store data which is specific to the group.\n\t// Usually can be used to store parameter values which would be useful\n\t// during proposal voting or tallying to resolve things like voting\n\t// weights or rights for example.\n\tSetMeta(any)\n\n\t// GetMeta returns the group metadata.\n\tGetMeta() any\n}\n\n// NewMemberGroup creates a new group of members.\nfunc NewMemberGroup(name string, members MemberStorage) (MemberGroup, error) {\n\tif members == nil {\n\t\treturn nil, errors.New(\"member storage is required\")\n\t}\n\n\tname = strings.TrimSpace(name)\n\tif name == \"\" {\n\t\treturn nil, errors.New(\"member group name is required\")\n\t}\n\n\treturn \u0026memberGroup{\n\t\tname:    name,\n\t\tmembers: members,\n\t}, nil\n}\n\n// MustNewMemberGroup creates a new group of members or panics on error.\nfunc MustNewMemberGroup(name string, members MemberStorage) MemberGroup {\n\tg, err := NewMemberGroup(name, members)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn g\n}\n\ntype memberGroup struct {\n\tname    string\n\tmembers MemberStorage\n\tmeta    any\n}\n\n// Name returns the name of the group.\nfunc (g memberGroup) Name() string {\n\treturn g.name\n}\n\n// Members returns the members that belong to the group.\nfunc (g memberGroup) Members() MemberStorage {\n\treturn g.members\n}\n\n// SetMeta sets any metadata relevant to the group.\nfunc (g *memberGroup) SetMeta(meta any) {\n\tg.meta = meta\n}\n\n// GetMeta returns the group metadata.\nfunc (g memberGroup) GetMeta() any {\n\treturn g.meta\n}\n\n// NewReadonlyMemberGroup creates a new readonly member group.\nfunc NewReadonlyMemberGroup(g MemberGroup) (*ReadonlyMemberGroup, error) {\n\tif g == nil {\n\t\treturn nil, errors.New(\"member group is required\")\n\t}\n\treturn \u0026ReadonlyMemberGroup{g}, nil\n}\n\n// MustNewReadonlyMemberGroup creates a new readonly member group or panics on error.\nfunc MustNewReadonlyMemberGroup(g MemberGroup) *ReadonlyMemberGroup {\n\tgroup, err := NewReadonlyMemberGroup(g)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn group\n}\n\n// ReadonlyMemberGroup defines a readonly member group.\ntype ReadonlyMemberGroup struct {\n\tgroup MemberGroup\n}\n\n// Name returns the name of the group.\nfunc (g ReadonlyMemberGroup) Name() string {\n\tif g.group == nil {\n\t\treturn \"\"\n\t}\n\treturn g.group.Name()\n}\n\n// Members returns the members that belong to the group.\nfunc (g ReadonlyMemberGroup) Members() *ReadonlyMemberStorage {\n\tif g.group == nil {\n\t\treturn nil\n\t}\n\treturn MustNewReadonlyMemberStorage(g.group.Members())\n}\n\n// GetMeta returns the group metadata.\nfunc (g ReadonlyMemberGroup) GetMeta() any {\n\tif g.group == nil {\n\t\treturn nil\n\t}\n\treturn g.group.GetMeta()\n}\n"
                      },
                      {
                        "name": "member_group_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\nfunc TestMemberGroupNew(t *testing.T) {\n\tg, err := commondao.NewMemberGroup(\"\", nil)\n\turequire.ErrorContains(t, err, \"member storage is required\")\n\n\tstorage := commondao.NewMemberStorage()\n\tg, err = commondao.NewMemberGroup(\"\", storage)\n\turequire.ErrorContains(t, err, \"member group name is required\")\n\n\tname := \"Tier 1\"\n\tg, err = commondao.NewMemberGroup(name, storage)\n\turequire.NoError(t, err, \"expect no error\")\n\tuassert.Equal(t, name, g.Name(), \"expect group name to match\")\n\tuassert.NotNil(t, g.Members(), \"expect members to be not nil\")\n\tuassert.Nil(t, g.GetMeta(), \"expect default group meta to be nil\")\n}\n\nfunc TestMemberGroupMeta(t *testing.T) {\n\tg, err := commondao.NewMemberGroup(\"Test\", commondao.NewMemberStorage())\n\turequire.NoError(t, err, \"expect no error\")\n\n\tg.SetMeta(42)\n\tv := g.GetMeta()\n\turequire.NotEqual(t, nil, v, \"expect metadata to be not nil\")\n\n\tmeta, ok := v.(int)\n\turequire.True(t, ok, \"expect meta type to be int\")\n\tuassert.Equal(t, 42, meta, \"expect metadata to match\")\n}\n"
                      },
                      {
                        "name": "member_grouping.gno",
                        "body": "package commondao\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\ntype (\n\t// MemberGroupIterFn defines a callback to iterate DAO members groups.\n\tMemberGroupIterFn func(MemberGroup) bool\n\n\t// MemberGrouping defines an interface for storing multiple member groups.\n\t// Member grouping can be used by implementations that require grouping users\n\t// by roles or by tiers for example.\n\tMemberGrouping interface {\n\t\t// Size returns the number of groups that grouping contains.\n\t\tSize() int\n\n\t\t// Has checks if a group exists.\n\t\tHas(name string) bool\n\n\t\t// Add adds an new member group if it doesn't exists.\n\t\tAdd(name string) (MemberGroup, error)\n\n\t\t// Get returns a member group.\n\t\tGet(name string) (_ MemberGroup, found bool)\n\n\t\t// Delete deletes a member group.\n\t\tDelete(name string) error\n\n\t\t// IterateByOffset iterates all member groups.\n\t\t// The callback can return true to stop iteration.\n\t\tIterateByOffset(offset, count int, fn MemberGroupIterFn) (stopped bool)\n\t}\n)\n\n// NewMemberGrouping creates a new members grouping.\nfunc NewMemberGrouping(options ...MemberGroupingOption) MemberGrouping {\n\tg := \u0026memberGrouping{\n\t\tcreateStorage: func(string) MemberStorage { return NewMemberStorage() },\n\t}\n\n\tfor _, apply := range options {\n\t\tapply(g)\n\t}\n\treturn g\n}\n\ntype memberGrouping struct {\n\tgroups        avl.Tree // string(name) -\u003e MemberGroup\n\tcreateStorage func(group string) MemberStorage\n}\n\n// Size returns the number of groups that grouping contains.\nfunc (g memberGrouping) Size() int {\n\treturn g.groups.Size()\n}\n\n// Has checks if a group exists.\nfunc (g memberGrouping) Has(name string) bool {\n\treturn g.groups.Has(name)\n}\n\n// Add adds an new member group if it doesn't exists.\nfunc (g *memberGrouping) Add(name string) (MemberGroup, error) {\n\tif g.groups.Has(name) {\n\t\treturn nil, errors.New(\"member group already exists: \" + name)\n\t}\n\n\tmg, err := NewMemberGroup(name, g.createStorage(name))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tg.groups.Set(name, mg)\n\treturn mg, nil\n}\n\n// Get returns a member group.\nfunc (g memberGrouping) Get(name string) (_ MemberGroup, found bool) {\n\tv, found := g.groups.Get(name)\n\tif !found {\n\t\treturn nil, false\n\t}\n\treturn v.(MemberGroup), true\n}\n\n// Delete deletes a member group.\nfunc (g *memberGrouping) Delete(name string) error {\n\tg.groups.Remove(name)\n\treturn nil\n}\n\n// IterateByOffset iterates all member groups.\nfunc (g memberGrouping) IterateByOffset(offset, count int, fn MemberGroupIterFn) bool {\n\treturn g.groups.IterateByOffset(offset, count, func(_ string, v any) bool {\n\t\treturn fn(v.(MemberGroup))\n\t})\n}\n\n// NewReadonlyMemberGrouping creates a new grouping if member.\nfunc NewReadonlyMemberGrouping(g MemberGrouping) (*ReadonlyMemberGrouping, error) {\n\tif g == nil {\n\t\treturn nil, errors.New(\"member grouping is required\")\n\t}\n\treturn \u0026ReadonlyMemberGrouping{g}, nil\n}\n\n// MustNewReadonlyMemberGrouping creates a new grouping if member or panics on error.\nfunc MustNewReadonlyMemberGrouping(g MemberGrouping) *ReadonlyMemberGrouping {\n\tgrouping, err := NewReadonlyMemberGrouping(g)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn grouping\n}\n\n// ReadonlyMemberGrouping defines a type for storing multiple readonly member groups.\ntype ReadonlyMemberGrouping struct {\n\tgrouping MemberGrouping\n}\n\n// Size returns the number of groups that grouping contains.\nfunc (g ReadonlyMemberGrouping) Size() int {\n\tif g.grouping == nil {\n\t\treturn 0\n\t}\n\treturn g.grouping.Size()\n}\n\n// Has checks if a group exists.\nfunc (g ReadonlyMemberGrouping) Has(name string) bool {\n\tif g.grouping == nil {\n\t\treturn false\n\t}\n\treturn g.grouping.Has(name)\n}\n\n// Get returns a member group.\nfunc (g ReadonlyMemberGrouping) Get(name string) (_ *ReadonlyMemberGroup, found bool) {\n\tif g.grouping == nil {\n\t\treturn nil, false\n\t}\n\n\tgroup, found := g.grouping.Get(name)\n\tif !found {\n\t\treturn nil, false\n\t}\n\treturn MustNewReadonlyMemberGroup(group), true\n}\n\n// IterateByOffset iterates all member groups.\nfunc (g ReadonlyMemberGrouping) IterateByOffset(offset, count int, fn func(*ReadonlyMemberGroup) bool) bool {\n\tif g.grouping == nil {\n\t\treturn false\n\t}\n\n\treturn g.grouping.IterateByOffset(offset, count, func(group MemberGroup) bool {\n\t\treturn fn(MustNewReadonlyMemberGroup(group))\n\t})\n}\n"
                      },
                      {
                        "name": "member_grouping_options.gno",
                        "body": "package commondao\n\n// MemberGroupingOption configures member groupings.\ntype MemberGroupingOption func(MemberGrouping)\n\n// UseStorageFactory assigns a custom member storage creation function to the grouping.\n// Creation function is called each time a member group is added, with the name of the\n// group as the only argument, to create a storage where the new group stores its members.\nfunc UseStorageFactory(fn func(group string) MemberStorage) MemberGroupingOption {\n\tif fn == nil {\n\t\tpanic(\"storage factory function must not be nil\")\n\t}\n\n\treturn func(g MemberGrouping) {\n\t\tgrouping, ok := g.(*memberGrouping)\n\t\tif !ok {\n\t\t\tpanic(\"storage factory not supported by member grouping\")\n\t\t}\n\n\t\tgrouping.createStorage = fn\n\t}\n}\n\n// WithGroups creates multiple members groups.\n// To use a custom member storage factory to create the groups make sure that this option\n// comes after the `UseStorageFactory()` option, otherwise groups are created using the\n// default factory which is `commondao.NewMemberStorage()`.\nfunc WithGroups(names ...string) MemberGroupingOption {\n\treturn func(g MemberGrouping) {\n\t\tfor _, name := range names {\n\t\t\tif _, err := g.Add(name); err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}\n\t}\n}\n"
                      },
                      {
                        "name": "member_grouping_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\nfunc TestMemberGroupingAdd(t *testing.T) {\n\tt.Run(\"defauls\", func(t *testing.T) {\n\t\tname := \"Foo\"\n\t\tg := commondao.NewMemberGrouping()\n\n\t\tuassert.False(t, g.Has(name), \"expect grouping group not to be found\")\n\t\tuassert.Equal(t, 0, g.Size(), \"expect grouping to be empty\")\n\t})\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tname := \"Foo\"\n\t\tg := commondao.NewMemberGrouping()\n\t\tmg, err := g.Add(name)\n\n\t\turequire.NoError(t, err, \"expect no error\")\n\t\tuassert.True(t, g.Has(name), \"expect grouping group to be found\")\n\t\tuassert.Equal(t, 1, g.Size(), \"expect grouping to have a single group\")\n\n\t\turequire.True(t, mg != nil, \"expected grouping group to be not nil\")\n\t\tuassert.Equal(t, name, mg.Name(), \"expect group to have the right name\")\n\t})\n\n\tt.Run(\"duplicated name\", func(t *testing.T) {\n\t\tname := \"Foo\"\n\t\tg := commondao.NewMemberGrouping()\n\t\t_, err := g.Add(name)\n\t\turequire.NoError(t, err, \"expect no error\")\n\n\t\t_, err = g.Add(name)\n\t\tuassert.ErrorContains(t, err, \"member group already exists: Foo\", \"expect duplication error\")\n\t})\n}\n\nfunc TestMemberGroupingGet(t *testing.T) {\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tname := \"Foo\"\n\t\tg := commondao.NewMemberGrouping()\n\t\tg.Add(name)\n\n\t\tmg, found := g.Get(name)\n\n\t\turequire.True(t, found, \"expect grouping group to be found\")\n\t\turequire.True(t, mg != nil, \"expect grouping group to be not nil\")\n\t\tuassert.Equal(t, name, mg.Name(), \"expect group to have the right name\")\n\t})\n\n\tt.Run(\"group not found\", func(t *testing.T) {\n\t\tg := commondao.NewMemberGrouping()\n\n\t\t_, found := g.Get(\"Foo\")\n\n\t\turequire.False(t, found, \"expect grouping group to be not found\")\n\t})\n}\n\nfunc TestMemberGroupingDelete(t *testing.T) {\n\tname := \"Foo\"\n\tg := commondao.NewMemberGrouping()\n\tg.Add(name)\n\n\terr := g.Delete(name)\n\n\turequire.NoError(t, err, \"expect no error\")\n\tuassert.False(t, g.Has(name), \"expect grouping group not to be found\")\n}\n\nfunc TestMemberGroupingIterate(t *testing.T) {\n\tgroups := []string{\"Tier 1\", \"Tier 2\", \"Tier 3\"}\n\tg := commondao.NewMemberGrouping()\n\tfor _, name := range groups {\n\t\tg.Add(name)\n\t}\n\n\tvar i int\n\tg.IterateByOffset(0, g.Size(), func(mg commondao.MemberGroup) bool {\n\t\turequire.True(t, mg != nil, \"expect member group not to be nil\")\n\t\turequire.Equal(t, groups[i], mg.Name(), \"expect group to be iterated in order\")\n\n\t\ti++\n\t\treturn false\n\t})\n\n\tuassert.Equal(t, len(groups), i, \"expect all groups to be iterated\")\n}\n\nfunc TestMemberGroupingUseStorageFactory(t *testing.T) {\n\tvar (\n\t\tgroupName     string\n\t\tdefaultMember = address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t)\n\n\tfn := func(group string) commondao.MemberStorage {\n\t\tgroupName = group\n\n\t\t// Create storage and add a default member\n\t\ts := commondao.NewMemberStorage()\n\t\ts.Add(defaultMember)\n\t\treturn s\n\t}\n\n\tgrouping := commondao.NewMemberGrouping(commondao.UseStorageFactory(fn))\n\tgroup, err := grouping.Add(\"foo\")\n\n\turequire.NoError(t, err, \"expect no group creation error\")\n\tuassert.True(t, group.Members().Has(defaultMember), \"expect storage to have a default member\")\n\tuassert.Equal(t, \"foo\", groupName, \"expect group name to match\")\n}\n\nfunc TestMemberGroupingWithGroups(t *testing.T) {\n\tdefaultGroups := []string{\"foo\", \"bar\", \"baz\"}\n\tgrouping := commondao.NewMemberGrouping(commondao.WithGroups(defaultGroups...))\n\n\turequire.Equal(t, len(defaultGroups), grouping.Size(), \"expect groups to be created\")\n\tfor _, group := range defaultGroups {\n\t\tuassert.True(t, grouping.Has(group), \"expect group to be found: \"+group)\n\t}\n}\n"
                      },
                      {
                        "name": "member_storage.gno",
                        "body": "package commondao\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/moul/addrset\"\n)\n\ntype (\n\t// MemberIterFn defines a callback to iterate DAO members.\n\tMemberIterFn func(address) bool\n\n\t// MemberStorage defines an interface for member storages.\n\tMemberStorage interface {\n\t\t// Size returns the number of members in the storage.\n\t\tSize() int\n\n\t\t// Has checks if a member exists in the storage.\n\t\tHas(address) bool\n\n\t\t// Add adds a member to the storage.\n\t\t// Returns true if the member is added, or false if it already existed.\n\t\tAdd(address) bool\n\n\t\t// Remove removes a member from the storage.\n\t\t// Returns true if member was removed, or false if it was not found.\n\t\tRemove(address) bool\n\n\t\t// Grouping returns member groups when supported.\n\t\t// When nil is returned it means that grouping of members is not supported.\n\t\t// Member groups can be used by implementations that require grouping users\n\t\t// by roles or by tiers for example.\n\t\tGrouping() MemberGrouping\n\n\t\t// IterateByOffset iterates members starting at the given offset.\n\t\t// The callback can return true to stop iteration.\n\t\tIterateByOffset(offset, count int, fn MemberIterFn) (stopped bool)\n\t}\n)\n\n// NewMemberStorage creates a new member storage.\n// Function returns a new member storage that doesn't support member groups.\n// This type of storage is useful when there is no need to group members.\nfunc NewMemberStorage() MemberStorage {\n\treturn \u0026memberStorage{}\n}\n\n// NewMemberStorageWithGrouping a new member storage with support for member groups.\n// Member groups can be used by implementations that require grouping users by roles\n// or by tiers for example.\nfunc NewMemberStorageWithGrouping(options ...MemberGroupingOption) MemberStorage {\n\treturn \u0026memberStorage{grouping: NewMemberGrouping(options...)}\n}\n\ntype memberStorage struct {\n\taddrset.Set\n\n\tgrouping MemberGrouping\n}\n\n// Size returns the number of members in the storage.\n// The result is the number of members within the base underlying storage,\n// grouped members are not included, they must be counted separately.\nfunc (s memberStorage) Size() int {\n\treturn s.Set.Size()\n}\n\n// Has checks if a member exists in the storage.\n// Member is also searched within all defined member groups.\nfunc (s memberStorage) Has(member address) bool {\n\t// Check underlying member address storage\n\tif s.Set.Has(member) {\n\t\treturn true\n\t}\n\n\tif s.grouping == nil {\n\t\treturn false\n\t}\n\n\t// Check groups when member is not found in underlying storage\n\treturn s.grouping.IterateByOffset(0, s.grouping.Size(), func(g MemberGroup) bool {\n\t\treturn g.Members().Has(member)\n\t})\n}\n\n// Grouping returns member groups.\nfunc (s memberStorage) Grouping() MemberGrouping {\n\treturn s.grouping\n}\n\n// IterateByOffset iterates members starting at the given offset.\n// The callback can return true to stop iteration.\nfunc (s memberStorage) IterateByOffset(offset, count int, fn MemberIterFn) bool {\n\tvar stopped bool\n\ts.Set.IterateByOffset(offset, count, func(member address) bool {\n\t\tstopped = fn(member)\n\t\treturn stopped\n\t})\n\treturn stopped\n}\n\n// NewReadonlyMemberStorage creates a new readonly member storage.\nfunc NewReadonlyMemberStorage(s MemberStorage) (*ReadonlyMemberStorage, error) {\n\tif s == nil {\n\t\treturn nil, errors.New(\"member storage is required\")\n\t}\n\treturn \u0026ReadonlyMemberStorage{s}, nil\n}\n\n// MustNewReadonlyMemberStorage creates a new readonly member storage or panics on error.\nfunc MustNewReadonlyMemberStorage(s MemberStorage) *ReadonlyMemberStorage {\n\tstorage, err := NewReadonlyMemberStorage(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn storage\n}\n\n// ReadonlyMemberStorage defines a readonly member storage.\ntype ReadonlyMemberStorage struct {\n\tstorage MemberStorage\n}\n\n// Size returns the number of members in the storage.\nfunc (s ReadonlyMemberStorage) Size() int {\n\tif s.storage == nil {\n\t\treturn 0\n\t}\n\treturn s.storage.Size()\n}\n\n// Has checks if a member exists in the storage.\nfunc (s ReadonlyMemberStorage) Has(member address) bool {\n\tif s.storage == nil {\n\t\treturn false\n\t}\n\treturn s.storage.Has(member)\n}\n\n// Grouping returns member groups.\nfunc (s ReadonlyMemberStorage) Grouping() *ReadonlyMemberGrouping {\n\tif s.storage == nil {\n\t\treturn nil\n\t}\n\n\tif g := s.storage.Grouping(); g != nil {\n\t\treturn MustNewReadonlyMemberGrouping(g)\n\t}\n\treturn nil\n}\n\n// IterateByOffset iterates members starting at the given offset.\n// The callback can return true to stop iteration.\nfunc (s ReadonlyMemberStorage) IterateByOffset(offset, count int, fn MemberIterFn) bool {\n\tif s.storage != nil {\n\t\treturn s.storage.IterateByOffset(offset, count, fn)\n\t}\n\treturn false\n}\n\n// CountStorageMembers returns the total number of members in the storage.\n// It counts all members in each group and the ones without group.\nfunc CountStorageMembers(s *ReadonlyMemberStorage) int {\n\tif s == nil {\n\t\treturn 0\n\t}\n\n\tc := s.Size()\n\ts.Grouping().IterateByOffset(0, s.Grouping().Size(), func(g *ReadonlyMemberGroup) bool {\n\t\tc += g.Members().Size()\n\t\treturn false\n\t})\n\treturn c\n}\n"
                      },
                      {
                        "name": "member_storage_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\nfunc TestMemberStorageWithGrouping(t *testing.T) {\n\t// Prepare\n\ttiers := []struct {\n\t\tName    string\n\t\tWeight  int\n\t\tMembers []address\n\t}{\n\t\t{\n\t\t\tName:   \"Tier 1\",\n\t\t\tWeight: 3,\n\t\t\tMembers: []address{\n\t\t\t\t\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\",\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName:   \"Tier 2\",\n\t\t\tWeight: 2,\n\t\t\tMembers: []address{\n\t\t\t\t\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\t},\n\t\t},\n\t}\n\n\tstorage := commondao.NewMemberStorageWithGrouping()\n\tfor _, tier := range tiers {\n\t\tmg, err := storage.Grouping().Add(tier.Name)\n\t\turequire.NoError(t, err, \"expect no error adding tier\")\n\n\t\tmg.SetMeta(tier.Weight)\n\n\t\tfor _, addr := range tier.Members {\n\t\t\tok := mg.Members().Add(addr)\n\t\t\turequire.True(t, ok, \"expect member to be added\")\n\t\t}\n\t}\n\n\t// Assert\n\tfor i := 0; i \u003c len(tiers); i++ {\n\t\ttier := tiers[i]\n\t\tmg, found := storage.Grouping().Get(tier.Name)\n\t\turequire.True(t, found, \"expect member group to be found\")\n\n\t\tv := mg.GetMeta()\n\t\turequire.True(t, v != nil, \"expect meta to be not nil\")\n\n\t\tweight, ok := v.(int)\n\t\turequire.True(t, ok, \"expect group metadata to be an integer\")\n\t\tuassert.Equal(t, tier.Weight, weight, \"expect group weight to match\")\n\n\t\tvar i int\n\t\tmg.Members().IterateByOffset(0, len(tier.Members), func(addr address) bool {\n\t\t\tuassert.Equal(t, tier.Members[i], addr, \"expect tier member to match\")\n\n\t\t\ti++\n\t\t\treturn false\n\t\t})\n\n\t\tuassert.Equal(t, len(tier.Members), i, \"expect all tier members to be iterated\")\n\t}\n}\n\nfunc TestMemberStorageHas(t *testing.T) {\n\tcases := []struct {\n\t\tname  string\n\t\tfound bool\n\t\tsetup func() commondao.MemberStorage\n\t}{\n\t\t{\n\t\t\tname:  \"found\",\n\t\t\tfound: true,\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := commondao.NewMemberStorage()\n\t\t\t\ts.Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"not found\",\n\t\t\tfound: false,\n\t\t\tsetup: func() commondao.MemberStorage { return commondao.NewMemberStorage() },\n\t\t},\n\t\t{\n\t\t\tname:  \"found in base storage with grouping\",\n\t\t\tfound: true,\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := commondao.NewMemberStorageWithGrouping()\n\t\t\t\ts.Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\n\t\t\t\tg, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tg.Members().Add(\"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"found with grouping\",\n\t\t\tfound: true,\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := commondao.NewMemberStorageWithGrouping()\n\t\t\t\tg, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tg.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"not found with grouping\",\n\t\t\tfound: false,\n\t\t\tsetup: func() commondao.MemberStorage { return commondao.NewMemberStorageWithGrouping() },\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\tfound := storage.Has(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\n\t\t\t// Assert\n\t\t\turequire.Equal(t, tc.found, found)\n\t\t})\n\t}\n}\n\nfunc TestCountStorageMembers(t *testing.T) {\n\tstorage := commondao.NewMemberStorageWithGrouping()\n\tstorage.Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\n\n\tg, err := storage.Grouping().Add(\"A\")\n\turequire.NoError(t, err, \"expect no error creating member group A\")\n\n\tg.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tg.Members().Add(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tg, err = storage.Grouping().Add(\"B\")\n\turequire.NoError(t, err, \"expect no error creating member group B\")\n\n\tg.Members().Add(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\") // Add a member that exists in other group\n\n\ts := commondao.MustNewReadonlyMemberStorage(storage)\n\tuassert.Equal(t, 4, commondao.CountStorageMembers(s))\n}\n"
                      },
                      {
                        "name": "proposal.gno",
                        "body": "package commondao\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nconst (\n\tStatusActive    ProposalStatus = \"active\"\n\tStatusPassed                   = \"passed\"\n\tStatusRejected                 = \"rejected\"\n\tStatusExecuted                 = \"executed\"\n\tStatusFailed                   = \"failed\"\n\tStatusWithdrawn                = \"withdrawn\"\n)\n\nconst (\n\tChoiceNone       VoteChoice = \"\"\n\tChoiceYes                   = \"YES\"\n\tChoiceNo                    = \"NO\"\n\tChoiceNoWithVeto            = \"NO WITH VETO\"\n\tChoiceAbstain               = \"ABSTAIN\"\n)\n\nconst (\n\tQuorumOneThird     float64 = 0.33 // percentage, checked as \u003e= than quorum\n\tQuorumMoreThanHalf         = 0.51\n\tQuorumTwoThirds            = 0.66\n\tQuorumThreeFourths         = 0.75\n\tQuorumFull                 = 1\n)\n\n// MaxCustomVoteChoices defines the maximum number of custom\n// vote choices that a proposal definition can define.\nconst MaxCustomVoteChoices = 10\n\nvar (\n\tErrInvalidCreatorAddress      = errors.New(\"invalid proposal creator address\")\n\tErrMaxCustomVoteChoices       = errors.New(\"max number of custom vote choices exceeded\")\n\tErrProposalDefinitionRequired = errors.New(\"proposal definition is required\")\n\tErrNoQuorum                   = errors.New(\"no quorum\")\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// ExecFunc defines a type for functions that executes proposals.\n\tExecFunc func(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 // TODO: Add support for multiple voting records\n\t\tstatusReason   string\n\t\tvoteChoices    *avl.Tree // string(VoteChoice) -\u003e struct{}\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, they esentially\n\t// allow the definition for 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 creationd date.\n\t\tVotingPeriod() time.Duration\n\n\t\t// Tally counts the number of votes and verifies if proposal passes.\n\t\t// It receives a voting context containing a readonly record with the votes\n\t\t// that has been submitted for the proposal and also the list of DAO members.\n\t\tTally(VotingContext) (passes bool, _ error)\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\t// CustomizableVoteChoices defines an interface for proposal definitions that want\n\t// to customize the list of allowed voting choices.\n\tCustomizableVoteChoices interface {\n\t\t// CustomVoteChoices returns a list of valid voting choices.\n\t\t// Choices are considered valid only when there are at least two possible choices\n\t\t// otherwise proposal defaults to using YES, NO and ABSTAIN as valid choices.\n\t\tCustomVoteChoices() []VoteChoice\n\t}\n)\n\n// MustValidate validates that a proposal is valid for the current state or panics on error.\nfunc MustValidate(v Validable) {\n\tif v == nil {\n\t\tpanic(\"validable proposal definition is nil\")\n\t}\n\n\tif err := v.Validate(); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// MustExecute executes an executable proposal or panics on error.\nfunc MustExecute(e Executable) {\n\tif e == nil {\n\t\tpanic(\"executable proposal definition is nil\")\n\t}\n\n\tfn := e.Executor()\n\tif fn == nil {\n\t\treturn\n\t}\n\n\tif err := fn(cross); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// NewProposal creates a new DAO proposal.\nfunc NewProposal(id uint64, creator address, d ProposalDefinition) (*Proposal, error) {\n\tif d == nil {\n\t\treturn nil, ErrProposalDefinitionRequired\n\t}\n\n\tif !creator.IsValid() {\n\t\treturn nil, ErrInvalidCreatorAddress\n\t}\n\n\tnow := time.Now()\n\tp := \u0026Proposal{\n\t\tid:             id,\n\t\tstatus:         StatusActive,\n\t\tdefinition:     d,\n\t\tcreator:        creator,\n\t\trecord:         \u0026VotingRecord{},\n\t\tvoteChoices:    avl.NewTree(),\n\t\tvotingDeadline: now.Add(d.VotingPeriod()),\n\t\tcreatedAt:      now,\n\t}\n\n\tif v, ok := d.(CustomizableVoteChoices); ok {\n\t\tchoices := v.CustomVoteChoices()\n\t\tif len(choices) \u003e MaxCustomVoteChoices {\n\t\t\treturn nil, ErrMaxCustomVoteChoices\n\t\t}\n\n\t\tfor _, c := range choices {\n\t\t\tp.voteChoices.Set(string(c), struct{}{})\n\t\t}\n\t}\n\n\t// Use default voting choices when the definition returns none or a single vote choice\n\tif p.voteChoices.Size() \u003c 2 {\n\t\tp.voteChoices.Set(string(ChoiceYes), struct{}{})\n\t\tp.voteChoices.Set(string(ChoiceNo), struct{}{})\n\t\tp.voteChoices.Set(string(ChoiceAbstain), struct{}{})\n\t}\n\treturn p, nil\n}\n\n// ID returns the unique proposal identifies.\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 record that contains all the votes submitted for the proposal.\nfunc (p Proposal) VotingRecord() *VotingRecord {\n\treturn p.record\n}\n\n// StatusReason returns an optional reason that lead to the current proposal status.\n// Reason is mostyl 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// VoteChoices returns the list of vote choices allowed for the proposal.\nfunc (p Proposal) VoteChoices() []VoteChoice {\n\tchoices := make([]VoteChoice, 0, p.voteChoices.Size())\n\tp.voteChoices.Iterate(\"\", \"\", func(c string, _ any) bool {\n\t\tchoices = append(choices, VoteChoice(c))\n\t\treturn false\n\t})\n\treturn choices\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 proposal status is active and when the definition supports validation.\nfunc (p Proposal) Validate() error {\n\tif p.status != StatusActive {\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// IsVoteChoiceValid checks if a vote choice is valid for the proposal.\nfunc (p Proposal) IsVoteChoiceValid(c VoteChoice) bool {\n\treturn p.voteChoices.Has(string(c))\n}\n\n// Tally counts votes and updates proposal status with the current outcome.\n// Proposal status is updated to \"passed\" when proposal is approved\n// or to \"rejected\" if proposal doesn't pass.\nfunc (p *Proposal) Tally(members MemberStorage) error {\n\tif p.status != StatusActive {\n\t\treturn ErrStatusIsNotActive\n\t}\n\n\tctx := MustNewVotingContext(p.VotingRecord(), members)\n\tpasses, err := p.Definition().Tally(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif passes {\n\t\tp.status = StatusPassed\n\t} else {\n\t\tp.status = StatusRejected\n\t}\n\treturn nil\n}\n\n// IsQuorumReached checks if a participation quorum is reach.\nfunc IsQuorumReached(quorum float64, r ReadonlyVotingRecord, members ReadonlyMemberStorage) bool {\n\tif members.Size() \u003c= 0 || quorum \u003c= 0 {\n\t\treturn false\n\t}\n\n\tvar totalCount int\n\tr.IterateVotesCount(func(c VoteChoice, voteCount int) bool {\n\t\t// Don't count explicit abstentions or invalid votes\n\t\tif c != ChoiceNone \u0026\u0026 c != ChoiceAbstain {\n\t\t\ttotalCount += r.VoteCount(c)\n\t\t}\n\t\treturn false\n\t})\n\n\tpercentage := float64(totalCount) / float64(members.Size())\n\treturn percentage \u003e= quorum\n}\n"
                      },
                      {
                        "name": "proposal_storage.gno",
                        "body": "package commondao\n\nimport (\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n)\n\n// ProposalStorage defines an interface for proposal storages.\ntype ProposalStorage interface {\n\t// Has checks if a proposal exists.\n\tHas(id uint64) bool\n\n\t// Get returns a proposal or nil when proposal doesn't exist.\n\tGet(id uint64) *Proposal\n\n\t// Add adds a proposal to the storage.\n\tAdd(*Proposal)\n\n\t// Remove removes a proposal from the storage.\n\tRemove(id uint64)\n\n\t// Size returns the number of proposals that the storage contains.\n\tSize() int\n\n\t// Iterate iterates proposals.\n\tIterate(offset, count int, reverse bool, fn func(*Proposal) bool) bool\n}\n\n// NewProposalStorage creates a new proposal storage.\nfunc NewProposalStorage() ProposalStorage {\n\treturn \u0026proposalStorage{avl.NewTree()}\n}\n\ntype proposalStorage struct {\n\tstorage *avl.Tree // string(proposal ID) -\u003e *Proposal\n}\n\n// Has checks if a proposal exists.\nfunc (s proposalStorage) Has(id uint64) bool {\n\treturn s.storage.Has(makeProposalKey(id))\n}\n\n// Get returns a proposal or nil when proposal doesn't exist.\nfunc (s proposalStorage) Get(id uint64) *Proposal {\n\tif v, found := s.storage.Get(makeProposalKey(id)); found {\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\tif fn == nil {\n\t\treturn false\n\t}\n\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_storage_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\nfunc TestProposalStorageAdd(t *testing.T) {\n\tp, _ := commondao.NewProposal(1, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", testPropDef{})\n\ts := commondao.NewProposalStorage()\n\tinitialSize := s.Size()\n\n\ts.Add(p)\n\n\tuassert.Equal(t, 0, initialSize, \"expect initial storage to be empty\")\n\tuassert.Equal(t, 1, s.Size(), \"expect storage to have one proposal\")\n\tuassert.True(t, s.Has(p.ID()), \"expect proposal to be found\")\n}\n\nfunc TestProposalStorageGet(t *testing.T) {\n\tp, _ := commondao.NewProposal(1, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", testPropDef{})\n\ts := commondao.NewProposalStorage()\n\ts.Add(p)\n\n\tp2 := s.Get(p.ID())\n\n\turequire.NotEqual(t, nil, p2, \"expect proposal to be found\")\n\tuassert.Equal(t, p.ID(), p2.ID(), \"expect proposal ID to match\")\n}\n\nfunc TestProposalStorageRemove(t *testing.T) {\n\tp, _ := commondao.NewProposal(1, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", testPropDef{})\n\ts := commondao.NewProposalStorage()\n\ts.Add(p)\n\n\ts.Remove(p.ID())\n\n\tuassert.Equal(t, 0, s.Size(), \"expect storage to be empty\")\n\tuassert.False(t, s.Has(p.ID()), \"expect proposal to be not found\")\n}\n\nfunc TestProposalStorageIterate(t *testing.T) {\n\tvar (\n\t\ti   int\n\t\tids = []uint64{22, 33, 44}\n\t\ts   = commondao.NewProposalStorage()\n\t)\n\n\tfor _, id := range ids {\n\t\tp, _ := commondao.NewProposal(id, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", testPropDef{})\n\t\ts.Add(p)\n\t}\n\n\ts.Iterate(0, s.Size(), false, func(p *commondao.Proposal) bool {\n\t\tuassert.Equal(t, ids[i], p.ID(), \"expect proposal ID to match\")\n\n\t\ti++\n\t\treturn i == s.Size()\n\t})\n\n\tuassert.Equal(t, len(ids), i, \"expect storage to iterate all proposals\")\n\n\ti = s.Size() - 1\n\ts.Iterate(0, s.Size(), true, func(p *commondao.Proposal) bool {\n\t\tuassert.Equal(t, ids[i], p.ID(), \"expect proposal ID to match\")\n\n\t\ti--\n\t\treturn i == -1\n\t})\n\n\tuassert.Equal(t, -1, i, \"expect storage to iterate all proposals in reverse order\")\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\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nfunc TestProposalNew(t *testing.T) {\n\tcases := []struct {\n\t\tname       string\n\t\tcreator    address\n\t\tdefinition commondao.ProposalDefinition\n\t\terr        error\n\t}{\n\t\t{\n\t\t\tname:       \"success\",\n\t\t\tcreator:    \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tdefinition: testPropDef{votingPeriod: time.Minute * 10},\n\t\t},\n\t\t{\n\t\t\tname:       \"invalid creator address\",\n\t\t\tcreator:    \"invalid\",\n\t\t\tdefinition: testPropDef{},\n\t\t\terr:        commondao.ErrInvalidCreatorAddress,\n\t\t},\n\t\t{\n\t\t\tname:    \"max custom vote choices exceeded\",\n\t\t\tcreator: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tdefinition: testPropDef{\n\t\t\t\tvoteChoices: make([]commondao.VoteChoice, commondao.MaxCustomVoteChoices+1),\n\t\t\t},\n\t\t\terr: commondao.ErrMaxCustomVoteChoices,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tid := uint64(1)\n\n\t\t\tp, err := commondao.NewProposal(id, tc.creator, tc.definition)\n\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, \"unexpected error\")\n\t\t\tuassert.Equal(t, p.ID(), id)\n\t\t\tuassert.NotEqual(t, p.Definition(), nil)\n\t\t\tuassert.True(t, p.Status() == commondao.StatusActive)\n\t\t\tuassert.Equal(t, p.Creator(), tc.creator)\n\t\t\tuassert.False(t, p.CreatedAt().IsZero())\n\t\t\tuassert.NotEqual(t, p.VotingRecord(), nil)\n\t\t\tuassert.Empty(t, p.StatusReason())\n\t\t\tuassert.True(t, p.VotingDeadline() == p.CreatedAt().Add(tc.definition.VotingPeriod()))\n\t\t})\n\t}\n}\n\nfunc TestProposalVoteChoices(t *testing.T) {\n\tcases := []struct {\n\t\tname       string\n\t\tdefinition commondao.ProposalDefinition\n\t\tchoices    []commondao.VoteChoice\n\t}{\n\t\t{\n\t\t\tname:       \"custom choices\",\n\t\t\tdefinition: testPropDef{voteChoices: []commondao.VoteChoice{\"FOO\", \"BAR\", \"BAZ\"}},\n\t\t\tchoices: []commondao.VoteChoice{\n\t\t\t\t\"BAR\",\n\t\t\t\t\"BAZ\",\n\t\t\t\t\"FOO\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"defaults because of empty custom choice list\",\n\t\t\tdefinition: testPropDef{voteChoices: []commondao.VoteChoice{}},\n\t\t\tchoices: []commondao.VoteChoice{\n\t\t\t\tcommondao.ChoiceAbstain,\n\t\t\t\tcommondao.ChoiceNo,\n\t\t\t\tcommondao.ChoiceYes,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"defaults because of single custom choice list\",\n\t\t\tdefinition: testPropDef{voteChoices: []commondao.VoteChoice{\"FOO\"}},\n\t\t\tchoices: []commondao.VoteChoice{\n\t\t\t\tcommondao.ChoiceAbstain,\n\t\t\t\tcommondao.ChoiceNo,\n\t\t\t\tcommondao.ChoiceYes,\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\tp, _ := commondao.NewProposal(1, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", testPropDef{\n\t\t\t\tvoteChoices: tc.choices,\n\t\t\t})\n\n\t\t\tchoices := p.VoteChoices()\n\n\t\t\turequire.Equal(t, len(choices), len(tc.choices), \"expect vote choice count to match\")\n\t\t\tfor i, c := range choices {\n\t\t\t\turequire.True(t, tc.choices[i] == c, \"expect vote choice to match\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsQuorumReached(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\tquorum  float64\n\t\tmembers []address\n\t\tvotes   []commondao.Vote\n\t\tfail    bool\n\t}{\n\t\t{\n\t\t\tname:   \"one third\",\n\t\t\tquorum: commondao.QuorumOneThird,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"one third no quorum\",\n\t\t\tquorum: commondao.QuorumOneThird,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t},\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"simple majority\",\n\t\t\tquorum: commondao.QuorumMoreThanHalf,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t\t\"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"simple majority no quorum\",\n\t\t\tquorum: commondao.QuorumMoreThanHalf,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t\t\"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t},\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"two thirds\",\n\t\t\tquorum: commondao.QuorumTwoThirds,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"two thirds no quorum\",\n\t\t\tquorum: commondao.QuorumTwoThirds,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t},\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"three fourths\",\n\t\t\tquorum: commondao.QuorumThreeFourths,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t\t\"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"three fourths no quorum\",\n\t\t\tquorum: commondao.QuorumThreeFourths,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\",\n\t\t\t\t\"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t},\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"full\",\n\t\t\tquorum: commondao.QuorumFull,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"full no quorum\",\n\t\t\tquorum: commondao.QuorumFull,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t},\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"no quorum with empty vote\",\n\t\t\tquorum: commondao.QuorumMoreThanHalf,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\tChoice:  commondao.ChoiceNone,\n\t\t\t\t},\n\t\t\t},\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"no quorum with abstention\",\n\t\t\tquorum: commondao.QuorumMoreThanHalf,\n\t\t\tmembers: []address{\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\",\n\t\t\t\t\tChoice:  commondao.ChoiceAbstain,\n\t\t\t\t},\n\t\t\t},\n\t\t\tfail: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"invalid quorum percentage\",\n\t\t\tquorum: -1,\n\t\t\tfail:   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\tmembers := commondao.NewMemberStorage()\n\t\t\tstorage := commondao.MustNewReadonlyMemberStorage(members)\n\t\t\tfor _, m := range tc.members {\n\t\t\t\tmembers.Add(m)\n\t\t\t}\n\n\t\t\tvar record commondao.VotingRecord\n\t\t\tfor _, v := range tc.votes {\n\t\t\t\trecord.AddVote(v)\n\t\t\t}\n\n\t\t\tsuccess := commondao.IsQuorumReached(tc.quorum, record.Readonly(), *storage)\n\n\t\t\tif tc.fail {\n\t\t\t\tuassert.False(t, success, \"expect quorum to fail\")\n\t\t\t} else {\n\t\t\t\tuassert.True(t, success, \"expect quorum to succeed\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestProposalTally(t *testing.T) {\n\terrTest := errors.New(\"test\")\n\tcreator := address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tcases := []struct {\n\t\tname   string\n\t\tsetup  func() *commondao.Proposal\n\t\tstatus commondao.ProposalStatus\n\t\terr    error\n\t}{\n\t\t{\n\t\t\tname: \"passed\",\n\t\t\tsetup: func() *commondao.Proposal {\n\t\t\t\tp, _ := commondao.NewProposal(1, creator, testPropDef{tallyResult: true})\n\t\t\t\treturn p\n\t\t\t},\n\t\t\tstatus: commondao.StatusPassed,\n\t\t},\n\t\t{\n\t\t\tname: \"rejected\",\n\t\t\tsetup: func() *commondao.Proposal {\n\t\t\t\tp, _ := commondao.NewProposal(1, creator, testPropDef{tallyResult: false})\n\t\t\t\treturn p\n\t\t\t},\n\t\t\tstatus: commondao.StatusRejected,\n\t\t},\n\t\t{\n\t\t\tname: \"proposal is not active\",\n\t\t\tsetup: func() *commondao.Proposal {\n\t\t\t\tp, _ := commondao.NewProposal(1, creator, testPropDef{tallyResult: true})\n\t\t\t\tp.Tally(commondao.NewMemberStorage())\n\t\t\t\treturn p\n\t\t\t},\n\t\t\terr: commondao.ErrStatusIsNotActive,\n\t\t},\n\t\t{\n\t\t\tname: \"tally error\",\n\t\t\tsetup: func() *commondao.Proposal {\n\t\t\t\tp, _ := commondao.NewProposal(1, creator, testPropDef{tallyErr: errTest})\n\t\t\t\treturn p\n\t\t\t},\n\t\t\terr: errTest,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tp := tc.setup()\n\t\t\tmembers := commondao.NewMemberStorage()\n\n\t\t\terr := p.Tally(members)\n\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\turequire.Equal(t, string(tc.status), string(p.Status()))\n\t\t})\n\t}\n}\n\nfunc TestMustValidate(t *testing.T) {\n\tuassert.NotPanics(t, func() {\n\t\tcommondao.MustValidate(testPropDef{})\n\t}, \"expect validation to succeed\")\n\n\tuassert.PanicsWithMessage(t, \"validable proposal definition is nil\", func() {\n\t\tcommondao.MustValidate(nil)\n\t}, \"expect validation to panic with nil definition\")\n\n\tuassert.PanicsWithMessage(t, \"boom!\", func() {\n\t\tcommondao.MustValidate(testPropDef{validationErr: errors.New(\"boom!\")})\n\t}, \"expect validation to panic\")\n}\n\n// Executable non crossing proposal definition for unit tests\ntype testPropDef struct {\n\tvotingPeriod            time.Duration\n\ttallyResult             bool\n\tvalidationErr, tallyErr error\n\tvoteChoices             []commondao.VoteChoice\n}\n\nfunc (testPropDef) Title() string                 { return \"\" }\nfunc (testPropDef) Body() string                  { return \"\" }\nfunc (d testPropDef) VotingPeriod() time.Duration { return d.votingPeriod }\nfunc (d testPropDef) Validate() error             { return d.validationErr }\n\nfunc (d testPropDef) Tally(commondao.VotingContext) (bool, error) {\n\treturn d.tallyResult, d.tallyErr\n}\n\nfunc (d testPropDef) CustomVoteChoices() []commondao.VoteChoice {\n\tif len(d.voteChoices) \u003e 0 {\n\t\treturn d.voteChoices\n\t}\n\treturn []commondao.VoteChoice{commondao.ChoiceYes, commondao.ChoiceNo, commondao.ChoiceAbstain}\n}\n"
                      },
                      {
                        "name": "record.gno",
                        "body": "package commondao\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// ErrVoteExists indicates that a user already voted.\nvar ErrVoteExists = errors.New(\"user already voted\")\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.\n\tVote struct {\n\t\t// Address is the address of the user that this vote belons to.\n\t\tAddress address\n\n\t\t// Choice contains the voted choice.\n\t\tChoice VoteChoice\n\n\t\t// Reason contains an optional reason for the vote.\n\t\tReason string\n\n\t\t// Context can store any custom voting values related to the vote.\n\t\t//\n\t\t// Warning: When using context be careful if references/pointers are\n\t\t// assigned to it because they could potentially be accessed anywhere,\n\t\t// which could lead to unwanted indirect modifications.\n\t\tContext any\n\t}\n)\n\n// ReadonlyVotingRecord defines an read only voting record.\ntype ReadonlyVotingRecord struct {\n\tvotes avl.Tree // string(address) -\u003e Vote\n\tcount avl.Tree // 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 submited 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, found := r.count.Get(string(c)); found {\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, found := r.votes.Get(user.String()); found {\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.Address.String())\n\n\t// When a previous vote exists update counter for the previous choice\n\tupdated = r.votes.Set(vote.Address.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// FindMostVotedChoice returns the most voted choice.\n// ChoiceNone is returned when there is a tie between different\n// voting choices or when the voting record has are no votes.\nfunc FindMostVotedChoice(r ReadonlyVotingRecord) VoteChoice {\n\tvar (\n\t\tchoice                  VoteChoice\n\t\tcurrentCount, prevCount int\n\t)\n\n\tr.IterateVotesCount(func(c VoteChoice, count int) bool {\n\t\tif currentCount \u003c= count {\n\t\t\tchoice = c\n\t\t\tprevCount = currentCount\n\t\t\tcurrentCount = count\n\t\t}\n\t\treturn false\n\t})\n\n\tif prevCount \u003c currentCount {\n\t\treturn choice\n\t}\n\treturn ChoiceNone\n}\n\n// SelectChoiceByAbsoluteMajority select the vote choice by absolute majority.\n// Vote choice is a majority when chosen by more than half of the votes.\n// Absolute majority considers abstentions when counting votes.\nfunc SelectChoiceByAbsoluteMajority(r ReadonlyVotingRecord, membersCount int) (VoteChoice, bool) {\n\tchoice := FindMostVotedChoice(r)\n\tif choice != ChoiceNone \u0026\u0026 r.VoteCount(choice) \u003e int(membersCount/2) {\n\t\treturn choice, true\n\t}\n\treturn ChoiceNone, false\n}\n\n// SelectChoiceBySuperMajority select the vote choice by super majority using a 2/3s threshold.\n// Abstentions are considered when calculating the super majority choice.\nfunc SelectChoiceBySuperMajority(r ReadonlyVotingRecord, membersCount int) (VoteChoice, bool) {\n\tif membersCount \u003c 3 {\n\t\treturn ChoiceNone, false\n\t}\n\n\tchoice := FindMostVotedChoice(r)\n\tif choice != ChoiceNone \u0026\u0026 r.VoteCount(choice) \u003e= int(math.Ceil((2*float64(membersCount))/3)) {\n\t\treturn choice, true\n\t}\n\treturn ChoiceNone, false\n}\n\n// SelectChoiceByPlurality selects the vote choice by plurality.\n// The choice will be considered a majority if it has votes and if there is no other\n// choice with the same number of votes. A tie won't be considered majority.\nfunc SelectChoiceByPlurality(r ReadonlyVotingRecord) (VoteChoice, bool) {\n\tvar (\n\t\tchoice       VoteChoice\n\t\tcurrentCount int\n\t\tisMajority   bool\n\t)\n\n\tr.IterateVotesCount(func(c VoteChoice, count int) bool {\n\t\t// Don't consider explicit abstentions or invalid votes\n\t\tif c == ChoiceAbstain || c == ChoiceNone {\n\t\t\treturn false\n\t\t}\n\n\t\tif currentCount \u003c count {\n\t\t\tchoice = c\n\t\t\tcurrentCount = count\n\t\t\tisMajority = true\n\t\t} else if currentCount == count {\n\t\t\tisMajority = false\n\t\t}\n\t\treturn false\n\t})\n\n\tif isMajority {\n\t\treturn choice, true\n\t}\n\treturn ChoiceNone, false\n}\n\n// CollectVotes returns an voting record containing votes of members from one or more groups.\n// Returned tree uses member account address as key and `commondao.Vote` as value.\nfunc CollectVotes(ctx VotingContext, groups ...string) (*VotingRecord, error) {\n\tif len(groups) == 0 {\n\t\treturn nil, errors.New(\"one or more group names are required to collect votes\")\n\t}\n\n\tvar (\n\t\tvotes    VotingRecord\n\t\tgrouping = ctx.Members.Grouping()\n\t)\n\n\tfor _, name := range groups {\n\t\tgroup, found := grouping.Get(name)\n\t\tif !found {\n\t\t\treturn nil, errors.New(\"member group not found: \" + name)\n\t\t}\n\n\t\tgroup.Members().IterateByOffset(0, group.Members().Size(), func(member address) bool {\n\t\t\tv, found := ctx.VotingRecord.GetVote(member)\n\t\t\tif found {\n\t\t\t\tvotes.AddVote(v)\n\t\t\t}\n\t\t\treturn false\n\t\t})\n\t}\n\treturn \u0026votes, nil\n}\n"
                      },
                      {
                        "name": "record_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\nfunc TestVotingRecordDefaults(t *testing.T) {\n\tvar (\n\t\trecord commondao.VotingRecord\n\t\tuser   address = \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n\t)\n\n\tuassert.Equal(t, record.Size(), 0)\n\tuassert.Equal(t, record.VoteCount(commondao.ChoiceYes), 0)\n\tuassert.Equal(t, record.VoteCount(commondao.ChoiceNo), 0)\n\tuassert.Equal(t, record.VoteCount(commondao.ChoiceAbstain), 0)\n\tuassert.False(t, record.HasVoted(user))\n}\n\nfunc TestVotingRecordAddVote(t *testing.T) {\n\tcases := []struct {\n\t\tname                            string\n\t\tsetup                           func(*commondao.VotingRecord)\n\t\tvotes                           []commondao.Vote\n\t\tyesCount, noCount, abstainCount int\n\t\tupdated                         bool\n\t}{\n\t\t{\n\t\t\tname: \"single vote\",\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t},\n\t\t\tyesCount: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple votes\",\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\t\t\tChoice:  commondao.ChoiceAbstain,\n\t\t\t\t},\n\t\t\t},\n\t\t\tyesCount:     1,\n\t\t\tnoCount:      2,\n\t\t\tabstainCount: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"vote exists\",\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t},\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceAbstain,\n\t\t\t\t})\n\t\t\t},\n\t\t\tyesCount:     1,\n\t\t\tabstainCount: 0,\n\t\t\tupdated:      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\tvar (\n\t\t\t\trecord  commondao.VotingRecord\n\t\t\t\tupdated bool\n\t\t\t)\n\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026record)\n\t\t\t}\n\n\t\t\tfor _, v := range tc.votes {\n\t\t\t\tupdated = updated || record.AddVote(v)\n\t\t\t}\n\n\t\t\turequire.Equal(t, updated, tc.updated, \"expect vote to be updated\")\n\t\t\turequire.Equal(t, record.Size(), len(tc.votes), \"expect record size to match\")\n\n\t\t\tvar i int\n\t\t\trecord.Iterate(0, record.Size(), false, func(v commondao.Vote) bool {\n\t\t\t\tuassert.Equal(t, v.Address, tc.votes[i].Address)\n\t\t\t\tuassert.Equal(t, string(v.Choice), string(tc.votes[i].Choice))\n\t\t\t\tuassert.True(t, record.HasVoted(v.Address))\n\n\t\t\t\ti++\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tuassert.Equal(t, record.VoteCount(commondao.ChoiceYes), tc.yesCount, \"expect YES vote count to match\")\n\t\t\tuassert.Equal(t, record.VoteCount(commondao.ChoiceNo), tc.noCount, \"expect NO vote count to match\")\n\t\t\tuassert.Equal(t, record.VoteCount(commondao.ChoiceAbstain), tc.abstainCount, \"expect ABSTAIN vote count to match\")\n\t\t})\n\t}\n}\n\nfunc TestFindMostVotedChoice(t *testing.T) {\n\tcases := []struct {\n\t\tname   string\n\t\tsetup  func(*commondao.VotingRecord)\n\t\tchoice commondao.VoteChoice\n\t}{\n\t\t{\n\t\t\tname:   \"no votes\",\n\t\t\tchoice: commondao.ChoiceNone,\n\t\t},\n\t\t{\n\t\t\tname: \"one vote\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice: commondao.ChoiceYes,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple votes\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice: commondao.ChoiceNo,\n\t\t},\n\t\t{\n\t\t\tname: \"tie\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice: commondao.ChoiceNone,\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 record commondao.VotingRecord\n\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026record)\n\t\t\t}\n\n\t\t\tchoice := commondao.FindMostVotedChoice(record.Readonly())\n\n\t\t\tuassert.Equal(t, string(choice), string(tc.choice))\n\t\t})\n\t}\n}\n\nfunc TestSelectChoiceByAbsoluteMajority(t *testing.T) {\n\tcases := []struct {\n\t\tname         string\n\t\tsetup        func(*commondao.VotingRecord)\n\t\tchoice       commondao.VoteChoice\n\t\tmembersCount int\n\t\tsuccess      bool\n\t}{\n\t\t{\n\t\t\tname:         \"no votes\",\n\t\t\tchoice:       commondao.ChoiceNone,\n\t\t\tmembersCount: 3,\n\t\t\tsuccess:      false,\n\t\t},\n\t\t{\n\t\t\tname: \"majority\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice:       commondao.ChoiceYes,\n\t\t\tmembersCount: 3,\n\t\t\tsuccess:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"no majority\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice:       \"\",\n\t\t\tmembersCount: 3,\n\t\t\tsuccess:      false,\n\t\t},\n\t\t{\n\t\t\tname: \"majority with abstain vote\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\t\t\tChoice:  commondao.ChoiceAbstain,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice:       commondao.ChoiceYes,\n\t\t\tmembersCount: 3,\n\t\t\tsuccess:      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\tvar record commondao.VotingRecord\n\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026record)\n\t\t\t}\n\n\t\t\tchoice, success := commondao.SelectChoiceByAbsoluteMajority(record.Readonly(), tc.membersCount)\n\n\t\t\tuassert.Equal(t, string(tc.choice), string(choice), \"choice\")\n\t\t\tuassert.Equal(t, tc.success, success, \"success\")\n\t\t})\n\t}\n}\n\nfunc TestSelectChoiceBySuperMajority(t *testing.T) {\n\tcases := []struct {\n\t\tname         string\n\t\tsetup        func(*commondao.VotingRecord)\n\t\tchoice       commondao.VoteChoice\n\t\tmembersCount int\n\t\tsuccess      bool\n\t}{\n\t\t{\n\t\t\tname:         \"no votes\",\n\t\t\tchoice:       commondao.ChoiceNone,\n\t\t\tmembersCount: 3,\n\t\t\tsuccess:      false,\n\t\t},\n\t\t{\n\t\t\tname: \"majority\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice:       commondao.ChoiceYes,\n\t\t\tmembersCount: 3,\n\t\t\tsuccess:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"no majority\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice:       \"\",\n\t\t\tmembersCount: 3,\n\t\t\tsuccess:      false,\n\t\t},\n\t\t{\n\t\t\tname: \"majority with abstain vote\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\t\t\tChoice:  commondao.ChoiceAbstain,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice:       commondao.ChoiceYes,\n\t\t\tmembersCount: 3,\n\t\t\tsuccess:      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\tvar record commondao.VotingRecord\n\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026record)\n\t\t\t}\n\n\t\t\tchoice, success := commondao.SelectChoiceBySuperMajority(record.Readonly(), tc.membersCount)\n\n\t\t\tuassert.Equal(t, string(tc.choice), string(choice), \"choice\")\n\t\t\tuassert.Equal(t, tc.success, success, \"success\")\n\t\t})\n\t}\n}\n\nfunc TestSelectChoiceByPlurality(t *testing.T) {\n\tcases := []struct {\n\t\tname    string\n\t\tsetup   func(*commondao.VotingRecord)\n\t\tchoice  commondao.VoteChoice\n\t\tsuccess bool\n\t}{\n\t\t{\n\t\t\tname:    \"no votes\",\n\t\t\tchoice:  commondao.ChoiceNone,\n\t\t\tsuccess: false,\n\t\t},\n\t\t{\n\t\t\tname: \"plurality\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice:  commondao.ChoiceYes,\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tname: \"no plurality\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice:  \"\",\n\t\t\tsuccess: false,\n\t\t},\n\t\t{\n\t\t\tname: \"plurality with abstain vote\",\n\t\t\tsetup: func(r *commondao.VotingRecord) {\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g12chzmwxw8sezcxe9h2csp0tck76r4ptwdlyyqk\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t})\n\t\t\t\tr.AddVote(commondao.Vote{\n\t\t\t\t\tAddress: \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\t\t\tChoice:  commondao.ChoiceAbstain,\n\t\t\t\t})\n\t\t\t},\n\t\t\tchoice:  commondao.ChoiceYes,\n\t\t\tsuccess: 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\tvar record commondao.VotingRecord\n\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026record)\n\t\t\t}\n\n\t\t\tchoice, success := commondao.SelectChoiceByPlurality(record.Readonly())\n\n\t\t\tuassert.Equal(t, string(tc.choice), string(choice), \"choice\")\n\t\t\tuassert.Equal(t, tc.success, success, \"success\")\n\t\t})\n\t}\n}\n\nfunc TestCollectVotes(t *testing.T) {\n\tcases := []struct {\n\t\tname   string\n\t\tsetup  func() commondao.MemberStorage\n\t\tvotes  []commondao.Vote\n\t\tgroups []string\n\t\terror  string\n\t}{\n\t\t{\n\t\t\tname: \"one group\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := commondao.NewMemberStorageWithGrouping()\n\t\t\t\tone, _ := s.Grouping().Add(\"one\")\n\t\t\t\tone.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\tone.Members().Add(\"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tgroups: []string{\"one\"},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"two groups\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := commondao.NewMemberStorageWithGrouping()\n\t\t\t\tone, _ := s.Grouping().Add(\"one\")\n\t\t\t\tone.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\tone.Members().Add(\"g125t352u4pmdrr57emc4pe04y40sknr5ztng5mt\")\n\t\t\t\ttwo, _ := s.Grouping().Add(\"two\")\n\t\t\t\ttwo.Members().Add(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tgroups: []string{\"one\", \"two\"},\n\t\t\tvotes: []commondao.Vote{\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t\t\tChoice:  commondao.ChoiceYes,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress: \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\",\n\t\t\t\t\tChoice:  commondao.ChoiceNo,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"no group names\",\n\t\t\tsetup: func() commondao.MemberStorage { return commondao.NewMemberStorageWithGrouping() },\n\t\t\terror: \"one or more group names are required to collect votes\",\n\t\t},\n\t\t{\n\t\t\tname:   \"member group not found\",\n\t\t\tsetup:  func() commondao.MemberStorage { return commondao.NewMemberStorageWithGrouping() },\n\t\t\tgroups: []string{\"foo\"},\n\t\t\terror:  \"member group 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\tvar r commondao.VotingRecord\n\t\t\tfor _, v := range tc.votes {\n\t\t\t\tr.AddVote(v)\n\t\t\t}\n\n\t\t\tstorage := tc.setup()\n\t\t\tctx := commondao.MustNewVotingContext(\u0026r, storage)\n\n\t\t\t// Act\n\t\t\tvotes, err := commondao.CollectVotes(ctx, tc.groups...)\n\n\t\t\t// Assert\n\t\t\tif tc.error != \"\" {\n\t\t\t\turequire.ErrorContains(t, err, tc.error)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"unexpected error\")\n\t\t\turequire.Equal(t, len(tc.votes), votes.Size(), \"expect number of votes to match\")\n\n\t\t\tvar i int\n\t\t\tvotes.Iterate(0, votes.Size(), false, func(v commondao.Vote) bool {\n\t\t\t\twant := tc.votes[i]\n\n\t\t\t\turequire.Equal(t, want.Address, v.Address, \"expect vote address to match\")\n\t\t\t\turequire.Equal(t, string(want.Choice), string(v.Choice), \"expect vote choice 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": "voting_context.gno",
                        "body": "package commondao\n\nimport \"errors\"\n\n// VotingContext contains current voting context, which includes a voting\n// record for the votes that has been submitted for a proposal and also\n// the list of DAO members.\ntype VotingContext struct {\n\tVotingRecord ReadonlyVotingRecord\n\tMembers      ReadonlyMemberStorage\n}\n\n// NewVotingContext creates a new voting context.\nfunc NewVotingContext(r *VotingRecord, s MemberStorage) (VotingContext, error) {\n\tif r == nil {\n\t\treturn VotingContext{}, errors.New(\"voting record is required\")\n\t}\n\n\tif s == nil {\n\t\treturn VotingContext{}, errors.New(\"member storage is required\")\n\t}\n\n\tmembers := MustNewReadonlyMemberStorage(s)\n\treturn VotingContext{\n\t\tVotingRecord: r.Readonly(),\n\t\tMembers:      *members,\n\t}, nil\n}\n\n// MustNewVotingContext creates a new voting context or panics on error.\nfunc MustNewVotingContext(r *VotingRecord, s MemberStorage) VotingContext {\n\tctx, err := NewVotingContext(r, s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn ctx\n}\n"
                      },
                      {
                        "name": "z_commondao_execute_0_filetest.gno",
                        "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"chain/runtime\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nconst member address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\" // @devx\n\nvar (\n\tdao          *commondao.CommonDAO\n\tproposal     *commondao.Proposal\n\texecutorPath string\n)\n\ntype testPropDef struct{}\n\nfunc (testPropDef) Title() string                               { return \"\" }\nfunc (testPropDef) Body() string                                { return \"\" }\nfunc (testPropDef) VotingPeriod() time.Duration                 { return 0 }\nfunc (testPropDef) Tally(commondao.VotingContext) (bool, error) { return true, nil }\n\nfunc (testPropDef) Executor() commondao.ExecFunc {\n\treturn func(realm) error {\n\t\texecutorPath = runtime.PreviousRealm().PkgPath()\n\t\treturn nil\n\t}\n}\n\nfunc init() {\n\tdao = commondao.New(commondao.WithMember(member))\n\tproposal = dao.MustPropose(member, testPropDef{})\n}\n\nfunc main() {\n\t// Make sure proposal is executed in a realm different than the one where definition has been defined\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/testing/dao\"))\n\n\terr := dao.Execute(proposal.ID())\n\n\tprintln(err == nil)\n\tprintln(string(proposal.Status()))\n\tprintln(executorPath)\n}\n\n// Output:\n// true\n// executed\n// gno.land/r/testing/dao\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 = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\" // @devx\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 0 }\nfunc (testPropDef) Tally(commondao.VotingContext) (bool, error) { return true, nil }\n\nfunc (testPropDef) Executor() commondao.ExecFunc {\n\treturn func(realm) error {\n\t\treturn errors.New(\"test error\")\n\t}\n}\n\nfunc init() {\n\tdao = commondao.New(commondao.WithMember(member))\n\tproposal = dao.MustPropose(member, testPropDef{})\n}\n\nfunc main() {\n\terr := dao.Execute(proposal.ID())\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\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nconst member address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\" // @devx\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) Tally(commondao.VotingContext) (bool, error) { return true, nil }\n\nfunc (testPropDef) Executor() commondao.ExecFunc {\n\treturn func(realm) error {\n\t\texecuted = true\n\t\treturn nil\n\t}\n}\n\nfunc init() {\n\tdao = commondao.New(\n\t\tcommondao.WithMember(member),\n\t\tcommondao.DisableVotingDeadlineCheck(), // Disable to be able to execute before voting deadline\n\t)\n\tproposal = dao.MustPropose(member, testPropDef{})\n}\n\nfunc main() {\n\t// Make sure proposal is executed in a realm different than the one where definition has been defined\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/testing/dao\"))\n\n\t// Should be able to execute before voting deadline because deadline check is disabled\n\terr := dao.Execute(proposal.ID())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tprintln(executed)\n\tprintln(string(proposal.Status()))\n}\n\n// Output:\n// true\n// executed\n"
                      },
                      {
                        "name": "z_commondao_must_execute_0_filetest.gno",
                        "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\ntype testPropDef struct{}\n\nfunc (testPropDef) Title() string                               { return \"\" }\nfunc (testPropDef) Body() string                                { return \"\" }\nfunc (testPropDef) VotingPeriod() time.Duration                 { return 0 }\nfunc (testPropDef) Executor() commondao.ExecFunc                { return nil }\nfunc (testPropDef) Tally(commondao.VotingContext) (bool, error) { return true, nil }\n\nfunc main() {\n\t// Make sure proposal is executed in a realm different than the one where definitions has been defined\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/testing/dao\"))\n\n\tcommondao.MustExecute(testPropDef{})\n\tprintln(\"ok\")\n}\n\n// Output:\n// ok\n"
                      },
                      {
                        "name": "z_commondao_must_execute_1_filetest.gno",
                        "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\ntype testPropDef struct{}\n\nfunc (testPropDef) Title() string                               { return \"\" }\nfunc (testPropDef) Body() string                                { return \"\" }\nfunc (testPropDef) VotingPeriod() time.Duration                 { return 0 }\nfunc (testPropDef) Tally(commondao.VotingContext) (bool, error) { return true, nil }\n\nfunc (testPropDef) Executor() commondao.ExecFunc {\n\treturn func(realm) error {\n\t\treturn errors.New(\"boom!\")\n\t}\n}\n\nfunc main() {\n\t// Make sure proposal is executed in a realm different than the one where definition has been defined\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/testing/dao\"))\n\n\tcommondao.MustExecute(testPropDef{})\n}\n\n// Error:\n// boom!\n"
                      },
                      {
                        "name": "z_commondao_must_execute_2_filetest.gno",
                        "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nfunc main() {\n\tcommondao.MustExecute(nil)\n}\n\n// Error:\n// executable proposal definition is 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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "FfpmhfEOmZkjUwyd1rYF1VT8OapqdhBKIyjbzlN2HbxVpWThyLT2q+dMDhECSWOfrXby3J0BLGE/7qkPp202Tg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "expect",
                    "path": "gno.land/p/jeronimoalbi/expect",
                    "files": [
                      {
                        "name": "boolean.gno",
                        "body": "package expect\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// NewBooleanChecker creates a new checker of boolean values\nfunc NewBooleanChecker(ctx Context, value bool) BooleanChecker {\n\treturn BooleanChecker{ctx, value}\n}\n\n// BooleanChecker asserts boolean values.\ntype BooleanChecker struct {\n\tctx   Context\n\tvalue bool\n}\n\n// Not negates the next called expectation.\nfunc (c BooleanChecker) Not() BooleanChecker {\n\tc.ctx.negated = !c.ctx.negated\n\treturn c\n}\n\n// ToEqual asserts that current value is equal to an expected value.\nfunc (c BooleanChecker) ToEqual(v bool) {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value == v, func(ctx Context) string {\n\t\tgot := formatBoolean(c.value)\n\t\tif !ctx.IsNegated() {\n\t\t\twant := formatBoolean(v)\n\t\t\treturn ufmt.Sprintf(\"Expected values to match\\nGot: %s\\nWant: %s\", got, want)\n\t\t}\n\t\treturn ufmt.Sprintf(\"Expected values to be different\\nGot: %s\", got)\n\t})\n}\n\n// ToBeFalsy asserts that current value is falsy.\nfunc (c BooleanChecker) ToBeFalsy() {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(!c.value, func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn \"Expected value to be falsy\"\n\t\t}\n\t\treturn \"Expected value not to be falsy\"\n\t})\n}\n\n// ToBeTruthy asserts that current value is truthy.\nfunc (c BooleanChecker) ToBeTruthy() {\n\tc.ctx.T().Helper()\n\tc.ctx.CheckExpectation(c.value, func(ctx Context) string {\n\t\tif !ctx.IsNegated() {\n\t\t\treturn \"Expected value to be truthy\"\n\t\t}\n\t\treturn \"Expected value not to be truthy\"\n\t})\n}\n\nfunc asBoolean(value any) (bool, error) {\n\tif value == nil {\n\t\treturn false, nil\n\t}\n\n\tvar s string\n\tswitch v := value.(type) {\n\tcase bool:\n\t\treturn v, nil\n\tcase string:\n\t\ts = v\n\tcase []byte:\n\t\ts = string(v)\n\tcase Stringer:\n\t\ts = v.String()\n\tdefault:\n\t\treturn false, ErrIncompatibleType\n\t}\n\n\tif s != \"\" {\n\t\treturn strconv.ParseBool(s)\n\t}\n\treturn false, nil\n}\n\nfunc formatBoolean(value bool) string {\n\treturn strconv.FormatBool(value)\n}\n"
                      },
                      {
                        "name": "boolean_test.gno",
                        "body": "package expect_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/jeronimoalbi/expect\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\n)\n\nconst (\n\tcaller = address(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\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() {\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\n\texpect.Func(t, func() { Fail(cross) }).ToCrossPanic()\n\texpect.Func(t, func() { Fail(cross) }).ToCrossPanic().WithMessage(msg)\n\n\texpect.Func(t, func() error { Fail(cross); return nil }).ToCrossPanic()\n\texpect.Func(t, func() error { Fail(cross); return nil }).ToCrossPanic().WithMessage(msg)\n\n\texpect.Func(t, func() any { Fail(cross); return nil }).ToCrossPanic()\n\texpect.Func(t, func() any { Fail(cross); return nil }).ToCrossPanic().WithMessage(msg)\n\n\texpect.Func(t, func() (any, error) { Fail(cross); return nil, nil }).ToCrossPanic()\n\texpect.Func(t, func() (any, error) { Fail(cross); return nil, nil }).ToCrossPanic().WithMessage(msg)\n\n\texpect.Func(t, func() { Success(cross) }).Not().ToCrossPanic()\n\texpect.Func(t, func() error { Success(cross); return nil }).Not().ToCrossPanic()\n\texpect.Func(t, func() any { Success(cross); return nil }).Not().ToCrossPanic()\n\texpect.Func(t, func() (any, error) { Success(cross); 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\"\n)\n\nconst (\n\tcaller = address(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\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() {\n\twantMsg := \"Tick Tock\"\n\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\n\texpect.Func(t, func() { Success(cross) }).ToCrossPanic()\n\texpect.Func(t, func() { Fail(cross) }).ToCrossPanic().WithMessage(wantMsg)\n\n\texpect.Func(t, func() error { Success(cross); return nil }).ToCrossPanic()\n\texpect.Func(t, func() error { Fail(cross); return nil }).ToCrossPanic().WithMessage(wantMsg)\n\n\texpect.Func(t, func() any { Success(cross); return nil }).ToCrossPanic()\n\texpect.Func(t, func() any { Fail(cross); return nil }).ToCrossPanic().WithMessage(wantMsg)\n\n\texpect.Func(t, func() (any, error) { Success(cross); return nil, nil }).ToCrossPanic()\n\texpect.Func(t, func() (any, error) { Fail(cross); 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\"\n)\n\nconst (\n\tcaller = address(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\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() {\n\ttesting.SetRealm(testing.NewUserRealm(caller))\n\n\texpect.Func(t, func() { Fail(cross) }).Not().ToCrossPanic()\n\texpect.Func(t, func() { Fail(cross) }).ToCrossPanic().Not().WithMessage(msg)\n\n\texpect.Func(t, func() error { Fail(cross); return nil }).Not().ToCrossPanic()\n\texpect.Func(t, func() error { Fail(cross); return nil }).ToCrossPanic().Not().WithMessage(msg)\n\n\texpect.Func(t, func() any { Fail(cross); return nil }).Not().ToCrossPanic()\n\texpect.Func(t, func() any { Fail(cross); return nil }).ToCrossPanic().Not().WithMessage(msg)\n\n\texpect.Func(t, func() (any, error) { Fail(cross); return nil, nil }).Not().ToCrossPanic()\n\texpect.Func(t, func() (any, error) { Fail(cross); 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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "ICORexl49U1bwx/0O5ym067a2VlaiZ/4ji3MlnEVVQh1rzMaIzjQM/ZYlJcGlc/5re0TeDf4j2ZMDv1qsJAYCQ=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
                  "package": {
                    "name": "typeutil",
                    "path": "gno.land/p/moul/typeutil",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/moul/typeutil\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"
                      },
                      {
                        "name": "typeutil.gno",
                        "body": "// Package typeutil provides utility functions for converting between different types\n// and checking their states. It aims to provide consistent behavior across different\n// types while remaining lightweight and dependency-free.\npackage typeutil\n\nimport (\n\t\"errors\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// stringer is the interface that wraps the String method.\ntype stringer interface {\n\tString() string\n}\n\n// ToString converts any value to its string representation.\n// It supports a wide range of Go types including:\n//   - Basic: string, bool\n//   - Numbers: int, int8-64, uint, uint8-64, float32, float64\n//   - Special: time.Time, address, []byte\n//   - Slices: []T for most basic types\n//   - Maps: map[string]string, map[string]any\n//   - Interface: types implementing String() string\n//\n// Example usage:\n//\n//\tstr := typeutil.ToString(42)               // \"42\"\n//\tstr = typeutil.ToString([]int{1, 2})      // \"[1 2]\"\n//\tstr = typeutil.ToString(map[string]string{ // \"map[a:1 b:2]\"\n//\t    \"a\": \"1\",\n//\t    \"b\": \"2\",\n//\t})\nfunc ToString(val any) string {\n\tif val == nil {\n\t\treturn \"\"\n\t}\n\n\t// First check if value implements Stringer interface\n\tif s, ok := val.(interface{ String() string }); ok {\n\t\treturn s.String()\n\t}\n\n\tswitch v := val.(type) {\n\t// Pointer types - dereference and recurse\n\tcase *string:\n\t\tif v == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn *v\n\tcase *int:\n\t\tif v == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn strconv.Itoa(*v)\n\tcase *bool:\n\t\tif v == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn strconv.FormatBool(*v)\n\tcase *time.Time:\n\t\tif v == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn v.String()\n\tcase *address:\n\t\tif v == nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn string(*v)\n\n\t// String types\n\tcase string:\n\t\treturn v\n\tcase stringer:\n\t\treturn v.String()\n\n\t// Special types\n\tcase time.Time:\n\t\treturn v.String()\n\tcase address:\n\t\treturn string(v)\n\tcase []byte:\n\t\treturn string(v)\n\tcase struct{}:\n\t\treturn \"{}\"\n\n\t// Integer types\n\tcase int:\n\t\treturn strconv.Itoa(v)\n\tcase int8:\n\t\treturn strconv.FormatInt(int64(v), 10)\n\tcase int16:\n\t\treturn strconv.FormatInt(int64(v), 10)\n\tcase int32:\n\t\treturn strconv.FormatInt(int64(v), 10)\n\tcase int64:\n\t\treturn strconv.FormatInt(v, 10)\n\tcase uint:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint8:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint16:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint32:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint64:\n\t\treturn strconv.FormatUint(v, 10)\n\n\t// Float types\n\tcase float32:\n\t\treturn strconv.FormatFloat(float64(v), 'f', -1, 32)\n\tcase float64:\n\t\treturn strconv.FormatFloat(v, 'f', -1, 64)\n\n\t// Boolean\n\tcase bool:\n\t\tif v {\n\t\t\treturn \"true\"\n\t\t}\n\t\treturn \"false\"\n\n\t// Slice types\n\tcase []string:\n\t\treturn join(v)\n\tcase []int:\n\t\treturn join(v)\n\tcase []int32:\n\t\treturn join(v)\n\tcase []int64:\n\t\treturn join(v)\n\tcase []float32:\n\t\treturn join(v)\n\tcase []float64:\n\t\treturn join(v)\n\tcase []any:\n\t\treturn join(v)\n\tcase []time.Time:\n\t\treturn joinTimes(v)\n\tcase []stringer:\n\t\treturn join(v)\n\tcase []address:\n\t\treturn joinAddresses(v)\n\tcase [][]byte:\n\t\treturn joinBytes(v)\n\n\t// Map types with various key types\n\tcase map[any]any, map[string]any, map[string]string, map[string]int:\n\t\tvar b strings.Builder\n\t\tb.WriteString(\"map[\")\n\t\tfirst := true\n\n\t\tswitch m := v.(type) {\n\t\tcase map[any]any:\n\t\t\t// Convert all keys to strings for consistent ordering\n\t\t\tkeys := make([]string, 0)\n\t\t\tkeyMap := make(map[string]any)\n\n\t\t\tfor k := range m {\n\t\t\t\tkeyStr := ToString(k)\n\t\t\t\tkeys = append(keys, keyStr)\n\t\t\t\tkeyMap[keyStr] = k\n\t\t\t}\n\t\t\tsort.Strings(keys)\n\n\t\t\tfor _, keyStr := range keys {\n\t\t\t\tif !first {\n\t\t\t\t\tb.WriteString(\" \")\n\t\t\t\t}\n\t\t\t\torigKey := keyMap[keyStr]\n\t\t\t\tb.WriteString(keyStr)\n\t\t\t\tb.WriteString(\":\")\n\t\t\t\tb.WriteString(ToString(m[origKey]))\n\t\t\t\tfirst = false\n\t\t\t}\n\n\t\tcase map[string]any:\n\t\t\tkeys := make([]string, 0)\n\t\t\tfor k := range m {\n\t\t\t\tkeys = append(keys, k)\n\t\t\t}\n\t\t\tsort.Strings(keys)\n\n\t\t\tfor _, k := range keys {\n\t\t\t\tif !first {\n\t\t\t\t\tb.WriteString(\" \")\n\t\t\t\t}\n\t\t\t\tb.WriteString(k)\n\t\t\t\tb.WriteString(\":\")\n\t\t\t\tb.WriteString(ToString(m[k]))\n\t\t\t\tfirst = false\n\t\t\t}\n\n\t\tcase map[string]string:\n\t\t\tkeys := make([]string, 0)\n\t\t\tfor k := range m {\n\t\t\t\tkeys = append(keys, k)\n\t\t\t}\n\t\t\tsort.Strings(keys)\n\n\t\t\tfor _, k := range keys {\n\t\t\t\tif !first {\n\t\t\t\t\tb.WriteString(\" \")\n\t\t\t\t}\n\t\t\t\tb.WriteString(k)\n\t\t\t\tb.WriteString(\":\")\n\t\t\t\tb.WriteString(m[k])\n\t\t\t\tfirst = false\n\t\t\t}\n\n\t\tcase map[string]int:\n\t\t\tkeys := make([]string, 0)\n\t\t\tfor k := range m {\n\t\t\t\tkeys = append(keys, k)\n\t\t\t}\n\t\t\tsort.Strings(keys)\n\n\t\t\tfor _, k := range keys {\n\t\t\t\tif !first {\n\t\t\t\t\tb.WriteString(\" \")\n\t\t\t\t}\n\t\t\t\tb.WriteString(k)\n\t\t\t\tb.WriteString(\":\")\n\t\t\t\tb.WriteString(strconv.Itoa(m[k]))\n\t\t\t\tfirst = false\n\t\t\t}\n\t\t}\n\t\tb.WriteString(\"]\")\n\t\treturn b.String()\n\n\t// Default\n\tdefault:\n\t\treturn \"\u003cunknown\u003e\"\n\t}\n}\n\nfunc join(slice any) string {\n\tif IsZero(slice) {\n\t\treturn \"[]\"\n\t}\n\n\titems := ToInterfaceSlice(slice)\n\tif items == nil {\n\t\treturn \"[]\"\n\t}\n\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfor i, item := range items {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\" \")\n\t\t}\n\t\tb.WriteString(ToString(item))\n\t}\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n\nfunc joinTimes(slice []time.Time) string {\n\tif len(slice) == 0 {\n\t\treturn \"[]\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfor i, t := range slice {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\" \")\n\t\t}\n\t\tb.WriteString(t.String())\n\t}\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n\nfunc joinAddresses(slice []address) string {\n\tif len(slice) == 0 {\n\t\treturn \"[]\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfor i, addr := range slice {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\" \")\n\t\t}\n\t\tb.WriteString(string(addr))\n\t}\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n\nfunc joinBytes(slice [][]byte) string {\n\tif len(slice) == 0 {\n\t\treturn \"[]\"\n\t}\n\tvar b strings.Builder\n\tb.WriteString(\"[\")\n\tfor i, bytes := range slice {\n\t\tif i \u003e 0 {\n\t\t\tb.WriteString(\" \")\n\t\t}\n\t\tb.WriteString(string(bytes))\n\t}\n\tb.WriteString(\"]\")\n\treturn b.String()\n}\n\n// ToBool converts any value to a boolean based on common programming conventions.\n// For example:\n//   - Numbers: 0 is false, any other number is true\n//   - Strings: \"\", \"0\", \"false\", \"f\", \"no\", \"n\", \"off\" are false, others are true\n//   - Slices/Maps: empty is false, non-empty is true\n//   - nil: always false\n//   - bool: direct value\nfunc ToBool(val any) bool {\n\tif IsZero(val) {\n\t\treturn false\n\t}\n\n\t// Handle special string cases\n\tif str, ok := val.(string); ok {\n\t\tstr = strings.ToLower(strings.TrimSpace(str))\n\t\treturn str != \"\" \u0026\u0026 str != \"0\" \u0026\u0026 str != \"false\" \u0026\u0026 str != \"f\" \u0026\u0026 str != \"no\" \u0026\u0026 str != \"n\" \u0026\u0026 str != \"off\"\n\t}\n\n\treturn true\n}\n\n// IsZero returns true if the value represents a \"zero\" or \"empty\" state for its type.\n// For example:\n//   - Numbers: 0\n//   - Strings: \"\"\n//   - Slices/Maps: empty\n//   - nil: true\n//   - bool: false\n//   - time.Time: IsZero()\n//   - address: empty string\nfunc IsZero(val any) bool {\n\tif val == nil {\n\t\treturn true\n\t}\n\n\tswitch v := val.(type) {\n\t// Pointer types - nil pointer is zero, otherwise check pointed value\n\tcase *bool:\n\t\treturn v == nil || !*v\n\tcase *string:\n\t\treturn v == nil || *v == \"\"\n\tcase *int:\n\t\treturn v == nil || *v == 0\n\tcase *time.Time:\n\t\treturn v == nil || v.IsZero()\n\tcase *address:\n\t\treturn v == nil || string(*v) == \"\"\n\n\t// Bool\n\tcase bool:\n\t\treturn !v\n\n\t// String types\n\tcase string:\n\t\treturn v == \"\"\n\tcase stringer:\n\t\treturn v.String() == \"\"\n\n\t// Integer types\n\tcase int:\n\t\treturn v == 0\n\tcase int8:\n\t\treturn v == 0\n\tcase int16:\n\t\treturn v == 0\n\tcase int32:\n\t\treturn v == 0\n\tcase int64:\n\t\treturn v == 0\n\tcase uint:\n\t\treturn v == 0\n\tcase uint8:\n\t\treturn v == 0\n\tcase uint16:\n\t\treturn v == 0\n\tcase uint32:\n\t\treturn v == 0\n\tcase uint64:\n\t\treturn v == 0\n\n\t// Float types\n\tcase float32:\n\t\treturn v == 0\n\tcase float64:\n\t\treturn v == 0\n\n\t// Special types\n\tcase []byte:\n\t\treturn len(v) == 0\n\tcase time.Time:\n\t\treturn v.IsZero()\n\tcase address:\n\t\treturn string(v) == \"\"\n\n\t// Slices (check if empty)\n\tcase []string:\n\t\treturn len(v) == 0\n\tcase []int:\n\t\treturn len(v) == 0\n\tcase []int32:\n\t\treturn len(v) == 0\n\tcase []int64:\n\t\treturn len(v) == 0\n\tcase []float32:\n\t\treturn len(v) == 0\n\tcase []float64:\n\t\treturn len(v) == 0\n\tcase []any:\n\t\treturn len(v) == 0\n\tcase []time.Time:\n\t\treturn len(v) == 0\n\tcase []address:\n\t\treturn len(v) == 0\n\tcase [][]byte:\n\t\treturn len(v) == 0\n\tcase []stringer:\n\t\treturn len(v) == 0\n\n\t// Maps (check if empty)\n\tcase map[string]string:\n\t\treturn len(v) == 0\n\tcase map[string]any:\n\t\treturn len(v) == 0\n\n\tdefault:\n\t\treturn false // non-nil unknown types are considered non-zero\n\t}\n}\n\n// ToInterfaceSlice converts various slice types to []any\nfunc ToInterfaceSlice(val any) []any {\n\tswitch v := val.(type) {\n\tcase []any:\n\t\treturn v\n\tcase []string:\n\t\tresult := make([]any, len(v))\n\t\tfor i, s := range v {\n\t\t\tresult[i] = s\n\t\t}\n\t\treturn result\n\tcase []int:\n\t\tresult := make([]any, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = n\n\t\t}\n\t\treturn result\n\tcase []int32:\n\t\tresult := make([]any, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = n\n\t\t}\n\t\treturn result\n\tcase []int64:\n\t\tresult := make([]any, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = n\n\t\t}\n\t\treturn result\n\tcase []float32:\n\t\tresult := make([]any, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = n\n\t\t}\n\t\treturn result\n\tcase []float64:\n\t\tresult := make([]any, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = n\n\t\t}\n\t\treturn result\n\tcase []bool:\n\t\tresult := make([]any, len(v))\n\t\tfor i, b := range v {\n\t\t\tresult[i] = b\n\t\t}\n\t\treturn result\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// ToMapStringInterface converts a map with string keys and any value type to map[string]any\nfunc ToMapStringInterface(m any) (map[string]any, error) {\n\tresult := make(map[string]any)\n\n\tswitch v := m.(type) {\n\tcase map[string]any:\n\t\treturn v, nil\n\tcase map[string]string:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]int:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]int64:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]float64:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]bool:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string][]string:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = ToInterfaceSlice(val)\n\t\t}\n\tcase map[string][]int:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = ToInterfaceSlice(val)\n\t\t}\n\tcase map[string][]any:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]map[string]any:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[string]map[string]string:\n\t\tfor k, val := range v {\n\t\t\tif converted, err := ToMapStringInterface(val); err == nil {\n\t\t\t\tresult[k] = converted\n\t\t\t} else {\n\t\t\t\treturn nil, errors.New(\"failed to convert nested map at key: \" + k)\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn nil, errors.New(\"unsupported map type: \" + ToString(m))\n\t}\n\n\treturn result, nil\n}\n\n// ToMapIntInterface converts a map with int keys and any value type to map[int]any\nfunc ToMapIntInterface(m any) (map[int]any, error) {\n\tresult := make(map[int]any)\n\n\tswitch v := m.(type) {\n\tcase map[int]any:\n\t\treturn v, nil\n\tcase map[int]string:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]int:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]int64:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]float64:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]bool:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int][]string:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = ToInterfaceSlice(val)\n\t\t}\n\tcase map[int][]int:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = ToInterfaceSlice(val)\n\t\t}\n\tcase map[int][]any:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]map[string]any:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tcase map[int]map[int]any:\n\t\tfor k, val := range v {\n\t\t\tresult[k] = val\n\t\t}\n\tdefault:\n\t\treturn nil, errors.New(\"unsupported map type: \" + ToString(m))\n\t}\n\n\treturn result, nil\n}\n\n// ToStringSlice converts various slice types to []string\nfunc ToStringSlice(val any) []string {\n\tswitch v := val.(type) {\n\tcase []string:\n\t\treturn v\n\tcase []any:\n\t\tresult := make([]string, len(v))\n\t\tfor i, item := range v {\n\t\t\tresult[i] = ToString(item)\n\t\t}\n\t\treturn result\n\tcase []int:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.Itoa(n)\n\t\t}\n\t\treturn result\n\tcase []int32:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatInt(int64(n), 10)\n\t\t}\n\t\treturn result\n\tcase []int64:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatInt(n, 10)\n\t\t}\n\t\treturn result\n\tcase []float32:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatFloat(float64(n), 'f', -1, 32)\n\t\t}\n\t\treturn result\n\tcase []float64:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatFloat(n, 'f', -1, 64)\n\t\t}\n\t\treturn result\n\tcase []bool:\n\t\tresult := make([]string, len(v))\n\t\tfor i, b := range v {\n\t\t\tresult[i] = strconv.FormatBool(b)\n\t\t}\n\t\treturn result\n\tcase []time.Time:\n\t\tresult := make([]string, len(v))\n\t\tfor i, t := range v {\n\t\t\tresult[i] = t.String()\n\t\t}\n\t\treturn result\n\tcase []address:\n\t\tresult := make([]string, len(v))\n\t\tfor i, addr := range v {\n\t\t\tresult[i] = string(addr)\n\t\t}\n\t\treturn result\n\tcase [][]byte:\n\t\tresult := make([]string, len(v))\n\t\tfor i, b := range v {\n\t\t\tresult[i] = string(b)\n\t\t}\n\t\treturn result\n\tcase []stringer:\n\t\tresult := make([]string, len(v))\n\t\tfor i, s := range v {\n\t\t\tresult[i] = s.String()\n\t\t}\n\t\treturn result\n\tcase []uint:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatUint(uint64(n), 10)\n\t\t}\n\t\treturn result\n\tcase []uint8:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatUint(uint64(n), 10)\n\t\t}\n\t\treturn result\n\tcase []uint16:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatUint(uint64(n), 10)\n\t\t}\n\t\treturn result\n\tcase []uint32:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatUint(uint64(n), 10)\n\t\t}\n\t\treturn result\n\tcase []uint64:\n\t\tresult := make([]string, len(v))\n\t\tfor i, n := range v {\n\t\t\tresult[i] = strconv.FormatUint(n, 10)\n\t\t}\n\t\treturn result\n\tdefault:\n\t\t// Try to convert using reflection if it's a slice\n\t\tif slice := ToInterfaceSlice(val); slice != nil {\n\t\t\tresult := make([]string, len(slice))\n\t\t\tfor i, item := range slice {\n\t\t\t\tresult[i] = ToString(item)\n\t\t\t}\n\t\t\treturn result\n\t\t}\n\t\treturn nil\n\t}\n}\n"
                      },
                      {
                        "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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "fh33HYs3AFFZHhbk4VF/MdLabdcnSc34CCXmTeG2NDArT9NYrapaO4gkkFkvvJuLQhtkzHdrW7PkwYOCZa31bQ=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
                  "package": {
                    "name": "ulist",
                    "path": "gno.land/p/moul/ulist",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/moul/ulist\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"
                      },
                      {
                        "name": "ulist.gno",
                        "body": "// Package ulist provides an append-only list implementation using a binary tree structure,\n// optimized for scenarios requiring sequential inserts with auto-incrementing indices.\n//\n// The implementation uses a binary tree where new elements are added by following a path\n// determined by the binary representation of the index. This provides automatic balancing\n// for append operations without requiring any balancing logic.\n//\n// Unlike the AVL tree-based list implementation (p/demo/avl/list), ulist is specifically\n// designed for append-only operations and does not require rebalancing. This makes it more\n// efficient for sequential inserts but less flexible for general-purpose list operations.\n//\n// Key differences from AVL list:\n// * Append-only design (no arbitrary inserts)\n// * No tree rebalancing needed\n// * Simpler implementation\n// * More memory efficient for sequential operations\n// * Less flexible than AVL (no arbitrary inserts/reordering)\n//\n// Key characteristics:\n// * O(log n) append and access operations\n// * Perfect balance for power-of-2 sizes\n// * No balancing needed\n// * Memory efficient\n// * Natural support for range queries\n// * Support for soft deletion of elements\n// * Forward and reverse iteration capabilities\n// * Offset-based iteration with count control\npackage ulist\n\n// TODO: Make avl/pager compatible in some way. Explain the limitations (not always 10 items because of nil ones).\n// TODO: Use this ulist in moul/collection for the primary index.\n// TODO: Consider adding a \"compact\" method that removes nil nodes.\n// TODO: Benchmarks.\n\nimport (\n\t\"errors\"\n)\n\n// List represents an append-only binary tree list\ntype List struct {\n\troot       *treeNode\n\ttotalSize  int\n\tactiveSize int\n}\n\n// Entry represents a key-value pair in the list, where Index is the position\n// and Value is the stored data\ntype Entry struct {\n\tIndex int\n\tValue any\n}\n\n// treeNode represents a node in the binary tree\ntype treeNode struct {\n\tdata  any\n\tleft  *treeNode\n\tright *treeNode\n}\n\n// Error variables\nvar (\n\tErrOutOfBounds = errors.New(\"index out of bounds\")\n\tErrDeleted     = errors.New(\"element already deleted\")\n)\n\n// New creates a new empty List instance\nfunc New() *List {\n\treturn \u0026List{}\n}\n\n// Append adds one or more values to the end of the list.\n// Values are added sequentially, and the list grows automatically.\nfunc (l *List) Append(values ...any) {\n\tfor _, value := range values {\n\t\tindex := l.totalSize\n\t\tnode := l.findNode(index, true)\n\t\tnode.data = value\n\t\tl.totalSize++\n\t\tl.activeSize++\n\t}\n}\n\n// Get retrieves the value at the specified index.\n// Returns nil if the index is out of bounds or if the element was deleted.\nfunc (l *List) Get(index int) any {\n\tnode := l.findNode(index, false)\n\tif node == nil {\n\t\treturn nil\n\t}\n\treturn node.data\n}\n\n// Delete marks the elements at the specified indices as deleted.\n// Returns ErrOutOfBounds if any index is invalid or ErrDeleted if\n// the element was already deleted.\nfunc (l *List) Delete(indices ...int) error {\n\tif len(indices) == 0 {\n\t\treturn nil\n\t}\n\tif l == nil || l.totalSize == 0 {\n\t\treturn ErrOutOfBounds\n\t}\n\n\tfor _, index := range indices {\n\t\tif index \u003c 0 || index \u003e= l.totalSize {\n\t\t\treturn ErrOutOfBounds\n\t\t}\n\n\t\tnode := l.findNode(index, false)\n\t\tif node == nil || node.data == nil {\n\t\t\treturn ErrDeleted\n\t\t}\n\t\tnode.data = nil\n\t\tl.activeSize--\n\t}\n\n\treturn nil\n}\n\n// Set updates or restores a value at the specified index if within bounds\n// Returns ErrOutOfBounds if the index is invalid\nfunc (l *List) Set(index int, value any) error {\n\tif l == nil || index \u003c 0 || index \u003e= l.totalSize {\n\t\treturn ErrOutOfBounds\n\t}\n\n\tnode := l.findNode(index, false)\n\tif node == nil {\n\t\treturn ErrOutOfBounds\n\t}\n\n\t// If this is restoring a deleted element\n\tif value != nil \u0026\u0026 node.data == nil {\n\t\tl.activeSize++\n\t}\n\n\t// If this is deleting an element\n\tif value == nil \u0026\u0026 node.data != nil {\n\t\tl.activeSize--\n\t}\n\n\tnode.data = value\n\treturn nil\n}\n\n// Size returns the number of active (non-deleted) elements in the list\nfunc (l *List) Size() int {\n\tif l == nil {\n\t\treturn 0\n\t}\n\treturn l.activeSize\n}\n\n// TotalSize returns the total number of elements ever added to the list,\n// including deleted elements\nfunc (l *List) TotalSize() int {\n\tif l == nil {\n\t\treturn 0\n\t}\n\treturn l.totalSize\n}\n\n// IterCbFn is a callback function type used in iteration methods.\n// Return true to stop iteration, false to continue.\ntype IterCbFn func(index int, value any) bool\n\n// Iterator performs iteration between start and end indices, calling cb for each entry.\n// If start \u003e end, iteration is performed in reverse order.\n// Returns true if iteration was stopped early by the callback returning true.\n// Skips deleted elements.\nfunc (l *List) Iterator(start, end int, cb IterCbFn) bool {\n\t// For empty list or invalid range\n\tif l == nil || l.totalSize == 0 {\n\t\treturn false\n\t}\n\tif start \u003c 0 \u0026\u0026 end \u003c 0 {\n\t\treturn false\n\t}\n\tif start \u003e= l.totalSize \u0026\u0026 end \u003e= l.totalSize {\n\t\treturn false\n\t}\n\n\t// Normalize indices\n\tif start \u003c 0 {\n\t\tstart = 0\n\t}\n\tif end \u003c 0 {\n\t\tend = 0\n\t}\n\tif end \u003e= l.totalSize {\n\t\tend = l.totalSize - 1\n\t}\n\tif start \u003e= l.totalSize {\n\t\tstart = l.totalSize - 1\n\t}\n\n\t// Handle reverse iteration\n\tif start \u003e end {\n\t\tfor i := start; i \u003e= end; i-- {\n\t\t\tval := l.Get(i)\n\t\t\tif val != nil {\n\t\t\t\tif cb(i, val) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\t// Handle forward iteration\n\tfor i := start; i \u003c= end; i++ {\n\t\tval := l.Get(i)\n\t\tif val != nil {\n\t\t\tif cb(i, val) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// IteratorByOffset performs iteration starting from offset for count elements.\n// If count is positive, iterates forward; if negative, iterates backward.\n// The iteration stops after abs(count) elements or when reaching list bounds.\n// Skips deleted elements.\nfunc (l *List) IteratorByOffset(offset int, count int, cb IterCbFn) bool {\n\tif count == 0 || l == nil || l.totalSize == 0 {\n\t\treturn false\n\t}\n\n\t// Normalize offset\n\tif offset \u003c 0 {\n\t\toffset = 0\n\t}\n\tif offset \u003e= l.totalSize {\n\t\toffset = l.totalSize - 1\n\t}\n\n\t// Determine end based on count direction\n\tvar end int\n\tif count \u003e 0 {\n\t\tend = l.totalSize - 1\n\t} else {\n\t\tend = 0\n\t}\n\n\twrapperReturned := false\n\n\t// Wrap the callback to limit iterations\n\tremaining := abs(count)\n\twrapper := func(index int, value any) bool {\n\t\tif remaining \u003c= 0 {\n\t\t\twrapperReturned = true\n\t\t\treturn true\n\t\t}\n\t\tremaining--\n\t\treturn cb(index, value)\n\t}\n\tret := l.Iterator(offset, end, wrapper)\n\tif wrapperReturned {\n\t\treturn false\n\t}\n\treturn ret\n}\n\n// abs returns the absolute value of x\nfunc abs(x int) int {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\n// findNode locates or creates a node at the given index in the binary tree.\n// The tree is structured such that the path to a node is determined by the binary\n// representation of the index. For example, a tree with 15 elements would look like:\n//\n//\t          0\n//\t       /      \\\n//\t     1         2\n//\t   /   \\     /   \\\n//\t  3    4    5     6\n//\t / \\  / \\  / \\   / \\\n//\t7  8 9 10 11 12 13 14\n//\n// To find index 13 (binary 1101):\n// 1. Start at root (0)\n// 2. Calculate bits needed (4 bits for index 13)\n// 3. Skip the highest bit position and start from bits-2\n// 4. Read bits from left to right:\n//   - 1 -\u003e go right to 2\n//   - 1 -\u003e go right to 6\n//   - 0 -\u003e go left to 13\n//\n// Special cases:\n// - Index 0 always returns the root node\n// - For create=true, missing nodes are created along the path\n// - For create=false, returns nil if any node is missing\nfunc (l *List) findNode(index int, create bool) *treeNode {\n\t// For read operations, check bounds strictly\n\tif !create \u0026\u0026 (l == nil || index \u003c 0 || index \u003e= l.totalSize) {\n\t\treturn nil\n\t}\n\n\t// For create operations, allow index == totalSize for append\n\tif create \u0026\u0026 (l == nil || index \u003c 0 || index \u003e l.totalSize) {\n\t\treturn nil\n\t}\n\n\t// Initialize root if needed\n\tif l.root == nil {\n\t\tif !create {\n\t\t\treturn nil\n\t\t}\n\t\tl.root = \u0026treeNode{}\n\t\treturn l.root\n\t}\n\n\tnode := l.root\n\n\t// Special case for root node\n\tif index == 0 {\n\t\treturn node\n\t}\n\n\t// Calculate the number of bits needed (inline highestBit logic)\n\tbits := 0\n\tn := index + 1\n\tfor n \u003e 0 {\n\t\tn \u003e\u003e= 1\n\t\tbits++\n\t}\n\n\t// Start from the second highest bit\n\tfor level := bits - 2; level \u003e= 0; level-- {\n\t\tbit := (index \u0026 (1 \u003c\u003c uint(level))) != 0\n\n\t\tif bit {\n\t\t\tif node.right == nil {\n\t\t\t\tif !create {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tnode.right = \u0026treeNode{}\n\t\t\t}\n\t\t\tnode = node.right\n\t\t} else {\n\t\t\tif node.left == nil {\n\t\t\t\tif !create {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tnode.left = \u0026treeNode{}\n\t\t\t}\n\t\t\tnode = node.left\n\t\t}\n\t}\n\n\treturn node\n}\n\n// MustDelete deletes elements at the specified indices.\n// Panics if any index is invalid or if any element was already deleted.\nfunc (l *List) MustDelete(indices ...int) {\n\tif err := l.Delete(indices...); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// MustGet retrieves the value at the specified index.\n// Panics if the index is out of bounds or if the element was deleted.\nfunc (l *List) MustGet(index int) any {\n\tif l == nil || index \u003c 0 || index \u003e= l.totalSize {\n\t\tpanic(ErrOutOfBounds)\n\t}\n\tvalue := l.Get(index)\n\tif value == nil {\n\t\tpanic(ErrDeleted)\n\t}\n\treturn value\n}\n\n// MustSet updates or restores a value at the specified index.\n// Panics if the index is out of bounds.\nfunc (l *List) MustSet(index int, value any) {\n\tif err := l.Set(index, value); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// GetRange returns a slice of Entry containing elements between start and end indices.\n// If start \u003e end, elements are returned in reverse order.\n// Deleted elements are skipped.\nfunc (l *List) GetRange(start, end int) []Entry {\n\tvar entries []Entry\n\tl.Iterator(start, end, func(index int, value any) bool {\n\t\tentries = append(entries, Entry{Index: index, Value: value})\n\t\treturn false\n\t})\n\treturn entries\n}\n\n// GetByOffset returns a slice of Entry starting from offset for count elements.\n// If count is positive, returns elements forward; if negative, returns elements backward.\n// The operation stops after abs(count) elements or when reaching list bounds.\n// Deleted elements are skipped.\nfunc (l *List) GetByOffset(offset int, count int) []Entry {\n\tvar entries []Entry\n\tl.IteratorByOffset(offset, count, func(index int, value any) bool {\n\t\tentries = append(entries, Entry{Index: index, Value: value})\n\t\treturn false\n\t})\n\treturn entries\n}\n\n// IList defines the interface for an ulist.List compatible structure.\ntype IList interface {\n\t// Basic operations\n\tAppend(values ...any)\n\tGet(index int) any\n\tDelete(indices ...int) error\n\tSize() int\n\tTotalSize() int\n\tSet(index int, value any) error\n\n\t// Must variants that panic instead of returning errors\n\tMustDelete(indices ...int)\n\tMustGet(index int) any\n\tMustSet(index int, value any)\n\n\t// Range operations\n\tGetRange(start, end int) []Entry\n\tGetByOffset(offset int, count int) []Entry\n\n\t// Iterator operations\n\tIterator(start, end int, cb IterCbFn) bool\n\tIteratorByOffset(offset int, count int, cb IterCbFn) bool\n}\n\n// Verify that List implements IList\nvar _ IList = (*List)(nil)\n"
                      },
                      {
                        "name": "ulist_test.gno",
                        "body": "package ulist\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/moul/typeutil\"\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.Equal(t, tt.expectedErr.Error(), err.Error())\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.Equal(t, ErrOutOfBounds.Error(), err.Error())\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.Equal(t, ErrDeleted.Error(), err.Error())\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.Equal(t, tt.expectedErr.Error(), err.Error())\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "WxZhY1UICHSnaHB8+paJzVOx4jN/FWOSp+LW3xr9nehPrYemXru/RWWcAsCud8Tn4mWCTaZVkDXKQUF52gx5Zw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "message",
                    "path": "gno.land/p/jeronimoalbi/message",
                    "files": [
                      {
                        "name": "broker.gno",
                        "body": "package message\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/ulist\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nvar (\n\t// ErrInvalidTopic is triggered when an invalid topic is used.\n\tErrInvalidTopic = errors.New(\"invalid topic\")\n\n\t// ErrRequiredCallback is triggered when subscribing without a callback.\n\tErrRequiredCallback = errors.New(\"message callback is required\")\n\n\t// ErrRequiredSubscriptionID is triggered when unsubscribing without an ID.\n\tErrRequiredSubscriptionID = errors.New(\"message sibscription ID is required\")\n\n\t// ErrRequiredTopic is triggered when (un)subscribing without a topic.\n\tErrRequiredTopic = errors.New(\"message topic is required\")\n)\n\n// NewBroker creates a new message broker.\nfunc NewBroker() *Broker {\n\treturn \u0026Broker{}\n}\n\n// Broker is a message broker that handles subscriptions and message publishing.\ntype Broker struct {\n\tcallbacks avl.Tree // string(topic) -\u003e *ulist.List(Callback)\n}\n\n// Topics returns the list of current subscription topics.\nfunc (b Broker) Topics() []Topic {\n\tvar topics []Topic\n\tb.callbacks.Iterate(\"\", \"\", func(k string, _ any) bool {\n\t\ttopic := Topic(k)\n\t\tif topic == TopicAll {\n\t\t\t// Skip catchall topic from the list\n\t\t\treturn false\n\t\t}\n\n\t\ttopics = append(topics, topic)\n\t\treturn false\n\t})\n\treturn topics\n}\n\n// Subscribe subscribes to messages published for a topic.\n// It returns the callback ID within the topic.\nfunc (b *Broker) Subscribe(topic Topic, cb Callback) (id int, _ error) {\n\tkey := strings.TrimSpace(string(topic))\n\tif key == \"\" {\n\t\treturn 0, ErrRequiredTopic\n\t}\n\n\tif cb == nil {\n\t\treturn 0, ErrRequiredCallback\n\t}\n\n\tv, _ := b.callbacks.Get(key)\n\tcallbacks, _ := v.(*ulist.List)\n\tif callbacks == nil {\n\t\tcallbacks = ulist.New()\n\t}\n\n\tcallbacks.Append(cb)\n\tb.callbacks.Set(key, callbacks)\n\treturn callbacks.TotalSize(), nil\n}\n\n// Unsubscribe unsubscribes a callback from a message topic.\n// ID is the callback ID within the topic, returned on subscription.\nfunc (b *Broker) Unsubscribe(topic Topic, id int) (unsubscribed bool, _ error) {\n\tkey := strings.TrimSpace(string(topic))\n\tif key == \"\" {\n\t\treturn false, ErrRequiredTopic\n\t}\n\n\tif id == 0 {\n\t\treturn false, ErrRequiredSubscriptionID\n\t}\n\n\tv, found := b.callbacks.Get(key)\n\tif !found {\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, found := b.callbacks.Get(key)\n\tif found {\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, found = b.callbacks.Get(string(TopicAll))\n\tif found {\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\"\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\"\n\t\"gno.land/p/jeronimoalbi/message\"\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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "kvjsjufNVqbpu+88zM43NkJzMRQ/HC1u8AdLNY1sm+Y80MWFdnVqSxZRxovnEbZDt9IIpaG7usNbOTYabwkLfg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "storage",
                    "path": "gno.land/p/nt/commondao/v0/exts/storage",
                    "files": [
                      {
                        "name": "README.md",
                        "body": "# CommonDAO Package Storage Extension\n\nStorage package is an extension of `gno.land/p/nt/commondao/v0` that provides\nalternative storage implementations.\n\n## Member Storage\n\nThis custom implementation of `MemberStorage` is an implementation with\ngrouping support that automatically adds or removes members from the storage\nwhen members are added or removed from any of the member groups.\n\nThe implementation provided by the `commondao` package doesn't automatically\nadd members to the storage when any of the groups change, if need then users\nhave to be added explicitly.\n\nAdding or removing users automatically could be beneficial in some cases where\nimplementation requires iterating all unique users within the storage and\nwithin each group, or counting all unique users within them. It also makes it\ncheaper to iterate because having all users within the same storage doesn't\nrequire to iterate each group.\n\nPackage also provide a `GetMemberGroups()` function that takes advantage of\nthis storage which can be used to return the names of the groups that an\naccount is a member of.\n\nExample usage:\n\n```go\nimport (\n  \"gno.land/p/nt/commondao/v0\"\n  \"gno.land/p/nt/commondao/v0/exts/storage\"\n)\n\nfunc main() {\n  // Create a new member storage with grouping\n  s := storage.NewMemberStorage()\n  \n  // Create a member group for moderators\n  moderators, err := s.Grouping().Add(\"moderators\")\n  if err != nil {\n    panic(err)\n  }\n\n  // Add members to the moderators group\n  moderators.Members().Add(\"g1...a\")\n  moderators.Members().Add(\"g1...b\")\n\n  // Create a DAO that uses the new member storage\n  dao := commondao.New(commondao.WithMemberStorage(s))\n\n  // Output: 2\n  println(dao.Members().Size())\n}\n```\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/nt/commondao/v0/exts/storage\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "member_storage.gno",
                        "body": "package storage\n\nimport (\n\t\"gno.land/p/jeronimoalbi/message\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n)\n\nconst (\n\tmsgMemberAdd    message.Topic = \"MemberAdd\"\n\tmsgMemberRemove               = \"MemberRemove\"\n)\n\n// GetMemberGroups returns the groups that a member belongs to.\n// It returns no groups if member storage was not created using this package.\nfunc GetMemberGroups(s commondao.MemberStorage, member address) []string {\n\tstorage, ok := s.(*memberStorage)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tv, found := storage.memberGroups.Get(member.String())\n\tif !found {\n\t\treturn nil\n\t}\n\n\ttree := v.(*avl.Tree)\n\tgroups := make([]string, 0, tree.Size())\n\ttree.Iterate(\"\", \"\", func(group string, _ any) bool {\n\t\tgroups = append(groups, group)\n\t\treturn false\n\t})\n\treturn groups\n}\n\n// NewMemberStorage creates a new CommonDAO member storage with grouping support.\n//\n// This is a custom storage that automatically adds or removes members that are added\n// or removed from any of the member groups. This allows for quick and inexpensive\n// checks for the number of total unique storage users, including users added to groups,\n// and also to iterate all of them without needing to iterate individual groups.\nfunc NewMemberStorage() commondao.MemberStorage {\n\t// Create a new broker to allow storages to publish and subscribe to messages\n\t// It is used to add/remove users from the storage each time one or more groups change.\n\tbroker := message.NewBroker()\n\n\t// Define a factory for creating custom member storages when groups are created.\n\t// Custom storage publishes when a member is added or removed from a group.\n\tinnerFactory := func(group string) commondao.MemberStorage {\n\t\treturn \u0026groupMemberStorage{\n\t\t\tMemberStorage: commondao.NewMemberStorage(),\n\t\t\tmessages:      broker,\n\t\t\tgroup:         group,\n\t\t}\n\t}\n\n\t// Create a member storage that automatically adds or removes members each time\n\t// a member group changes. This allows the storage to keep all members within\n\t// the same \"root\" storage for easier iteration.\n\tstorage := \u0026memberStorage{\n\t\tMemberStorage: commondao.NewMemberStorageWithGrouping(\n\t\t\tcommondao.UseStorageFactory(innerFactory),\n\t\t),\n\t\tmessages: broker,\n\t}\n\n\t// Subscribe to messages published by member groups\n\tstorage.messages.Subscribe(msgMemberAdd, storage.handleMemberAddMsg)\n\tstorage.messages.Subscribe(msgMemberRemove, storage.handleMemberRemoveMsg)\n\treturn storage\n}\n\ntype memberStorage struct {\n\tcommondao.MemberStorage\n\n\tmemberGroups avl.Tree // string(address) -\u003e *avl.Tree(group -\u003e struct{})\n\tmessages     message.Subscriber\n}\n\nfunc (s *memberStorage) handleMemberAddMsg(msg message.Message) {\n\tdata := msg.Data.(groupMemberUpdateData)\n\tkey := data.Member.String()\n\tv, _ := s.memberGroups.Get(key)\n\tgroups, ok := v.(*avl.Tree)\n\tif !ok {\n\t\t// Create a new tree to track member's groups\n\t\tgroups = avl.NewTree()\n\t\ts.memberGroups.Set(key, groups)\n\n\t\t// Add the new member to the storage\n\t\ts.MemberStorage.Add(data.Member)\n\t}\n\n\t// Keep track of the new member group\n\tgroups.Set(data.Group, struct{}{})\n}\n\nfunc (s *memberStorage) handleMemberRemoveMsg(msg message.Message) {\n\tdata := msg.Data.(groupMemberUpdateData)\n\tkey := data.Member.String()\n\tv, found := s.memberGroups.Get(key)\n\tif !found {\n\t\t// Member should always be found\n\t\treturn\n\t}\n\n\t// Remove the group from the list of groups member belongs\n\tgroups := v.(*avl.Tree)\n\tgroups.Remove(data.Group)\n\n\t// Remove the member from the storage when it doesn't belong to any group\n\tif groups.Size() == 0 {\n\t\ts.memberGroups.Remove(key)\n\t\ts.MemberStorage.Remove(data.Member)\n\t}\n}\n\n// Size returns the number of members in the storage.\n// It also includes unique members that belong to any number of member groups.\nfunc (s memberStorage) Size() int {\n\treturn s.MemberStorage.Size()\n}\n\n// IterateByOffset iterates members starting at the given offset.\n// The callback can return true to stop iteration.\n// It also iterates unique members that belong to any of the member groups.\nfunc (s memberStorage) IterateByOffset(offset, count int, fn commondao.MemberIterFn) bool {\n\treturn s.MemberStorage.IterateByOffset(offset, count, fn)\n}\n\n// groupMemberUpdateData defines a data type for group member updates.\ntype groupMemberUpdateData struct {\n\tGroup  string\n\tMember address\n}\n\n// groupMemberStorage defines a member storage for member groups.\n// This type of storage publishes messages when a member is added or removed from a group.\ntype groupMemberStorage struct {\n\tcommondao.MemberStorage\n\n\tgroup    string\n\tmessages message.Publisher\n}\n\n// Add adds a member to the storage.\n// Returns true if the member is added, or false if it already existed.\nfunc (s *groupMemberStorage) Add(member address) bool {\n\ts.messages.Publish(msgMemberAdd, groupMemberUpdateData{\n\t\tGroup:  s.group,\n\t\tMember: member,\n\t})\n\treturn s.MemberStorage.Add(member)\n}\n\n// Remove removes a member from the storage.\n// Returns true if member was removed, or false if it was not found.\nfunc (s *groupMemberStorage) Remove(member address) bool {\n\ts.messages.Publish(msgMemberRemove, groupMemberUpdateData{\n\t\tGroup:  s.group,\n\t\tMember: member,\n\t})\n\treturn s.MemberStorage.Remove(member)\n}\n"
                      },
                      {
                        "name": "member_storage_test.gno",
                        "body": "package storage_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n\n\t\"gno.land/p/nt/commondao/v0/exts/storage\"\n)\n\nfunc TestMemberStorageGroupingMemberAdd(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsetup  func() commondao.MemberStorage\n\t\tgroup  string\n\t\tmember address\n\t}{\n\t\t{\n\t\t\tname: \"empty group\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\ts.Grouping().Add(\"foo\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tgroup:  \"foo\",\n\t\t\tmember: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t},\n\t\t{\n\t\t\tname: \"group with members\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\tgroup, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tgroup.Members().Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\n\t\t\t\tgroup.Members().Add(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tgroup:  \"foo\",\n\t\t\tmember: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple groups with members\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\tgroup, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tgroup.Members().Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\n\t\t\t\tgroup, _ = s.Grouping().Add(\"bar\")\n\t\t\t\tgroup.Members().Add(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tgroup:  \"bar\",\n\t\t\tmember: \"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\t// Arrange\n\t\t\ts := tt.setup()\n\t\t\tgroup, _ := s.Grouping().Get(tt.group)\n\n\t\t\t// Act\n\t\t\tgroup.Members().Add(tt.member)\n\n\t\t\t// Assert\n\t\t\turequire.True(t, s.Has(tt.member), \"expect member to also be added to parent storage\")\n\t\t})\n\t}\n}\n\nfunc TestMemberStorageGroupingMemberRemove(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsetup  func() commondao.MemberStorage\n\t\tgroup  string\n\t\tmember address\n\t}{\n\t\t{\n\t\t\tname: \"one group one member\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\tgroup, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tgroup.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tgroup:  \"foo\",\n\t\t\tmember: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t},\n\t\t{\n\t\t\tname: \"one group multiple members\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\tgroup, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tgroup.Members().Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\n\t\t\t\tgroup.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\tgroup.Members().Add(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tgroup:  \"foo\",\n\t\t\tmember: \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple groups multiple members\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\tgroup, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tgroup.Members().Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\n\t\t\t\tgroup.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\tgroup, _ = s.Grouping().Add(\"bar\")\n\t\t\t\tgroup.Members().Add(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tgroup:  \"foo\",\n\t\t\tmember: \"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\t// Arrange\n\t\t\ts := tt.setup()\n\t\t\tgroup, _ := s.Grouping().Get(tt.group)\n\n\t\t\t// Act\n\t\t\tgroup.Members().Remove(tt.member)\n\n\t\t\t// Assert\n\t\t\turequire.False(t, s.Has(tt.member), \"expect member to also be removed from parent storage\")\n\t\t})\n\t}\n}\n\nfunc TestMemberStorageSize(t *testing.T) {\n\ttests := []struct {\n\t\tname  string\n\t\tsetup func() commondao.MemberStorage\n\t\tsize  int\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\treturn storage.NewMemberStorage()\n\t\t\t},\n\t\t\tsize: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"member without group\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\ts.Add(\"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 members without group\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\ts.Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\ts.Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tsize: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"member in group\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\tgroup, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tgroup.Members().Add(\"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 members in group\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\tgroup, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tgroup.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\tgroup.Members().Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tsize: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple members in different groups\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\tgroup, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tgroup.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\tgroup, _ = s.Grouping().Add(\"bar\")\n\t\t\t\tgroup.Members().Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\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\t// Arrange\n\t\t\ts := tt.setup()\n\n\t\t\t// Act\n\t\t\tsize := s.Size()\n\n\t\t\t// Assert\n\t\t\turequire.Equal(t, tt.size, size)\n\t\t})\n\t}\n}\n\nfunc TestMemberStorageIterateByOffset(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tsetup   func() commondao.MemberStorage\n\t\tmembers []address\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\treturn storage.NewMemberStorage()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"without group\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\ts.Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\ts.Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tmembers: []address{\n\t\t\t\t\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\",\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"single group\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\tgroup, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tgroup.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\tgroup.Members().Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tmembers: []address{\n\t\t\t\t\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\",\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple groups\",\n\t\t\tsetup: func() commondao.MemberStorage {\n\t\t\t\ts := storage.NewMemberStorage()\n\t\t\t\tgroup, _ := s.Grouping().Add(\"foo\")\n\t\t\t\tgroup.Members().Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\tgroup, _ = s.Grouping().Add(\"bar\")\n\t\t\t\tgroup.Members().Add(\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\")\n\t\t\t\treturn s\n\t\t\t},\n\t\t\tmembers: []address{\n\t\t\t\t\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\",\n\t\t\t\t\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\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\t// Arrange\n\t\t\tvar i int\n\t\t\ts := tt.setup()\n\t\t\tmembers := make([]address, s.Size())\n\n\t\t\t// Act\n\t\t\ts.IterateByOffset(0, s.Size(), func(addr address) bool {\n\t\t\t\tmembers[i] = addr\n\t\t\t\ti++\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\t// Assert\n\t\t\turequire.Equal(t, len(tt.members), len(members), \"expect iterated members to match\")\n\n\t\t\tfor i, member := range members {\n\t\t\t\turequire.Equal(t, tt.members[i], member, \"expect member to match\")\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "/4uCoTLWLe2ce+tj2V6zzDcLDDzVKIt+zKMJ3Tj2skMiSTxKgIwBUQZfqU5ZGAx7YrVnbfX5/4C4vqCFH9eeIg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "permissions",
                    "path": "gno.land/p/gnoland/boards/exts/permissions",
                    "files": [
                      {
                        "name": "README.md",
                        "body": "# Boards Permissions Extension\n\nThis is a `gno.land/p/gnoland/boards` package extension that provides a custom\n`Permissions` implementation that uses an underlying DAO to manage users and\nroles.\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]:# (filetests/z_readme_filetest.gno go)\n```go\npackage main\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/gnoland/boards/exts/permissions\"\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 = \"foo\"\n\nfunc main() {\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 := permissions.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\n// Output:\n// Hello Bob!\n```\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/gnoland/boards/exts/permissions\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "options.gno",
                        "body": "package permissions\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// Option configures permissions.\ntype Option func(*Permissions)\n\n// UseSingleUserRole configures permissions to only allow one role per user.\nfunc UseSingleUserRole() Option {\n\treturn func(p *Permissions) {\n\t\tp.singleUserRole = true\n\t}\n}\n\n// WithSuperRole configures permissions to have a super role.\n// A super role is the one that have all permissions.\n// This type of role doesn't need to be mapped to any permission.\nfunc WithSuperRole(r boards.Role) Option {\n\treturn func(p *Permissions) {\n\t\tif p.superRole != \"\" {\n\t\t\tpanic(\"permissions super role can be assigned only once\")\n\t\t}\n\n\t\tname := string(r)\n\t\tp.dao.Members().Grouping().Add(name)\n\t\tp.superRole = r\n\t}\n}\n"
                      },
                      {
                        "name": "permissions.gno",
                        "body": "package permissions\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/commondao/v0\"\n\t\"gno.land/p/nt/commondao/v0/exts/storage\"\n)\n\n// ValidatorFunc defines a function type for permissions validators.\ntype ValidatorFunc func(boards.Permissions, boards.Args) error\n\n// Permissions manages users, roles and permissions.\n//\n// This type is a default `gno.land/p/gnoland/boards` package `Permissions` implementation\n// that handles boards users, roles and permissions using an underlying DAO. It also supports\n// optionally setting validation functions to be triggered within `WithPermission()` method\n// 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 permissions can\n// be configured to only allow one role per user.\ntype Permissions struct {\n\tsuperRole      boards.Role\n\tdao            *commondao.CommonDAO\n\tvalidators     *avl.Tree // string(boards.Permission) -\u003e BasicPermissionValidator\n\tpublic         *avl.Tree // string(boards.Permission) -\u003e struct{}{}\n\tsingleUserRole bool\n}\n\n// New creates a new permissions type.\nfunc New(options ...Option) *Permissions {\n\ts := storage.NewMemberStorage()\n\tps := \u0026Permissions{\n\t\tvalidators: avl.NewTree(),\n\t\tpublic:     avl.NewTree(),\n\t\tdao:        commondao.New(commondao.WithMemberStorage(s)),\n\t}\n\n\tfor _, apply := range options {\n\t\tapply(ps)\n\t}\n\treturn ps\n}\n\n// DAO returns the underlying permissions DAO.\nfunc (ps Permissions) DAO() *commondao.CommonDAO {\n\treturn ps.dao\n}\n\n// ValidateFunc adds a custom permission validator function.\n// If an existing permission function exists it's ovewritten by the new one.\nfunc (ps *Permissions) ValidateFunc(p boards.Permission, fn ValidatorFunc) {\n\tps.validators.Set(string(p), 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 = avl.NewTree()\n\tfor _, p := range permissions {\n\t\tps.public.Set(string(p), struct{}{})\n\t}\n}\n\n// AddRole add 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\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 member group for the role if it exists or otherwise create a new group\n\tgrouping := ps.dao.Members().Grouping()\n\tname := string(r)\n\tgroup, found := grouping.Get(name)\n\tif !found {\n\t\tvar err error\n\t\tgroup, err = grouping.Add(name)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\t// Save permissions within the member group overwritting any existing permissions\n\tgroup.SetMeta(append([]boards.Permission{p}, extra...))\n}\n\n// RoleExists checks if a role exists.\nfunc (ps Permissions) RoleExists(r boards.Role) bool {\n\treturn r == ps.superRole || ps.dao.Members().Grouping().Has(string(r))\n}\n\n// GetUserRoles returns the list of roles assigned to a user.\nfunc (ps Permissions) GetUserRoles(user address) []boards.Role {\n\tgroups := storage.GetMemberGroups(ps.dao.Members(), user)\n\tif groups == nil {\n\t\treturn nil\n\t}\n\n\troles := make([]boards.Role, len(groups))\n\tfor i, name := range groups {\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\tname := string(r)\n\tgroup, found := ps.dao.Members().Grouping().Get(name)\n\tif !found {\n\t\treturn false\n\t}\n\treturn group.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(string(perm)) {\n\t\treturn true\n\t}\n\n\tgroups := storage.GetMemberGroups(ps.dao.Members(), user)\n\tif groups == nil {\n\t\treturn false\n\t}\n\n\tgrouping := ps.dao.Members().Grouping()\n\tfor _, name := range groups {\n\t\trole := boards.Role(name)\n\t\tif ps.superRole == role {\n\t\t\treturn true\n\t\t}\n\n\t\tgroup, found := grouping.Get(name)\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\n\t\tmeta := group.GetMeta()\n\t\tfor _, p := range meta.([]boards.Permission) {\n\t\t\tif p == perm {\n\t\t\t\treturn true\n\t\t\t}\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\tgroups := storage.GetMemberGroups(ps.dao.Members(), user)\n\tisGuest := len(roles) == 0\n\n\t// Clear current user roles\n\tgrouping := ps.dao.Members().Grouping()\n\tfor _, name := range groups {\n\t\tgroup, found := grouping.Get(name)\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\n\t\tgroup.Members().Remove(user)\n\t}\n\n\t// Add user to the storage as guest when no roles are assigned\n\tif isGuest {\n\t\tps.dao.Members().Add(user)\n\t\treturn\n\t}\n\n\t// Add user to role groups\n\tfor _, r := range roles {\n\t\tname := string(r)\n\t\tgroup, found := grouping.Get(name)\n\t\tif !found {\n\t\t\tpanic(\"invalid role: \" + name)\n\t\t}\n\n\t\tgroup.Members().Add(user)\n\t}\n}\n\n// RemoveUser removes a user from permissions.\nfunc (ps *Permissions) RemoveUser(user address) bool {\n\tgroups := storage.GetMemberGroups(ps.dao.Members(), user)\n\tif groups == nil {\n\t\treturn ps.dao.Members().Remove(user)\n\t}\n\n\tgrouping := ps.dao.Members().Grouping()\n\tfor _, name := range groups {\n\t\tgroup, found := grouping.Get(name)\n\t\tif !found {\n\t\t\tcontinue\n\t\t}\n\n\t\tgroup.Members().Remove(user)\n\t}\n\treturn true\n}\n\n// HasUser checks if a user exists.\nfunc (ps Permissions) HasUser(user address) bool {\n\treturn ps.dao.Members().Has(user)\n}\n\n// UsersCount returns the total number of users the permissioner contains.\nfunc (ps Permissions) UsersCount() int {\n\treturn ps.dao.Members().Size()\n}\n\n// IterateUsers iterates permissions' users.\nfunc (ps Permissions) IterateUsers(start, count int, fn boards.UsersIterFn) (stopped bool) {\n\tps.dao.Members().IterateByOffset(start, count, func(addr address) bool {\n\t\tuser := boards.User{Address: addr}\n\t\tgroups := storage.GetMemberGroups(ps.dao.Members(), addr)\n\t\tif groups != nil {\n\t\t\tuser.Roles = make([]boards.Role, len(groups))\n\t\t\tfor i, name := range groups {\n\t\t\t\tuser.Roles[i] = boards.Role(name)\n\t\t\t}\n\t\t}\n\n\t\treturn fn(user)\n\t})\n\treturn\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\")\n\t}\n\n\t// Execute custom validation before calling the callback\n\tv, found := ps.validators.Get(string(p))\n\tif found {\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\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nvar _ boards.Permissions = (*Permissions)(nil)\n\nfunc TestBasicPermissionsWithPermission(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: \"bar\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", \"bar\")\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: \"bar\",\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\", \"bar\")\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: \"bar\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", \"bar\")\n\t\t\t\tperms.SetUserRoles(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\t\t\treturn perms\n\t\t\t},\n\t\t\terr: \"unauthorized\",\n\t\t},\n\t\t{\n\t\t\tname:       \"is not a DAO member\",\n\t\t\tuser:       \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\",\n\t\t\tpermission: \"bar\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\treturn New()\n\t\t\t},\n\t\t\terr: \"unauthorized\",\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, tc.err, testCaseFn, \"expect panic with message\")\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\turequire.NotPanics(t, 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\", \"fooPerm\", \"barPerm\", \"bazPerm\")\n\turequire.False(t, perms.HasPermission(user, \"fooPerm\"))\n\turequire.False(t, perms.HasPermission(user, \"barPerm\"))\n\turequire.False(t, perms.HasPermission(user, \"bazPerm\"))\n\n\t// Assign a couple of public permissions\n\tperms.SetPublicPermissions(\"fooPerm\", \"bazPerm\")\n\turequire.True(t, perms.HasPermission(user, \"fooPerm\"))\n\turequire.False(t, perms.HasPermission(user, \"barPerm\"))\n\turequire.True(t, perms.HasPermission(user, \"bazPerm\"))\n\n\t// Clear all public permissions\n\tperms.SetPublicPermissions()\n\turequire.False(t, perms.HasPermission(user, \"fooPerm\"))\n\turequire.False(t, perms.HasPermission(user, \"barPerm\"))\n\turequire.False(t, perms.HasPermission(user, \"bazPerm\"))\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\", \"x\")\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\", \"x\")\n\t\t\t\tperms.AddRole(\"foo\", \"x\")\n\t\t\t\tperms.AddRole(\"bar\", \"x\")\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\", \"x\")\n\t\t\t\tperms.AddRole(\"bar\", \"x\")\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\", \"x\")\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\", \"x\")\n\t\t\t\tperms.AddRole(\"foo\", \"x\")\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\", \"x\")\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, got, tc.want)\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: \"bar\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", \"bar\")\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: \"bar\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", \"bar\")\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: \"other\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", \"bar\")\n\t\t\t\tperms.AddRole(\"baz\", \"other\")\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: \"other\",\n\t\t\tsetup: func() *Permissions {\n\t\t\t\tperms := New()\n\t\t\t\tperms.AddRole(\"foo\", \"bar\")\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(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\", \"permission1\")\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\", \"permission1\")\n\t\t\t\tperms.AddRole(\"b\", \"permission2\")\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\", \"permission1\")\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\", \"permission1\")\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\", \"permission1\")\n\t\t\t\tperms.AddRole(\"b\", \"permission2\")\n\t\t\t\tperms.AddRole(\"c\", \"permission2\")\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\", \"permission1\")\n\t\t\t\tperms.AddRole(\"b\", \"permission2\")\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\", \"permission1\")\n\t\t\t\tperms.AddRole(\"b\", \"permission2\")\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\", \"permission1\")\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\", \"permission1\")\n\t\t\t\tperms.AddRole(\"b\", \"permission2\")\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, tc.err, setUserRoles, \"expected an error\")\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\turequire.NotPanics(t, 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 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 TestBasicPermissionsIterateUsers(t *testing.T) {\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}\n\n\tperms := New()\n\tperms.AddRole(\"foo\", \"perm1\")\n\tperms.AddRole(\"bar\", \"perm2\")\n\tfor _, u := range users {\n\t\tperms.SetUserRoles(u.Address, u.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:  3,\n\t\t},\n\t\t{\n\t\t\tname:  \"exact users count\",\n\t\t\tcount: 3,\n\t\t\twant:  3,\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},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar i int\n\t\t\tperms.IterateUsers(0, len(users), func(u boards.User) bool {\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\ti++\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tuassert.Equal(t, i, len(users), \"expect iterator to iterate all users\")\n\t\t})\n\t}\n}\n"
                      },
                      {
                        "name": "z_readme_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/gnoland/boards/exts/permissions\"\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 = \"foo\"\n\nfunc main() {\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 := permissions.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\n// Output:\n// Hello Bob!\n"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "rA1JE3C9y6jEiO41ZiQHaZ4DFHsdK0WmM3bvz6I53qVFC+fk8hqPtQ7QsPYlBJ8FladFln1Mv2famTqCFc1Nbw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "mdform",
                    "path": "gno.land/p/jeronimoalbi/mdform",
                    "files": [
                      {
                        "name": "README.md",
                        "body": "# Markdown Form Package\n\nThe package provides a very simplistic [Gno-Flavored Markdown form](/r/docs/markdown#forms) generator.\n\nForms can be created by sequentially calling form methods to create each one of the form fields.\n\nExample usage:\n\n```go\nimport \"gno.land/p/jeronimoalbi/mdform\"\n\nfunc Render(string) string {\n    form := mdform.New()\n\n    // Add a text input field\n    form.Input(\n        \"name\",\n        \"placeholder\", \"Name\",\n        \"value\", \"John Doe\",\n    )\n\n    // Add a select field with three possible values\n    form.Select(\n        \"country\",\n        \"United States\",\n        \"description\", \"Select your country\",\n    )\n    form.Select(\n        \"country\",\n        \"Spain\",\n    )\n    form.Select(\n        \"country\",\n        \"Germany\",\n    )\n\n    // Add a checkbox group with two possible values\n    form.Checkbox(\n        \"interests\",\n        \"music\",\n        \"description\", \"What do you like to do?\",\n    )\n    form.Checkbox(\n        \"interests\",\n        \"tech\",\n        \"checked\", \"true\",\n    )\n\n    return form.String()\n}\n```\n\nForm output:\n\n```html\n\u003cgno-form exec=\"FunctionName\"\u003e\n    \u003cgno-input name=\"name\" placeholder=\"Name\" value=\"John Doe\" /\u003e\n    \u003cgno-select name=\"country\" value=\"United States\" description=\"Select your country\" /\u003e\n    \u003cgno-select name=\"country\" value=\"Spain\" /\u003e\n    \u003cgno-select name=\"country\" value=\"Germany\" /\u003e\n    \u003cgno-input type=\"checkbox\" name=\"interests\" value=\"music\" description=\"What do you like to do?\" /\u003e\n    \u003cgno-input type=\"checkbox\" name=\"interests\" value=\"tech\" checked=\"true\" /\u003e\n\u003c/gno-form\u003e\n```\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/jeronimoalbi/mdform\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "mdform.gno",
                        "body": "package mdform\n\nimport (\n\t\"html\"\n\t\"strings\"\n)\n\nconst (\n\tInputTypeText     = \"text\"\n\tInputTypeNumber   = \"number\"\n\tInputTypeEmail    = \"email\"\n\tInputTypePhone    = \"tel\"\n\tInputTypePassword = \"password\"\n\tInputTypeRadio    = \"radio\"\n\tInputTypeCheckbox = \"checkbox\"\n)\n\nvar (\n\tformAttributes     = []string{\"exec\", \"path\"}\n\tinputAttributes    = []string{\"checked\", \"description\", \"placeholder\", \"readonly\", \"required\", \"type\", \"value\"}\n\ttextareaAttributes = []string{\"placeholder\", \"readonly\", \"required\", \"rows\", \"value\"}\n\tselectAttributes   = []string{\"description\", \"readonly\", \"required\", \"selected\"}\n)\n\n// New creates a new form.\nfunc New(attributes ...string) *Form {\n\tassertEvenAttributes(attributes)\n\n\tform := \u0026Form{}\n\tfor i := 0; i \u003c len(attributes); i += 2 {\n\t\tname, value := attributes[i], attributes[i+1]\n\n\t\tassertIsValidAttribute(name, formAttributes)\n\n\t\tform.attrs = append(form.attrs, formatAttribute(name, value))\n\t}\n\treturn form\n}\n\n// Form is a form that can be rendered to Gno-Flavored Markdown.\ntype Form struct {\n\tattrs  []string\n\tfields []string\n}\n\n// Input appends a new input to form fields.\n// Use `Form.Radio()` or `Form.Checkbox()` to append those types of inputs to the form.\n// Method panics when appending inputs of type radio or checkbox, or when attributes are not valid.\nfunc (f *Form) Input(name string, attributes ...string) *Form {\n\tname = strings.TrimSpace(name)\n\tif name == \"\" {\n\t\tpanic(\"form input name is required\")\n\t}\n\n\tassertEvenAttributes(attributes)\n\n\tattrs := []string{formatAttribute(\"name\", name)}\n\tfor i := 0; i \u003c len(attributes); i += 2 {\n\t\tname, value := attributes[i], attributes[i+1]\n\t\tif name == \"type\" {\n\t\t\tswitch value {\n\t\t\tcase InputTypeRadio:\n\t\t\t\tpanic(\"use form.Radio() to create inputs of type radio\")\n\t\t\tcase InputTypeCheckbox:\n\t\t\t\tpanic(\"use form.Checkbox() to create inputs of type checkbox\")\n\t\t\t}\n\t\t}\n\n\t\tassertIsValidAttribute(name, inputAttributes)\n\n\t\tattrs = append(attrs, formatAttribute(name, value))\n\t}\n\n\tf.fields = append(f.fields, \"\u003cgno-input \"+strings.Join(attrs, \" \")+\" /\u003e\")\n\treturn f\n}\n\n// Radio appends a new input of type radio to form fields.\n// Method panics when attributes are not valid.\nfunc (f *Form) Radio(name, value string, attributes ...string) *Form {\n\treturn f.appendInputType(InputTypeRadio, name, value, attributes...)\n}\n\n// Checkbox appends a new input of type checkbox to form fields.\n// Method panics when attributes are not valid.\nfunc (f *Form) Checkbox(name, value string, attributes ...string) *Form {\n\treturn f.appendInputType(InputTypeCheckbox, name, value, attributes...)\n}\n\n// Textarea appends a new textarea to form fields.\n// Method panics when attributes are not valid.\nfunc (f *Form) Textarea(name string, attributes ...string) *Form {\n\tname = strings.TrimSpace(name)\n\tif name == \"\" {\n\t\tpanic(\"form textarea name is required\")\n\t}\n\n\tassertEvenAttributes(attributes)\n\n\tattrs := []string{formatAttribute(\"name\", name)}\n\tfor i := 0; i \u003c len(attributes); i += 2 {\n\t\tname, value := attributes[i], attributes[i+1]\n\n\t\tassertIsValidAttribute(name, textareaAttributes)\n\n\t\tattrs = append(attrs, formatAttribute(name, value))\n\t}\n\n\tf.fields = append(f.fields, \"\u003cgno-textarea \"+strings.Join(attrs, \" \")+\" /\u003e\")\n\treturn f\n}\n\n// Select appends a new select to form fields.\n// Method panics when attributes are not valid.\nfunc (f *Form) Select(name, value string, attributes ...string) *Form {\n\tname = strings.TrimSpace(name)\n\tif name == \"\" {\n\t\tpanic(\"form select name is required\")\n\t}\n\n\tassertEvenAttributes(attributes)\n\n\tattrs := []string{\n\t\tformatAttribute(\"name\", name),\n\t\tformatAttribute(\"value\", value),\n\t}\n\n\tfor i := 0; i \u003c len(attributes); i += 2 {\n\t\tname, value := attributes[i], attributes[i+1]\n\n\t\tassertIsValidAttribute(name, selectAttributes)\n\n\t\tattrs = append(attrs, formatAttribute(name, value))\n\t}\n\n\tf.fields = append(f.fields, \"\u003cgno-select \"+strings.Join(attrs, \" \")+\" /\u003e\")\n\treturn f\n}\n\n// String returns the form as Gno-Flavored Markdown.\nfunc (f Form) String() string {\n\tfields := strings.Join(f.fields, \"\\n\")\n\tattrs := strings.Join(f.attrs, \" \")\n\tif len(attrs) \u003e 0 {\n\t\tattrs = \" \" + attrs\n\t}\n\n\treturn \"\u003cgno-form\" + attrs + \"\u003e\\n\" + fields + \"\\n\u003c/gno-form\u003e\\n\"\n}\n\nfunc (f *Form) appendInputType(typeName, name, value string, attributes ...string) *Form {\n\tname = strings.TrimSpace(name)\n\tif name == \"\" {\n\t\tpanic(\"form \" + typeName + \" input name is required\")\n\t}\n\n\tassertEvenAttributes(attributes)\n\n\tattrs := []string{\n\t\tformatAttribute(\"type\", typeName),\n\t\tformatAttribute(\"name\", name),\n\t\tformatAttribute(\"value\", value),\n\t}\n\n\tfor i := 0; i \u003c len(attributes); i += 2 {\n\t\tname, value := attributes[i], attributes[i+1]\n\t\tif name == \"type\" || name == \"value\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tassertIsValidAttribute(name, inputAttributes)\n\n\t\tattrs = append(attrs, formatAttribute(name, value))\n\t}\n\n\tf.fields = append(f.fields, \"\u003cgno-input \"+strings.Join(attrs, \" \")+\" /\u003e\")\n\treturn f\n}\n\nfunc formatAttribute(name, value string) string {\n\treturn name + `=\"` + html.EscapeString(value) + `\"`\n}\n\nfunc assertEvenAttributes(attrs []string) {\n\tif len(attrs)%2 != 0 {\n\t\tpanic(\"expected an even number of attribute arguments\")\n\t}\n}\n\nfunc assertIsValidAttribute(attr string, attrs []string) {\n\tfor _, name := range attrs {\n\t\tif name == attr {\n\t\t\treturn\n\t\t}\n\t}\n\n\tpanic(\"invalid attribute: \" + attr)\n}\n"
                      },
                      {
                        "name": "mdform_filetest.gno",
                        "body": "package main\n\nimport \"gno.land/p/jeronimoalbi/mdform\"\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\"\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestNew(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, tc.err, fn, \"expect form to fail\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotPanics(t, 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(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, tc.err, fn, \"expect form input to fail\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotPanics(t, 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(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, 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, 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(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, 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, 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(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, tc.err, fn, \"expect form textarea to fail\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotPanics(t, 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(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, tc.err, fn, \"expect form textarea to fail\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotPanics(t, 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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "jhpdaN8bkb2uhoWJzrkUFyL9xulzDBJYrB70mpyF3/gW7aUZJuqCK/kYFzCc2vwn4M49oNpwcDCU2H7xTea+Rg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "pager",
                    "path": "gno.land/p/jeronimoalbi/pager",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/jeronimoalbi/pager\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "pager.gno",
                        "body": "// Package pager provides pagination functionality through a generic pager implementation.\n//\n// Example usage:\n//\n//\timport (\n//\t    \"strconv\"\n//\t    \"strings\"\n//\n//\t    \"gno.land/p/jeronimoalbi/pager\"\n//\t)\n//\n//\tfunc Render(path string) string {\n//\t    // Define the items to paginate\n//\t    items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n//\n//\t    // Create a pager that paginates 4 items at a time\n//\t    p, err := pager.New(path, len(items), pager.WithPageSize(4))\n//\t    if err != nil {\n//\t        panic(err)\n//\t    }\n//\n//\t    // Render items for the current page\n//\t    var output strings.Builder\n//\t    p.Iterate(func(i int) bool {\n//\t        output.WriteString(\"- \" + strconv.Itoa(items[i]) + \"\\n\")\n//\t        return false\n//\t    })\n//\n//\t    // Render page picker\n//\t    if p.HasPages() {\n//\t        output.WriteString(\"\\n\" + pager.Picker(p))\n//\t    }\n//\n//\t    return output.String()\n//\t}\npackage pager\n\nimport (\n\t\"errors\"\n\t\"math\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar ErrInvalidPageNumber = errors.New(\"invalid page number\")\n\n// PagerIterFn defines a callback to iterate page items.\ntype PagerIterFn func(index int) (stop bool)\n\n// New creates a new pager.\nfunc New(rawURL string, totalItems int, options ...PagerOption) (Pager, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn Pager{}, err\n\t}\n\n\tp := Pager{\n\t\tquery:          u.RawQuery,\n\t\tpageQueryParam: DefaultPageQueryParam,\n\t\tpageSize:       DefaultPageSize,\n\t\tpage:           1,\n\t\ttotalItems:     totalItems,\n\t}\n\tfor _, apply := range options {\n\t\tapply(\u0026p)\n\t}\n\n\tp.pageCount = int(math.Ceil(float64(p.totalItems) / float64(p.pageSize)))\n\n\trawPage := u.Query().Get(p.pageQueryParam)\n\tif rawPage != \"\" {\n\t\tp.page, _ = strconv.Atoi(rawPage)\n\t\tif p.page == 0 || p.page \u003e p.pageCount {\n\t\t\treturn Pager{}, ErrInvalidPageNumber\n\t\t}\n\t}\n\n\treturn p, nil\n}\n\n// MustNew creates a new pager or panics if there is an error.\nfunc MustNew(rawURL string, totalItems int, options ...PagerOption) Pager {\n\tp, err := New(rawURL, totalItems, options...)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn p\n}\n\n// Pager allows paging items.\ntype Pager struct {\n\tquery, pageQueryParam                 string\n\tpageSize, page, pageCount, totalItems int\n}\n\n// TotalItems returns the total number of items to paginate.\nfunc (p Pager) TotalItems() int {\n\treturn p.totalItems\n}\n\n// PageSize returns the size of each page.\nfunc (p Pager) PageSize() int {\n\treturn p.pageSize\n}\n\n// Page returns the current page number.\nfunc (p Pager) Page() int {\n\treturn p.page\n}\n\n// PageCount returns the number pages.\nfunc (p Pager) PageCount() int {\n\treturn p.pageCount\n}\n\n// Offset returns the index of the first page item.\nfunc (p Pager) Offset() int {\n\treturn (p.page - 1) * p.pageSize\n}\n\n// HasPages checks if pager has more than one page.\nfunc (p Pager) HasPages() bool {\n\treturn p.pageCount \u003e 1\n}\n\n// GetPageURI returns the URI for a page.\n// An empty string is returned when page doesn't exist.\nfunc (p Pager) GetPageURI(page int) string {\n\tif page \u003c 1 || page \u003e p.PageCount() {\n\t\treturn \"\"\n\t}\n\n\tvalues, _ := url.ParseQuery(p.query)\n\tvalues.Set(p.pageQueryParam, strconv.Itoa(page))\n\treturn \"?\" + values.Encode()\n}\n\n// PrevPageURI returns the URI path to the previous page.\n// An empty string is returned when current page is the first page.\nfunc (p Pager) PrevPageURI() string {\n\tif p.page == 1 || !p.HasPages() {\n\t\treturn \"\"\n\t}\n\treturn p.GetPageURI(p.page - 1)\n}\n\n// NextPageURI returns the URI path to the next page.\n// An empty string is returned when current page is the last page.\nfunc (p Pager) NextPageURI() string {\n\tif p.page == p.pageCount {\n\t\t// Current page is the last page\n\t\treturn \"\"\n\t}\n\treturn p.GetPageURI(p.page + 1)\n}\n\n// Iterate allows iterating page items.\nfunc (p Pager) Iterate(fn PagerIterFn) bool {\n\tif p.totalItems == 0 {\n\t\treturn true\n\t}\n\n\tstart := p.Offset()\n\tend := start + p.PageSize()\n\tif end \u003e p.totalItems {\n\t\tend = p.totalItems\n\t}\n\n\tfor i := start; i \u003c end; i++ {\n\t\tif fn(i) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// TODO: Support different types of pickers (ex. with clickable page numbers)\n\n// Picker returns a string with the pager as Markdown.\n// An empty string is returned when the pager has no pages.\nfunc Picker(p Pager) string {\n\tif !p.HasPages() {\n\t\treturn \"\"\n\t}\n\n\tvar out strings.Builder\n\n\tif s := p.PrevPageURI(); s != \"\" {\n\t\tout.WriteString(\"[«](\" + s + \") | \")\n\t} else {\n\t\tout.WriteString(\"\\\\- | \")\n\t}\n\n\tout.WriteString(\"page \" + strconv.Itoa(p.Page()) + \" of \" + strconv.Itoa(p.PageCount()))\n\n\tif s := p.NextPageURI(); s != \"\" {\n\t\tout.WriteString(\" | [»](\" + s + \")\")\n\t} else {\n\t\tout.WriteString(\" | \\\\-\")\n\t}\n\n\treturn out.String()\n}\n"
                      },
                      {
                        "name": "pager_options.gno",
                        "body": "package pager\n\nimport \"strings\"\n\nconst (\n\tDefaultPageSize       = 50\n\tDefaultPageQueryParam = \"page\"\n)\n\n// PagerOption configures the pager.\ntype PagerOption func(*Pager)\n\n// WithPageSize assigns a page size to a pager.\nfunc WithPageSize(size int) PagerOption {\n\treturn func(p *Pager) {\n\t\tif size \u003c 1 {\n\t\t\tp.pageSize = DefaultPageSize\n\t\t} else {\n\t\t\tp.pageSize = size\n\t\t}\n\t}\n}\n\n// WithPageQueryParam assigns the name of the URL query param for the page value.\nfunc WithPageQueryParam(name string) PagerOption {\n\treturn func(p *Pager) {\n\t\tname = strings.TrimSpace(name)\n\t\tif name == \"\" {\n\t\t\tname = DefaultPageQueryParam\n\t\t}\n\t\tp.pageQueryParam = name\n\t}\n}\n"
                      },
                      {
                        "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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "kOAc9bNCERkbBbI5ZhU8yThYGZ5nS95Rq52VYyFEOh12jdu5emisahr+vqGIhCfBz2eIRZCDqhJZSilmTkqvbQ=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5",
                  "package": {
                    "name": "coinsort",
                    "path": "gno.land/p/leon/coinsort",
                    "files": [
                      {
                        "name": "coinsort.gno",
                        "body": "// Package coinsort provides helpers to sort a slice of banker.Coins using the\n// classic sort.Sort API (without relying on sort.Slice).\n//\n// Usage examples:\n//\n//\tcoins := banker.GetCoins(\"g1....\")\n//\n//\t// Ascending by balance\n//\tcoinsort.SortByBalance(coins)\n//\n//\t// Custom order – largest balance first\n//\tcoinsort.SortBy(coins, func(a, b std.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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "oTqwfRRirdEnTEcQ5yfAcl3THvXyC9PD8xFYtMyGBB5PUo6xehYLekChvUwXI5MMgmRfd9hnbG6+gWPzFXiS3A=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5",
                  "package": {
                    "name": "ctg",
                    "path": "gno.land/p/leon/ctg",
                    "files": [
                      {
                        "name": "converter.gno",
                        "body": "// Package ctg is a simple utility package with helpers\n// for bech32 address conversions.\npackage ctg\n\nimport (\n\t\"crypto/bech32\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// ConvertCosmosToGno takes a Bech32 Cosmos address (prefix \"cosmos\")\n// and returns the same address re-encoded with the gno.land prefix \"g\".\nfunc ConvertCosmosToGno(addr string) (address, error) {\n\tprefix, decoded, err := bech32.Decode(addr)\n\tif err != nil {\n\t\treturn \"\", ufmt.Errorf(\"bech32 decode failed: %v\", err)\n\t}\n\n\tif prefix != \"cosmos\" {\n\t\treturn \"\", ufmt.Errorf(\"expected a cosmos address, got prefix %q\", prefix)\n\t}\n\n\treturn address(mustEncode(\"g\", decoded)), nil\n}\n\nfunc mustEncode(hrp string, data []byte) string {\n\tenc, err := bech32.Encode(hrp, data)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn enc\n}\n\n// ConvertAnyToGno converts *any* valid Bech32 address to its gno.land form\n// by preserving the underlying payload but replacing the prefix with \"g\".\n// No prefix check is performed; invalid Bech32 input still returns an error.\nfunc ConvertAnyToGno(addr string) (address, error) {\n\t_, decoded, err := bech32.Decode(addr)\n\tif err != nil {\n\t\treturn \"\", ufmt.Errorf(\"bech32 decode failed: %v\", err)\n\t}\n\treturn address(mustEncode(\"g\", decoded)), nil\n}\n\n// ConvertGnoToAny converts a gno.land address (prefixed with \"g\") to another Bech32\n// prefix given by prefix. The function ensures the source address really\n// is a gno.land address before proceeding.\n//\n// Example:\n//\n//\tcosmosAddr, _ := ConvertGnoToAny(\"cosmos\", \"g1k98jx9...\")\n//\tfmt.Println(cosmosAddr) // → cosmos1....\nfunc ConvertGnoToAny(prefix string, addr address) (string, error) {\n\torigPrefix, decoded, err := bech32.Decode(string(addr))\n\tif err != nil {\n\t\treturn \"\", ufmt.Errorf(\"bech32 decode failed: %v\", err)\n\t}\n\tif origPrefix != \"g\" {\n\t\treturn \"\", ufmt.Errorf(\"expected a gno address but got prefix %q\", origPrefix)\n\t}\n\treturn mustEncode(prefix, decoded), nil\n}\n"
                      },
                      {
                        "name": "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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "DQml+RSRtDkS326hCqvzqvCnDQFMt22QdbnCSwRqQApSn4Aaq7h1PRVSTKOW4K1UEVDfQJScA4dsgWe13gFcBg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5",
                  "package": {
                    "name": "svgbtn",
                    "path": "gno.land/p/leon/svgbtn",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/leon/svgbtn\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\"\n"
                      },
                      {
                        "name": "svgbtn.gno",
                        "body": "// Package svgbtn provides utilities for generating SVG-styled buttons as Markdown image links.\n//\n// Buttons are rendered as SVG images with customizable size, colors, labels, and links.\n// This package includes preconfigured styles such as Primary, Danger, Success, Small, Wide,\n// Text-like, and Icon buttons, as well as a factory method for dynamic button creation.\n//\n// Example usage:\n//\n//\tfunc Render(_ string) string {\n//\t\tbtn := svgbtn.PrimaryButton(120, 40, \"Click Me\", \"https://example.com\")\n//\t\treturn btn\n//\t}\n//\n// See more examples at gno.land/r/leon:buttons\n//\n// All buttons are returned as Markdown-compatible strings: [svg_data](link).\npackage svgbtn\n\nimport (\n\t\"gno.land/p/demo/svg\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Button creates a base SVG button with given size, colors, label, and link.\n// - `width`, `height`: size in pixels\n// - `btnColor`: background color (e.g. \"#007BFF\")\n// - `textColor`: label color (e.g. \"#FFFFFF\")\n// - `text`: visible button label\n// - `link`: URL to wrap the image in markdown-style [svg](link)\nfunc Button(width, height int, btnColor, textColor, text, link string) string {\n\treturn ButtonWithRadius(width, height, height/5, btnColor, textColor, text, link)\n}\n\n// ButtonWithRadius creates a base SVG button with custom border radius.\n// - `width`, `height`: size in pixels\n// - `radius`: border radius in pixels\n// - `btnColor`: background color (e.g. \"#007BFF\")\n// - `textColor`: label color (e.g. \"#FFFFFF\")\n// - `text`: visible button label\n// - `link`: URL to wrap the image in markdown-style [svg](link)\nfunc ButtonWithRadius(width, height, radius int, btnColor, textColor, text, link string) string {\n\tcanvas := svg.NewCanvas(width, height).\n\t\tWithViewBox(0, 0, width, height).\n\t\tAddStyle(\"text\", \"font-family:sans-serif;font-size:14px;text-anchor:middle;dominant-baseline:middle;\")\n\n\tbg := svg.NewRectangle(0, 0, width, height, btnColor)\n\tbg.RX = radius\n\tbg.RY = radius\n\n\tlabel := svg.NewText(width/2, height/2, text, textColor)\n\n\tcanvas.Append(bg, label)\n\n\treturn ufmt.Sprintf(\"[%s](%s)\", canvas.Render(text), link)\n}\n\n// PrimaryButton renders a blue button with white text.\nfunc PrimaryButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#007BFF\", \"#ffffff\", text, link)\n}\n\n// DangerButton renders a red button with white text.\nfunc DangerButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#DC3545\", \"#ffffff\", text, link)\n}\n\n// SuccessButton renders a green button with white text.\nfunc SuccessButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#28A745\", \"#ffffff\", text, link)\n}\n\n// SmallButton renders a compact gray button with white text.\nfunc SmallButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#6C757D\", \"#ffffff\", text, link)\n}\n\n// WideButton renders a wider cyan button with white text.\nfunc WideButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#17A2B8\", \"#ffffff\", text, link)\n}\n\n// TextButton renders a white button with colored text, like a hyperlink.\nfunc TextButton(width, height int, text, link string) string {\n\treturn Button(width, height, \"#ffffff\", \"#007BFF\", text, link)\n}\n\n// IconButton renders a square button with an icon character (e.g. emoji).\nfunc IconButton(width, height int, icon, link string) string {\n\treturn Button(width, height, \"#E0E0E0\", \"#000000\", icon, link)\n}\n\n// ButtonFactory provides a named-style constructor for buttons.\n// Supported kinds: \"primary\", \"danger\", \"success\", \"small\", \"wide\", \"text\", \"icon\".\nfunc ButtonFactory(kind string, width, height int, text, link string) string {\n\tswitch kind {\n\tcase \"primary\":\n\t\treturn PrimaryButton(width, height, text, link)\n\tcase \"danger\":\n\t\treturn DangerButton(width, height, text, link)\n\tcase \"success\":\n\t\treturn SuccessButton(width, height, text, link)\n\tcase \"small\":\n\t\treturn SmallButton(width, height, text, link)\n\tcase \"wide\":\n\t\treturn WideButton(width, height, text, link)\n\tcase \"text\":\n\t\treturn TextButton(width, height, text, link)\n\tcase \"icon\":\n\t\treturn IconButton(width, height, text, link)\n\tdefault:\n\t\treturn PrimaryButton(width, height, text, link)\n\t}\n}\n"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "107VVEt371gFUH+aTWI4u0y1cmcRt0vQ8b7VZ/ov6osVubpOrcVxg4pPG8/1Cgh/4UV/wogQJfk1L5LF0hv2zw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "md",
                    "path": "gno.land/p/mason/md",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/mason/md\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "md.gno",
                        "body": "package md\n\nimport (\n\t\"strings\"\n)\n\ntype MD struct {\n\telements []string\n}\n\nfunc New() *MD {\n\treturn \u0026MD{elements: []string{}}\n}\n\nfunc (m *MD) H1(text string) {\n\tm.elements = append(m.elements, \"# \"+text)\n}\n\nfunc (m *MD) H3(text string) {\n\tm.elements = append(m.elements, \"### \"+text)\n}\n\nfunc (m *MD) P(text string) {\n\tm.elements = append(m.elements, text)\n}\n\nfunc (m *MD) Code(text string) {\n\tm.elements = append(m.elements, \"  ```\\n\"+text+\"\\n```\\n\")\n}\n\nfunc (m *MD) Im(path string, caption string) {\n\tm.elements = append(m.elements, \"![\"+caption+\"](\"+path+\" \\\"\"+caption+\"\\\")\")\n}\n\nfunc (m *MD) Bullet(point string) {\n\tm.elements = append(m.elements, \"- \"+point)\n}\n\nfunc Link(text, url string, title ...string) string {\n\tif len(title) \u003e 0 \u0026\u0026 title[0] != \"\" {\n\t\treturn \"[\" + text + \"](\" + url + \" \\\"\" + title[0] + \"\\\")\"\n\t}\n\treturn \"[\" + text + \"](\" + url + \")\"\n}\n\nfunc (m *MD) Render() string {\n\treturn strings.Join(m.elements, \"\\n\\n\")\n}\n"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "nrCQuagWwAIrPmgmOhfDcj8SXg+TDuOBvBuNGumOY1Ea5h2fbEvM6TUyki764RsVLjbcR66QO901PQyNkzYneA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
                  "package": {
                    "name": "once",
                    "path": "gno.land/p/moul/once",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/moul/once\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"
                      },
                      {
                        "name": "once.gno",
                        "body": "// Package once provides utilities for one-time execution patterns.\n// It extends the concept of sync.Once with error handling and panic options.\npackage once\n\nimport (\n\t\"errors\"\n)\n\n// Once represents a one-time execution guard\ntype Once struct {\n\tdone    bool\n\terr     error\n\tpaniced bool\n\tvalue   any // stores the result of the execution\n}\n\n// New creates a new Once instance\nfunc New() *Once {\n\treturn \u0026Once{}\n}\n\n// Do executes fn only once and returns nil on subsequent calls\nfunc (o *Once) Do(fn func()) {\n\tif o.done {\n\t\treturn\n\t}\n\tdefer func() { o.done = true }()\n\tfn()\n}\n\n// DoErr executes fn only once and returns the same error on subsequent calls\nfunc (o *Once) DoErr(fn func() error) error {\n\tif o.done {\n\t\treturn o.err\n\t}\n\tdefer func() { o.done = true }()\n\to.err = fn()\n\treturn o.err\n}\n\n// DoOrPanic executes fn only once and panics on subsequent calls\nfunc (o *Once) DoOrPanic(fn func()) {\n\tif o.done {\n\t\tpanic(\"once: multiple execution attempted\")\n\t}\n\tdefer func() { o.done = true }()\n\tfn()\n}\n\n// DoValue executes fn only once and returns its value, subsequent calls return the cached value\nfunc (o *Once) DoValue(fn func() any) any {\n\tif o.done {\n\t\treturn o.value\n\t}\n\tdefer func() { o.done = true }()\n\to.value = fn()\n\treturn o.value\n}\n\n// DoValueErr executes fn only once and returns its value and error\n// Subsequent calls return the cached value and error\nfunc (o *Once) DoValueErr(fn func() (any, error)) (any, error) {\n\tif o.done {\n\t\treturn o.value, o.err\n\t}\n\tdefer func() { o.done = true }()\n\to.value, o.err = fn()\n\treturn o.value, o.err\n}\n\n// Reset resets the Once instance to its initial state\n// This is mainly useful for testing purposes\nfunc (o *Once) Reset() {\n\to.done = false\n\to.err = nil\n\to.paniced = false\n\to.value = nil\n}\n\n// IsDone returns whether the Once has been executed\nfunc (o *Once) IsDone() bool {\n\treturn o.done\n}\n\n// Error returns the error from the last execution if any\nfunc (o *Once) Error() error {\n\treturn o.err\n}\n\nvar (\n\tErrNotExecuted = errors.New(\"once: not executed yet\")\n)\n\n// Value returns the stored value and an error if not executed yet\nfunc (o *Once) Value() (any, error) {\n\tif !o.done {\n\t\treturn nil, ErrNotExecuted\n\t}\n\treturn o.value, nil\n}\n"
                      },
                      {
                        "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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "XcRfw/GK75ekkms8iqSOJCJb3yKg1IUif6cDQrjtuC9Hw/IUwNrwUaw3z0oehP9KTzUOGxmXLh32EfbakaXvWg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
                  "package": {
                    "name": "authz",
                    "path": "gno.land/p/moul/authz",
                    "files": [
                      {
                        "name": "authz.gno",
                        "body": "// Package authz provides flexible authorization control for privileged actions.\n//\n// # Authorization Strategies\n//\n// The package supports multiple authorization strategies:\n//   - Member-based: Single user or team of users\n//   - Contract-based: Async authorization (e.g., via DAO)\n//   - Auto-accept: Allow all actions\n//   - Drop: Deny all actions\n//\n// Core Components\n//\n//   - Authority interface: Base interface implemented by all authorities\n//   - Authorizer: Main wrapper object for authority management\n//   - MemberAuthority: Manages authorized addresses\n//   - ContractAuthority: Delegates to another contract\n//   - AutoAcceptAuthority: Accepts all actions\n//   - DroppedAuthority: Denies all actions\n//\n// Quick Start\n//\n//\t// Initialize with contract deployer as authority\n//\tvar member address(...)\n//\tvar auth = authz.NewWithMembers(member)\n//\n//\t// Create functions that require authorization\n//\tfunc UpdateConfig(newValue string) error {\n//\t\tcrossing()\n//\t\treturn auth.DoByPrevious(\"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\"chain/runtime\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/p/moul/once\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/avl/v0/rotree\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Authorizer is the main wrapper object that handles authority management.\n// It is configured with a replaceable Authority implementation.\ntype Authorizer struct {\n\tauth Authority\n}\n\n// Authority represents an entity that can authorize privileged actions.\n// It is implemented by MemberAuthority, ContractAuthority, AutoAcceptAuthority,\n// and DroppedAuthority.\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// NewWithCurrent creates a new Authorizer with the auth realm's address as authority\nfunc NewWithCurrent() *Authorizer {\n\treturn \u0026Authorizer{\n\t\tauth: NewMemberAuthority(runtime.CurrentRealm().Address()),\n\t}\n}\n\n// NewWithPrevious creates a new Authorizer with the previous realm's address as authority\nfunc NewWithPrevious() *Authorizer {\n\treturn \u0026Authorizer{\n\t\tauth: NewMemberAuthority(runtime.PreviousRealm().Address()),\n\t}\n}\n\n// NewWithCurrent creates a new Authorizer with the auth realm's address as authority\nfunc NewWithMembers(addrs ...address) *Authorizer {\n\treturn \u0026Authorizer{\n\t\tauth: NewMemberAuthority(addrs...),\n\t}\n}\n\n// NewWithOrigin creates a new Authorizer with the origin caller's address as\n// authority.\n// This is typically used in the init() function.\nfunc NewWithOrigin() *Authorizer {\n\torigin := runtime.OriginCaller()\n\tprevious := runtime.PreviousRealm()\n\tif origin != previous.Address() {\n\t\tpanic(\"NewWithOrigin() should be called from init() where runtime.PreviousRealm() is origin\")\n\t}\n\treturn \u0026Authorizer{\n\t\tauth: NewMemberAuthority(origin),\n\t}\n}\n\n// NewWithAuthority creates a new Authorizer with a specific authority\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\nfunc (a *Authorizer) Transfer(caller address, newAuthority Authority) error {\n\t// Ask auth authority to validate the transfer\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 by the auth realm.\nfunc (a *Authorizer) DoByCurrent(title string, action PrivilegedAction, args ...any) error {\n\tcurrent := runtime.CurrentRealm()\n\tcaller := current.Address()\n\treturn a.auth.Authorize(caller, title, action, args...)\n}\n\n// DoByPrevious executes a privileged action by the previous realm.\nfunc (a *Authorizer) DoByPrevious(title string, action PrivilegedAction, args ...any) error {\n\tprevious := runtime.PreviousRealm()\n\tcaller := previous.Address()\n\treturn a.auth.Authorize(caller, title, action, args...)\n}\n\n// String returns a string representation of the auth authority\nfunc (a *Authorizer) String() string {\n\tauthStr := a.auth.String()\n\n\tswitch a.auth.(type) {\n\tcase *MemberAuthority:\n\tcase *ContractAuthority:\n\tcase *AutoAcceptAuthority:\n\tcase *droppedAuthority:\n\tdefault:\n\t\t// this way official \"dropped\" is different from \"*custom*: dropped\" (autoclaimed).\n\t\treturn ufmt.Sprintf(\"custom_authority[%s]\", authStr)\n\t}\n\treturn authStr\n}\n\n// MemberAuthority is the default implementation using addrset for member\n// management.\ntype MemberAuthority struct {\n\tmembers addrset.Set\n}\n\nfunc NewMemberAuthority(members ...address) *MemberAuthority {\n\tauth := \u0026MemberAuthority{}\n\tfor _, addr := range members {\n\t\tauth.members.Add(addr)\n\t}\n\treturn auth\n}\n\nfunc (a *MemberAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {\n\tif !a.members.Has(caller) {\n\t\treturn errors.New(\"unauthorized\")\n\t}\n\n\tif err := action(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (a *MemberAuthority) String() string {\n\taddrs := []string{}\n\ta.members.Tree().Iterate(\"\", \"\", func(key string, _ any) bool {\n\t\taddrs = append(addrs, key)\n\t\treturn false\n\t})\n\taddrsStr := strings.Join(addrs, \",\")\n\treturn ufmt.Sprintf(\"member_authority[%s]\", addrsStr)\n}\n\n// AddMember adds a new member to the authority\nfunc (a *MemberAuthority) AddMember(caller address, addr address) error {\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\nfunc (a *MemberAuthority) AddMembers(caller address, addrs ...address) error {\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\nfunc (a *MemberAuthority) RemoveMember(caller address, addr address) error {\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\nfunc NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority {\n\treturn \u0026ContractAuthority{\n\t\tcontractPath:    path,\n\t\tcontractAddr:    chain.PackageAddress(path),\n\t\tcontractHandler: handler,\n\t\tproposer:        NewAutoAcceptAuthority(), // default: anyone can propose\n\t}\n}\n\n// NewRestrictedContractAuthority creates a new contract authority with a\n// proposer restriction.\nfunc NewRestrictedContractAuthority(path string, handler PrivilegedActionHandler, proposer Authority) Authority {\n\tif path == \"\" {\n\t\tpanic(\"contract path cannot be empty\")\n\t}\n\tif handler == nil {\n\t\tpanic(\"contract handler cannot be nil\")\n\t}\n\tif proposer == nil {\n\t\tpanic(\"proposer cannot be nil\")\n\t}\n\treturn \u0026ContractAuthority{\n\t\tcontractPath:    path,\n\t\tcontractAddr:    chain.PackageAddress(path),\n\t\tcontractHandler: handler,\n\t\tproposer:        proposer,\n\t}\n}\n\nfunc (a *ContractAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {\n\tif a.contractHandler == nil {\n\t\treturn errors.New(\"contract handler is not set\")\n\t}\n\n\t// setup a once instance to ensure the action is executed only once\n\texecutionOnce := once.Once{}\n\n\t// Wrap the action to ensure it can only be executed by the contract\n\twrappedAction := func() error {\n\t\tcurrent := runtime.CurrentRealm().Address()\n\t\tif current != a.contractAddr {\n\t\t\treturn errors.New(\"action can only be executed by the contract\")\n\t\t}\n\t\treturn executionOnce.DoErr(func() error {\n\t\t\treturn action()\n\t\t})\n\t}\n\n\t// Use the proposer authority to control who can create proposals\n\treturn a.proposer.Authorize(caller, title+\"_proposal\", func() error {\n\t\tif err := a.contractHandler(title, wrappedAction); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}, args...)\n}\n\nfunc (a *ContractAuthority) String() string {\n\treturn ufmt.Sprintf(\"contract_authority[contract=%s]\", a.contractPath)\n}\n\n// AutoAcceptAuthority implements an authority that accepts all actions\n// AutoAcceptAuthority is a simple authority that automatically accepts all\n// actions.\n// It can be used as a proposer authority to allow anyone to create proposals.\ntype AutoAcceptAuthority struct{}\n\nfunc NewAutoAcceptAuthority() *AutoAcceptAuthority {\n\treturn \u0026AutoAcceptAuthority{}\n}\n\nfunc (a *AutoAcceptAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {\n\treturn action()\n}\n\nfunc (a *AutoAcceptAuthority) String() string {\n\treturn \"auto_accept_authority\"\n}\n\n// droppedAuthority implements an authority that denies all actions\ntype droppedAuthority struct{}\n\nfunc NewDroppedAuthority() Authority {\n\treturn \u0026droppedAuthority{}\n}\n\nfunc (a *droppedAuthority) Authorize(caller address, title string, action PrivilegedAction, args ...any) error {\n\treturn errors.New(\"dropped authority: all actions are denied\")\n}\n\nfunc (a *droppedAuthority) String() string {\n\treturn \"dropped_authority\"\n}\n"
                      },
                      {
                        "name": "authz_test.gno",
                        "body": "package authz\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\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(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tauth := NewWithCurrent()\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(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(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tauth := NewWithCurrent()\n\n\t// Test successful action with args\n\texecuted := false\n\targs := []any{\"test_arg\", 123}\n\terr := auth.DoByCurrent(\"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(\"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(\"test_action\", func() error {\n\t\treturn expectedErr\n\t})\n\n\tuassert.True(t, err == expectedErr, \"expected specific error\")\n}\n\nfunc TestAuthorizerTransfer(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tauth := NewWithCurrent()\n\n\t// Test transfer to new member authority\n\tbob := testutils.TestAddress(\"bob\")\n\tnewAuth := NewMemberAuthority(bob)\n\n\terr := auth.Transfer(alice, newAuth)\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\n\ttesting.SetRealm(testing.NewUserRealm(bob)) // doesn't matter that it's bob\n\tcarol := testutils.TestAddress(\"carol\")\n\n\terr = auth.Transfer(carol, NewMemberAuthority(alice))\n\tuassert.True(t, err != nil, \"expected error\")\n\n\t// Test transfer to contract authority\n\tcontractAuth := NewContractAuthority(\"gno.land/r/test\", func(title string, action PrivilegedAction) error {\n\t\treturn action()\n\t})\n\n\terr = auth.Transfer(bob, contractAuth)\n\tuassert.True(t, err == nil, \"expected no error\")\n\tuassert.True(t, auth.Authority() == contractAuth, \"expected contract authority\")\n}\n\nfunc TestAuthorizerTransferChain(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\t// Create a chain of transfers\n\tauth := NewWithCurrent()\n\n\t// First transfer to a new member authority\n\tbob := testutils.TestAddress(\"bob\")\n\tmemberAuth := NewMemberAuthority(bob)\n\n\terr := auth.Transfer(alice, memberAuth)\n\tuassert.True(t, err == nil, \"unexpected error in first transfer\")\n\n\t// Then transfer to a contract authority\n\tcontractAuth := NewContractAuthority(\"gno.land/r/test\", func(title string, action PrivilegedAction) error {\n\t\treturn action()\n\t})\n\terr = auth.Transfer(bob, contractAuth)\n\tuassert.True(t, err == nil, \"unexpected error in second transfer\")\n\n\t// Finally transfer to an auto-accept authority\n\tautoAuth := NewAutoAcceptAuthority()\n\tcodeRealm := testing.NewCodeRealm(\"gno.land/r/test\")\n\tcode := codeRealm.Address()\n\ttesting.SetRealm(codeRealm)\n\terr = auth.Transfer(code, autoAuth)\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 TestAuthorizerTransferVulnerability(t *testing.T) {\n\tadmin := testutils.TestAddress(\"admin\")\n\tattacker := testutils.TestAddress(\"attacker\")\n\n\t// Setup: Authorizer controlled by 'admin'\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\tauth := NewWithCurrent() // 'admin' is the initial authority\n\n\t// Check initial state\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// Simulate attacker's context\n\ttesting.SetRealm(testing.NewUserRealm(attacker))\n\n\t// Vulnerability: Attacker calls Transfer, providing 'admin' as the 'caller'\n\t// argument, even though the actual caller is 'attacker'.\n\tattackerAuth := NewMemberAuthority(attacker)\n\terr := auth.Transfer(admin, attackerAuth) // Vulnerability point\n\n\t// Assertions:\n\t// 1. Transfer should succeed if vulnerability exists (checks only provided 'caller').\n\t//    NOTE: If this test FAILS, the vulnerability is likely fixed!\n\tuassert.NoError(t, err, \"transfer should succeed due to vulnerability\")\n\n\t// 2. Authority should now be the attacker's.\n\tfinalAuth, ok := auth.Authority().(*MemberAuthority)\n\tuassert.True(t, ok)\n\tuassert.True(t, finalAuth == attackerAuth)\n\n\t// 3. Attacker should be the sole member.\n\tuassert.True(t, finalAuth.Has(attacker))\n\tuassert.False(t, finalAuth.Has(admin))\n\n\t// Verify attacker can now perform actions\n\tactionExecuted := false\n\terr = auth.DoByCurrent(\"attacker_action\", func() error {\n\t\tactionExecuted = true\n\t\treturn nil\n\t})\n\tuassert.NoError(t, err)\n\tuassert.True(t, actionExecuted)\n}\n\nfunc TestAuthorizerWithDroppedAuthority(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tauth := NewWithCurrent()\n\n\t// Transfer to dropped authority\n\terr := auth.Transfer(alice, NewDroppedAuthority())\n\tuassert.True(t, err == nil, \"expected no error\")\n\n\t// Try to execute action\n\terr = auth.DoByCurrent(\"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\terr = auth.Transfer(alice, NewMemberAuthority(alice))\n\tuassert.True(t, err != nil, \"expected error when transferring from dropped authority\")\n}\n\nfunc TestContractAuthorityHandlerExecutionOnce(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(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(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(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(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(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(t *testing.T) {\n\tauth := NewWithCurrent()\n\taddr := runtime.CurrentRealm().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\terr := auth.Transfer(addr, NewAutoAcceptAuthority())\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(t *testing.T) {\n\t/*\n\t\t// Test empty path - should panic\n\t\tpanicked := false\n\t\tfunc() {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tpanicked = true\n\t\t\t\t}\n\t\t\t}()\n\t\t\tNewContractAuthority(\"\", nil)\n\t\t}()\n\t\tuassert.True(t, panicked, \"expected panic for empty path\")\n\t*/\n\n\t// Test nil handler - should return error on Authorize\n\tauth := NewContractAuthority(\"gno.land/r/test\", nil)\n\tcode := testing.NewCodeRealm(\"gno.land/r/test\").Address()\n\terr := auth.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\thandler := func(title string, action PrivilegedAction) error {\n\t\treturn nil\n\t}\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(t *testing.T) {\n\tauth := NewWithCurrent()\n\taddr := runtime.CurrentRealm().Address()\n\n\t// Test initial string representation\n\tstr := auth.String()\n\tuassert.Equal(t, str, \"member_authority[g134ru6z8r00teg3r342h3yqf9y55mztdvlj4758]\")\n\n\t// Test string after transfer\n\tautoAuth := NewAutoAcceptAuthority()\n\terr := auth.Transfer(addr, autoAuth)\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\n\tcustomAuth := \u0026mockAuthority{}\n\tbob := testutils.TestAddress(\"bob\")\n\terr = auth.Transfer(bob, customAuth)\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(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\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]\"\n\tuassert.Equal(t, contractStr, expectedContractStr)\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\nfunc TestContractAuthorityUnauthorizedCaller(t *testing.T) {\n\tcontractPath := \"gno.land/r/testcontract\"\n\tcontractAddr := chain.PackageAddress(contractPath)\n\tunauthorizedAddr := testutils.TestAddress(\"unauthorized\")\n\n\t// Handler that checks the caller before proceeding\n\thandlerExecutedCorrectly := false // Tracks if handler logic ran correctly\n\thandlerErrorMsg := \"handler: caller is not the contract\"\n\tcontractHandler := func(title string, action PrivilegedAction) error {\n\t\tcaller := runtime.CurrentRealm().Address()\n\t\tif caller != contractAddr {\n\t\t\treturn errors.New(handlerErrorMsg)\n\t\t}\n\t\t// Only execute action and mark success if caller is correct\n\t\thandlerExecutedCorrectly = true\n\t\treturn action()\n\t}\n\n\tcontractAuth := NewContractAuthority(contractPath, contractHandler)\n\tauthorizer := NewWithAuthority(contractAuth) // Start with ContractAuthority\n\n\tactionExecuted := false\n\tprivilegedAction := func() error {\n\t\tactionExecuted = true\n\t\treturn nil\n\t}\n\n\t// 1. Attempt action from unauthorized user\n\ttesting.SetRealm(testing.NewUserRealm(unauthorizedAddr))\n\terr := authorizer.DoByCurrent(\"test_action_unauthorized\", privilegedAction)\n\n\t// Assertions for unauthorized call\n\tuassert.Error(t, err, \"DoByCurrent should return an error for unauthorized caller\")\n\tuassert.ErrorContains(t, err, handlerErrorMsg, \"Error should originate from the handler check\")\n\tuassert.False(t, handlerExecutedCorrectly, \"Handler should not have executed successfully for unauthorized caller\")\n\tuassert.False(t, actionExecuted, \"Privileged action should not have executed for unauthorized caller\")\n\n\t// 2. Attempt action from the correct contract\n\thandlerExecutedCorrectly = false // Reset flag\n\tactionExecuted = false           // Reset flag\n\ttesting.SetRealm(testing.NewCodeRealm(contractPath))\n\terr = authorizer.DoByCurrent(\"test_action_authorized\", privilegedAction)\n\n\t// Assertions for authorized call\n\tuassert.NoError(t, err, \"DoByCurrent should succeed for authorized contract caller\")\n\tuassert.True(t, handlerExecutedCorrectly, \"Handler should have executed successfully for authorized caller\")\n\tuassert.True(t, actionExecuted, \"Privileged action should have executed for authorized caller\")\n}\n\nfunc crossThrough(rlm runtime.Realm, cr func()) {\n\ttesting.SetRealm(rlm)\n\tcr()\n}\n\nfunc TestAuthorizerDoByPrevious(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\n\tauth := NewWithMembers(alice)\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\texecuted := false\n\t\targs := []any{\"test_arg\", 123}\n\t\terr := auth.DoByPrevious(\"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\n\t\texpectedErr := errors.New(\"test error\")\n\t\terr = auth.DoByPrevious(\"test_action\", func() error {\n\t\t\treturn expectedErr\n\t\t})\n\t\tuassert.ErrorContains(t, err, expectedErr.Error(), \"expected error\")\n\t})\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\tcrossThrough(testing.NewCodeRealm(\"gno.land/r/test/test\"), func() {\n\t\texecuted := false\n\t\terr := auth.DoByPrevious(\"test_action\", func() error {\n\t\t\texecuted = true\n\t\t\treturn nil\n\t\t}, \"unauthorized_arg\")\n\n\t\tuassert.ErrorContains(t, err, \"unauthorized\", \"expected error\")\n\t\tuassert.False(t, executed, \"action should not have been executed\")\n\t})\n}\n"
                      },
                      {
                        "name": "example_test.gno",
                        "body": "package authz\n\nimport (\n\t\"chain/runtime\"\n)\n\n// Example_basic demonstrates initializing and using a basic member authority\nfunc Example_basic() {\n\t// Initialize with contract deployer as authority\n\tauth := NewWithOrigin()\n\n\t// Use the authority to perform a privileged action\n\tauth.DoByCurrent(\"update_config\", func() error {\n\t\t// config = newValue\n\t\treturn nil\n\t})\n}\n\n// Example_addingMembers demonstrates how to add new members to a member authority\nfunc Example_addingMembers() {\n\t// Initialize with contract deployer as authority\n\taddr := runtime.CurrentRealm().Address()\n\tauth := NewWithCurrent()\n\n\t// Add a new member to the authority\n\tmemberAuth := auth.Authority().(*MemberAuthority)\n\tmemberAuth.AddMember(addr, address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"))\n}\n\n// Example_contractAuthority demonstrates using a contract-based authority\nfunc Example_contractAuthority() {\n\t// Initialize with contract authority (e.g., DAO)\n\tauth := NewWithAuthority(\n\t\tNewContractAuthority(\n\t\t\t\"gno.land/r/demo/dao\",\n\t\t\tmockDAOHandler, // defined elsewhere for example\n\t\t),\n\t)\n\n\t// Privileged actions will be handled by the contract\n\tauth.DoByCurrent(\"update_params\", func() error {\n\t\t// Executes after DAO approval\n\t\treturn nil\n\t})\n}\n\n// Example_restrictedContractAuthority demonstrates a contract authority with member-only proposals\nfunc Example_restrictedContractAuthority() {\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\tauth.DoByCurrent(\"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})\n}\n\n// Example_switchingAuthority demonstrates switching from member to contract authority\nfunc Example_switchingAuthority() {\n\t// Start with member authority (deployer)\n\taddr := runtime.CurrentRealm().Address()\n\tauth := NewWithCurrent()\n\n\t// Create and switch to contract authority\n\tdaoAuthority := NewContractAuthority(\n\t\t\"gno.land/r/demo/dao\",\n\t\tmockDAOHandler,\n\t)\n\tauth.Transfer(addr, daoAuthority)\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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "6QPFUgyQJzyFL6cZb6VwWw+mlbf+IZixCHQ/Hc1b6T1IlEM3w3vtwZR5roLYsy9ZJiry4pNJok19rwR5+oGlmA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
                  "package": {
                    "name": "txlink",
                    "path": "gno.land/p/moul/txlink",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/moul/txlink\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"
                      },
                      {
                        "name": "txlink.gno",
                        "body": "// Package txlink provides utilities for creating transaction-related links\n// compatible with Gnoweb, Gnobro, and other clients within the Gno ecosystem.\n//\n// This package is optimized for generating lightweight transaction links with\n// flexible arguments, allowing users to build dynamic links that integrate\n// seamlessly with various Gno clients.\n//\n// The package offers a way to generate clickable transaction MD links\n// for the current \"relative realm\":\n//\n//  Using a builder pattern for more structured URLs:\n//     txlink.NewLink(\"MyFunc\").\n//         AddArgs(\"k1\", \"v1\", \"k2\", \"v2\"). // or multiple at once\n//         SetSend(\"1000000ugnot\").\n//         URL()\n//\n// The builder pattern (TxBuilder) provides a fluent interface for constructing\n// transaction URLs in the current \"relative realm\". Like Call, it supports both\n// local realm paths and fully qualified paths through the underlying Call\n// implementation.\n//\n// The Call function remains the core implementation, used both directly and\n// internally by the builder pattern to generate the final URLs.\n//\n// This package is a streamlined alternative to helplink, providing similar\n// functionality for transaction links without the full feature set of helplink.\n\npackage txlink\n\nimport (\n\t\"chain/runtime\"\n\t\"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 := runtime.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$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"/p/moul/txlink$help\u0026func=testFunc\u0026key=value\", \"\"},\n\t\t{\"noArgsFunc\", []string{}, \"/p/moul/txlink$help\u0026func=noArgsFunc\", \"\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"/p/moul/txlink$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$help\u0026func=test\u0026key=hello+world\", \"\"},\n\t\t{\"test\", []string{\"key\", \"a\u0026b=c\"}, \"/p/moul/txlink$help\u0026func=test\u0026key=a%26b%3Dc\", \"\"},\n\t\t{\"test\", []string{\"key\", \"\"}, \"/p/moul/txlink$help\u0026func=test\u0026key=\", \"\"},\n\t\t{\"testSend\", []string{\"key\", \"hello world\", \".send\", \"1000000ugnot\"}, \"/p/moul/txlink$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$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$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$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$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$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$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$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$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$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$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$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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "pdg2Y3PPVKj37q8l9MQmSFAk2WHrLXy0DMr3UslfVb9Ia+br7yBR1NuG1Kicd1pSd3of3/dnLraVf+Bw87oAAg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
                  "package": {
                    "name": "helplink",
                    "path": "gno.land/p/moul/helplink",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/moul/helplink\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"
                      },
                      {
                        "name": "helplink.gno",
                        "body": "// Package helplink provides utilities for creating help page links compatible\n// with Gnoweb, Gnobro, and other clients that support the Gno contracts'\n// flavored Markdown format.\n//\n// This package simplifies the generation of dynamic, context-sensitive help\n// links, enabling users to navigate relevant documentation seamlessly within\n// the Gno ecosystem.\n//\n// For a more lightweight alternative, consider using p/moul/txlink.\n//\n// The primary functions — Func, FuncURL, and Home — are intended for use with\n// the \"relative realm\". When specifying a custom Realm, you can create links\n// that utilize either the current realm path or a fully qualified path to\n// another realm.\npackage helplink\n\nimport (\n\t\"chain/runtime\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/txlink\"\n)\n\nvar chainDomain = runtime.ChainDomain()\n\n// Func returns a markdown link for the specific function with optional\n// key-value arguments, for the current realm.\nfunc Func(title string, fn string, args ...string) string {\n\treturn Realm(\"\").Func(title, fn, args...)\n}\n\n// FuncURL returns a URL for the specified function with optional key-value\n// arguments, for the current realm.\nfunc FuncURL(fn string, args ...string) string {\n\treturn Realm(\"\").FuncURL(fn, args...)\n}\n\n// Home returns the URL for the help homepage of the current realm.\nfunc Home() string {\n\treturn Realm(\"\").Home()\n}\n\n// Realm represents a specific realm for generating help links.\ntype Realm string\n\n// prefix returns the URL prefix for the realm.\nfunc (r Realm) prefix() string {\n\t// relative\n\tif r == \"\" {\n\t\treturn \"\"\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$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$help\u0026func=testFunc\u0026key=value)\", \"\"},\n\t\t{\"No Args\", \"noArgsFunc\", []string{}, \"[No Args](/p/moul/helplink$help\u0026func=noArgsFunc)\", \"\"},\n\t\t{\"Odd Args\", \"oddArgsFunc\", []string{\"key\"}, \"[Odd Args](/p/moul/helplink$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$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"/p/moul/helplink$help\u0026func=testFunc\u0026key=value\", \"\"},\n\t\t{\"noArgsFunc\", []string{}, \"/p/moul/helplink$help\u0026func=noArgsFunc\", \"\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"/p/moul/helplink$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\tcd := runtime.ChainDomain()\n\ttests := []struct {\n\t\trealm_XXX Realm\n\t\twant      string\n\t}{\n\t\t{\"\", \"$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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "wkxLzzJsynFUoNtDbvXQ0Lr+CJM6JQDkTuodrCqHFINttZt3frQu8pmgQqXqeDYU4jHQkQu9qSAt16aqo1/11g=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
                  "package": {
                    "name": "mdtable",
                    "path": "gno.land/p/moul/mdtable",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/moul/mdtable\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"
                      },
                      {
                        "name": "mdtable.gno",
                        "body": "// Package mdtable provides a simple way to create Markdown tables.\n//\n// Example usage:\n//\n//\timport \"gno.land/p/moul/mdtable\"\n//\n//\tfunc Render(path string) string {\n//\t    table := mdtable.Table{\n//\t        Headers: []string{\"ID\", \"Title\", \"Status\", \"Date\"},\n//\t    }\n//\t    table.Append([]string{\"#1\", \"Add a new validator\", \"succeed\", \"2024-01-01\"})\n//\t    table.Append([]string{\"#2\", \"Change parameter\", \"timed out\", \"2024-01-02\"})\n//\t    return table.String()\n//\t}\n//\n// Output:\n//\n//\t| ID | Title | Status | Date |\n//\t| --- | --- | --- | --- |\n//\t| #1 | Add a new validator | succeed | 2024-01-01 |\n//\t| #2 | Change parameter | timed out | 2024-01-02 |\npackage mdtable\n\nimport (\n\t\"strings\"\n)\n\ntype Table struct {\n\tHeaders []string\n\tRows    [][]string\n\t// XXX: optional headers alignment.\n}\n\nfunc (t *Table) Append(row []string) {\n\tt.Rows = append(t.Rows, row)\n}\n\nfunc (t Table) String() string {\n\t// XXX: switch to using text/tabwriter when porting to Gno to support\n\t// better-formatted raw Markdown output.\n\n\tif len(t.Headers) == 0 \u0026\u0026 len(t.Rows) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar sb strings.Builder\n\n\tif len(t.Headers) == 0 {\n\t\tt.Headers = make([]string, len(t.Rows[0]))\n\t}\n\n\t// Print header.\n\tsb.WriteString(\"| \" + strings.Join(t.Headers, \" | \") + \" |\\n\")\n\tsb.WriteString(\"|\" + strings.Repeat(\" --- |\", len(t.Headers)) + \"\\n\")\n\n\t// Print rows.\n\tfor _, row := range t.Rows {\n\t\tescapedRow := make([]string, len(row))\n\t\tfor i, cell := range row {\n\t\t\tescapedRow[i] = strings.ReplaceAll(cell, \"|\", \"\u0026#124;\") // Escape pipe characters.\n\t\t}\n\t\tsb.WriteString(\"| \" + strings.Join(escapedRow, \" | \") + \" |\\n\")\n\t}\n\n\treturn sb.String()\n}\n"
                      },
                      {
                        "name": "mdtable_test.gno",
                        "body": "package mdtable_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/moul/mdtable\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "WCvRdwXM/2b78ZZyr87vSSIoV7qc+WxQuOotlku26VxwLzr2FyV0ine3DWmQ+fqtuTNMti2b4ax2A1RDoTFBCQ=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1manfred47kzduec920z88wfr64ylksmdcedlf5",
                  "package": {
                    "name": "realmpath",
                    "path": "gno.land/p/moul/realmpath",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/moul/realmpath\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"\n"
                      },
                      {
                        "name": "realmpath.gno",
                        "body": "// Package realmpath is a lightweight Render.path parsing and link generation\n// library with an idiomatic API, closely resembling that of net/url.\n//\n// This package provides utilities for parsing request paths and query\n// parameters, allowing you to extract path segments and manipulate query\n// values.\n//\n// Example usage:\n//\n//\timport \"gno.land/p/moul/realmpath\"\n//\n//\tfunc Render(path string) string {\n//\t    // Parsing a sample path with query parameters\n//\t    path = \"hello/world?foo=bar\u0026baz=foobar\"\n//\t    req := realmpath.Parse(path)\n//\n//\t    // Accessing parsed path and query parameters\n//\t    println(req.Path)             // Output: hello/world\n//\t    println(req.PathPart(0))      // Output: hello\n//\t    println(req.PathPart(1))      // Output: world\n//\t    println(req.Query.Get(\"foo\")) // Output: bar\n//\t    println(req.Query.Get(\"baz\")) // Output: foobar\n//\n//\t    // Rebuilding the URL\n//\t    println(req.String())         // Output: /r/current/realm:hello/world?baz=foobar\u0026foo=bar\n//\t}\npackage realmpath\n\nimport (\n\t\"chain/runtime\"\n\t\"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.\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 = runtime.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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "EzzyEVXI1UOLIOUmU1Yh/NYDM+F3/u56bpdPih+cG0RSos8C0y7kkpAfUlwAUvZzY1AUEuqgCMm+TDMTuDae/Q=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n\nPackage `combinederr` provides a combined error type for aggregating multiple errors into a single error value.\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "FD9uwPBnt5eu2QVfYjGFbD4Hm2GAgWJAfXpNcrvmMuV1yQWogZ40IzHK5r4e1J9oRXeB2t/uDX68EA3XCFPpxA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n\nPackage `mdalert` provides support for creating Markdown alerts with standard alert types (note, tip, important, warning, caution).\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "Rer2XnTHI9nmS4F4Um42yYCEvjs5incsAxOd2K+8CSQnBpMwynPq7JaIpkZrBuXtnE9hZIaEwV1p60N3YDvuqA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "authorizable",
                    "path": "gno.land/p/nt/ownable/v0/exts/authorizable",
                    "files": [
                      {
                        "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\"chain/runtime\"\n\n\t\"gno.land/p/nt/avl/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       *avl.Tree // 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// The auth mode (current-realm vs previous-realm) is inherited from the\n// provided Ownable. Use the ownable constructors to choose the mode:\n//\n//\t// Current-realm auth (caller is the realm making the call):\n//\tauthorizable.New(ownable.New())\n//\n//\t// Previous-realm auth (caller is the user/realm one step back):\n//\tauthorizable.New(ownable.NewWithOrigin())\n//\n//\t// Explicit address, current-realm auth:\n//\tauthorizable.New(ownable.NewWithAddress(addr))\n//\n//\t// Explicit address, previous-realm auth:\n//\tauthorizable.New(ownable.NewWithAddressByPrevious(addr))\nfunc New(o *ownable.Ownable) *Authorizable {\n\ta := \u0026Authorizable{\n\t\tOwnable:    o,\n\t\tauthorized: avl.NewTree(),\n\t}\n\n\t// Add owner to auth list\n\ta.authorized.Set(a.Owner().String(), struct{}{})\n\treturn a\n}\n\nfunc (a *Authorizable) AddToAuthList(addr address) error {\n\tif !a.Owned() {\n\t\treturn ErrNotSuperuser\n\t}\n\treturn a.addToAuthList(addr)\n}\n\nfunc (a *Authorizable) addToAuthList(addr address) error {\n\tif _, exists := a.authorized.Get(addr.String()); exists {\n\t\treturn ErrAlreadyInList\n\t}\n\n\ta.authorized.Set(addr.String(), struct{}{})\n\n\treturn nil\n}\n\nfunc (a *Authorizable) DeleteFromAuthList(addr address) error {\n\tif !a.Owned() {\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\nfunc (a *Authorizable) OnAuthList() error {\n\tcurrent := runtime.CurrentRealm().Address()\n\treturn a.onAuthList(current)\n}\n\nfunc (a *Authorizable) PreviousOnAuthList() error {\n\tprevious := runtime.PreviousRealm().Address()\n\treturn a.onAuthList(previous)\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() {\n\terr := a.OnAuthList()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc (a Authorizable) AssertPreviousOnAuthList() {\n\terr := a.PreviousOnAuthList()\n\tif 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\nvar (\n\talice   = testutils.TestAddress(\"alice\")\n\tbob     = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestNew(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(t *testing.T) {\n\ta := New(ownable.NewWithAddress(alice))\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tif err := a.OnAuthList(); err == ErrNotInAuthList {\n\t\tt.Fatalf(\"expected alice to be on the list\")\n\t}\n}\n\nfunc TestNotOnAuthList(t *testing.T) {\n\ta := New(ownable.NewWithAddress(alice))\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\n\tif err := a.OnAuthList(); err == nil {\n\t\tt.Fatalf(\"expected bob to not be on the list\")\n\t}\n}\n\nfunc TestAddToAuthList(t *testing.T) {\n\ta := New(ownable.NewWithAddress(alice))\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tif err := a.AddToAuthList(bob); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\n\tif err := a.AddToAuthList(bob); err == nil {\n\t\tt.Fatalf(\"Expected AddToAuth to error while bob called it, but it didn't\")\n\t}\n}\n\nfunc TestDeleteFromList(t *testing.T) {\n\ta := New(ownable.NewWithAddress(alice))\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tif err := a.AddToAuthList(bob); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\tif err := a.AddToAuthList(charlie); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\n\t// Try an unauthorized deletion\n\tif err := a.DeleteFromAuthList(alice); err == nil {\n\t\tt.Fatalf(\"Expected DelFromAuth to error with %v\", err)\n\t}\n\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\n\tif err := a.DeleteFromAuthList(charlie); err != nil {\n\t\tt.Fatalf(\"Expected no error, got %v\", err)\n\t}\n}\n\nfunc TestAssertOnList(t *testing.T) {\n\ta := New(ownable.NewWithAddress(alice))\n\n\ttesting.SetRealm(testing.NewUserRealm(bob))\n\n\tuassert.PanicsWithMessage(t, ErrNotInAuthList.Error(), func() {\n\t\ta.AssertOnAuthList()\n\t})\n}\n"
                      },
                      {
                        "name": "errors.gno",
                        "body": "package authorizable\n\nimport \"errors\"\n\nvar (\n\tErrNotInAuthList = errors.New(\"authorizable: caller is not in authorized list\")\n\tErrNotSuperuser  = errors.New(\"authorizable: caller is not superuser\")\n\tErrAlreadyInList = errors.New(\"authorizable: address is already in authorized list\")\n)\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/nt/ownable/v0/exts/authorizable\"\ngno = \"0.9\"\n"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "YvQIFzsM2M3IBacTs5/Y4K9TaTlpQ0zPxqYNhc401EhCTPVBIcohRjKaDbpLs3PyNHdX/HigExJDdlHSA5nn7A=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l",
                  "package": {
                    "name": "validators",
                    "path": "gno.land/p/sys/validators",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/sys/validators\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"\n"
                      },
                      {
                        "name": "types.gno",
                        "body": "package validators\n\nimport (\n\t\"errors\"\n)\n\n// ValsetProtocol defines the validator set protocol (PoA / PoS / PoC / ?)\ntype ValsetProtocol interface {\n\t// AddValidator adds a new validator to the validator set.\n\t// If the validator is already present, the method should error out\n\t//\n\t// TODO: This API is not ideal -- the address should be derived from\n\t// the public key, and not be passed in as such, but currently Gno\n\t// does not support crypto address derivation\n\tAddValidator(address_XXX address, pubKey string, power uint64) (Validator, error)\n\n\t// RemoveValidator removes the given validator from the set.\n\t// If the validator is not present in the set, the method should error out\n\tRemoveValidator(address_XXX address) (Validator, error)\n\n\t// IsValidator returns a flag indicating if the given\n\t// bech32 address is part of the validator set\n\tIsValidator(address_XXX address) bool\n\n\t// GetValidator returns the validator using the given address\n\tGetValidator(address_XXX address) (Validator, error)\n\n\t// GetValidators returns the currently active validator set\n\tGetValidators() []Validator\n}\n\n// Validator represents a single chain validator\ntype Validator struct {\n\tAddress     address // bech32 address\n\tPubKey      string  // bech32 representation of the public key\n\tVotingPower uint64\n}\n\nconst (\n\tValidatorAddedEvent   = \"ValidatorAdded\"   // emitted when a validator was added to the set\n\tValidatorRemovedEvent = \"ValidatorRemoved\" // emitted when a validator was removed from the set\n)\n\nvar (\n\t// ErrValidatorExists is returned when the validator is already in the set\n\tErrValidatorExists = errors.New(\"validator already exists\")\n\n\t// ErrValidatorMissing is returned when the validator is not in the set\n\tErrValidatorMissing = errors.New(\"validator doesn't exist\")\n)\n"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "u75Cz80X9URlJ/9XLH2oWEh+LIDQsMkU/MufRIJgzUJ0F9vkhmJDLeZdC0P7fJLi4piLyFiDBtkr2L8MlGTcPg=="
                }
              ],
              "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\n\nPackage `poa` implements a Proof of Authority validator set management system with simple add/remove constraints.\n"
                      },
                      {
                        "name": "doc.gno",
                        "body": "// v0 - Unaudited: This is an initial version that has not yet been formally audited.\n// A fully audited version will be published as a subsequent release.\n// Use in production at your own risk.\n//\n// Package poa implements a Proof of Authority validator set management system.\npackage poa\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/nt/poa/v0\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"\n"
                      },
                      {
                        "name": "option.gno",
                        "body": "package poa\n\nimport \"gno.land/p/sys/validators\"\n\ntype Option func(*PoA)\n\n// WithInitialSet sets the initial PoA validator set\nfunc WithInitialSet(validators []validators.Validator) Option {\n\treturn func(p *PoA) {\n\t\tfor _, validator := range validators {\n\t\t\tp.validators.Set(validator.Address.String(), validator)\n\t\t}\n\t}\n}\n"
                      },
                      {
                        "name": "poa.gno",
                        "body": "package poa\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/sys/validators\"\n)\n\nvar ErrInvalidVotingPower = errors.New(\"invalid voting power\")\n\n// PoA specifies the Proof of Authority validator set, with simple add / remove constraints.\n//\n// To add:\n// - proposed validator must not be part of the set already\n// - proposed validator voting power must be \u003e 0\n//\n// To remove:\n// - proposed validator must be part of the set already\ntype PoA struct {\n\tvalidators *avl.Tree // 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: avl.NewTree(),\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\t_, exists := p.validators.Get(address_XXX.String())\n\n\treturn exists\n}\n\nfunc (p *PoA) GetValidator(address_XXX address) (validators.Validator, error) {\n\tvalidatorRaw, exists := p.validators.Get(address_XXX.String())\n\tif !exists {\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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "/xnCIjKm4GP5lBzVxivCzzW4IYXSHK2ghqW+xNSSlddG1ybX5R+r+3M2X3xOEmS4Kp07mpJyF8wnB49WcYM/ow=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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\n\nPackage `treasury` provides treasury management for handling coin and GRC20 token transfers in Gno realms.\n"
                      },
                      {
                        "name": "banker_coins.gno",
                        "body": "package treasury\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime\"\n\t\"errors\"\n\n\t\"gno.land/p/aeddi/panictoerr\"\n)\n\nvar ErrNoStdBankerProvided = errors.New(\"no std banker provided\")\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.\nfunc (cb *CoinsBanker) Send(p Payment) error {\n\tif runtime.CurrentRealm().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// NewCoinsBanker creates a new CoinsBanker with the current Realm's address\n// as the owner.\nfunc NewCoinsBanker(banker_ banker.Banker) (*CoinsBanker, error) {\n\towner := runtime.CurrentRealm().Address()\n\n\treturn NewCoinsBankerWithOwner(owner, banker_)\n}\n\n// NewCoinsBankerWithOwner creates a new CoinsBanker with the given address.\nfunc NewCoinsBankerWithOwner(owner address, banker_ banker.Banker) (*CoinsBanker, error) {\n\tif owner == \"\" {\n\t\treturn nil, ErrNoOwnerProvided\n\t}\n\n\t// NOTE: Should we add methods to std.Banker to check both its type and the\n\t// associated Realm for this kind of case?\n\t// For example:\n\t// if banker.Type() != std.BankerTypeRealmSend { panic(\"banker must be of type std.BankerTypeRealmSend\") }\n\t// if banker.Realm().Address() != owner { panic(\"banker must be owned by the given owner address\") }\n\tif banker_ == nil {\n\t\treturn nil, ErrNoStdBankerProvided\n\t}\n\n\treturn \u0026CoinsBanker{\n\t\towner:  owner,\n\t\tbanker: banker_,\n\t}, nil\n}\n\n// coinsPayment represents a payment that is issued by a CoinsBanker.\ntype coinsPayment struct {\n\tcoins     chain.Coins // The coins being sent.\n\ttoAddress address     // The recipient of the payment.\n}\n\nvar _ Payment = (*coinsPayment)(nil)\n\n// BankerID implements Payment.\nfunc (coinsPayment) BankerID() string {\n\treturn CoinsBanker{}.ID()\n}\n\n// String implements Payment.\nfunc (cp coinsPayment) String() string {\n\treturn cp.coins.String() + \" to \" + cp.toAddress.String()\n}\n\n// NewCoinsPayment creates a new coinsPayment.\nfunc NewCoinsPayment(coins chain.Coins, toAddress address) Payment {\n\treturn coinsPayment{\n\t\tcoins:     coins,\n\t\ttoAddress: toAddress,\n\t}\n}\n"
                      },
                      {
                        "name": "banker_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() {\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)\n\tcbanker, err := treasury.NewCoinsBanker(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(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(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_grc20.gno",
                        "body": "package treasury\n\nimport (\n\t\"chain/runtime\"\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tErrNoListerProvided   = errors.New(\"no lister provided\")\n\tErrGRC20TokenNotFound = errors.New(\"GRC20 token not found\")\n)\n\n// GRC20Banker is a Banker that sends GRC20 tokens listed using a getter\n// set during initialization.\ntype GRC20Banker struct {\n\towner  address         // The address of this GRC20 banker owner.\n\tlister TokenListerFunc // Allows to list tokens from methods that require it.\n}\n\n// TokenListerFunc is a function type that returns a map of GRC20 tokens.\ntype TokenListerFunc func() map[string]*grc20.Token\n\nvar _ Banker = (*GRC20Banker)(nil)\n\n// ID implements Banker.\nfunc (GRC20Banker) ID() string {\n\treturn \"GRC20\"\n}\n\n// Send implements Banker.\nfunc (gb *GRC20Banker) Send(p Payment) error {\n\tif runtime.CurrentRealm().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.\n\treturn token.RealmTeller().Transfer(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// NewGRC20Banker creates a new GRC20Banker with the current Realm's address\n// as the owner.\nfunc NewGRC20Banker(lister TokenListerFunc) (*GRC20Banker, error) {\n\towner := runtime.CurrentRealm().Address()\n\n\treturn NewGRC20BankerWithOwner(owner, lister)\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/demo/tokens/grc20\"\n\t\"gno.land/p/nt/treasury/v0\"\n)\n\nconst amount = int64(1000)\n\nfunc createToken(name string, toMint address) *grc20.Token {\n\t// Create the token.\n\tsymbol := strings.ToUpper(name)\n\ttoken, ledger := grc20.NewToken(name, symbol, 0)\n\n\t// Mint the requested amount.\n\tledger.Mint(toMint, amount)\n\n\treturn token\n}\n\nfunc main() {\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.NewGRC20Banker(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(\"TestToken0\", ownerAddr),\n\t\tcreateToken(\"TestToken1\", ownerAddr),\n\t\tcreateToken(\"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.NewGRC20Banker(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(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(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(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(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\"chain/runtime\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/nt/avl/v0/pager\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nconst (\n\tDefaultHistoryPreviewSize = 5  // Number of payments in the history preview.\n\tDefaultHistoryPageSize    = 20 // Number of payments per page in the history.\n)\n\n// Render renders content based on the given path.\nfunc (t *Treasury) Render(path string) string {\n\treturn t.router.Render(path)\n}\n\n// RenderLanding renders the landing page of the treasury.\nfunc (t *Treasury) RenderLanding(path string) string {\n\tvar out string\n\n\t// Render each banker.\n\tfor _, bankerID := range t.ListBankerIDs() {\n\t\tout += t.RenderBanker(bankerID, path)\n\t}\n\n\treturn out\n}\n\n// RenderBanker renders the details of a specific banker.\nfunc (t *Treasury) RenderBanker(bankerID string, path string) string {\n\t// Get the banker associated to this ID.\n\tbr, ok := t.bankers.Get(bankerID)\n\tif !ok {\n\t\treturn md.Paragraph(\"Banker not found: \" + bankerID)\n\t}\n\tbanker := br.(*bankerRecord).banker\n\n\t// Render banker title.\n\tout := md.H2(bankerID + \" Banker\")\n\n\t// Render address section.\n\tout += md.H3(\"Address\")\n\tout += md.Paragraph(banker.Address())\n\n\t// Render balances section.\n\tout += md.H3(\"Balances\")\n\tbalances := banker.Balances()\n\tif len(balances) == 0 {\n\t\tout += md.Paragraph(\"No balances found.\")\n\t} else {\n\t\ttable := mdtable.Table{Headers: []string{\"Denom\", \"Amount\"}}\n\t\tfor _, balance := range balances {\n\t\t\ttable.Append([]string{balance.Denom, strconv.FormatInt(balance.Amount, 10)})\n\t\t}\n\t\tout += table.String()\n\t}\n\n\thistorySize := DefaultHistoryPreviewSize\n\n\t// Check if the query parameter \"history_size\" is present and parse it.\n\tif req, err := url.Parse(path); err == nil \u0026\u0026 req.Query() != nil {\n\t\tsize, err := strconv.Atoi(req.Query().Get(\"history_size\"))\n\t\tif err == nil \u0026\u0026 size \u003e= 0 {\n\t\t\thistorySize = size\n\t\t}\n\t}\n\n\t// Skip history rendering if historySize is 0.\n\tif historySize == 0 {\n\t\treturn out\n\t}\n\n\t// Render history section.\n\tout += md.H3(\"History\")\n\thistory, _ := t.History(bankerID, 1, historySize)\n\tif len(history) == 0 {\n\t\tout += md.Paragraph(\"No payments sent yet.\")\n\t} else {\n\t\tif len(history) == 1 {\n\t\t\tout += md.Paragraph(\"Last payment:\")\n\t\t} else {\n\t\t\tcount := strconv.FormatInt(int64(len(history)), 10)\n\t\t\tout += md.Paragraph(\"Last \" + count + \" payments:\")\n\t\t}\n\n\t\t// Render each payment in the history.\n\t\tfor _, payment := range history {\n\t\t\tout += md.BulletItem(payment.String())\n\t\t}\n\t\tout += \"\\n\"\n\n\t\t// Get the current Realm's package path for linking.\n\t\tcurRealm := runtime.CurrentRealm().PkgPath()\n\t\tif from := strings.IndexRune(curRealm, '/'); 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\", curRealm[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, ok := t.bankers.Get(bankerID)\n\tif !ok {\n\t\treturn md.Paragraph(\"Banker not found: \" + bankerID)\n\t}\n\thistory := br.(*bankerRecord).history\n\n\t// Render banker history title.\n\tout := md.H2(bankerID + \" Banker History\")\n\n\t// Get the current page of tokens based on the request path.\n\tp := pager.NewPager(history.Tree(), DefaultHistoryPageSize, true)\n\tpage, err := p.GetPageByPath(path)\n\tif err != nil {\n\t\treturn md.Paragraph(\"Error retrieving page: \" + err.Error())\n\t}\n\n\t// Render full history section.\n\tif history.Len() == 0 {\n\t\tout += md.Paragraph(\"No payments sent yet.\")\n\t} else {\n\t\tif history.Len() == 1 {\n\t\t\tout += md.Paragraph(\"1 payment:\")\n\t\t} else {\n\t\t\tcount := strconv.FormatInt(int64(history.Len()), 10)\n\t\t\tout += md.Paragraph(count + \" payments (sorted by latest, descending):\")\n\t\t}\n\t\tfor _, item := range page.Items {\n\t\t\tout += md.BulletItem(item.Value.(Payment).String())\n\t\t}\n\t}\n\tout += \"\\n\"\n\n\t// Add the page picker.\n\tout += md.Paragraph(page.Picker(path))\n\n\treturn out\n}\n\n// initRenderRouter registers the routes for rendering the treasury pages.\nfunc (t *Treasury) initRenderRouter() {\n\tt.router = mux.NewRouter()\n\n\t// Landing page.\n\tt.router.HandleFunc(\"\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(t.RenderLanding(req.RawPath))\n\t})\n\n\t// Banker details.\n\tt.router.HandleFunc(\"{banker}\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(t.RenderBanker(req.GetVar(\"banker\"), req.RawPath))\n\t})\n\n\t// Banker full history.\n\tt.router.HandleFunc(\"{banker}/history\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(t.RenderBankerHistory(req.GetVar(\"banker\"), req.RawPath))\n\t})\n}\n"
                      },
                      {
                        "name": "treasury.gno",
                        "body": "package treasury\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/avl/v0/pager\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tErrNoBankerProvided  = errors.New(\"no banker provided\")\n\tErrDuplicateBanker   = errors.New(\"duplicate banker\")\n\tErrBankerNotFound    = errors.New(\"banker not found\")\n\tErrSendPaymentFailed = errors.New(\"failed to send payment\")\n)\n\n// New creates a new Treasury instance with the given bankers.\nfunc New(bankers []Banker) (*Treasury, error) {\n\tif len(bankers) == 0 {\n\t\treturn nil, ErrNoBankerProvided\n\t}\n\n\t// Create a new Treasury instance.\n\ttreasury := \u0026Treasury{bankers: avl.NewTree()}\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.\nfunc (t *Treasury) Send(p Payment) error {\n\t// Get the banker record corresponding to this Payment.\n\tbr, ok := t.bankers.Get(p.BankerID())\n\tif !ok {\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(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, ok := t.bankers.Get(bankerID)\n\tif !ok {\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, ok := t.bankers.Get(bankerID)\n\tif !ok {\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, ok := t.bankers.Get(bankerID)\n\tif !ok {\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/demo/tokens/grc20\"\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() {\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)\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)\n\t\tcoinsBanker, _    = treasury.NewCoinsBanker(banker_)\n\t\tgrc20Banker, _    = treasury.NewGRC20Banker(grc20Lister)\n\t\tgrc20BankerDup, _ = treasury.NewGRC20Banker(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(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(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/avl/v0\"\n\t\"gno.land/p/nt/avl/v0/list\"\n\t\"gno.land/p/nt/mux/v0\"\n)\n\n// Treasury is the main structure that holds all bankers and their payment\n// history. It also provides a router for rendering the treasury pages.\ntype Treasury struct {\n\tbankers *avl.Tree // string -\u003e *bankerRecord\n\trouter  *mux.Router\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.\ntype Banker interface {\n\tID() string          // Get the ID of the banker.\n\tSend(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// Payment is an interface that allows getting details about a payment.\ntype Payment interface {\n\tBankerID() string // Get the ID of the banker that can process this payment.\n\tString() string   // Get a string representation of the payment.\n}\n\n// Balance represents the balance of an asset held by a Banker.\ntype Balance struct {\n\tDenom  string // The denomination of the asset\n\tAmount int64  // The amount of the asset\n}\n\n// Common Banker errors.\nvar (\n\tErrCurrentRealmIsNotOwner = errors.New(\"current realm is not the owner of the banker\")\n\tErrNoOwnerProvided        = errors.New(\"no owner provided\")\n\tErrInvalidPaymentType     = errors.New(\"invalid payment type\")\n)\n"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "BriiFgnv5l/SCo3NS5KDHpUMl5LObDVNXOPaieNJ3+pn3t6IHKsUgkNY3MHvsSkg5WYtkyLoRPG8yiEuDwN7ZQ=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "table",
                    "path": "gno.land/p/sunspirit/table",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/sunspirit/table\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "table.gno",
                        "body": "package table\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Table defines the structure for a markdown table\ntype Table struct {\n\theader []string\n\trows   [][]string\n}\n\n// Validate checks if the number of columns in each row matches the number of columns in the header\nfunc (t *Table) Validate() error {\n\tnumCols := len(t.header)\n\tfor _, row := range t.rows {\n\t\tif len(row) != numCols {\n\t\t\treturn ufmt.Errorf(\"row %v does not match header length %d\", row, numCols)\n\t\t}\n\t}\n\treturn nil\n}\n\n// New creates a new Table instance, ensuring the header and rows match in size\nfunc New(header []string, rows [][]string) (*Table, error) {\n\tt := \u0026Table{\n\t\theader: header,\n\t\trows:   rows,\n\t}\n\n\tif err := t.Validate(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn t, nil\n}\n\n// Table returns a markdown string for the given Table\nfunc (t *Table) String() string {\n\tif err := t.Validate(); err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar sb strings.Builder\n\n\tsb.WriteString(\"| \" + strings.Join(t.header, \" | \") + \" |\\n\")\n\tsb.WriteString(\"| \" + strings.Repeat(\"---|\", len(t.header)) + \"\\n\")\n\n\tfor _, row := range t.rows {\n\t\tsb.WriteString(\"| \" + strings.Join(row, \" | \") + \" |\\n\")\n\t}\n\n\treturn sb.String()\n}\n\n// AddRow adds a new row to the table\nfunc (t *Table) AddRow(row []string) error {\n\tif len(row) != len(t.header) {\n\t\treturn ufmt.Errorf(\"row %v does not match header length %d\", row, len(t.header))\n\t}\n\tt.rows = append(t.rows, row)\n\treturn nil\n}\n\n// AddColumn adds a new column to the table with the specified values\nfunc (t *Table) AddColumn(header string, values []string) error {\n\tif len(values) != len(t.rows) {\n\t\treturn ufmt.Errorf(\"values length %d does not match the number of rows %d\", len(values), len(t.rows))\n\t}\n\n\t// Add the new header\n\tt.header = append(t.header, header)\n\n\t// Add the new column values to each row\n\tfor i, value := range values {\n\t\tt.rows[i] = append(t.rows[i], value)\n\t}\n\treturn nil\n}\n\n// RemoveRow removes a row from the table by its index\nfunc (t *Table) RemoveRow(index int) error {\n\tif index \u003c 0 || index \u003e= len(t.rows) {\n\t\treturn ufmt.Errorf(\"index %d is out of range\", index)\n\t}\n\tt.rows = append(t.rows[:index], t.rows[index+1:]...)\n\treturn nil\n}\n\n// RemoveColumn removes a column from the table by its index\nfunc (t *Table) RemoveColumn(index int) error {\n\tif index \u003c 0 || index \u003e= len(t.header) {\n\t\treturn ufmt.Errorf(\"index %d is out of range\", index)\n\t}\n\n\t// Remove the column from the header\n\tt.header = append(t.header[:index], t.header[index+1:]...)\n\n\t// Remove the corresponding column from each row\n\tfor i := range t.rows {\n\t\tt.rows[i] = append(t.rows[i][:index], t.rows[i][index+1:]...)\n\t}\n\treturn nil\n}\n"
                      },
                      {
                        "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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "BolO49LdRtgfaQQ/WbEI5Il5f0s8en+bjYM0ibZCMqwi82u7DVeAsp1j1oTHZOBv3IhMUoegV7UNeKkxna3vLw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "md",
                    "path": "gno.land/p/sunspirit/md",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/sunspirit/md\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "md.gno",
                        "body": "package md\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Builder helps to build a Markdown string from individual elements\ntype Builder struct {\n\telements []string\n}\n\n// NewBuilder creates a new Builder instance\nfunc NewBuilder() *Builder {\n\treturn \u0026Builder{}\n}\n\n// Add adds a Markdown element to the builder\nfunc (m *Builder) Add(md ...string) *Builder {\n\tm.elements = append(m.elements, md...)\n\treturn m\n}\n\n// Render returns the final Markdown string joined with the specified separator\nfunc (m *Builder) Render(separator string) string {\n\treturn strings.Join(m.elements, separator)\n}\n\n// Bold returns bold text for markdown\nfunc Bold(text string) string {\n\treturn ufmt.Sprintf(\"**%s**\", text)\n}\n\n// Italic returns italicized text for markdown\nfunc Italic(text string) string {\n\treturn ufmt.Sprintf(\"*%s*\", text)\n}\n\n// Strikethrough returns strikethrough text for markdown\nfunc Strikethrough(text string) string {\n\treturn ufmt.Sprintf(\"~~%s~~\", text)\n}\n\n// H1 returns a level 1 header for markdown\nfunc H1(text string) string {\n\treturn ufmt.Sprintf(\"# %s\\n\", text)\n}\n\n// H2 returns a level 2 header for markdown\nfunc H2(text string) string {\n\treturn ufmt.Sprintf(\"## %s\\n\", text)\n}\n\n// H3 returns a level 3 header for markdown\nfunc H3(text string) string {\n\treturn ufmt.Sprintf(\"### %s\\n\", text)\n}\n\n// H4 returns a level 4 header for markdown\nfunc H4(text string) string {\n\treturn ufmt.Sprintf(\"#### %s\\n\", text)\n}\n\n// H5 returns a level 5 header for markdown\nfunc H5(text string) string {\n\treturn ufmt.Sprintf(\"##### %s\\n\", text)\n}\n\n// H6 returns a level 6 header for markdown\nfunc H6(text string) string {\n\treturn ufmt.Sprintf(\"###### %s\\n\", text)\n}\n\n// BulletList returns an bullet list for markdown\nfunc BulletList(items []string) string {\n\tvar sb strings.Builder\n\tfor _, item := range items {\n\t\tsb.WriteString(ufmt.Sprintf(\"- %s\\n\", item))\n\t}\n\treturn sb.String()\n}\n\n// OrderedList returns an ordered list for markdown\nfunc OrderedList(items []string) string {\n\tvar sb strings.Builder\n\tfor i, item := range items {\n\t\tsb.WriteString(ufmt.Sprintf(\"%d. %s\\n\", i+1, item))\n\t}\n\treturn sb.String()\n}\n\n// TodoList returns a list of todo items with checkboxes for markdown\nfunc TodoList(items []string, done []bool) string {\n\tvar sb strings.Builder\n\n\tfor i, item := range items {\n\t\tcheckbox := \" \"\n\t\tif done[i] {\n\t\t\tcheckbox = \"x\"\n\t\t}\n\t\tsb.WriteString(ufmt.Sprintf(\"- [%s] %s\\n\", checkbox, item))\n\t}\n\treturn sb.String()\n}\n\n// Blockquote returns a blockquote for markdown\nfunc Blockquote(text string) string {\n\tlines := strings.Split(text, \"\\n\")\n\tvar sb strings.Builder\n\tfor _, line := range lines {\n\t\tsb.WriteString(ufmt.Sprintf(\"\u003e %s\\n\", line))\n\t}\n\n\treturn sb.String()\n}\n\n// InlineCode returns inline code for markdown\nfunc InlineCode(code string) string {\n\treturn ufmt.Sprintf(\"`%s`\", code)\n}\n\n// CodeBlock creates a markdown code block\nfunc CodeBlock(content string) string {\n\treturn ufmt.Sprintf(\"```\\n%s\\n```\", content)\n}\n\n// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting\nfunc LanguageCodeBlock(language, content string) string {\n\treturn ufmt.Sprintf(\"```%s\\n%s\\n```\", language, content)\n}\n\n// LineBreak returns the specified number of line breaks for markdown\nfunc LineBreak(count uint) string {\n\tif count \u003e 0 {\n\t\treturn strings.Repeat(\"\\n\", int(count)+1)\n\t}\n\treturn \"\"\n}\n\n// HorizontalRule returns a horizontal rule for markdown\nfunc HorizontalRule() string {\n\treturn \"---\\n\"\n}\n\n// Link returns a hyperlink for markdown\nfunc Link(text, url string) string {\n\treturn ufmt.Sprintf(\"[%s](%s)\", text, url)\n}\n\n// Image returns an image for markdown\nfunc Image(altText, url string) string {\n\treturn ufmt.Sprintf(\"![%s](%s)\", altText, url)\n}\n\n// Footnote returns a footnote for markdown\nfunc Footnote(reference, text string) string {\n\treturn ufmt.Sprintf(\"[%s]: %s\", reference, text)\n}\n\n// Paragraph wraps the given text in a Markdown paragraph\nfunc Paragraph(content string) string {\n\treturn ufmt.Sprintf(\"%s\\n\", content)\n}\n\n// MdTable is an interface for table types that can be converted to Markdown format\ntype MdTable interface {\n\tString() string\n}\n\n// Table takes any MdTable implementation and returns its markdown representation\nfunc Table(table MdTable) string {\n\treturn table.String()\n}\n\n// EscapeMarkdown escapes special markdown characters in a string\nfunc EscapeMarkdown(text string) string {\n\treturn ufmt.Sprintf(\"``%s``\", text)\n}\n"
                      },
                      {
                        "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\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "tYLFeOLuBdd5+wl/zfyS+BfGNT3U6Gxoxc1kUsM3w50f7DhD8yGTXyCgrr8DC/Fs2UOoBu4GNfY3LQxOXAuL1g=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen",
                  "package": {
                    "name": "piechart",
                    "path": "gno.land/p/samcrew/piechart",
                    "files": [
                      {
                        "name": "README.md",
                        "body": "# `piechart` - SVG pie charts \n\nGenerate pie charts with legends as SVG markup for gnoweb rendering.\n\n## Usage\n\n```go\nslices := []piechart.PieSlice{\n    {Value: 30, Color: \"#ff6b6b\", Label: \"Frontend\"},\n    {Value: 25, Color: \"#4ecdc4\", Label: \"Backend\"},\n    {Value: 20, Color: \"#45b7d1\", Label: \"DevOps\"},\n    {Value: 15, Color: \"#96ceb4\", Label: \"Mobile\"},\n    {Value: 10, Color: \"#ffeaa7\", Label: \"Other\"},\n}\n\n// With title\ntitledChart := piechart.Render(slices, \"Team Distribution\")\n\n// Without title  \nuntitledChart := piechart.Render(slices, \"\")\n```\n\n## API Reference\n\n```go\ntype PieSlice struct {\n    Value float64 // Numeric value for the slice\n    Color string  // Hex color code (e.g., \"#ff6b6b\")\n    Label string  // Display label for the slice\n}\n\n// slices: Array of PieSlice structs containing the data\n// title: Chart title (empty string for no title)\n// Returns: SVG markup as a string\nfunc Render(slices []PieSlice, title string) string\n```\n\n## Live Example\n\n- [/r/docs/charts:piechart](/r/docs/charts:piechart)\n- [/r/samcrew/daodemo/custom_condition:members](/r/samcrew/daodemo/custom_condition:members)"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/samcrew/piechart\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"
                      },
                      {
                        "name": "piechart.gno",
                        "body": "// Package piechart provides functionality to render a pie chart as an SVG image.\n// It takes a list of PieSlice objects, each representing a slice of the pie with a value,\n// color, and label, and generates an SVG representation of the pie chart.\npackage piechart\n\nimport (\n\t\"math\"\n\n\t\"gno.land/p/demo/svg\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sunspirit/md\"\n)\n\ntype PieSlice struct {\n\tValue float64\n\tColor string\n\tLabel string\n}\n\n// Render creates an SVG pie chart from given slices (value, color and label).\n// It returns an img svg markup as a string, including a markdown header if a non-empty title is provided.\nfunc Render(slices []PieSlice, title string) string {\n\t// Validate input slices length\n\tif len(slices) == 0 {\n\t\treturn \"\\npiechart fails: no data provided\"\n\t}\n\n\tconst (\n\t\tcanvasWidth  = 500\n\t\tcanvasHeight = 200\n\t\tcenterX      = 100.0\n\t\tcenterY      = 100.0\n\t\tradius       = 80.0\n\t\tlegendX      = 210\n\t\tlegendStartY = 30\n\t\tlineHeight   = 26\n\t\tsquareSize   = 16\n\t\tfontSize     = 16\n\t)\n\n\tcanvas := svg.NewCanvas(canvasWidth, canvasHeight)\n\n\t// Sum all values to compute slices proportions\n\tvar total float64\n\tfor _, s := range slices {\n\t\ttotal += s.Value\n\t}\n\n\t// Draw pie slices and legend in one pass\n\tstartAngle := -math.Pi / 2\n\tfor i, s := range slices {\n\t\tif s.Value \u003e 0 {\n\t\t\t// --- PIE SLICE ---\n\t\t\t// Calculate angle span for current slice\n\t\t\tangle := (s.Value / total) * 2 * math.Pi\n\t\t\tendAngle := startAngle + angle\n\n\t\t\t// Compute start and end points on the circle circumference\n\t\t\tcosStart, sinStart := math.Cos(startAngle), math.Sin(startAngle)\n\t\t\tcosEnd, sinEnd := math.Cos(endAngle), math.Sin(endAngle)\n\t\t\tx1 := centerX + radius*cosStart\n\t\t\ty1 := centerY + radius*sinStart\n\t\t\tx2 := centerX + radius*cosEnd\n\t\t\ty2 := centerY + radius*sinEnd\n\n\t\t\t// Determine if the arc should be a large arc (\u003e 180 degrees) (Arc direction)\n\t\t\tlargeArcFlag := 0\n\t\t\tif angle \u003e math.Pi {\n\t\t\t\tlargeArcFlag = 1\n\t\t\t}\n\n\t\t\t// Build the SVG path for the pie slice\n\t\t\tpath := ufmt.Sprintf(\n\t\t\t\t\"M%.2f,%.2f L%.2f,%.2f A%.2f,%.2f 0 %d 1 %.2f,%.2f Z\",\n\t\t\t\tcenterX, centerY, x1, y1, radius, radius, largeArcFlag, x2, y2,\n\t\t\t)\n\n\t\t\t// Colored slice\n\t\t\tcanvas.Append(svg.Path{\n\t\t\t\tD:    path,\n\t\t\t\tFill: s.Color,\n\t\t\t})\n\n\t\t\tstartAngle = endAngle\n\t\t}\n\n\t\t// --- LEGEND ---\n\t\ty := legendStartY + i*lineHeight\n\t\t// Colored square representing slice color\n\t\tcanvas.Append(svg.Rectangle{\n\t\t\tX:      legendX,\n\t\t\tY:      y - squareSize/2,\n\t\t\tWidth:  squareSize,\n\t\t\tHeight: squareSize,\n\t\t\tFill:   s.Color,\n\t\t})\n\n\t\t// Legend text showing label, value and percentage\n\t\ttext := ufmt.Sprintf(\"%s: %.0f (%.1f%%)\", s.Label, s.Value, s.Value*100/total)\n\t\tcanvas.Append(svg.Text{\n\t\t\tX:    legendX + squareSize + 8,\n\t\t\tY:    y + fontSize/3,\n\t\t\tText: text,\n\t\t\tFill: \"#54595D\",\n\t\t\tAttr: svg.BaseAttrs{\n\t\t\t\tStyle: ufmt.Sprintf(\"font-family:'Inter var',sans-serif;font-size:%dpx;\", fontSize),\n\t\t\t},\n\t\t})\n\t}\n\n\tif title == \"\" {\n\t\treturn canvas.Render(\"Pie Chart\")\n\t}\n\treturn md.H2(title) + canvas.Render(\"Pie Chart \"+title)\n}\n"
                      },
                      {
                        "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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "n6hDvd+cfgWhWzwPlm+ambaJyBZBrzG4PSIL9G5C9WkGJcgMzVHJiiIPdtmAgmFSpyqSjnJe3y60yzCQ+xQjmg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen",
                  "package": {
                    "name": "tablesort",
                    "path": "gno.land/p/samcrew/tablesort",
                    "files": [
                      {
                        "name": "README.md",
                        "body": "# `tablesort` - Sortable markdown tables\n\nGenerate sortable markdown tables with clickable column headers. Sorting state is managed via URL query parameters.\n\n## Usage\n\n```go\nimport \"gno.land/p/samcrew/tablesort\"\n\ntable := \u0026tablesort.Table{\n    Headings: []string{\"Name\", \"Age\", \"City\"},\n    Rows: [][]string{\n        {\"Alice\", \"25\", \"New York\"},\n        {\"Bob\", \"30\", \"London\"},\n        {\"Charlie\", \"22\", \"Paris\"},\n    },\n}\n\n// Basic usage\nu, _ := url.Parse(\"/users\")\nmarkdown := tablesort.Render(u, table, \"\")\n\n// Multiple tables on same page (use prefix to avoid conflicts)\nmarkdown1 := tablesort.Render(u, table, \"table1-\")\nmarkdown2 := tablesort.Render(u, table, \"table2-\")\n```\n\n## On-chain Example\n\n- [/r/gov/dao/v3/memberstore:members?filter=T1](/r/gov/dao/v3/memberstore:members?filter=T1)\n\n## API\n\n```go\ntype Table struct {\n    Headings []string   // Column headers\n    Rows     [][]string // Table data rows\n}\n\n// `u`: Current URL for generating sort links\n// `table`: Table data structure\n// `paramPrefix`: Prefix for URL params (use for multiple tables)\nfunc Render(u *url.URL, table *Table, paramPrefix string) string\n```\n\n**URL Parameters:**\n- `{prefix}sort-asc={column}`: Sort column ascending\n- `{prefix}sort-desc={column}`: Sort column descending\n\n**URL Examples:**\n- `/users?sort-desc=Name` - Sort by Name descending\n- `/page?users-sort-asc=Age\u0026orders-sort-desc=Total` - Multiple tables\n\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/samcrew/tablesort\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"
                      },
                      {
                        "name": "render.gno",
                        "body": "package tablesort\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/mason/md\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// Table holds the headings and rows for rendering.\n// Each row must have the same number of cells as there are headings.\ntype Table struct {\n\tHeadings []string   // [\"A\", \"B\", \"C\"]\n\tRows     [][]string // [[\"a1\",\"b1\",\"c1\"], [\"a2\",\"b2\",\"c2\"], ...]\n}\n\n// Render generates a Markdown table from a Table struct with sortable columns based on URL params.\n// paramPrefix is an optional prefix for in the URL to identify the tablesort Renders (e.g. \"members-\").\nfunc Render(u *url.URL, table *Table, paramPrefix string) string {\n\tdirection := \"\"\n\tcurrentHeading := \"\"\n\tif h := u.Query().Get(paramPrefix + \"sort-asc\"); h != \"\" {\n\t\tdirection = \"asc\"\n\t\tcurrentHeading = h\n\t} else if h := u.Query().Get(paramPrefix + \"sort-desc\"); h != \"\" {\n\t\tdirection = \"desc\"\n\t\tcurrentHeading = h\n\t}\n\n\tvar sb strings.Builder\n\n\t// Find the index of the column to sort\n\tcolIndex := -1\n\tfor i, h := range table.Headings {\n\t\tif h == currentHeading {\n\t\t\tcolIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Sort rows if necessary\n\tif colIndex != -1 {\n\t\tSortRows(table.Rows, colIndex, direction == \"asc\")\n\t}\n\n\t// Build header\n\tsb.WriteString(buildHeader(u, table.Headings, currentHeading, direction, paramPrefix))\n\tsb.WriteString(\"\\n\")\n\n\tnumCols := len(table.Headings)\n\n\t// Build rows\n\tfor i, row := range table.Rows {\n\t\t// Validate row length\n\t\tif len(row) != numCols {\n\t\t\treturn \"tablesort fails: row \" + ufmt.Sprintf(\"%d\", i+1) + \" has \" +\n\t\t\t\tufmt.Sprintf(\"%d\", len(row)) + \" cells, expected \" +\n\t\t\t\tufmt.Sprintf(\"%d\", numCols) + \", because there are \" + ufmt.Sprintf(\"%d\", numCols) + \" columns.\\n\"\n\t\t}\n\n\t\tsb.WriteString(\"|\")\n\t\tfor _, cell := range row {\n\t\t\tsb.WriteString(\" \" + cell + \" |\")\n\t\t}\n\t\tsb.WriteString(\"\\n\")\n\t}\n\n\treturn sb.String()\n}\n\n// buildHeader builds the Markdown header row with clickable links and arrows\nfunc buildHeader(u *url.URL, headings []string, currentHeading, direction string, paramPrefix string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"|\")\n\tfor _, h := range headings {\n\t\tarrow := \"\"\n\t\tif h == currentHeading {\n\t\t\tif direction == \"asc\" {\n\t\t\t\tarrow = \" ↑\"\n\t\t\t} else if direction == \"desc\" {\n\t\t\t\tarrow = \" ↓\"\n\t\t\t}\n\t\t}\n\n\t\t// Build URL for the header link with toggle logic\n\t\tnewURL := *u\n\t\tq := newURL.Query()\n\t\tif h == currentHeading {\n\t\t\t// Toggle sort direction\n\t\t\tif direction == \"asc\" {\n\t\t\t\tq.Del(paramPrefix + \"sort-asc\")\n\t\t\t\tq.Set(paramPrefix+\"sort-desc\", h)\n\t\t\t} else {\n\t\t\t\tq.Del(paramPrefix + \"sort-desc\")\n\t\t\t\tq.Set(paramPrefix+\"sort-asc\", h)\n\t\t\t}\n\t\t} else {\n\t\t\t// First click defaults to descending\n\t\t\tq.Del(paramPrefix + \"sort-asc\")\n\t\t\tq.Set(paramPrefix+\"sort-desc\", h)\n\t\t}\n\t\tnewURL.RawQuery = q.Encode()\n\t\tlink := md.Link(h+arrow, newURL.String())\n\t\tsb.WriteString(\" \" + link + \" |\")\n\t}\n\n\tsb.WriteString(\"\\n|\")\n\tfor range headings {\n\t\tsb.WriteString(\" --- |\")\n\t}\n\treturn sb.String()\n}\n"
                      },
                      {
                        "name": "tablesort.gno",
                        "body": "// Package tablesort provides functionality to render a Markdown table with sortable columns.\n// It allows users to click on column headers to sort the table in ascending or descending sort direction.\n// The sorting state is managed via URL query parameters.\n// It displays an error if the table is malformed (e.g. rows with missing cells).\n// Multiple tablesort can be rendered on the same page by using a paramPrefix for each Render (See the Render function).\npackage tablesort\n\nimport (\n\t\"sort\"\n)\n\n// rowSorter implements sort.Interface for sorting rows by a specific column.\ntype rowSorter struct {\n\trows      [][]string\n\tcolIndex  int\n\tascending bool\n}\n\nfunc (rs rowSorter) Len() int {\n\treturn len(rs.rows)\n}\n\nfunc (rs rowSorter) Less(i, j int) bool {\n\tiCell := rs.rows[i][rs.colIndex]\n\tjCell := rs.rows[j][rs.colIndex]\n\tif rs.ascending {\n\t\treturn iCell \u003c jCell\n\t}\n\treturn iCell \u003e jCell\n}\n\nfunc (rs rowSorter) Swap(i, j int) {\n\trs.rows[i], rs.rows[j] = rs.rows[j], rs.rows[i]\n}\n\n// SortRows sorts the rows slice by a given column index and direction\nfunc SortRows(rows [][]string, colIndex int, ascending bool) {\n\tsort.Sort(rowSorter{rows, colIndex, ascending})\n}\n"
                      },
                      {
                        "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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "5Pw10SxqXt3TKGbeHXoM7XEIii1HZ16zyedTi1xvr9pmYW5aI3WccPNdVLiwhBXwipq9M4XEkz3Ktz33tHzTuA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen",
                  "package": {
                    "name": "urlfilter",
                    "path": "gno.land/p/samcrew/urlfilter",
                    "files": [
                      {
                        "name": "README.md",
                        "body": "# `urlfilter` - URL-based filtering\n\nFilter items using URL query parameters with toggleable markdown links. Works with AVL tree structures where each filter contains its associated items.\n\nGiven filters `[\"T1\", \"T2\", \"size:XL\"]` and URL `/shop?filter=T1,size:XL`, it generates toggle links:\n\n- **T1** _(active, click to remove)_\n- ~~T2~~ _(inactive, click to add)_  \n- **size:XL** _(active, click to remove)_\n\n**Markdown output:**\n```markdown\n[**T1**](/p/samcrew/urlfilter?filter=size:XL) - [~~T2~~](/p/samcrew/urlfilter=T1,T2,size:XL) - [**size:XL**](/p/samcrew/urlfilter?filter=T1)\n```\n\n**Rendered as:**\n[**T1**](/p/samcrew/urlfilter?filter=size:XL) - [~~T2~~](/p/samcrew/urlfilter=T1,T2,size:XL) - [**size:XL**](/p/samcrew/urlfilter?filter=T1)\n\n## Usage\n\nThe package expects a two-level AVL tree structure:\n- **Top level**: Filter names as keys (e.g., \"T1\", \"size:XL\", \"on_sale\")  \n- **Second level**: Item trees containing the actual items for each filter\n\n```go\n// Build the main filters tree\nfilters := avl.NewTree()\n\n// Subtree for filter \"T1\" \nt1Items := avl.NewTree()\nt1Items.Set(\"key1\", \"item1\")\nt1Items.Set(\"key2\", \"item2\")\nfilters.Set(\"T1\", t1Items)\n\n// Subtree for filter \"size:XL\"\nt2Items := avl.NewTree()\nt2Items.Set(\"key3\", \"item3\")\nfilters.Set(\"T2\", t2Items)\n\n// URL with active filter \"T1\"\nu, _ := url.Parse(\"/shop?filter=T1\")\n\n// Apply filtering\nmdLinks, filteredItems := urlfilter.ApplyFilters(u, filters, \"filter\") // \"filter\" for /shop?*filter*=T1\n\n// mdLinks    → Markdown links for toggling filters  \n// filteredItems → AVL tree containing only filtered items\n```\n\n## API\n\n```go\nfunc ApplyFilters(u *url.URL, items *avl.Tree, paramName string) (string, *avl.Tree)\n```\n\n**Parameters:**\n- `u`: URL containing query parameters\n- `items`: Two-level AVL tree (filters → item trees)\n- `paramName`: Query parameter name (e.g., \"filter\" for /shop?filter=T1)\n\n**URL Format:**\n- Single filter: `?filter=T1`\n- Multiple filters: `?filter=T1,size:XL,on_sale`\n- Filter names are comma-separated\n\n**Returns:**\n- **Markdown links**: Toggleable filter links with formatting\n- **Filtered items**: AVL tree containing items from active filters\n  - If no filters active: returns all items\n  - Item keys are preserved, values show which filter matched\n\n# Example\n\n- [/r/gov/dao/v3/memberstore:members?filter=T1](/r/gov/dao/v3/memberstore:members?filter=T1)"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/p/samcrew/urlfilter\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1kfd9f5zlvcvy6aammcmqswa7cyjpu2nyt9qfen\"\n"
                      },
                      {
                        "name": "urlfilter.gno",
                        "body": "// Package urlfilter provides functionality to filter items based on URL query parameters.\n// It is designed to work with an avl.Tree structure where each key represents a filter\n// and each value is an avl.Tree containing items associated with that filter.\npackage urlfilter\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sunspirit/md\"\n)\n\n// ApplyFilters filters items based on the \"filter\" query parameter in the given URL\n// and generates a Markdown representation of all available filters.\n//\n// Expected `items` structure:\n//   - `items` is an *avl.Tree where each key is a filter name (e.g., \"T1\", \"size:XL\", \"on_sale\")\n//     and each value is an *avl.Tree 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 := avl.NewTree()\n//\n//\t// Subtree for filter \"T1\"\n//\tt1Items := avl.NewTree()\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 *avl.Tree, paramName string) (string, *avl.Tree) {\n\tactive := parseFilterMap(u.Query(), paramName)\n\tallFilters := make([]string, 0)\n\tresultTree := avl.NewTree()\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.(*avl.Tree)\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/avl/v0\"\n)\n\nfunc buildTestTree() *avl.Tree {\n\troot := avl.NewTree()\n\n\t// keyF1 subtree\n\tt1 := avl.NewTree()\n\tt1.Set(\"key1_1\", nil)\n\tt1.Set(\"key1_2\", nil)\n\troot.Set(\"keyF1\", t1)\n\n\t// keyF2 subtree\n\tt2 := avl.NewTree()\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "KTVcoznhejL7OVE3twlikeqMdw/oYpBR3j6KQu8Ec5MSJDp3eN8hD0BYubaKzkQjM2OZG3tehm4YDLT8FmD4pg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da",
                  "package": {
                    "name": "dao",
                    "path": "gno.land/r/gov/dao",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/gov/dao\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"
                      },
                      {
                        "name": "proxy.gno",
                        "body": "package dao\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// dao is the actual govDAO implementation, having all the needed business logic\nvar dao DAO\n\n// allowedDAOs contains realms that can be used to update the actual govDAO implementation,\n// and validate Proposals.\n// This is like that to be able to rollback using a previous govDAO implementation in case\n// the latest implementation has a breaking bug. After a test period, a proposal can be\n// executed to remove all previous govDAOs implementations and leave the last one.\nvar allowedDAOs []string\n\n// proposals contains all the proposals in history.\nvar proposals *Proposals = NewProposals()\n\n// Remember this realm for rendering.\nvar gRealm = runtime.CurrentRealm()\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(p string) string {\n\tif dao == nil {\n\t\treturn \"DAO not initialized\"\n\t}\n\treturn dao.Render(gRealm.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\tif dao == nil {\n\t\treturn false\n\t}\n\texecute, err := dao.PreExecuteProposal(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(cur, pid)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tif err := prop.executor.Execute(cross); err != nil {\n\t\tpanic(err.Error())\n\t}\n\treturn 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(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(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(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(cur realm, pid ProposalID) *Proposal {\n\tp, err := GetProposal(cur, 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\nfunc GetProposal(cur realm, pid ProposalID) (*Proposal, error) {\n\tif dao == nil {\n\t\treturn nil, errors.New(\"DAO not initialized\")\n\t}\n\tif err := dao.PreGetProposal(pid); err != nil {\n\t\treturn nil, err\n\t}\n\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\n\tif err := dao.PostGetProposal(pid, prop); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prop, nil\n}\n\n// UpdateImpl is a method intended to be used on a proposal.\n// This method will update the current govDAO implementation\n// to a new one. AllowedDAOs are a list of realms that can\n// call this method, in case the new DAO implementation had\n// a breaking bug. Any value set as nil will be ignored.\n// If AllowedDAOs field is not set correctly, the actual DAO\n// implementation wont be able to execute new Proposals!\nfunc UpdateImpl(cur realm, r UpdateRequest) {\n\tgRealm := runtime.PreviousRealm().PkgPath()\n\n\tif !InAllowedDAOs(gRealm) {\n\t\tpanic(\"permission denied for prev realm: \" + gRealm)\n\t}\n\n\tif r.AllowedDAOs != nil {\n\t\tallowedDAOs = r.AllowedDAOs\n\t}\n\n\tif r.DAO != nil {\n\t\tdao = r.DAO\n\t}\n}\n\nfunc AllowedDAOs() []string {\n\tdup := make([]string, len(allowedDAOs))\n\tcopy(dup, allowedDAOs)\n\treturn dup\n}\n\nfunc InAllowedDAOs(pkg string) bool {\n\tif len(allowedDAOs) == 0 {\n\t\treturn true // corner case for initialization\n\t}\n\tfor _, d := range allowedDAOs {\n\t\tif pkg == d {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
                      },
                      {
                        "name": "proxy_test.gno",
                        "body": "package dao\n\nimport (\n\t\"chain/runtime\"\n\t\"testing\"\n\n\t\"gno.land/p/nt/testutils/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nconst (\n\tv3 = \"gno.land/r/gov/dao/v3/impl\"\n\tv4 = \"gno.land/r/gov/dao/v4/impl\"\n\tv5 = \"gno.land/r/gov/dao/v5/impl\"\n\tv6 = \"gno.land/r/gov/dao/v6/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, UpdateRequest{\n\t\tDAO:         \u0026dummyDao{},\n\t\tAllowedDAOs: []string{v3},\n\t})\n\n\t// invalid package cannot add a new dao in charge\n\ttesting.SetRealm(testing.NewCodeRealm(invalid))\n\turequire.AbortsWithMessage(t, \"permission denied for prev realm: gno.land/r/invalid/dao\", func() {\n\t\tUpdateImpl(cross, 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(v3))\n\turequire.NotPanics(t, func() {\n\t\tUpdateImpl(cross, UpdateRequest{\n\t\t\tDAO: \u0026dummyDao{},\n\t\t})\n\t})\n\n\t// v3 that is in charge adds v5 in charge\n\ttesting.SetRealm(testing.NewCodeRealm(v3))\n\turequire.NotPanics(t, func() {\n\t\tUpdateImpl(cross, UpdateRequest{\n\t\t\tDAO:         \u0026dummyDao{},\n\t\t\tAllowedDAOs: []string{v3, v5},\n\t\t})\n\t})\n\n\t// v3 can still do updates\n\ttesting.SetRealm(testing.NewCodeRealm(v3))\n\turequire.NotPanics(t, func() {\n\t\tUpdateImpl(cross, UpdateRequest{\n\t\t\tAllowedDAOs: []string{v4},\n\t\t})\n\t})\n\n\t// not after removing himself from allowedDAOs list\n\ttesting.SetRealm(testing.NewCodeRealm(v3))\n\turequire.AbortsWithMessage(t, \"permission denied for prev realm: gno.land/r/gov/dao/v3/impl\", func() {\n\t\tUpdateImpl(cross, UpdateRequest{\n\t\t\tAllowedDAOs: []string{v3},\n\t\t})\n\t})\n\n\tvar pid ProposalID\n\ttesting.SetRealm(testing.NewUserRealm(alice))\n\turequire.NotPanics(t, func() {\n\t\te := NewSimpleExecutor(\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, NewProposalRequest(\"Proposal Title\", \"Description\", e))\n\t})\n\n\tp, err := GetProposal(cross, 1000)\n\tif p != nil || err == nil {\n\t\tpanic(\"proposal should not exist and should return an error\")\n\t}\n\tp = MustGetProposal(cross, 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 v4\n\ttesting.SetRealm(testing.NewCodeRealm(v4))\n\turequire.Equal(\n\t\tt,\n\t\t\"Render: gno.land/r/gov/dao/test\",\n\t\tRender(\"test\"),\n\t)\n\n\t// reset state\n\ttesting.SetRealm(testing.NewCodeRealm(v4))\n\tUpdateImpl(cross, UpdateRequest{\n\t\tDAO:         \u0026dummyDao{},\n\t\tAllowedDAOs: []string{},\n\t})\n}\n\ntype dummyDao struct{}\n\nfunc (dd *dummyDao) PreCreateProposal(r ProposalRequest) (address, error) {\n\treturn runtime.OriginCaller(), nil\n}\n\nfunc (dd *dummyDao) PostCreateProposal(r ProposalRequest, pid ProposalID) {\n}\n\nfunc (dd *dummyDao) VoteOnProposal(r VoteRequest) error {\n\treturn nil\n}\n\nfunc (dd *dummyDao) PreGetProposal(pid ProposalID) error {\n\treturn nil\n}\n\nfunc (dd *dummyDao) PostGetProposal(pid ProposalID, p *Proposal) error {\n\treturn nil\n}\n\nfunc (dd *dummyDao) PreExecuteProposal(pid ProposalID) (bool, error) {\n\treturn true, nil\n}\n\nfunc (dd *dummyDao) Render(pkgpath string, path string) string {\n\treturn \"Render: \" + pkgpath + \"/\" + path\n}\n"
                      },
                      {
                        "name": "types.gno",
                        "body": "package dao\n\nimport (\n\t\"chain/runtime\"\n\t\"errors\"\n\n\t\"gno.land/p/nt/avl/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 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*avl.Tree // *avl.Tree[ProposalID]*Proposal\n}\n\nfunc NewProposals() *Proposals {\n\treturn \u0026Proposals{Tree: avl.NewTree()}\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, ok := ps.Get(pid.String())\n\tif !ok {\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\nfunc NewSimpleExecutor(callback func(realm) error, description string) *SimpleExecutor {\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: runtime.CurrentRealm().PkgPath(),\n\t}\n}\n\n// SimpleExecutor implements the Executor interface using\n// a callback function and a description string.\ntype SimpleExecutor struct {\n\tcallback      func(realm) error\n\tdesc          string\n\tcreationRealm string\n}\n\nfunc (e *SimpleExecutor) Execute(cur realm) error {\n\t// Check if executor was created using the constructor func\n\tif e.callback == nil {\n\t\treturn nil\n\t}\n\n\treturn e.callback(cross)\n}\n\nfunc (e *SimpleExecutor) String() string {\n\treturn e.desc\n}\n\nfunc (e *SimpleExecutor) CreationRealm() string {\n\treturn e.creationRealm\n}\n\nfunc NewSafeExecutor(e Executor) *SafeExecutor {\n\treturn \u0026SafeExecutor{\n\t\te: e,\n\t}\n}\n\n// SafeExecutor wraps an Executor to only allow its execution\n// by allowed govDAOs.\ntype SafeExecutor struct {\n\te Executor\n}\n\nfunc (e *SafeExecutor) Execute(cur realm) error {\n\t// Verify the caller is an adequate Realm\n\tif !InAllowedDAOs(runtime.PreviousRealm().PkgPath()) {\n\t\treturn errors.New(\"execution only allowed by validated govDAOs\")\n\t}\n\n\treturn e.e.Execute(cross)\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\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(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(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(r VoteRequest) error\n\n\t// PreGetProposal is called when someone is trying to get a proposal by ID.\n\t// Is intended to be used to validate who can query proposals, just in case\n\t// the actual govDAO implementation wants to limit the access.\n\tPreGetProposal(pid ProposalID) error\n\n\t// PostGetProposal is called after the proposal has been obtained. Intended to be\n\t// used by govDAO implementations if they need to check Proposal data to know if\n\t// the caller is allowed to get that kind of Proposal or not.\n\tPostGetProposal(pid ProposalID, p *Proposal) 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(pid ProposalID) (bool, 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\tRender(pkgpath string, path string) string\n}\n\ntype UpdateRequest struct {\n\tDAO         DAO\n\tAllowedDAOs []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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "RsNO6mONy++Wk/8frCFxZRWVYg83PWwZV9FrSNpulLAfts3dK5A8jXT4J7OE6IWq2YClrTvPHZYK+VOhnABTnQ=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7",
                  "package": {
                    "name": "gnoblog",
                    "path": "gno.land/r/gnoland/blog",
                    "files": [
                      {
                        "name": "admin.gno",
                        "body": "package gnoblog\n\nimport (\n\t\"chain/runtime\"\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n)\n\nvar (\n\terrNotAdmin     = errors.New(\"access restricted: not admin\")\n\terrNotModerator = errors.New(\"access restricted: not moderator\")\n\terrNotCommenter = errors.New(\"access restricted: not commenter\")\n)\n\nvar (\n\tadminAddr     = address(\"g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh\") // govdao t1 multisig\n\tmoderatorList = avl.NewTree()\n\tcommenterList = avl.NewTree()\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) // FIXME: delete instead?\n}\n\nfunc NewPostProposalRequest(_ realm, slug, title, body, publicationDate, authors, tags string) dao.ProposalRequest {\n\tcaller := runtime.PreviousRealm().Address()\n\te := dao.NewSimpleExecutor(\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 := runtime.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) // FIXME: delete instead?\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_, found := moderatorList.Get(addr.String())\n\treturn found\n}\n\nfunc isCommenter(addr address) bool {\n\t_, found := commenterList.Get(addr.String())\n\treturn found\n}\n\nfunc assertIsAdmin() {\n\tcaller := runtime.OriginCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(errNotAdmin.Error())\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := runtime.OriginCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(errNotModerator.Error())\n}\n\nfunc assertIsCommenter() {\n\tcaller := runtime.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 gnoblog\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/blog\"\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/p/nt/urequire/v0\"\n)\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(\"g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh\")\n\tinPause = false\n\tmoderatorList = avl.NewTree()\n\tcommenterList = avl.NewTree()\n}\n\nfunc TestBlog_AdminControls(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, errNotAdmin.Error(), func() {\n\t\t\tAdminSetInPause(cross, 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, func() {\n\t\t\tAdminSetInPause(cross, 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, 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, errNotAdmin.Error(), func() {\n\t\t\tAdminSetInPause(cross, false)\n\t\t})\n\t})\n}\n\nfunc TestBlog_AddRemoveModerator(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, mod)\n\n\t_, found := moderatorList.Get(mod.String())\n\turequire.True(t, found)\n\n\t// Remove the moderator\n\tAdminRemoveModerator(cross, 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(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, mod)\n\n\t// Add a commenter as a mod\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tModAddCommenter(cross, commenter)\n\n\t_, ok := commenterList.Get(commenter.String())\n\tuassert.True(t, ok)\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, errNotModerator.Error(), func() {\n\t\tModAddCommenter(cross, testutils.TestAddress(\"evil\"))\n\t})\n\n\t// Remove a commenter\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tModDelCommenter(cross, commenter)\n\n\tactive, _ := commenterList.Get(commenter.String())\n\tuassert.False(t, active.(bool))\n}\n\nfunc TestBlog_ManagePost(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, mod)\n\n\t// Add the post\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tModAddPost(\n\t\tcross,\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, slug)\n\tuassert.TypedNil(t, b.GetPost(slug))\n}\n\nfunc TestBlog_ManageComment(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, mod)\n\n\t// Add a commenter as a mod\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tModAddCommenter(cross, commenter)\n\n\t_, ok := commenterList.Get(commenter.String())\n\tuassert.True(t, ok)\n\n\t// Add the post\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tModAddPost(\n\t\tcross,\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, func() {\n\t\tAddComment(cross, slug, \"comment\")\n\t})\n\n\t// Delete the comment\n\ttesting.SetOriginCaller(mod)\n\ttesting.SetRealm(testing.NewUserRealm(mod))\n\tuassert.NotAborts(t, func() {\n\t\tModDelComment(cross, slug, 0)\n\t})\n}\n"
                      },
                      {
                        "name": "gnoblog.gno",
                        "body": "package gnoblog\n\nimport (\n\t\"chain/runtime\"\n\n\t\"gno.land/p/demo/blog\"\n)\n\nvar b = \u0026blog.Blog{\n\tTitle:  \"Gno.land's blog\",\n\tPrefix: \"/r/gnoland/blog:\",\n}\n\nfunc AddComment(_ realm, postSlug, comment string) {\n\tassertIsCommenter()\n\tassertNotInPause()\n\n\tcaller := runtime.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 gnoblog\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, \"slug1\", \"title1\", \"body1\", \"2022-05-20T13:17:22Z\", \"moul\", \"tag1,tag2\")\n\t\tModAddPost(cross, \"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|||\n\n### [title1](/r/gnoland/blog:p/slug1)\n20 May 2022\n\n|||\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 g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh 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, \"slug1\", \"comment1\")\n\t\tAddComment(cross, \"slug2\", \"comment2\")\n\t\tAddComment(cross, \"slug1\", \"comment3\")\n\t\tAddComment(cross, \"slug2\", \"comment4\")\n\t\tAddComment(cross, \"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 g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh 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 g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003ch5\u003ecomment2\n\n\u003c/h5\u003e\u003ch6\u003eby g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh 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 g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh 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 g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003ch5\u003ecomment2\n\n\u003c/h5\u003e\u003ch6\u003eby g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh 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, 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 gnoblog\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "7WD9mneohMdy/9zrN2D8Hy2P2TCewzxGcDtQfXKQdW9zkCwxxlB/4XxRR+HWR/Zdv0D5fyuPMG+lpWb6BVmpuA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l",
                  "package": {
                    "name": "users",
                    "path": "gno.land/r/sys/users",
                    "files": [
                      {
                        "name": "admin.gno",
                        "body": "package users\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/gov/dao\"\n)\n\nconst initControllerPath = \"gno.land/r/sys/users/init\"\n\nvar controllers = addrset.Set{} // caller whitelist\n\nfunc init() {\n\t// auto-whitelist the init controller for bootstrapping for testing chain.\n\tif chainID := runtime.ChainID(); chainID == \"dev\" {\n\t\tcontrollers.Add(chain.PackageAddress(initControllerPath))\n\t}\n}\n\n// AddControllerAtGenesis allows adding a controller during chain genesis (height 0).\n// This is mostly useful for testing.\nfunc AddControllerAtGenesis(_ realm, addr address) {\n\theight := runtime.ChainHeight()\n\tif height \u003e 0 {\n\t\tpanic(\"AddControllerAtGenesis can only be called at genesis (height 0)\")\n\t}\n\n\tif !addr.IsValid() {\n\t\tpanic(ErrInvalidAddress)\n\t}\n\n\tcontrollers.Add(addr)\n}\n\n// ProposeNewController allows GovDAO to add a whitelisted caller\nfunc ProposeNewController(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(cb, \"\"))\n}\n\n// ProposeControllerRemoval allows GovDAO to add a whitelisted caller\nfunc ProposeControllerRemoval(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(cb, \"\"))\n}\n\n// ProposeControllerAdditionAndRemoval allows GovDAO to add a new caller and remove an old caller in the same proposal.\nfunc ProposeControllerAdditionAndRemoval(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\terr := addToWhitelist(toAdd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn deleteFromWhitelist(toRemove)\n\t}\n\n\tdesc := ufmt.Sprint(\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(cb, \"\"))\n}\n\n// Helpers\n\nfunc deleteFromWhitelist(addr address) error {\n\tif !controllers.Has(addr) {\n\t\treturn NewErrNotWhitelisted()\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": "errors.gno",
                        "body": "package users\n\nimport (\n\t\"chain/runtime\"\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\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\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-zA-Z0-9_]{1,64}$\")\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\ntype ErrNotWhitelisted struct {\n\tCurrent  runtime.Realm // not whitelisted\n\tPrevious runtime.Realm // for context\n}\n\nfunc NewErrNotWhitelisted() ErrNotWhitelisted {\n\treturn ErrNotWhitelisted{\n\t\tCurrent:  runtime.CurrentRealm(),\n\t\tPrevious: runtime.PreviousRealm(),\n\t}\n}\n\nfunc (e ErrNotWhitelisted) Error() string {\n\treturn ufmt.Sprintf(\"%scurrent realm/user does not exist in whitelist: %v (previous: %v)\",\n\t\tprefix, e.Current, e.Previous)\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/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nvar (\n\tnameStore    = avl.NewTree() // name/aliases \u003e *UserData\n\taddressStore = avl.NewTree() // address \u003e *UserData\n\n\treAddressLookalike = regexp.MustCompile(`^g1[a-z0-9]{20,38}$`)\n\treAlphanum         = regexp.MustCompile(`^[a-zA-Z0-9_]{1,64}$`)\n)\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\nfunc (u UserData) IsDeleted() bool {\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.\nfunc RegisterUser(cur realm, name string, address_XXX address) error {\n\t// At genesis (height 0), allow any caller to register users.\n\t// After genesis, only whitelisted controllers can register.\n\tif runtime.ChainHeight() \u003e 0 \u0026\u0026 !controllers.Has(runtime.PreviousRealm().Address()) {\n\t\treturn NewErrNotWhitelisted()\n\t}\n\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\n\tif nameStore.Has(name) {\n\t\treturn ErrNameTaken\n\t}\n\n\traw, ok := addressStore.Get(address_XXX.String())\n\tif ok {\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\n\tchain.Emit(RegisterUserEvent,\n\t\t\"name\", name,\n\t\t\"address\", address_XXX.String(),\n\t)\n\treturn nil\n}\n\n// UpdateName adds a name that is associated with a specific address\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) error {\n\tif u == nil { // either doesnt exists or was deleted\n\t\treturn ErrUserNotExistOrDeleted\n\t}\n\n\t// Validate caller\n\tif !controllers.Has(runtime.CurrentRealm().Address()) {\n\t\treturn NewErrNotWhitelisted()\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\n\tif nameStore.Has(newName) {\n\t\treturn ErrNameTaken\n\t}\n\n\tu.username = newName\n\tnameStore.Set(newName, u)\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// Delete marks a user and all their aliases as deleted.\nfunc (u *UserData) Delete() error {\n\tif u == nil {\n\t\treturn ErrUserNotExistOrDeleted\n\t}\n\n\t// Validate caller\n\tif !controllers.Has(runtime.CurrentRealm().Address()) {\n\t\treturn NewErrNotWhitelisted()\n\t}\n\n\tu.deleted = true\n\n\tchain.Emit(DeleteUserEvent, \"address\", u.addr.String())\n\treturn nil\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 !reAlphanum.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/avl/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(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, 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, \"\", aliceAddr), ErrEmptyUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross, alice, \"\"), ErrInvalidAddress.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross, alice, \"invalidaddress\"), ErrInvalidAddress.Error())\n\n\t\tuassert.ErrorContains(t, RegisterUser(cross, \"username with a space\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t,\n\t\t\tRegisterUser(cross, \"verylongusernameverylongusernameverylongusernameverylongusername1\", aliceAddr),\n\t\t\tErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(cross, \"namewith^\u0026()\", aliceAddr), ErrInvalidUsername.Error())\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, alice, aliceAddr))\n\n\t\t// Try registering again\n\t\tuassert.ErrorContains(t, RegisterUser(cross, \"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, alice, aliceAddr))\n\n\t\t// Try registering alice's name with bob's address\n\t\tuassert.ErrorContains(t, RegisterUser(cross, 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, alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\turequire.NoError(t, data.Delete())\n\n\t\t// Try re-registering after deletion\n\t\tuassert.ErrorContains(t, RegisterUser(cross, \"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, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", aliceAddr), ErrNameLikeAddress.Error())\n\t\t// Beginning of address as username\n\t\tuassert.ErrorContains(t, RegisterUser(cross, \"g1jg8mtutu9khhfwc4nxmu\", aliceAddr), ErrNameLikeAddress.Error())\n\t\tuassert.NoError(t, RegisterUser(cross, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress\", aliceAddr))\n\t})\n}\n\nfunc TestUpdateName(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, alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\t{\n\t\t\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\t\t\tuassert.NoError(t, data.UpdateName(\"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, alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\t{\n\t\t\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\t\t\tuassert.NoError(t, data.UpdateName(\"alice2\"))\n\t\t\tuassert.NoError(t, data.UpdateName(\"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, alice, aliceAddr))\n\n\t\tdata := ResolveAddress(aliceAddr)\n\t\tuassert.Error(t, data.UpdateName(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(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, alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\t{\n\t\t\turequire.NoError(t, data.Delete())\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(\"newalice\"), ErrUserNotExistOrDeleted.Error())\n\t\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t\t}\n\t})\n\n\tt.Run(\"invalid_inputs\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(cross, alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\t{\n\t\t\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\t\t\tuassert.ErrorContains(t, data.UpdateName(\"\"), ErrEmptyUsername.Error())\n\t\t\tuassert.ErrorContains(t, data.UpdateName(\"username with a space\"), ErrInvalidUsername.Error())\n\t\t\tuassert.ErrorContains(t,\n\t\t\t\tdata.UpdateName(\"verylongusernameverylongusernameverylongusernameverylongusername1\"),\n\t\t\t\tErrInvalidUsername.Error())\n\t\t\tuassert.ErrorContains(t, data.UpdateName(\"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, 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(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"), ErrNameLikeAddress.Error())\n\t\t\t// Beginning of address as username\n\t\t\tuassert.ErrorContains(t, data.UpdateName(\"g1jg8mtutu9khhfwc4nxmu\"), ErrNameLikeAddress.Error())\n\t\t\tuassert.NoError(t, data.UpdateName(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress\"))\n\t\t\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/sys/users\"))\n\t\t}\n\t})\n}\n\nfunc TestDelete(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(), 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, alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\turequire.NoError(t, data.Delete())\n\t\tdata = ResolveAddress(aliceAddr)\n\t\tuassert.ErrorContains(t, data.Delete(), 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, alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\tuassert.NoError(t, data.Delete())\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\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 = avl.NewTree()\n\taddressStore = avl.NewTree()\n}\n"
                      },
                      {
                        "name": "users.gno",
                        "body": "package users\n\nimport \"gno.land/p/nt/avl/v0/rotree\"\n\n// ResolveName returns the latest UserData of a specific user by name or alias\nfunc ResolveName(name string) (data *UserData, isCurrent bool) {\n\traw, ok := nameStore.Get(name)\n\tif !ok {\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, ok := addressStore.Get(addr.String())\n\tif !ok {\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\"testing\"\n\n\t\"gno.land/p/nt/uassert/v0\"\n\t\"gno.land/p/nt/urequire/v0\"\n)\n\nfunc TestResolveName(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, 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, alice, aliceAddr))\n\t\tdata, _ := ResolveName(alice)\n\t\turequire.NoError(t, data.UpdateName(\"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, 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(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(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, 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, alice, aliceAddr))\n\t\tdata, _ := ResolveName(alice)\n\t\turequire.NoError(t, data.UpdateName(\"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, 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(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(t *testing.T) {\n\ttesting.SetRealm(testing.NewCodeRealm(initControllerPath))\n\tcleanStore(t)\n\n\turequire.NoError(t, RegisterUser(cross, 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, ok := roNS.Get(alice)\n\t\tuassert.True(t, ok)\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()\n\t\traw, ok := nameStore.Get(alice)\n\t\tuassert.False(t, raw.(*UserData).deleted)\n\n\t\t// Addr store\n\t\taliceDataRaw, ok = roAS.Get(aliceAddr.String())\n\t\tuassert.True(t, ok)\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()\n\t\traw, ok = 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())\n\t\turequire.True(t, aliceData.IsDeleted())\n\n\t\t// Should be nil because of makeSafeFn\n\t\trawRoData, ok := roNS.Get(alice)\n\t\t// uassert.False(t, ok)\n\t\t// XXX: not sure what to do here, as the tree technically has the data so returns ok\n\t\t// However the data is intercepted and something else (nil in this case) is returned.\n\t\t// should we handle this somehow?\n\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(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, 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, 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\n// TODO Uncomment after gnoweb /u/ page.\n//func TestUserRenderLink(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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "W+Jp2V4hzz5mYqRkJRSlyJ0rcqfx3X9bi65Zt7yQyqdgC0ixFgVND+uU6bvZ8A7hCxzw27rH3Ge95JG1FjHfaA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "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.\npackage init\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n// Bootstrap registers this package as a controller in r/sys/users.\nfunc Bootstrap(_ realm) {\n\tusers.AddControllerAtGenesis(cross, chain.PackageAddress(\"gno.land/r/sys/users/init\"))\n}\n\n// RegisterUser registers a new user in r/sys/users.\nfunc RegisterUser(_ realm, name string, addr address) {\n\tif err := users.RegisterUser(cross, 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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "6tyMmvigYiRCT6M0UGZb48VGBooTED8xgYZBwuQkoI5T9E7MH0gEM3pJr9MxdPzBW66y+XSUhLSQS5o1x1oYiw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "boards2",
                    "path": "gno.land/r/gnoland/boards2/v1",
                    "files": [
                      {
                        "name": "README.md",
                        "body": "# Boards2\n\nBoards2 is a social discussion forum for open communication and community-driven conversations.\n\nUsers can start discussions by creating or reposting threads and then submitting comments or replies to\nother user comments.\n\nDiscussions happen within different boards, where each board is an independent self managed community.\n\nBoards2 allows users to create two types of boards, one is the invite only board where only invited users\ncan create threads and comment, and where non invited users can only read the content and discussions; The\nother type of board is the open board where any user with a specific amount of GNOT in their account can\ncreate threads, repost and comment.\n\n## Open Boards Quick Start\n\nIf you are new to Gno.land in general, the quick start guide below can help you get started.\n\nWhat you need to create threads and start commenting within open boards is having a specific amount of GNOT\nin your Gno.land user account, which by default initially is 3000 GNOT. This initial GNOT amount could be\nchanged over time to a different amount, so this requirement can change.\n\n### How To Get a Gno.land Address\n\nTo use Boards2 you'll need a Gno.land address. You can quickly setup your account using [Adena] or any\nGno.land compatible wallet by following these steps:\n\n- Download [Adena], or a Gno.land compatible wallet\n- Once installed, you have to create a new account or add an existing one following wallet's instructions\n- If you don't have GNOT you will need to use a faucet to get some, if the network allows it\n\nFor testing networks you can use the official [Faucet Hub] to receive GNOT in your account.\n\n### How to Start Using Open Boards\n\nOnce you have the required GNOT amount in your account you can start commenting, creating and reposting\nthreads within any open board.\n\nTo comment and engage on an open board discussion visit a thread and click on the \"Comment\" link. You can\nalso reply to any of the thread's comments by clicking on the \"Reply\" link.\n\nTo create threads, visit an open board and then click on the \"Create Thread\" link, there you will have to\nenter a title and some content for the thread body.\n\nThread and comments content can be written as plaintext, or Markdown if you want to format the content so\nit's rendered as rich text.\n\nYou can also repost any thread, even the ones from invite only boards, into any open board. To do so visit\nthe thread you want to repost and click on the \"Repost\" link at the bottom of the thread, there you will have\nto enter the open board where you want the repost to be created, a title for the thread repost and optionally\nalso some content to render at the top of the repost. The optional content can also be written as plaintext\nor Markdown, like threads.\n\nAfter your thread, repost or comment is created, you can easily share the link with others so they can join\nthe discussion!\n\n## Boards\n\nBoards2 realm enables the creation of different communities though independent boards.\n\nWhen a board is created, and independetly of the board type, it initially has a single \"owner\" member\nassigned by default, which is the user that creates it. The member is called \"owner\" because by default it\nhas the `owner` role, which grants all permissions within that board.\n\nMembers of a board with the `owner` or `admin` role, independently of the board type, can invite other\nmembers, or otherwise users can request being invited to be a member by visiting the board and clicking the\n\"Request Invite\" link. Requested invites can be accepted or revoked though the board's \"Invite Requests\" view\nor using these public realm functions:\n\n```go\n// AcceptInvite accepts a board invite request.\nfunc AcceptInvite(_ realm, boardID boards.ID, user address)\n\n// RevokeInvite revokes a board invite request\nfunc RevokeInvite(_ realm, boardID boards.ID, user address)\n```\n\nThere are four possible roles that invited users can have when they are members of a board:\n- `owner`: Grants all available permissions\n- `admin`: Grants basic, moderator and advanced permissions, like being able to rename boards, add or remove\n   members, or change their role.\n- `moderator`: Grants basic and moderation related permissions, like being able to ban or unban users, or\n  flag content.\n- `guest`: Grants basic permissions that allow creating threads, reposting and commenting.\n\nDefault board configuration, permissions and roles are defined in the [permissions file].\n\nNo roles or number of members is enforced for boards, so technically a board can be updated to have no\nmembers, or for example, boards could exists without any \"owner\" if all members with `owner` role are removed\nfrom it.\n\nOther custom user defined roles can exists on top of the default ones though [custom board] implementations.\n\n### Custom Boards\n\nBoards2 realm allows users to customize the mechanics of their boards when the default ones doesn't make\nsense to that community, or when users want to integrate a board with their realms.\n\nAn example of this would be a case where thread creation should be allowed only though a new `publisher`\nrole, or a case where a community have their own DAO realm and governance implementation and are looking to\nintegrate it into their board mechanics by creating threads though proposals that must be approved for the\nthread to be published.\n\nEach board can customize the way it works by implementing the [Permissions] interface that is defined in the\n[gno.land/p/gnoland/boards] package. It is though the implementation of that interface within a new realm\nthat the default board mechanics can be customized. The new realm can then be used to create an instance of\na custom `Permissions` implementation to replace the one assigned by default to a board.\n\nRight now only Boards2 realm `owner` members are allowed to change default board permissions using a public\nrealm function:\n\n```go\n// SetPermissions sets a permissions implementation for boards2 realm or a board\nfunc SetPermissions(_ realm, boardID boards.ID, p boards.Permissions)\n```\n\n\u003e This function will be replaced by a proposal that would need to pass for the custom permissions to be\n\u003e applied to a board once Boards2 governance is implemented.\n\n`Permissions` implementation allow communities to customize the way they want to manage users and roles,\nwhere or how they should be stored, and the requirements or effects different board actions have.\n\nBoards2 provides a custom `Permissions` implementation in [gno.land/r/gnoland/boards2/v1/permissions] that\ncan be imported by realms and used to implement custom boards.\n\n### Boards Governance\n\nBy default boards are created with an undelying DAO, so each new board is linked to an independent DAO which\nis used to organize members by role, and can also be used to update boards in a permissionless manner.\n\nRight now is possible to integrate with the underlying DAO and change the default board mechanics to rely on\nproposals using a [custom board] implementation, by creating a new realm that imports and uses the\n[gno.land/r/gnoland/boards2/v1/permissions] realm, which exposes the underlying DAO.\n\n\u003e Current Boards2 realm implementation doesn't run proposals, but some of the current mechanics will rely on\n\u003e DAO proposals to actually execute changes.\n\n## Moderation\n\n### Flagging\n\nThreads and comments are moderated by flagging, which requires the `moderator`, `admin` or `owner` roles.\n\nA reason is required each time content is flagged by a member. Content is replaced by a feedback message\nand a link to the list of flagging reasons given by moderators when a moderation flagging threshold is\nreached. By default the threshold is of a single flag.\n\n\u003e Right now is not possible to show the content of a thread or comment that has been hidden because of\n\u003e moderation, but future Boards2 versions might implement a way to handle moderation disputes and allow\n\u003e restoring the thread or comment content.\n\n\u003e Boards2 realm `owners` are allowed to moderate content with a single flag within any board at this point,\n\u003e but this might be changed to work though a DAO proposal.\n\nEach board's `owner` or `admin` members are free to change the flagging threshold within a single board to a\ngreater value using a public realm function:\n\n```go\n// SetFlaggingThreshold sets the number of flags required to hide a thread or comment\nfunc SetFlaggingThreshold(_ realm, boardID boards.ID, threshold int)\n```\n\n### Banning\n\nMembers with the `moderator`, `admin` or `owner` roles are the only ones that are allowed to ban or unban\na user within a board.\n\nUsers can be banned with a reason for any number of hours. Within this period banned users are not allowed\nto interact or make any changes.\n\nOnly invited `guest` members and open board users can be banned, banning board owners, admins and moderators\nis not allowed.\n\nBanning and unbanning can be done by calling these public realm functions:\n\n```go\n// Ban bans a user from a board for a period of time\nfunc Ban(_ realm, boardID boards.ID, user address, hours uint, reason string)\n\n// Unban unbans a user from a board\nfunc Unban(_ realm, boardID boards.ID, user address, reason string)\n```\n\n## Freezing\n\nBoards2 realm allows `owner` or `admin` members of a board to freeze the board or any of its threads.\nFreezing makes the board or thread readonly, disallowing any changes or additions until unfrozen.\n\nThe following public realm function can be called for freezing:\n\n```go\n// FreezeBoard freezes a board so no more threads and comments can be created or modified\nfunc FreezeBoard(_ realm, boardID boards.ID)\n\n// UnfreezeBoard removes frozen status from a board\nfunc UnfreezeBoard(_ realm, boardID boards.ID)\n\n// FreezeThread freezes a thread so thread cannot be replied, modified or deleted\nfunc FreezeThread(_ realm, boardID, threadID boards.ID)\n\n// UnfreezeThread removes frozen status from a thread\nfunc UnfreezeThread(_ realm, boardID, threadID boards.ID)\n```\n\n\n[permissions file]: https://gno.land/r/gnoland/boards2/v1$source\u0026file=permissions.gno\n[gno.land/r/gnoland/boards2/v1/permissions]: https://gno.land/r/gnoland/boards2/v1/permissions/\n[custom board]: #custom-boards\n[Adena]: https://www.adena.app/\n[Faucet Hub]: https://faucet.gno.land/\n[gno.land/p/gnoland/boards]: https://gno.land/p/gnoland/boards\n[Permissions]: https://gno.land/p/gnoland/boards$source\u0026file=permissions.gno#L23\n"
                      },
                      {
                        "name": "boards.gno",
                        "body": "package boards2\n\nimport (\n\t\"chain/runtime\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/gnoland/boards/exts/permissions\"\n\t\"gno.land/p/moul/realmpath\"\n\t\"gno.land/p/moul/txlink\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// TODO: Refactor globals in favor of a cleaner pattern\nvar (\n\tgRealmLink        txlink.Realm\n\tgNotice           string\n\tgHelp             string\n\tgListedBoardsByID avl.Tree // string(id) -\u003e *boards.Board\n\tgInviteRequests   avl.Tree // string(board id) -\u003e *avl.Tree(address -\u003e time.Time)\n\tgBannedUsers      avl.Tree // string(board id) -\u003e *avl.Tree(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\tgRealmPath      = strings.TrimPrefix(runtime.CurrentRealm().PkgPath(), \"gno.land\")\n\tgPerms          = initRealmPermissions(\n\t\t\"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\",\n\t\t\"g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh\", // govdao t1 multisig\n\t)\n\n\t// TODO: Allow updating open account amount though a proposal (GovDAO, CommonDAO?)\n\tgOpenAccountAmount = int64(3_000_000_000) // ugnot required for open board actions\n)\n\nfunc init() {\n\t// Save current realm path so it's available during render calls\n\tgRealmLink = txlink.Realm(runtime.CurrentRealm().PkgPath())\n}\n\n// initRealmPermissions returns the default realm permissions.\nfunc initRealmPermissions(owners ...address) boards.Permissions {\n\tperms := permissions.New(\n\t\tpermissions.UseSingleUserRole(),\n\t\tpermissions.WithSuperRole(RoleOwner),\n\t)\n\tperms.AddRole(RoleAdmin, PermissionBoardCreate)\n\tfor _, owner := range owners {\n\t\tperms.SetUserRoles(owner, RoleOwner)\n\t}\n\n\tperms.ValidateFunc(PermissionBoardCreate, validateBasicBoardCreate)\n\tperms.ValidateFunc(PermissionMemberInvite, validateBasicMemberInvite)\n\tperms.ValidateFunc(PermissionRoleChange, validateBasicRoleChange)\n\treturn perms\n}\n\n// getInviteRequests returns invite requests for a board.\nfunc getInviteRequests(boardID boards.ID) (_ *avl.Tree, found bool) {\n\tv, exists := gInviteRequests.Get(boardID.Key())\n\tif !exists {\n\t\treturn nil, false\n\t}\n\treturn v.(*avl.Tree), true\n}\n\n// getBannedUsers returns banned users within a board.\nfunc getBannedUsers(boardID boards.ID) (_ *avl.Tree, found bool) {\n\tv, exists := gBannedUsers.Get(boardID.Key())\n\tif !exists {\n\t\treturn nil, false\n\t}\n\treturn v.(*avl.Tree), 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(gRealmLink)\n\treturn r\n}\n"
                      },
                      {
                        "name": "flag.gno",
                        "body": "package boards2\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\n// DefaultFlaggingThreshold defines the default number of flags that hides flaggable items.\nconst DefaultFlaggingThreshold = 1\n\nvar gFlaggingThresholds avl.Tree // 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, ok := gFlaggingThresholds.Get(bid.String()); ok {\n\t\treturn v.(int)\n\t}\n\treturn DefaultFlaggingThreshold\n}\n"
                      },
                      {
                        "name": "format.gno",
                        "body": "package boards2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/moul/md\"\n\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\nfunc summaryOf(text string, length int) string {\n\tlines := strings.SplitN(text, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n\nfunc userLink(addr address) string {\n\tif u := users.ResolveAddress(addr); u != nil {\n\t\treturn md.UserLink(u.Name())\n\t}\n\treturn md.UserLink(addr.String())\n}\n\nfunc getRoleBadge(post *boards.Post) string {\n\tif post == nil || post.Board == nil || post.Board.Permissions == nil {\n\t\treturn \"\"\n\t}\n\n\tperms := post.Board.Permissions\n\tcreator := post.Creator\n\n\t// Check roles in order of priority\n\tif perms.HasRole(creator, RoleOwner) {\n\t\treturn \" `owner`\"\n\t}\n\tif perms.HasRole(creator, RoleAdmin) {\n\t\treturn \" `admin`\"\n\t}\n\tif perms.HasRole(creator, RoleModerator) {\n\t\treturn \" `mod`\"\n\t}\n\treturn \"\"\n}\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/gnoland/boards2/v1\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "meta.gno",
                        "body": "package boards2\n\nimport \"gno.land/p/gnoland/boards\"\n\n// BoardMeta defines a type for board metadata.\ntype BoardMeta struct {\n\t// HiddenThreads contains hidden board threads.\n\tHiddenThreads boards.PostStorage\n}\n\n// ThreadMeta defines a type for thread metadata.\ntype ThreadMeta struct {\n\t// AllReplies contains all existing thread comments and replies.\n\tAllReplies boards.PostStorage\n}\n"
                      },
                      {
                        "name": "permissions.gno",
                        "body": "package boards2\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/gnoland/boards/exts/permissions\"\n)\n\nconst (\n\tRoleOwner     boards.Role = \"owner\"\n\tRoleAdmin                 = \"admin\"\n\tRoleModerator             = \"moderator\"\n\tRoleGuest                 = \"guest\"\n)\n\nconst (\n\tPermissionBoardCreate         boards.Permission = \"board:create\"\n\tPermissionBoardFlaggingUpdate                   = \"board:flagging-update\"\n\tPermissionBoardFreeze                           = \"board:freeze\"\n\tPermissionBoardRename                           = \"board:rename\"\n\tPermissionMemberInvite                          = \"member:invite\"\n\tPermissionMemberInviteRevoke                    = \"member:invite-remove\"\n\tPermissionMemberRemove                          = \"member:remove\"\n\tPermissionPermissionsUpdate                     = \"permissions:update\"\n\tPermissionRealmHelp                             = \"realm:help\"\n\tPermissionRealmLock                             = \"realm:lock\"\n\tPermissionRealmNotice                           = \"realm:notice\"\n\tPermissionReplyCreate                           = \"reply:create\"\n\tPermissionReplyDelete                           = \"reply:delete\"\n\tPermissionReplyFlag                             = \"reply:flag\"\n\tPermissionRoleChange                            = \"role:change\"\n\tPermissionThreadCreate                          = \"thread:create\"\n\tPermissionThreadDelete                          = \"thread:delete\"\n\tPermissionThreadEdit                            = \"thread:edit\"\n\tPermissionThreadFlag                            = \"thread:flag\"\n\tPermissionThreadFreeze                          = \"thread:freeze\"\n\tPermissionThreadRepost                          = \"thread:repost\"\n\tPermissionUserBan                               = \"user:ban\"\n\tPermissionUserUnban                             = \"user:unban\"\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\tPermissionReplyCreate,\n\t)\n\tperms.AddRole(\n\t\tRoleAdmin,\n\t\tPermissionBoardRename,\n\t\tPermissionBoardFlaggingUpdate,\n\t\tPermissionMemberInvite,\n\t\tPermissionMemberInviteRevoke,\n\t\tPermissionMemberRemove,\n\t\tPermissionThreadCreate,\n\t\tPermissionThreadEdit,\n\t\tPermissionThreadDelete,\n\t\tPermissionThreadRepost,\n\t\tPermissionThreadFlag,\n\t\tPermissionThreadFreeze,\n\t\tPermissionReplyCreate,\n\t\tPermissionReplyDelete,\n\t\tPermissionReplyFlag,\n\t\tPermissionRoleChange,\n\t\tPermissionUserBan,\n\t\tPermissionUserUnban,\n\t)\n\tperms.AddRole(\n\t\tRoleModerator,\n\t\tPermissionThreadCreate,\n\t\tPermissionThreadEdit,\n\t\tPermissionThreadRepost,\n\t\tPermissionThreadFlag,\n\t\tPermissionReplyCreate,\n\t\tPermissionReplyFlag,\n\t\tPermissionUserBan,\n\t\tPermissionUserUnban,\n\t)\n\tperms.AddRole(\n\t\tRoleGuest,\n\t\tPermissionThreadCreate,\n\t\tPermissionThreadRepost,\n\t\tPermissionReplyCreate,\n\t)\n\tperms.SetUserRoles(owner, RoleOwner)\n\tperms.ValidateFunc(PermissionBoardRename, validateOpenBoardRename)\n\tperms.ValidateFunc(PermissionMemberInvite, validateOpenMemberInvite)\n\tperms.ValidateFunc(PermissionRoleChange, validateOpenRoleChange)\n\tperms.ValidateFunc(PermissionThreadCreate, validateOpenThreadCreate)\n\tperms.ValidateFunc(PermissionReplyCreate, validateOpenReplyCreate)\n\treturn perms\n}\n"
                      },
                      {
                        "name": "permissions_validators_basic.gno",
                        "body": "package boards2\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n// validateBasicBoardCreate validates PermissionBoardCreate.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board name\n// 3. Board ID\n// 4. Is board listed\n// 5. Is board open\nfunc validateBasicBoardCreate(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\tname, ok := args[1].(string)\n\tif !ok {\n\t\treturn errors.New(\"expected board name to be a string\")\n\t}\n\n\topen, ok := args[4].(bool)\n\tif !ok {\n\t\treturn errors.New(\"expected board open flag to be a boolean\")\n\t}\n\n\tif open \u0026\u0026 !perms.HasRole(caller, RoleOwner) {\n\t\treturn errors.New(\"only owners can create open boards\")\n\t}\n\n\tif err := checkBoardNameIsNotAddress(name); err != nil {\n\t\treturn err\n\t}\n\n\tif err := checkBoardNameBelongsToAddress(caller, name); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// validateBasicBoardRename validates PermissionBoardRename.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Current board name\n// 4. New board name\nfunc validateBasicBoardRename(_ boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\tnewName, ok := args[3].(string)\n\tif !ok {\n\t\treturn errors.New(\"expected new board name to be a string\")\n\t}\n\n\tif err := checkBoardNameIsNotAddress(newName); err != nil {\n\t\treturn err\n\t}\n\n\tif err := checkBoardNameBelongsToAddress(caller, newName); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// validateBasicMemberInvite validates PermissionMemberInvite.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Invites\nfunc validateBasicMemberInvite(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\tinvites, ok := args[2].([]Invite)\n\tif !ok {\n\t\treturn errors.New(\"expected valid user invites\")\n\t}\n\n\t// Make sure that only owners invite other owners\n\tcallerIsOwner := perms.HasRole(caller, RoleOwner)\n\tfor _, v := range invites {\n\t\tif v.Role == RoleOwner \u0026\u0026 !callerIsOwner {\n\t\t\treturn errors.New(\"only owners are allowed to invite other owners\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// validateBasicRoleChange validates PermissionRoleChange.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Member address\n// 4. Role\nfunc validateBasicRoleChange(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\t// Owners and Admins can change roles.\n\t// Admins should not be able to assign or remove the Owner role from members.\n\tif perms.HasRole(caller, RoleAdmin) {\n\t\trole, ok := args[3].(boards.Role)\n\t\tif !ok {\n\t\t\treturn errors.New(\"expected a valid member role\")\n\t\t}\n\n\t\tif role == RoleOwner {\n\t\t\treturn errors.New(\"admins are not allowed to promote members to Owner\")\n\t\t} else {\n\t\t\tmember, ok := args[2].(address)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"expected a valid member address\")\n\t\t\t}\n\n\t\t\tif perms.HasRole(member, RoleOwner) {\n\t\t\t\treturn errors.New(\"admins are not allowed to remove the Owner role\")\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc checkBoardNameIsNotAddress(s string) error {\n\tif address(s).IsValid() {\n\t\treturn errors.New(\"addresses are not allowed as board name\")\n\t}\n\treturn nil\n}\n\nfunc checkBoardNameBelongsToAddress(owner address, name string) error {\n\t// When the board name is the name of a registered user\n\t// check that caller is the owner of the name.\n\tuser, _ := users.ResolveName(name)\n\tif user != nil \u0026\u0026 user.Addr() != owner {\n\t\treturn errors.New(\"board name is a user name registered to a different user\")\n\t}\n\treturn nil\n}\n"
                      },
                      {
                        "name": "permissions_validators_open.gno",
                        "body": "package boards2\n\nimport (\n\t\"chain/banker\"\n\t\"errors\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\n// validateOpenBoardRename validates PermissionBoardRename.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Current board name\n// 4. New board name\nfunc validateOpenBoardRename(_ boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\tnewName, ok := args[3].(string)\n\tif !ok {\n\t\treturn errors.New(\"expected new board name to be a string\")\n\t}\n\n\tif err := checkBoardNameIsNotAddress(newName); err != nil {\n\t\treturn err\n\t}\n\n\tif err := checkBoardNameBelongsToAddress(caller, newName); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// validateOpenMemberInvite validates PermissionMemberInvite.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Invites\nfunc validateOpenMemberInvite(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\tinvites, ok := args[2].([]Invite)\n\tif !ok {\n\t\treturn errors.New(\"expected valid user invites\")\n\t}\n\n\t// Make sure that only owners invite other owners\n\tcallerIsOwner := perms.HasRole(caller, RoleOwner)\n\tfor _, v := range invites {\n\t\tif v.Role == RoleOwner \u0026\u0026 !callerIsOwner {\n\t\t\treturn errors.New(\"only owners are allowed to invite other owners\")\n\t\t}\n\t}\n\treturn nil\n}\n\n// validateOpenRoleChange validates PermissionRoleChange.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Member address\n// 4. Role\nfunc validateOpenRoleChange(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\t// Owners and Admins can change roles.\n\t// Admins should not be able to assign or remove the Owner role from members.\n\tif perms.HasRole(caller, RoleAdmin) {\n\t\trole, ok := args[3].(boards.Role)\n\t\tif !ok {\n\t\t\treturn errors.New(\"expected a valid member role\")\n\t\t}\n\n\t\tif role == RoleOwner {\n\t\t\treturn errors.New(\"admins are not allowed to promote members to Owner\")\n\t\t} else {\n\t\t\tmember, ok := args[2].(address)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"expected a valid member address\")\n\t\t\t}\n\n\t\t\tif perms.HasRole(member, RoleOwner) {\n\t\t\t\treturn errors.New(\"admins are not allowed to remove the Owner role\")\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// validateOpenThreadCreate validates PermissionThreadCreate.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Thread ID\n// 4. Title\n// 5. Body\nfunc validateOpenThreadCreate(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\t// Owners and admins can create threads without special requirements\n\tif perms.HasRole(caller, RoleOwner) || perms.HasRole(caller, RoleAdmin) {\n\t\treturn nil\n\t}\n\n\t// Require non members to have some GNOT in their accounts\n\tif err := checkAccountHasAmount(caller, gOpenAccountAmount); err != nil {\n\t\treturn ufmt.Errorf(\"caller is not allowed to create threads: %s\", err)\n\t}\n\treturn nil\n}\n\n// validateOpenReplyCreate validates PermissionReplyCreate.\n//\n// Expected `args` values:\n// 1. Caller address\n// 2. Board ID\n// 3. Thread ID\n// 4. Parent ID\n// 5. Reply ID\n// 6. Body\nfunc validateOpenReplyCreate(perms boards.Permissions, args boards.Args) error {\n\tcaller, ok := args[0].(address)\n\tif !ok {\n\t\treturn errors.New(\"expected a valid caller address\")\n\t}\n\n\t// All board members can reply\n\tif perms.HasUser(caller) {\n\t\treturn nil\n\t}\n\n\t// Require non members to have some GNOT in their accounts\n\tif err := checkAccountHasAmount(caller, gOpenAccountAmount); 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.NewBanker(banker.BankerTypeReadonly)\n\tcoins := bnk.GetCoins(addr)\n\tif coins.AmountOf(\"ugnot\") \u003c gOpenAccountAmount {\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\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\nconst (\n\t// MaxBoardNameLength defines the maximum length allowed for board names.\n\tMaxBoardNameLength = 50\n\n\t// MaxThreadTitleLength defines the maximum length allowed for thread titles.\n\tMaxThreadTitleLength = 100\n\n\t// MaxReplyLength defines the maximum length allowed for replies.\n\tMaxReplyLength = 1000\n)\n\nvar (\n\treBoardName = regexp.MustCompile(`(?i)^[a-z]+[a-z0-9_\\-]{2,50}$`)\n\n\t// Minimalistic Markdown line prefix checks that if allowed would\n\t// break the current UI when submitting a reply. It denies replies\n\t// with headings, blockquotes or horizontal lines.\n\treDeniedReplyLinePrefixes = regexp.MustCompile(`(?m)^\\s*(#|---|\u003e)+`)\n)\n\n// SetHelp sets or updates boards realm help content.\nfunc SetHelp(_ realm, content string) {\n\tcontent = strings.TrimSpace(content)\n\tcaller := runtime.PreviousRealm().Address()\n\targs := boards.Args{content}\n\tgPerms.WithPermission(caller, PermissionRealmHelp, args, crossingFn(func() {\n\t\tgHelp = content\n\t}))\n}\n\n// SetPermissions sets a permissions implementation for boards2 realm or a board.\nfunc SetPermissions(_ 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 := runtime.PreviousRealm().Address()\n\targs := boards.Args{boardID}\n\tgPerms.WithPermission(caller, PermissionPermissionsUpdate, args, crossingFn(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\"RealmPermissionsUpdated\",\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\"BoardPermissionsUpdated\",\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 by the realm.\n// An empty message removes the realm notice.\nfunc SetRealmNotice(_ realm, message string) {\n\tmessage = strings.TrimSpace(message)\n\tcaller := runtime.PreviousRealm().Address()\n\targs := boards.Args{message}\n\tgPerms.WithPermission(caller, PermissionRealmNotice, args, crossingFn(func() {\n\t\tgNotice = 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// 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(_ 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 := runtime.PreviousRealm().Address()\n\tid := gBoardsSequence.Next()\n\tboard := boards.New(id)\n\targs := boards.Args{caller, name, board.ID, listed, open}\n\tgPerms.WithPermission(caller, PermissionBoardCreate, args, crossingFn(func() {\n\t\tassertRealmIsNotLocked()\n\t\tassertBoardNameNotExists(name)\n\n\t\tboard.Name = name\n\t\tboard.Creator = caller\n\t\tboard.Meta = \u0026BoardMeta{\n\t\t\tHiddenThreads: boards.NewPostStorage(),\n\t\t}\n\n\t\tif open {\n\t\t\tboard.Permissions = createOpenBoardPermissions(caller)\n\t\t} else {\n\t\t\tboard.Permissions = createBasicBoardPermissions(caller)\n\t\t}\n\n\t\tif err := gBoards.Add(board); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t// Listed boards are also indexed separately for easier iteration and pagination\n\t\tif listed {\n\t\t\tgListedBoardsByID.Set(board.ID.Key(), board)\n\t\t}\n\n\t\tchain.Emit(\n\t\t\t\"BoardCreated\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\"name\", name,\n\t\t)\n\t}))\n\treturn board.ID\n}\n\n// RenameBoard changes the name of an existing board.\n//\n// A history of previous board names is kept when boards are renamed.\n// Because of that boards are also accessible using previous name(s).\nfunc RenameBoard(_ 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 := runtime.PreviousRealm().Address()\n\targs := boards.Args{caller, board.ID, name, newName}\n\tboard.Permissions.WithPermission(caller, PermissionBoardRename, args, crossingFn(func() {\n\t\tassertRealmIsNotLocked()\n\t\tassertBoardNameNotExists(newName)\n\n\t\tboard.Aliases = append(board.Aliases, board.Name)\n\t\tboard.Name = newName\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(_ realm, boardID boards.ID, title, body string) boards.ID {\n\tassertRealmIsNotLocked()\n\n\ttitle = strings.TrimSpace(title)\n\tassertTitleIsValid(title)\n\n\tcaller := runtime.PreviousRealm().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, crossingFn(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(_ realm, boardID, threadID, replyID boards.ID, body string) boards.ID {\n\tassertRealmIsNotLocked()\n\n\tbody = strings.TrimSpace(body)\n\tassertReplyBodyIsValid(body)\n\n\tcaller := runtime.PreviousRealm().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, crossingFn(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(_ realm, boardID, threadID, destinationBoardID boards.ID, title, body string) boards.ID {\n\tassertRealmIsNotLocked()\n\n\ttitle = strings.TrimSpace(title)\n\tassertTitleIsValid(title)\n\n\tcaller := runtime.PreviousRealm().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, crossingFn(func() {\n\t\tassertRealmIsNotLocked()\n\n\t\trepost.Title = title\n\t\trepost.Body = strings.TrimSpace(body)\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(_ realm, boardID, threadID boards.ID) {\n\tassertRealmIsNotLocked()\n\n\tcaller := runtime.PreviousRealm().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, crossingFn(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(_ realm, boardID, threadID, replyID boards.ID) {\n\tassertRealmIsNotLocked()\n\n\tcaller := runtime.PreviousRealm().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 thread\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 reply's parent\n\t\t\tif reply.ParentID != thread.ID {\n\t\t\t\tparent, found := meta.AllReplies.Get(reply.ParentID)\n\t\t\t\tif found {\n\t\t\t\t\tparent.Replies.Remove(replyID)\n\t\t\t\t}\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, crossingFn(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(_ realm, boardID, threadID boards.ID, title, body string) {\n\tassertRealmIsNotLocked()\n\n\ttitle = strings.TrimSpace(title)\n\tassertTitleIsValid(title)\n\n\tboard := mustGetBoard(boardID)\n\tassertBoardIsNotFrozen(board)\n\n\tcaller := runtime.PreviousRealm().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, crossingFn(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(_ 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 := runtime.PreviousRealm().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(_ realm, boardID boards.ID, member address) {\n\tassertMembersUpdateIsEnabled(boardID)\n\tassertMemberAddressIsValid(member)\n\n\tperms := mustGetPermissions(boardID)\n\torigin := runtime.OriginCaller()\n\tcaller := runtime.PreviousRealm().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, crossingFn(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(_ 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 := runtime.PreviousRealm().Address()\n\targs := boards.Args{caller, boardID, member, role}\n\tperms.WithPermission(caller, PermissionRoleChange, args, crossingFn(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\n// IterateRealmMembers iterates boards realm members.\n// The iteration is done only for realm members, board members are not iterated.\nfunc IterateRealmMembers(offset int, fn boards.UsersIterFn) (halted bool) {\n\tcount := gPerms.UsersCount() - offset\n\treturn gPerms.IterateUsers(offset, count, fn)\n}\n\n// GetBoard returns a single board.\nfunc GetBoard(boardID boards.ID) *boards.Board {\n\tboard := mustGetBoard(boardID)\n\tif !board.Permissions.HasRole(runtime.OriginCaller(), RoleOwner) {\n\t\tpanic(\"forbidden\")\n\t}\n\treturn board\n}\n\n// Wraps a function to cross back to Boards2 realm.\nfunc crossingFn(fn func()) func() {\n\treturn func() {\n\t\tfunc(realm) { fn() }(cross)\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 assertHasPermission(perms boards.Permissions, user address, p boards.Permission) {\n\tif !perms.HasPermission(user, p) {\n\t\tpanic(\"unauthorized\")\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 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\tif reDeniedReplyLinePrefixes.MatchString(body) {\n\t\tpanic(\"using Markdown headings, blockquotes or horizontal lines is not allowed in replies\")\n\t}\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\"chain/runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/nt/avl/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(_ 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 := runtime.PreviousRealm().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, crossingFn(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 = avl.NewTree()\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(_ realm, boardID boards.ID, user address, reason string) {\n\tassertAddressIsValid(user)\n\n\tboard := mustGetBoard(boardID)\n\tcaller := runtime.PreviousRealm().Address()\n\targs := boards.Args{boardID, user, reason}\n\tboard.Permissions.WithPermission(caller, PermissionUserUnban, args, crossingFn(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, found := banned.Get(user.String())\n\tif !found {\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\"chain/runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// SetFlaggingThreshold sets the number of flags required to hide a thread or comment.\n//\n// Threshold is only applicable within the board where it's setted.\nfunc SetFlaggingThreshold(_ 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 := runtime.PreviousRealm().Address()\n\targs := boards.Args{board.ID, threshold}\n\tboard.Permissions.WithPermission(caller, PermissionBoardFlaggingUpdate, args, crossingFn(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(_ 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 := runtime.PreviousRealm().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, crossingFn(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(_ 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 := runtime.PreviousRealm().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, crossingFn(func() {\n\t\tflagReply()\n\t}))\n}\n"
                      },
                      {
                        "name": "public_freeze.gno",
                        "body": "package boards2\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\t\"strconv\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// FreezeBoard freezes a board so no more threads and comments can be created or modified.\nfunc FreezeBoard(_ realm, boardID boards.ID) {\n\tsetBoardReadonly(boardID, true)\n}\n\n// UnfreezeBoard removes frozen status from a board.\nfunc UnfreezeBoard(_ realm, boardID boards.ID) {\n\tsetBoardReadonly(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(_ realm, boardID, threadID boards.ID) {\n\tsetThreadReadonly(boardID, threadID, true)\n}\n\n// UnfreezeThread removes frozen status from a thread.\n//\n// Fails if board is frozen.\nfunc UnfreezeThread(_ realm, boardID, threadID boards.ID) {\n\tsetThreadReadonly(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(boardID boards.ID, readonly bool) {\n\tassertRealmIsNotLocked()\n\n\tboard := mustGetBoard(boardID)\n\tif readonly {\n\t\tassertBoardIsNotFrozen(board)\n\t}\n\n\tcaller := runtime.PreviousRealm().Address()\n\targs := boards.Args{caller, board.ID, readonly}\n\tboard.Permissions.WithPermission(caller, PermissionBoardFreeze, args, crossingFn(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(boardID, threadID boards.ID, readonly bool) {\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 := runtime.PreviousRealm().Address()\n\targs := boards.Args{caller, board.ID, thread.ID, readonly}\n\tboard.Permissions.WithPermission(caller, PermissionThreadFreeze, args, crossingFn(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\"chain/runtime\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/nt/avl/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(_ realm, boardID boards.ID, user address, role boards.Role) {\n\tinviteMembers(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(_ realm, boardID boards.ID, invites ...Invite) {\n\tinviteMembers(boardID, invites...)\n}\n\n// RequestInvite request to be invited to a board.\nfunc RequestInvite(_ realm, boardID boards.ID) {\n\tassertMembersUpdateIsEnabled(boardID)\n\n\tif !runtime.PreviousRealm().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// TODO: Make open invite requests optional (per board)\n\n\tboard := mustGetBoard(boardID)\n\tuser := runtime.PreviousRealm().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 = avl.NewTree()\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(_ 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 := runtime.PreviousRealm().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, crossingFn(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(_ realm, boardID boards.ID, user address) {\n\tassertInviteRequestExists(boardID, user)\n\n\tboard := mustGetBoard(boardID)\n\tcaller := runtime.PreviousRealm().Address()\n\targs := boards.Args{boardID, user, RoleGuest}\n\tboard.Permissions.WithPermission(caller, PermissionMemberInviteRevoke, args, crossingFn(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(boardID boards.ID, invites ...Invite) {\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 := runtime.PreviousRealm().Address()\n\targs := boards.Args{caller, boardID, invites}\n\tperms.WithPermission(caller, PermissionMemberInvite, args, crossingFn(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\"chain/runtime\"\n\t\"strconv\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\n// LockRealm locks the realm making it readonly.\n//\n// WARNING: Realm can't be unlocked once locked.\n//\n// Realm can also be locked without locking realm members.\n// Realm members can be locked when locking the realm or afterwards.\n// This is relevant for two reasons, one so that members can be modified after the lock.\n// The other is for realm owners, who can delete threads and comments after the lock.\nfunc LockRealm(_ 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 := runtime.PreviousRealm().Address()\n\tgPerms.WithPermission(caller, PermissionRealmLock, boards.Args{}, crossingFn(func() {\n\t\tgLocked.realm = true\n\t\tgLocked.realmMembers = lockRealmMembers\n\n\t\tchain.Emit(\n\t\t\t\"RealmLocked\",\n\t\t\t\"caller\", caller.String(),\n\t\t\t\"lockRealmMembers\", strconv.FormatBool(lockRealmMembers),\n\t\t)\n\t}))\n}\n\n// IsRealmLocked checks if boards realm has been locked.\nfunc IsRealmLocked() bool {\n\treturn gLocked.realm\n}\n\n// AreRealmMembersLocked checks if realm members have been locked.\nfunc AreRealmMembersLocked() bool {\n\treturn gLocked.realmMembers\n}\n\nfunc assertRealmIsNotLocked() { // TODO: Add filtests for locked realm case to all public functions\n\tif gLocked.realm {\n\t\tpanic(\"realm is locked\")\n\t}\n}\n\nfunc assertRealmMembersAreNotLocked() { // TODO: Add filtests for locked members case to all public member functions\n\tif gLocked.realmMembers {\n\t\tpanic(\"realm and members are locked\")\n\t}\n}\n"
                      },
                      {
                        "name": "render.gno",
                        "body": "package boards2\n\nimport (\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/jeronimoalbi/mdform\"\n\t\"gno.land/p/jeronimoalbi/pager\"\n\t\"gno.land/p/leon/svgbtn\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/nt/mux/v0\"\n)\n\nconst (\n\tpageSizeDefault = 6\n\tpageSizeReplies = 10\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 gNotice != \"\" {\n\t\tb.WriteString(infoAlert(\"Notice\", gNotice))\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 gHelp != \"\" {\n\t\tres.Write(gHelp)\n\t\treturn\n\t}\n\n\tlink := gRealmLink.Call(\"SetHelp\", \"content\", \"\")\n\tres.Write(md.H3(\"Help content has not been uploaded\"))\n\tres.Write(\"Do you want to \" + md.Link(\"upload boards help\", link) + \"?\")\n}\n\nfunc renderBoardsList(res *mux.ResponseWriter, req *mux.Request) {\n\tres.Write(md.H1(\"Boards\"))\n\trenderBoardListMenu(res, req)\n\tres.Write(md.HorizontalRule())\n\n\tif gListedBoardsByID.Size() == 0 {\n\t\tres.Write(md.H3(\"Currently there are no boards\"))\n\t\tres.Write(\"Be the first to \" + md.Link(\"create a new board\", createBoardURI) + \"!\")\n\t\treturn\n\t}\n\n\tp, err := pager.New(req.RawPath, gListedBoardsByID.Size(), pager.WithPageSize(pageSizeDefault))\n\tif err != nil {\n\t\tpanic(err)\n\t}\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\tr := parseRealmPath(req.RawPath)\n\tif r.Query.Get(\"order\") == \"desc\" {\n\t\tr.Query.Set(\"order\", \"asc\")\n\t\tres.Write(md.Link(\"newest first\", r.String()) + \"\\n\\n\")\n\t\tgListedBoardsByID.ReverseIterateByOffset(p.Offset(), p.PageSize(), render)\n\t} else {\n\t\tr.Query.Set(\"order\", \"desc\")\n\t\tres.Write(md.Link(\"oldest first\", r.String()) + \"\\n\\n\")\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, err := pager.New(req.RawPath, perms.UsersCount(), pager.WithPageSize(pageSizeDefault))\n\tif err != nil {\n\t\tres.Write(err.Error())\n\t\treturn\n\t}\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\", gRealmLink.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\", gRealmLink.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, err := pager.New(req.RawPath, requests.Size(), pager.WithPageSize(pageSizeDefault))\n\tif err != nil {\n\t\tres.Write(err.Error())\n\t\treturn\n\t}\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\", gRealmLink.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\", gRealmLink.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, err := pager.New(req.RawPath, banned.Size(), pager.WithPageSize(pageSizeDefault))\n\tif err != nil {\n\t\tres.Write(err.Error())\n\t\treturn\n\t}\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\", gRealmLink.Call(\n\t\t\t\t\"Unban\",\n\t\t\t\t\"boardID\", board.ID.String(),\n\t\t\t\t\"user\", addr,\n\t\t\t\t\"reason\", \"\",\n\t\t\t)),\n\t\t})\n\t\treturn false\n\t})\n\n\tres.Write(table.String())\n\n\tif p.HasPages() {\n\t\tres.Write(\"\\n\" + pager.Picker(p))\n\t}\n}\n\nfunc infoAlert(title, msg string) string {\n\theader := strings.TrimSpace(\"[!INFO] \" + title)\n\treturn md.Blockquote(header + \"\\n\" + msg)\n}\n\nfunc rolesToString(roles []boards.Role) string {\n\tif len(roles) == 0 {\n\t\treturn \"\"\n\t}\n\n\tnames := make([]string, len(roles))\n\tfor i, r := range roles {\n\t\tnames[i] = string(r)\n\t}\n\treturn strings.Join(names, \", \")\n}\n\nfunc menuURL(name string) string {\n\t// TODO: Menu URL works because no other GET arguments are being used\n\treturn \"?menu=\" + name\n}\n\nfunc getCurrentMenu(rawURL string) string {\n\t_, rawQuery, found := strings.Cut(rawURL, \"?\")\n\tif !found {\n\t\treturn \"\"\n\t}\n\n\tquery, _ := url.ParseQuery(rawQuery)\n\treturn query.Get(\"menu\")\n}\n"
                      },
                      {
                        "name": "render_board.gno",
                        "body": "package boards2\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/jeronimoalbi/mdform\"\n\t\"gno.land/p/jeronimoalbi/pager\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/mdalert/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n)\n\nfunc renderBoard(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(md.H3(\"The board you are looking for does not exist\"))\n\t\tres.Write(\"Do you want to \" + md.Link(\"create a new board\", createBoardURI) + \"?\")\n\t\treturn\n\t}\n\n\tcreatorLink := userLink(board.Creator)\n\tdate := board.CreatedAt.Format(dateFormat)\n\n\tres.Write(md.H1(md.Link(\"Boards\", gRealmPath) + \" › \" + board.Name))\n\tif board.Readonly {\n\t\tres.Write(\n\t\t\tmdalert.Warning(\"Info\", \"Creating new threads and commenting are disabled within this board\") + \"\\n\",\n\t\t)\n\t}\n\n\tres.Write(\"Created by \" + creatorLink + \" on \" + date + \", #\" + board.ID.String())\n\tres.Write(\"  \\n\" + renderBoardMenu(board, req))\n\tres.Write(md.HorizontalRule())\n\n\tif board.Threads.Size() == 0 {\n\t\tres.Write(md.H3(\"This board doesn't have any threads\"))\n\t\tif !board.Readonly {\n\t\t\tstartConversationLink := md.Link(\"start a new conversation\", makeCreateThreadURI(board))\n\t\t\tres.Write(\"Do you want to \" + startConversationLink + \" in this board?\")\n\t\t}\n\t\treturn\n\t}\n\n\tp, err := pager.New(req.RawPath, board.Threads.Size(), pager.WithPageSize(pageSizeDefault))\n\tif err != nil {\n\t\tpanic(err)\n\t}\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\tr := parseRealmPath(req.RawPath)\n\tsortOrder := r.Query.Get(\"order\")\n\tif sortOrder == \"desc\" {\n\t\tr.Query.Set(\"order\", \"asc\")\n\t\tres.Write(md.Link(\"newest first\", r.String()) + \"\\n\\n\")\n\t} else {\n\t\tr.Query.Set(\"order\", \"desc\")\n\t\tres.Write(md.Link(\"oldest first\", r.String()) + \"\\n\\n\")\n\t}\n\n\tcount := p.PageSize()\n\tif sortOrder == \"desc\" {\n\t\tcount = -count // Reverse iterate\n\t}\n\n\tboard.Threads.Iterate(p.Offset(), count, render)\n\n\tif p.HasPages() {\n\t\tres.Write(md.HorizontalRule())\n\t\tres.Write(pager.Picker(p))\n\t}\n}\n\n// renderSubMenu renders a sub-menu with a distinct visual pattern.\nfunc renderSubMenu(items []string) string {\n\tif len(items) == 0 {\n\t\treturn \"\"\n\t}\n\treturn \"└─ \" + strings.Join(items, \" • \") + \"\\n\"\n}\n\nfunc renderBoardMenu(board *boards.Board, req *mux.Request) string {\n\tvar (\n\t\tb               strings.Builder\n\t\tboardMembersURL = makeBoardURI(board) + \"/members\"\n\t)\n\n\tif board.Readonly {\n\t\tb.WriteString(md.Link(\"List Members\", boardMembersURL))\n\t\tb.WriteString(\" • \")\n\t\tb.WriteString(md.Link(\"Unfreeze Board\", makeUnfreezeBoardURI(board)))\n\t\tb.WriteString(\"\\n\")\n\t} else {\n\t\tb.WriteString(\"↳ \")\n\t\tb.WriteString(md.Link(\"Create Thread\", makeCreateThreadURI(board)))\n\t\tb.WriteString(\" • \")\n\t\tb.WriteString(md.Link(\"Request Invite\", makeRequestInviteURI(board)))\n\t\tb.WriteString(\" • \")\n\n\t\tmenu := getCurrentMenu(req.RawPath)\n\t\tif menu == menuManageBoard {\n\t\t\tb.WriteString(md.Bold(\"Manage Board\"))\n\t\t} else {\n\t\t\tb.WriteString(md.Link(\"Manage Board\", menuURL(menuManageBoard)))\n\t\t}\n\n\t\tb.WriteString(\"  \\n\")\n\n\t\tif menu == menuManageBoard {\n\t\t\tsubMenuItems := []string{\n\t\t\t\tmd.Link(\"Invite Member\", makeInviteMemberURI(board)),\n\t\t\t\tmd.Link(\"List Invite Requests\", makeBoardURI(board)+\"/invites\"),\n\t\t\t\tmd.Link(\"List Members\", boardMembersURL),\n\t\t\t\tmd.Link(\"List Banned Users\", makeBoardURI(board)+\"/banned-users\"),\n\t\t\t\tmd.Link(\"Freeze Board\", makeFreezeBoardURI(board)),\n\t\t\t}\n\t\t\tb.WriteString(renderSubMenu(subMenuItems))\n\t\t}\n\t}\n\n\tb.WriteString(\"\\n\")\n\treturn b.String()\n}\n\nfunc renderInviteMember(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\tform := mdform.New(\"exec\", \"InviteMember\")\n\tform.Input(\n\t\t\"boardID\",\n\t\t\"placeholder\", \"Board ID\",\n\t\t\"value\", board.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"user\",\n\t\t\"placeholder\", \"Address\",\n\t\t\"required\", \"true\",\n\t)\n\tform.Select(\n\t\t\"role\",\n\t\tstring(RoleOwner),\n\t)\n\tform.Select(\n\t\t\"role\",\n\t\tstring(RoleAdmin),\n\t)\n\tform.Select(\n\t\t\"role\",\n\t\tstring(RoleModerator),\n\t)\n\tform.Select(\n\t\t\"role\",\n\t\tstring(RoleGuest),\n\t\t\"selected\", \"true\",\n\t)\n\n\tres.Write(md.H1(board.Name + \": Invite Member\"))\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\t\"Both open and invite only boards can have multiple members with different roles within a \"+\n\t\t\t\t\"board, where members can have a single role at a time.\",\n\t\t) +\n\t\t\tmd.Paragraph(\n\t\t\t\t\"Boards are independent communities which could apply different permissions per role than \"+\n\t\t\t\t\t\"other boards, but generally Boards2 supports four roles, _owner_, _admin_, _moderator_ \"+\n\t\t\t\t\t\"and _guest_.\",\n\t\t\t) +\n\t\t\tmd.Paragraph(\n\t\t\t\t\"Member will be added to \"+md.Link(board.Name, makeBoardURI(board))+\" board.\",\n\t\t\t),\n\t)\n\tres.Write(form.String())\n}\n"
                      },
                      {
                        "name": "render_post.gno",
                        "body": "package boards2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/jeronimoalbi/mdform\"\n\t\"gno.land/p/leon/svgbtn\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/nt/mdalert/v0\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc renderPost(post *boards.Post, path, indent string, levels int) 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(title))\n\t}\n\n\tb.WriteString(indent + \"\\n\")\n\tb.WriteString(renderPostContent(post, indent, levels))\n\n\tif post.Replies.Size() == 0 {\n\t\treturn b.String()\n\t}\n\n\t// XXX: This triggers for reply views\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))\n\t} else {\n\t\tb.WriteString(renderSubReplies(post, indent, levels-1))\n\t}\n\treturn b.String()\n}\n\nfunc renderPostContent(post *boards.Post, indent string, levels 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)\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(indentBody(indent, post.Body) + \"\\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\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\tif boards.IsThread(post) {\n\t\t\tactions = append(actions, md.Link(\"Show all Replies\", makeThreadURI(post)))\n\t\t} else {\n\t\t\tactions = append(actions, md.Link(\"View Thread\", makeThreadURI(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) 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)\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) + \"\\n\"\n\t}\n\n\ts += renderPost(post, \"\", \"\u003e \", 5)\n\treturn s\n}\n\nfunc renderSourcePost(post *boards.Post, indent string) (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 indentBody(indent, srcPost.Body) + \"\\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: \" + 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: \" + 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: \" + 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: \" + 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\ttable.Append([]string{userLink(f.User), 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: \" + 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: \" + 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\tmd.Blockquote(reply.Body),\n\t\t)\n\t} else {\n\t\tres.Write(md.H1(board.Name + \": Comment\"))\n\t\tres.Write(backLink + \"\\n\\n\")\n\t\tres.Write(\n\t\t\tmd.Paragraph(\n\t\t\t\tufmt.Sprintf(\"Commenting on the thread: %s\", md.Link(thread.Title, makeThreadURI(thread))),\n\t\t\t),\n\t\t)\n\t}\n\n\tres.Write(form.String())\n\tres.Write(\"\\n\\n**Done?** \" + svgbtn.ButtonWithRadius(136, 32, 4, \"#E2E2E2\", \"#54595D\", \"Return to thread\", makeThreadURI(thread)) + \"\\n\")\n}\n"
                      },
                      {
                        "name": "render_reply.gno",
                        "body": "package boards2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/jeronimoalbi/mdform\"\n\t\"gno.land/p/jeronimoalbi/pager\"\n\t\"gno.land/p/leon/svgbtn\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc renderReply(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"board\")\n\tboard, found := gBoards.GetByName(name)\n\tif !found {\n\t\tres.Write(\"Board not found\")\n\t\treturn\n\t}\n\n\trawID := req.GetVar(\"thread\")\n\tthreadID, err := strconv.Atoi(rawID)\n\tif err != nil {\n\t\tres.Write(\"Invalid thread ID: \" + 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: \" + 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))\n}\n\nfunc renderTopLevelReplies(post *boards.Post, path, indent string, levels int) string {\n\tp, err := pager.New(path, post.Replies.Size(), pager.WithPageSize(pageSizeReplies))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar (\n\t\tb              strings.Builder\n\t\tcommentsIndent = indent + \"\u003e \"\n\t)\n\n\trender := func(reply *boards.Post) bool {\n\t\tb.WriteString(indent + \"\\n\" + renderPost(reply, \"\", commentsIndent, levels-1))\n\t\treturn false\n\t}\n\n\tb.WriteString(\"\\n\" + md.HorizontalRule() + \"Sort by: \")\n\n\tr := parseRealmPath(path)\n\tsortOrder := r.Query.Get(\"order\")\n\tif sortOrder == \"desc\" {\n\t\tr.Query.Set(\"order\", \"asc\")\n\t\tb.WriteString(md.Link(\"newest first\", r.String()) + \"\\n\")\n\n\t} else {\n\t\tr.Query.Set(\"order\", \"desc\")\n\t\tb.WriteString(md.Link(\"oldest first\", r.String()) + \"\\n\")\n\t}\n\n\tcount := p.PageSize()\n\tif sortOrder == \"desc\" {\n\t\tcount = -count // Reverse iterate\n\t}\n\n\tpost.Replies.Iterate(p.Offset(), count, render)\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\nfunc renderSubReplies(post *boards.Post, indent string, levels int) string {\n\tvar (\n\t\tb              strings.Builder\n\t\tcommentsIndent = indent + \"\u003e \"\n\t)\n\n\tpost.Replies.Iterate(0, post.Replies.Size(), func(reply *boards.Post) bool {\n\t\tb.WriteString(indent + \"\\n\" + renderPost(reply, \"\", commentsIndent, levels-1))\n\t\treturn false\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: \" + 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: \" + rawID)\n\t\treturn\n\t}\n\n\tthread, found := getThread(board, boards.ID(threadID))\n\tif !found {\n\t\tres.Write(\"Thread not found\")\n\t\treturn\n\t}\n\n\treply, found := getReply(thread, boards.ID(replyID))\n\tif !found {\n\t\tres.Write(\"Reply not found\")\n\t\treturn\n\t}\n\n\tform := mdform.New(\"exec\", \"EditReply\")\n\tform.Input(\n\t\t\"boardID\",\n\t\t\"placeholder\", \"Board ID\",\n\t\t\"value\", board.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"threadID\",\n\t\t\"placeholder\", \"Thread ID\",\n\t\t\"value\", thread.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"replyID\",\n\t\t\"placeholder\", \"Reply ID\",\n\t\t\"value\", reply.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Textarea(\n\t\t\"body\",\n\t\t\"placeholder\", \"Comment\",\n\t\t\"value\", reply.Body,\n\t\t\"required\", \"true\",\n\t)\n\n\tres.Write(md.H1(board.Name + \": Edit Comment\"))\n\tres.Write(md.Link(\"← Back to thread\", makeThreadURI(thread)) + \"\\n\\n\")\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\tufmt.Sprintf(\"Editing a comment from the thread: %s\", md.Link(thread.Title, makeThreadURI(thread))),\n\t\t),\n\t)\n\tres.Write(form.String())\n\tres.Write(\"\\n\\n**Done?** \" + svgbtn.ButtonWithRadius(136, 32, 4, \"#E2E2E2\", \"#54595D\", \"Return to thread\", makeThreadURI(thread)) + \"\\n\")\n}\n"
                      },
                      {
                        "name": "render_thread.gno",
                        "body": "package boards2\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/jeronimoalbi/mdform\"\n\t\"gno.land/p/leon/svgbtn\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\n\nfunc 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: \" + 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\tres.Write(renderPost(thread, req.RawPath, \"\", 5))\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\tif boards.IsRepost(thread) {\n\t\tsummary += ` ⟳`\n\t\tpostURI += ` \"This is a thread repost\"`\n\t}\n\n\tb.WriteString(md.H6(md.Link(summary, postURI)))\n\tb.WriteString(\"Created by \" + 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: \" + 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: \" + rawID)\n\t\treturn\n\t}\n\n\tthread, found := getThread(board, boards.ID(threadID))\n\tif !found {\n\t\tres.Write(\"Thread not found\")\n\t\treturn\n\t}\n\n\tform := mdform.New(\"exec\", \"CreateRepost\")\n\tform.Input(\n\t\t\"boardID\",\n\t\t\"placeholder\", \"Board ID\",\n\t\t\"value\", board.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"threadID\",\n\t\t\"placeholder\", \"Thread ID\",\n\t\t\"value\", thread.ID.String(),\n\t\t\"readonly\", \"true\",\n\t)\n\tform.Input(\n\t\t\"destinationBoardID\",\n\t\t\"type\", mdform.InputTypeNumber,\n\t\t\"placeholder\", \"Board ID where to repost\",\n\t\t\"required\", \"true\",\n\t)\n\tform.Input(\n\t\t\"title\",\n\t\t\"value\", thread.Title,\n\t\t\"placeholder\", \"Title\",\n\t\t\"required\", \"true\",\n\t)\n\tform.Textarea(\n\t\t\"body\",\n\t\t\"placeholder\", \"Content\",\n\t\t\"rows\", \"10\",\n\t)\n\n\tres.Write(md.H1(board.Name + \": Repost Thread\"))\n\tres.Write(md.Link(\"← Back to thread\", makeThreadURI(thread)) + \"\\n\\n\")\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\t\"Threads can be reposted to other open boards or boards where you are a member \" +\n\t\t\t\t\"and are allowed to create new threads.\",\n\t\t),\n\t)\n\tres.Write(\n\t\tmd.Paragraph(\n\t\t\tufmt.Sprintf(\"Reposting the thread: %s.\", md.Link(thread.Title, makeThreadURI(thread))),\n\t\t),\n\t)\n\tres.Write(form.String())\n\tres.Write(\"\\n\\n**Done?** \" + svgbtn.ButtonWithRadius(136, 32, 4, \"#E2E2E2\", \"#54595D\", \"Return to thread\", makeThreadURI(thread)) + \"\\n\")\n}\n"
                      },
                      {
                        "name": "uris_board.gno",
                        "body": "package boards2\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/gnoland/boards\"\n)\n\nfunc makeBoardURI(b *boards.Board) string {\n\tpath := strings.TrimPrefix(string(gRealmLink), \"gno.land\")\n\treturn path + \":\" + url.PathEscape(b.Name)\n}\n\nfunc makeFreezeBoardURI(b *boards.Board) string {\n\treturn gRealmLink.Call(\n\t\t\"FreezeBoard\",\n\t\t\"boardID\", b.ID.String(),\n\t)\n}\n\nfunc makeUnfreezeBoardURI(b *boards.Board) string {\n\treturn gRealmLink.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 gRealmLink.Call(\n\t\t\"RequestInvite\",\n\t\t\"boardID\", b.ID.String(),\n\t)\n}\n"
                      },
                      {
                        "name": "uris_post.gno",
                        "body": "package boards2\n\nimport (\n\t\"gno.land/p/gnoland/boards\"\n)\n\nfunc makeThreadURI(p *boards.Post) string {\n\tif boards.IsThread(p) {\n\t\treturn makeBoardURI(p.Board) + \"/\" + p.ID.String()\n\t}\n\n\t// When post is a reply use the parent thread ID\n\treturn makeBoardURI(p.Board) + \"/\" + p.ThreadID.String()\n}\n\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 gRealmLink.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 gRealmLink.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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross, bid)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.AcceptInvite(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"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, bid)\n\n\t// Add user as a member idependently of the invite request\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.InviteMember(cross, bid, user, boards2.RoleGuest)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.AcceptInvite(cross, bid, user)\n}\n\n// Error:\n// user is already a member\n"
                      },
                      {
                        "name": "z_accept_invite_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.AcceptInvite(cross, bid, user)\n}\n\n// Error:\n// invite request not found\n"
                      },
                      {
                        "name": "z_accept_invite_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross, bid)\n}\n\nfunc main() {\n\t// Caller is not a member and has no permission to invite\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.AcceptInvite(cross, bid, user)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_ban_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.Ban(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\t// Invite the user as a moderator\n\tboards2.InviteMember(cross, bid, user, boards2.RoleModerator)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.Ban(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Try to ban a user without banning permissions\n\tboards2.Ban(cross, bid, \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\", boards2.BanDay, \"Reason\")\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_change_member_role_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.InviteMember(cross, bid, member, boards2.RoleAdmin)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmember  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tnewRole         = boards2.RoleAdmin\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"foo123\", false, false)\n\tboards2.InviteMember(cross, bid, member, boards2.RoleGuest)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner  address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\towner2 address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tadmin  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\tboards2.InviteMember(cross, bid, owner2, boards2.RoleOwner)\n\tboards2.InviteMember(cross, bid, admin, boards2.RoleAdmin)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\tboards2.ChangeMemberRole(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner  address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tadmin  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tadmin2 address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\tboards2.InviteMember(cross, bid, admin, boards2.RoleAdmin)\n\tboards2.InviteMember(cross, bid, admin2, boards2.RoleAdmin)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\tboards2.ChangeMemberRole(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.InviteMember(cross, bid, member, boards2.RoleAdmin)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tadmin address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\tboards2.InviteMember(cross, bid, admin, boards2.RoleGuest)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross, bid, admin, boards.Role(\"foo\"))\n}\n\n// Error:\n// invalid role: foo\n"
                      },
                      {
                        "name": "z_change_member_role_06_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tadmin address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\tboards2.InviteMember(cross, bid, admin, boards2.RoleGuest)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross, bid, \"foo\", boards2.RoleModerator)\n}\n\n// Error:\n// invalid member address: foo\n"
                      },
                      {
                        "name": "z_change_member_role_07_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.ChangeMemberRole(cross, 0, \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\", boards2.RoleGuest)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_create_board_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tbid := boards2.CreateBoard(cross, \"test123\", false, false)\n\tprintln(\"ID =\", bid)\n}\n\n// Output:\n// ID = 1\n"
                      },
                      {
                        "name": "z_create_board_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross, \"\", false, false)\n}\n\n// Error:\n// board name is empty\n"
                      },
                      {
                        "name": "z_create_board_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"test123\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross, boardName, false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross, boardName, false, false)\n}\n\n// Error:\n// board already exists\n"
                      },
                      {
                        "name": "z_create_board_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", false, false)\n}\n\n// Error:\n// addresses are not allowed as board name\n"
                      },
                      {
                        "name": "z_create_board_04_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc init() {\n\tuinit.RegisterUser(cross, \"gnoland\", address(\"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"))\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross, \"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": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar name = strings.Repeat(\"X\", boards2.MaxBoardNameLength+1)\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross, 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": "package main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner  address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmember address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tname           = \"test123\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Operate on realm DAO members instead of individual boards\n\tboards2.InviteMember(cross, 0, member, boards2.RoleOwner)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\t// Create a board as an invited realm member\n\tbid := boards2.CreateBoard(cross, name, false, false)\n\tprintln(\"ID =\", bid)\n}\n\n// Output:\n// ID = 1\n"
                      },
                      {
                        "name": "z_create_board_07_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross, \"test123\", false, false)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_create_board_08_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tname          = \"TestBoard\"\n)\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross, 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/v1:create-board) • [List Admin Users](/r/gnoland/boards2/v1:admin-users) • [Help](/r/gnoland/boards2/v1:help)\n//\n// ---\n// ### Currently there are no boards\n// Be the first to [create a new board](/r/gnoland/boards2/v1:create-board)!\n//\n// ==================\n// # [Boards](/r/gnoland/boards2/v1) › TestBoard\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1\n// ↳ [Create Thread](/r/gnoland/boards2/v1:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help\u0026func=RequestInvite\u0026boardID=1) • [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/v1:TestBoard/create-thread) in this board?\n"
                      },
                      {
                        "name": "z_create_board_09_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross, \"TEST123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Should fail because board name already exists with a different casing\n\tboards2.CreateBoard(cross, \"test123\", false, false)\n}\n\n// Error:\n// board already exists\n"
                      },
                      {
                        "name": "z_create_board_10_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\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, \"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": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\trid := boards2.CreateReply(cross, 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\n// Output:\n// true\n// true\n"
                      },
                      {
                        "name": "z_create_reply_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\n\t// Hide thread by flagging it so reply can't be submitted\n\tboards2.FlagThread(cross, bid, tid, \"reason\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross, bid, tid, 0, \"Test reply\")\n}\n\n// Error:\n// thread is hidden\n"
                      },
                      {
                        "name": "z_create_reply_05_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, 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, bid, tid, \"reason\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross, bid, tid, rid, \"reply1.1\")\n}\n\n// Error:\n// thread is hidden\n"
                      },
                      {
                        "name": "z_create_reply_06_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"thread\", \"thread\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"reply1\")\n\n\t// Hide reply by flagging it so sub reply can't be submitted\n\tboards2.FlagReply(cross, bid, tid, rid, \"reason\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.CreateReply(cross, bid, tid, 0, \"Test reply\")\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_create_reply_08_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross, bid, tid, 0, \"\")\n}\n\n// Error:\n// body is empty\n"
                      },
                      {
                        "name": "z_create_reply_09_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"First comment\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\trid2 := boards2.CreateReply(cross, 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\n// Output:\n// true\n// true\n"
                      },
                      {
                        "name": "z_create_reply_10_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tcomment         = \"Second comment\"\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"Parent comment\")\n\n\t// Flag parent post so it's hidden\n\tboards2.FlagReply(cross, bid, tid, rid, \"Reason\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"foo\", \"bar\")\n\tboards2.FreezeThread(cross, bid, tid)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// cannot reply to a frozen thread\n\tboards2.CreateReply(cross, bid, tid, 0, \"foobar\")\n}\n\n// Error:\n// thread is frozen\n"
                      },
                      {
                        "name": "z_create_reply_12_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross, bid, tid, 0, strings.Repeat(\"x\", boards2.MaxReplyLength+1))\n}\n\n// Error:\n// reply is too long, maximum allowed is 1000 characters\n"
                      },
                      {
                        "name": "z_create_reply_13_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateReply(cross, bid, tid, 0, \"\u003e Markdown blockquote\")\n}\n\n// Error:\n// using Markdown headings, blockquotes or horizontal lines is not allowed in replies\n"
                      },
                      {
                        "name": "z_create_reply_14_filetest.gno",
                        "body": "// Open board: Test creating a new reply as a non member user\npackage main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, true)\n\ttid = boards2.CreateThread(cross, 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() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Non members should be able to add replies\n\tboards2.CreateReply(cross, 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\n// Output:\n// true\n"
                      },
                      {
                        "name": "z_create_reply_15_filetest.gno",
                        "body": "// Open board: Test creating a new reply as a non member user that has no GNOT\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, true)\n\ttid = boards2.CreateThread(cross, bid, \"Title\", \"Body\")\n}\n\nfunc main() {\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, 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_repost_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tsrcBID = boards2.CreateBoard(cross, \"src-board\", false, false)\n\tdstBID = boards2.CreateBoard(cross, \"dst-board\", false, false)\n\n\tsrcTID = boards2.CreateThread(cross, srcBID, \"Foo\", \"bar\")\n\tboards2.FlagThread(cross, srcBID, srcTID, \"idk\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Repost should fail if source thread is flagged\n\tboards2.CreateRepost(cross, srcBID, srcTID, dstBID, \"foo\", \"bar\")\n}\n\n// Error:\n// thread is hidden\n"
                      },
                      {
                        "name": "z_create_repost_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID = 1024\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tsrcBID = boards2.CreateBoard(cross, \"src-board\", false, false)\n\tdstBID = boards2.CreateBoard(cross, \"dst-board\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Repost should fail if source thread doesn't exist\n\tboards2.CreateRepost(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tsrcBID = boards2.CreateBoard(cross, \"src-board\", false, false)\n\tdstBID = boards2.CreateBoard(cross, \"dst-board\", false, false)\n\n\tsrcTID = boards2.CreateThread(cross, srcBID, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.CreateRepost(cross, srcBID, srcTID, dstBID, \"foo\", \"bar\")\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_create_repost_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tsrcBID = boards2.CreateBoard(cross, \"src-board\", false, false)\n\tdstBID = boards2.CreateBoard(cross, \"dst-board\", false, false)\n\n\tsrcTID = boards2.CreateThread(cross, srcBID, \"original title\", \"original text\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Success case\n\ttID := boards2.CreateRepost(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tsrcBID boards.ID\n\tdstBID boards.ID\n\tsrcTID boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board with a thread\n\torigBID := boards2.CreateBoard(cross, \"origin-board\", false, false)\n\torigTID := boards2.CreateThread(cross, origBID, \"title\", \"text\")\n\n\t// Create a second board and repost a thread using an empty title\n\tsrcBID = boards2.CreateBoard(cross, \"source-board\", false, false)\n\tsrcTID = boards2.CreateRepost(cross, origBID, origTID, srcBID, \"original title\", \"original text\")\n\n\t// Create a third board to try reposting the repost\n\tdstBID = boards2.CreateBoard(cross, \"destination-board\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateRepost(cross, 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_thread_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\ttitle         = \"Test Thread\"\n\tbody          = \"Test body\"\n\tpath          = \"test-board/1\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\ttid := boards2.CreateThread(cross, 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/v1) › [test\\-board](/r/gnoland/boards2/v1:test-board)\n// ## Test Thread\n//\n// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Test body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_create_thread_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateThread(cross, 404, \"Foo\", \"bar\")\n}\n\n// Error:\n// board does not exist with ID: 404\n"
                      },
                      {
                        "name": "z_create_thread_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_create_thread_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateThread(cross, bid, \"\", \"bar\")\n}\n\n// Error:\n// title is empty\n"
                      },
                      {
                        "name": "z_create_thread_04_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateThread(cross, bid, \"Foo\", \"\")\n}\n\n// Error:\n// thread body is required\n"
                      },
                      {
                        "name": "z_create_thread_05_filetest.gno",
                        "body": "// Open board: Test creating a new thread as a non member user\npackage main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"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() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Non members should be able to create threads\n\ttid := boards2.CreateThread(cross, 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/v1) › [test123](/r/gnoland/boards2/v1:test123)\n// ## Test Thread\n//\n// **[g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj](/u/g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj)** · 2009-02-13 11:31pm UTC\n// Test body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test123/1/flag) • [Repost](/r/gnoland/boards2/v1:test123/1/repost) • [Comment](/r/gnoland/boards2/v1:test123/1/reply) • [Edit](/r/gnoland/boards2/v1:test123/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_create_thread_06_filetest.gno",
                        "body": "// Open board: Test creating a new thread as a non member user that has no GNOT\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, true)\n}\n\nfunc main() {\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, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteReply(cross, 404, 1, 1)\n}\n\n// Error:\n// board does not exist with ID: 404\n"
                      },
                      {
                        "name": "z_delete_reply_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteReply(cross, bid, 404, 1)\n}\n\n// Error:\n// thread does not exist with ID: 404\n"
                      },
                      {
                        "name": "z_delete_reply_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteReply(cross, bid, tid, 404)\n}\n\n// Error:\n// reply does not exist with ID: 404\n"
                      },
                      {
                        "name": "z_delete_reply_04_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"Parent\")\n\tboards2.CreateReply(cross, bid, tid, rid, \"Child reply\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteReply(cross, 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// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// bar\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2)\n// \u003e ⚠ This comment has been deleted\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/2/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=2\u0026threadID=1)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3)\n// \u003e \u003e Child reply\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=3\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_delete_reply_05_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n}\n\nfunc main() {\n\t// Call using a user that has not permission to delete replies\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.DeleteReply(cross, bid, tid, rid)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_delete_reply_06_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner  address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmember address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n\n\t// Invite a member using a role with permission to delete replies\n\tboards2.InviteMember(cross, bid, member, boards2.RoleAdmin)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\tboards2.DeleteReply(cross, 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": "// Open board: Test deleting a reply as the non member user that created it\npackage main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, true)\n\ttid = boards2.CreateThread(cross, 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, bid, tid, 0, \"Comment\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Delete the reply as the non member user that created it\n\tboards2.DeleteReply(cross, 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": "// Open board: Test deleting a reply of another non member user\npackage main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, true)\n\ttid = boards2.CreateThread(cross, 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, bid, tid, 0, \"Comment\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user2))\n\n\t// Try to delete the reply of another non member user\n\tboards2.DeleteReply(cross, bid, tid, rid)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_delete_thread_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\ttitle         = \"Test Thread\"\n\tbody          = \"Test body\"\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, title, body)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteThread(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteThread(cross, 404, 1)\n}\n\n// Error:\n// board does not exist with ID: 404\n"
                      },
                      {
                        "name": "z_delete_thread_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.DeleteThread(cross, bid, 404)\n}\n\n// Error:\n// thread does not exist with ID: 404\n"
                      },
                      {
                        "name": "z_delete_thread_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\t// Call using a user that has not permission to delete threads\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.DeleteThread(cross, bid, pid)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_delete_thread_04_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner  address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmember address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\n\t// Invite a member using a role with permission to delete threads\n\tboards2.InviteMember(cross, bid, member, boards2.RoleAdmin)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\tboards2.DeleteThread(cross, 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": "// Open board: Test deleting a thread as the non member user that created it\npackage main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID // Operate on board DAO\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"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, bid, \"Title\", \"Body\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Delete the thread as the non member user that created it\n\tboards2.DeleteThread(cross, 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": "// Open board: Test deleting a reply of another non member user\npackage main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"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, bid, \"Title\", \"Body\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user2))\n\n\t// Try to delete the thread of another non member user\n\tboards2.DeleteThread(cross, bid, tid)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_edit_reply_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross, 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\n// Output:\n// true\n"
                      },
                      {
                        "name": "z_edit_reply_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\n\t// Create a reply and a sub reply\n\tparentRID := boards2.CreateReply(cross, bid, tid, 0, \"Parent\")\n\trid = boards2.CreateReply(cross, bid, tid, parentRID, \"Child\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross, 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\n// Output:\n// true\n"
                      },
                      {
                        "name": "z_edit_reply_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.EditReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n\n\t// Flag the reply so it's hidden\n\tboards2.FlagReply(cross, bid, tid, rid, \"reason\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross, bid, tid, rid, \"body\")\n}\n\n// Error:\n// reply is hidden\n"
                      },
                      {
                        "name": "z_edit_reply_07_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid      boards.ID\n\ttid, rid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditReply(cross, bid, tid, rid, \"\")\n}\n\n// Error:\n// body is empty\n"
                      },
                      {
                        "name": "z_edit_thread_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross, bid, pid, title, body)\n\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v1) › [test\\-board](/r/gnoland/boards2/v1:test-board)\n// ## Test Thread\n//\n// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Test body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_edit_thread_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross, bid, pid, \"\", \"bar\")\n}\n\n// Error:\n// title is empty\n"
                      },
                      {
                        "name": "z_edit_thread_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross, bid, pid, \"Foo\", \"\")\n}\n\n// Error:\n// body is empty\n"
                      },
                      {
                        "name": "z_edit_thread_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross, bid, pid, \"Foo\", \"\")\n}\n\n// Error:\n// body is empty\n"
                      },
                      {
                        "name": "z_edit_thread_05_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.EditThread(cross, bid, pid, \"Foo\", \"bar\")\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_edit_thread_06_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\n\t// Invite a member using a role with permission to edit threads\n\tboards2.InviteMember(cross, bid, admin, boards2.RoleAdmin)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\tboards2.EditThread(cross, bid, pid, title, body)\n\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v1) › [test\\-board](/r/gnoland/boards2/v1:test-board)\n// ## Test Thread\n//\n// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Test body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_edit_thread_07_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid   boards.ID\n\tpid   boards.ID\n\ttitle = strings.Repeat(\"X\", boards2.MaxThreadTitleLength+1)\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.EditThread(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross, 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// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// bar\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2)\n// \u003e ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v1:test-board/1/2/flagging-reasons)\n"
                      },
                      {
                        "name": "z_flag_reply_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n\tboards2.FlagReply(cross, bid, tid, rid, \"Reason\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.FlagReply(cross, bid, tid, rid, \"Reason\")\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_flag_reply_06_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmoderator address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n\n\t// Invite a member using a role with permission to flag replies\n\tboards2.InviteMember(cross, bid, moderator, boards2.RoleModerator)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(moderator))\n\n\tboards2.FlagReply(cross, 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// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// bar\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2)\n// \u003e ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v1:test-board/1/2/flagging-reasons)\n"
                      },
                      {
                        "name": "z_flag_reply_07_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmoderator address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init() {\n\t// Created a board with flagging threshold greater than 1\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tboards2.SetFlaggingThreshold(cross, 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, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n\t// Also freeze board to make sure that realm owner can still flag the reply\n\tboards2.FreezeBoard(cross, bid)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross, 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// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// bar\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2)\n// \u003e ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v1:test-board/1/2/flagging-reasons)\n"
                      },
                      {
                        "name": "z_flag_reply_08_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid      boards.ID\n\trid, tid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\trid = boards2.CreateReply(cross, bid, tid, 0, \"body\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagReply(cross, bid, tid, rid, \"\")\n}\n\n// Error:\n// flagging reason is required\n"
                      },
                      {
                        "name": "z_flag_thread_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmoderator address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\n\t// Invite a moderator to the new board\n\tboards2.InviteMember(cross, bid, moderator, boards2.RoleModerator)\n\n\t// Create a new thread as a moderator\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Flag thread as owner\n\tboards2.FlagThread(cross, 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/v1) › test-board\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1\n// ↳ [Create Thread](/r/gnoland/boards2/v1:test-board/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help\u0026func=RequestInvite\u0026boardID=1) • [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/v1:test-board/create-thread) in this board?\n"
                      },
                      {
                        "name": "z_flag_thread_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagThread(cross, 404, 1, \"Reason\")\n}\n\n// Error:\n// board does not exist with ID: 404\n"
                      },
                      {
                        "name": "z_flag_thread_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\t// Make the next call as an uninvited user\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.FlagThread(cross, bid, pid, \"Reason\")\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_flag_thread_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagThread(cross, bid, 404, \"Reason\")\n}\n\n// Error:\n// thread not found\n"
                      },
                      {
                        "name": "z_flag_thread_04_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\tboards2.FlagThread(cross, bid, pid, \"Reason\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagThread(cross, bid, pid, \"Reason\")\n}\n\n// Error:\n// flagging hidden threads is not allowed\n"
                      },
                      {
                        "name": "z_flag_thread_05_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmoderator address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n\n\t// Invite a member using a role with permission to flag threads\n\tboards2.InviteMember(cross, bid, moderator, boards2.RoleModerator)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(moderator))\n\n\t// Flag thread as moderator\n\tboards2.FlagThread(cross, 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/v1:test-board/1/flagging-reasons)\n"
                      },
                      {
                        "name": "z_flag_thread_06_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmoderator address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\t// Created a board with a specific flagging threshold\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tboards2.SetFlaggingThreshold(cross, bid, 2)\n\n\t// Invite a moderator to the new board\n\tboards2.InviteMember(cross, 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, bid, \"Foo\", \"bar\")\n\tboards2.FlagThread(cross, bid, pid, \"Reason\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(moderator))\n\n\tboards2.FlagThread(cross, bid, pid, \"Reason\")\n}\n\n// Error:\n// post has been already flagged by g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\n"
                      },
                      {
                        "name": "z_flag_thread_07_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\t// Created a board with flagging threshold greater than 1\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tboards2.SetFlaggingThreshold(cross, 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, bid, \"Foo\", \"bar\")\n\t// Also freeze board to make sure that realm owner can still flag the thread\n\tboards2.FreezeBoard(cross, bid)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagThread(cross, 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/v1:test-board/1/flagging-reasons)\n"
                      },
                      {
                        "name": "z_flag_thread_08_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\tpid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n\tpid = boards2.CreateThread(cross, bid, \"Foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FlagThread(cross, bid, pid, \"\")\n}\n\n// Error:\n// flagging reason is required\n"
                      },
                      {
                        "name": "z_freeze_board_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FreezeBoard(cross, bid)\n\n\tprintln(boards2.IsBoardFrozen(bid))\n}\n\n// Output:\n// true\n"
                      },
                      {
                        "name": "z_freeze_board_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\tboards2.FreezeBoard(cross, bid)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FreezeBoard(cross, bid)\n}\n\n// Error:\n// board is frozen\n"
                      },
                      {
                        "name": "z_freeze_thread_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"foo\", \"bar\")\n\n\tboards2.FreezeBoard(cross, bid)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Attempt to freeze a thread on frozen board\n\tboards2.FreezeThread(cross, bid, tid)\n}\n\n// Error:\n// board is frozen\n"
                      },
                      {
                        "name": "z_freeze_thread_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"foo\", \"bar\")\n\n\tboards2.FreezeThread(cross, bid, tid)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Attempt to freeze a frozen thread\n\tboards2.FreezeThread(cross, bid, tid)\n}\n\n// Error:\n// thread is frozen\n"
                      },
                      {
                        "name": "z_freeze_thread_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tbid boards.ID\n\ttid boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\ttid = boards2.CreateThread(cross, bid, \"foo\", \"bar\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.FreezeThread(cross, bid, tid)\n\n\tprintln(boards2.IsThreadFrozen(bid, tid))\n}\n\n// Output:\n// true\n"
                      },
                      {
                        "name": "z_get_board_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Calling as an owner should succeed\n\tboard := boards2.GetBoard(bid)\n\tif board == nil {\n\t\treturn\n\t}\n\n\tprintln(board.ID == bid)\n}\n\n// Output:\n// true\n"
                      },
                      {
                        "name": "z_get_board_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tadmin address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\tboards2.InviteMember(cross, bid, admin, boards2.RoleAdmin)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\t// Calling as a non owner member should fail\n\tboards2.GetBoard(bid)\n}\n\n// Error:\n// forbidden\n"
                      },
                      {
                        "name": "z_get_board_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"))\n\n\t// Calling as a non member should fail\n\tboards2.GetBoard(bid)\n}\n\n// Error:\n// forbidden\n"
                      },
                      {
                        "name": "z_get_board_id_from_name_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tname          = \"test123\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, name, false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tbid2, found := boards2.GetBoardIDFromName(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tbid, found := boards2.GetBoardIDFromName(cross, \"foobar\")\n\n\tprintln(found)\n\tprintln(bid == 0)\n}\n\n// Output:\n// false\n// true\n"
                      },
                      {
                        "name": "z_invite_member_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.InviteMember(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tadmin address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tuser  address = \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\t// Add an admin member\n\tboards2.InviteMember(cross, bid, admin, boards2.RoleAdmin)\n}\n\nfunc main() {\n\t// Next call will be done by the admin member\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\tboards2.InviteMember(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\t// Add an admin member\n\tboards2.InviteMember(cross, bid, admin, boards2.RoleAdmin)\n}\n\nfunc main() {\n\t// Next call will be done by the admin member\n\ttesting.SetRealm(testing.NewUserRealm(admin))\n\n\tboards2.InviteMember(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.InviteMember(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\trole          = boards2.RoleOwner\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"foo123\", false, false) // Operate on board DAO members\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.InviteMember(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.InviteMember(cross, bid, user, role)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.InviteMember(cross, bid, user, role)\n}\n\n// Error:\n// user is already a member: g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\n"
                      },
                      {
                        "name": "z_invite_member_06_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.InviteMember(cross, 0, \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\", boards2.RoleGuest)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_is_banned_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\tboards2.Ban(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner  address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmember address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\trole           = boards2.RoleGuest\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\tboards2.InviteMember(cross, 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\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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_iterate_realm_members_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tbid           = boards.ID(0) // Operate on realm DAO instead of individual boards\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.InviteMember(cross, bid, \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\", boards2.RoleOwner)\n\tboards2.InviteMember(cross, bid, \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\", boards2.RoleAdmin)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.IterateRealmMembers(0, func(u boards.User) bool {\n\t\tprintln(u.Address, string(u.Roles[0]))\n\t\treturn false\n\t})\n}\n\n// Output:\n// g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq owner\n// g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh owner\n// g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj owner\n// g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5 admin\n"
                      },
                      {
                        "name": "z_lock_realm_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.LockRealm(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.LockRealm(cross, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Should fail because realm is already locked\n\tboards2.LockRealm(cross, false)\n}\n\n// Error:\n// realm is locked\n"
                      },
                      {
                        "name": "z_lock_realm_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main() {\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, false)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_lock_realm_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.LockRealm(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.LockRealm(cross, true)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Should fail because realm is already locked\n\tboards2.LockRealm(cross, true)\n}\n\n// Error:\n// realm and members are locked\n"
                      },
                      {
                        "name": "z_lock_realm_05_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc init() {\n\t// Lock the realm without locking realm members\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.LockRealm(cross, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.LockRealm(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\tboards2.InviteMember(cross, bid, user, boards2.RoleGuest)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RemoveMember(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RemoveMember(cross, 0, \"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\") // Operate on realm DAO instead of individual boards\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_remove_member_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RemoveMember(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\tboards2.InviteMember(cross, bid, user, boards2.RoleGuest)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\t// Users must be able to remove themselves without permissions\n\tboards2.RemoveMember(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tname            = \"foo123\"\n\tnewName         = \"bar123\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, name, false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross, name, newName)\n\n\t// Ensure board is renamed by the default board owner\n\tbid2, _ := boards2.GetBoardIDFromName(cross, newName)\n\tprintln(\"IDs match =\", bid == bid2)\n}\n\n// Output:\n// IDs match = true\n"
                      },
                      {
                        "name": "z_rename_board_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tname          = \"foo123\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross, name, false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross, name, \"\")\n}\n\n// Error:\n// board name is empty\n"
                      },
                      {
                        "name": "z_rename_board_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tname          = \"foo123\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross, name, false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross, name, name)\n}\n\n// Error:\n// board already exists\n"
                      },
                      {
                        "name": "z_rename_board_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross, \"unexisting\", \"foo\")\n}\n\n// Error:\n// board does not exist with name: unexisting\n"
                      },
                      {
                        "name": "z_rename_board_04_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tname          = \"foo123\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross, name, false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross, name, \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n}\n\n// Error:\n// addresses are not allowed as board name\n"
                      },
                      {
                        "name": "z_rename_board_05_filetest.gno",
                        "body": "package main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tmember  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tname            = \"foo123\"\n\tnewName         = \"barbaz123\"\n)\n\nvar bid boards.ID // Operate on board DAO\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tbid = boards2.CreateBoard(cross, name, false, false)\n\tboards2.InviteMember(cross, bid, member, boards2.RoleOwner)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(member))\n\n\tboards2.RenameBoard(cross, name, newName)\n\n\t// Ensure board is renamed by another board owner\n\tbid2, _ := boards2.GetBoardIDFromName(cross, newName)\n\tprintln(\"IDs match =\", bid == bid2)\n}\n\n// Output:\n// IDs match = true\n"
                      },
                      {
                        "name": "z_rename_board_06_filetest.gno",
                        "body": "package main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, name, false, false)\n\tboards2.InviteMember(cross, 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\tuinit.RegisterUser(cross, newName, member)\n\n\t// Invite a new member that doesn't own the user that matches the new board name\n\tboards2.InviteMember(cross, bid, member2, boards2.RoleOwner)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(member2))\n\n\tboards2.RenameBoard(cross, 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": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tname          = \"foo123\"\n)\n\nvar newName string\n\nfunc init() {\n\tnewName = strings.Repeat(\"A\", boards2.MaxBoardNameLength+1)\n\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross, name, false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RenameBoard(cross, 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": "package main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n\tuinit \"gno.land/r/sys/users/init\"\n)\n\nconst (\n\towner   address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, name, false, false)\n\tboards2.InviteMember(cross, 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\tuinit.RegisterUser(cross, newName, member)\n\n\t// Invite a new member that doesn't own the user that matches the new board name\n\tboards2.InviteMember(cross, bid, member2, boards2.RoleOwner)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(member2))\n\n\tboards2.RenameBoard(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\tname          = \"foo123\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.CreateBoard(cross, name, false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.RenameBoard(cross, name, \"barbaz\")\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_request_invite_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"))\n\n\tboards2.RequestInvite(cross, 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/v1$help\u0026func=AcceptInvite\u0026boardID=1\u0026user=g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5) • [revoke](/r/gnoland/boards2/v1$help\u0026func=RevokeInvite\u0026boardID=1\u0026user=g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5) |\n"
                      },
                      {
                        "name": "z_request_invite_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/demo/test\"))\n\n\tboards2.RequestInvite(cross, bid)\n}\n\n// Error:\n// caller must be user\n"
                      },
                      {
                        "name": "z_request_invite_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RequestInvite(cross, bid)\n}\n\n// Error:\n// caller is already a member\n"
                      },
                      {
                        "name": "z_request_invite_03_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross, bid)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.RequestInvite(cross, bid)\n}\n\n// Error:\n// invite request already exists\n"
                      },
                      {
                        "name": "z_request_invite_04_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.RequestInvite(cross, 404)\n}\n\n// Error:\n// board does not exist with ID: 404\n"
                      },
                      {
                        "name": "z_revoke_invite_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross, bid)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RevokeInvite(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.RevokeInvite(cross, bid, user)\n}\n\n// Error:\n// invite request not found\n"
                      },
                      {
                        "name": "z_revoke_invite_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\tboards2.RequestInvite(cross, bid)\n}\n\nfunc main() {\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, bid, user)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_set_flagging_threshold_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test-board\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetFlaggingThreshold(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address   = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tbid   boards.ID = 404\n)\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetFlaggingThreshold(cross, bid, 1)\n}\n\n// Error:\n// board does not exist with ID: 404\n"
                      },
                      {
                        "name": "z_set_flagging_threshold_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetFlaggingThreshold(cross, 1, 0)\n}\n\n// Error:\n// invalid flagging threshold\n"
                      },
                      {
                        "name": "z_set_permissions_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/gnoland/boards/exts/permissions\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetPermissions(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/gnoland/boards/exts/permissions\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\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() {\n\ttesting.SetRealm(testing.NewUserRealm(user))\n\n\tboards2.SetPermissions(cross, bid, perms)\n}\n\n// Error:\n// unauthorized\n"
                      },
                      {
                        "name": "z_set_permissions_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\t\"gno.land/p/gnoland/boards/exts/permissions\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar (\n\tperms boards.Permissions\n\tbid   boards.ID\n)\n\nfunc init() {\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, \"foobar\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetPermissions(cross, 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": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetRealmNotice(cross, \"This is a test realm message\")\n\n\t// Render content must contain the message\n\tprintln(boards2.Render(\"\"))\n}\n\n// Output:\n// \u003e [!INFO] Notice\n// \u003e This is a test realm message\n// # Boards\n// [Create Board](/r/gnoland/boards2/v1:create-board) • [List Admin Users](/r/gnoland/boards2/v1:admin-users) • [Help](/r/gnoland/boards2/v1:help)\n//\n// ---\n// ### Currently there are no boards\n// Be the first to [create a new board](/r/gnoland/boards2/v1:create-board)!\n"
                      },
                      {
                        "name": "z_set_realm_notice_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc init() {\n\t// Set an initial message so it can be cleared\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tboards2.SetRealmNotice(cross, \"This is a test realm message\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.SetRealmNotice(cross, \"\")\n\n\t// Render content must contain the message\n\tcontent := boards2.Render(\"\")\n\tprintln(strings.HasPrefix(content, \"\u003e This is a test realm message\\n\\n\"))\n\tprintln(strings.HasPrefix(content, \"# Boards\"))\n}\n\n// Output:\n// false\n// true\n"
                      },
                      {
                        "name": "z_set_realm_notice_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\" // @test2\n\nfunc main() {\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, \"Foo\")\n}\n\n// Error:\n// unauthorized\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/v1\"\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// | [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) | owner | [remove](/r/gnoland/boards2/v1$help\u0026func=RemoveMember\u0026boardID=0\u0026member=g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) • [change role](/r/gnoland/boards2/v1$help\u0026func=ChangeMemberRole\u0026boardID=0\u0026member=g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\u0026role=) |\n// | [g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh](/u/g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) | owner | [remove](/r/gnoland/boards2/v1$help\u0026func=RemoveMember\u0026boardID=0\u0026member=g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh) • [change role](/r/gnoland/boards2/v1$help\u0026func=ChangeMemberRole\u0026boardID=0\u0026member=g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh\u0026role=) |\n"
                      },
                      {
                        "name": "z_ui_board_00_filetest.gno",
                        "body": "// Render default board view.\npackage main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"TestBoard\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and then add 3 threads\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\n\t// Create thread \"A\" with a single comment\n\tthreadID := boards2.CreateThread(cross, boardID, \"A\", \"Body\")\n\tboards2.CreateReply(cross, boardID, threadID, 0, \"Body\")\n\n\t// The other 2 threads are created without comments\n\tboards2.CreateThread(cross, boardID, \"B\", \"Body\")\n\tthreadID = boards2.CreateThread(cross, boardID, \"C\", \"Body\")\n\n\t// Repost thread \"C\" into a different board\n\tdstBoardID := boards2.CreateBoard(cross, \"Bar\", false, false)\n\tboards2.CreateRepost(cross, boardID, threadID, dstBoardID, \"Title\", \"Body\")\n}\n\nfunc main() {\n\tprintln(boards2.Render(boardName))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v1) › TestBoard\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1\n// ↳ [Create Thread](/r/gnoland/boards2/v1:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help\u0026func=RequestInvite\u0026boardID=1) • [Manage Board](?menu=manageBoard)\n//\n// ---\n// Sort by: [oldest first](/r/gnoland/boards2/v1:TestBoard?order=desc)\n//\n// ###### [A](/r/gnoland/boards2/v1:TestBoard/1)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC\n// **1 replies • 0 reposts**\n//\n// ###### [B](/r/gnoland/boards2/v1:TestBoard/3)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n//\n// ###### [C](/r/gnoland/boards2/v1:TestBoard/4)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 1 reposts**\n"
                      },
                      {
                        "name": "z_ui_board_01_filetest.gno",
                        "body": "// Render board sorting threads from newest to oldest.\npackage main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"TestBoard\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and then add 3 threads\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\n\tboards2.CreateThread(cross, boardID, \"A\", \"Body\")\n\tboards2.CreateThread(cross, boardID, \"B\", \"Body\")\n\tboards2.CreateThread(cross, 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/v1) › TestBoard\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1\n// ↳ [Create Thread](/r/gnoland/boards2/v1:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help\u0026func=RequestInvite\u0026boardID=1) • [Manage Board](?menu=manageBoard)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v1:TestBoard?order=asc)\n//\n// ###### [C](/r/gnoland/boards2/v1:TestBoard/3)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n//\n// ###### [B](/r/gnoland/boards2/v1:TestBoard/2)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n//\n// ###### [A](/r/gnoland/boards2/v1:TestBoard/1)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n"
                      },
                      {
                        "name": "z_ui_board_02_filetest.gno",
                        "body": "// Render board view with the manage board menu expanded.\npackage main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"TestBoard\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\tboards2.CreateThread(cross, 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/v1) › TestBoard\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1\n// ↳ [Create Thread](/r/gnoland/boards2/v1:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help\u0026func=RequestInvite\u0026boardID=1) • **Manage Board**\n// └─ [Invite Member](/r/gnoland/boards2/v1:TestBoard/invite-member) • [List Invite Requests](/r/gnoland/boards2/v1:TestBoard/invites) • [List Members](/r/gnoland/boards2/v1:TestBoard/members) • [List Banned Users](/r/gnoland/boards2/v1:TestBoard/banned-users) • [Freeze Board](/r/gnoland/boards2/v1$help\u0026func=FreezeBoard\u0026boardID=1)\n//\n// ---\n// Sort by: [oldest first](/r/gnoland/boards2/v1:TestBoard?menu=manageBoard\u0026order=desc)\n//\n// ###### [A](/r/gnoland/boards2/v1:TestBoard/1)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n"
                      },
                      {
                        "name": "z_ui_board_03_filetest.gno",
                        "body": "// Render readonly board.\npackage main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"TestBoard\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a readonly board and then add a thread\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\tboards2.CreateThread(cross, boardID, \"A\", \"Body\")\n\tboards2.FreezeBoard(cross, boardID)\n}\n\nfunc main() {\n\tprintln(boards2.Render(boardName))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v1) › TestBoard\n// \u003e [!WARNING] Info\n// \u003e Creating new threads and commenting are disabled within this board\n//\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1\n// [List Members](/r/gnoland/boards2/v1:TestBoard/members) • [Unfreeze Board](/r/gnoland/boards2/v1$help\u0026func=UnfreezeBoard\u0026boardID=1\u0026replyID=\u0026threadID=)\n//\n// ---\n// Sort by: [oldest first](/r/gnoland/boards2/v1:TestBoard?order=desc)\n//\n// ###### [A](/r/gnoland/boards2/v1:TestBoard/1)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) `owner` on 2009-02-13 11:31pm UTC\n// **0 replies • 0 reposts**\n"
                      },
                      {
                        "name": "z_ui_board_04_filetest.gno",
                        "body": "// Render default board view when there are no threads.\npackage main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"TestBoard\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross, boardName, false, false)\n}\n\nfunc main() {\n\tprintln(boards2.Render(boardName))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v1) › TestBoard\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1\n// ↳ [Create Thread](/r/gnoland/boards2/v1:TestBoard/create-thread) • [Request Invite](/r/gnoland/boards2/v1$help\u0026func=RequestInvite\u0026boardID=1) • [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/v1:TestBoard/create-thread) in this board?\n"
                      },
                      {
                        "name": "z_ui_board_members_00_filetest.gno",
                        "body": "// Render board members view.\npackage main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"BoardName\"\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross, 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// | [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) | owner | [remove](/r/gnoland/boards2/v1$help\u0026func=RemoveMember\u0026boardID=1\u0026member=g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) • [change role](/r/gnoland/boards2/v1$help\u0026func=ChangeMemberRole\u0026boardID=1\u0026member=g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\u0026role=) |\n"
                      },
                      {
                        "name": "z_ui_home_00_filetest.gno",
                        "body": "// Render default realm view.\n// Default realm view must render the list of listed boards.\npackage main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create board \"AAA\" with a single thread\n\tboardID := boards2.CreateBoard(cross, \"AAA\", true, false)\n\tboards2.CreateThread(cross, boardID, \"Foo\", \"Bar\")\n\n\t// Create 2 more boards\n\tboards2.CreateBoard(cross, \"BBB\", true, false)\n\tboards2.CreateBoard(cross, \"CCC\", true, false)\n\tboards2.CreateBoard(cross, \"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/v1:create-board) • [List Admin Users](/r/gnoland/boards2/v1:admin-users) • [Help](/r/gnoland/boards2/v1:help)\n//\n// ---\n// Sort by: [oldest first](/r/gnoland/boards2/v1:?order=desc)\n//\n// ###### [AAA](/r/gnoland/boards2/v1:AAA)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #1\n// **1 threads**\n//\n// ###### [BBB](/r/gnoland/boards2/v1:BBB)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #2\n// **0 threads**\n//\n// ###### [CCC](/r/gnoland/boards2/v1:CCC)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #3\n// **0 threads**\n"
                      },
                      {
                        "name": "z_ui_home_01_filetest.gno",
                        "body": "// Render boards sorted from newest to oldest.\npackage main\n\nimport (\n\t\"testing\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.CreateBoard(cross, \"AAA\", true, false)\n\tboards2.CreateBoard(cross, \"BBB\", true, false)\n\tboards2.CreateBoard(cross, \"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/v1:create-board) • [List Admin Users](/r/gnoland/boards2/v1:admin-users) • [Help](/r/gnoland/boards2/v1:help)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v1:?order=asc)\n//\n// ###### [CCC](/r/gnoland/boards2/v1:CCC)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #3\n// **0 threads**\n//\n// ###### [BBB](/r/gnoland/boards2/v1:BBB)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC, #2\n// **0 threads**\n//\n// ###### [AAA](/r/gnoland/boards2/v1:AAA)\n// Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) 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/v1\"\n)\n\nfunc main() {\n\tprintln(boards2.Render(\"\"))\n}\n\n// Output:\n// # Boards\n// [Create Board](/r/gnoland/boards2/v1:create-board) • [List Admin Users](/r/gnoland/boards2/v1:admin-users) • [Help](/r/gnoland/boards2/v1:help)\n//\n// ---\n// ### Currently there are no boards\n// Be the first to [create a new board](/r/gnoland/boards2/v1:create-board)!\n"
                      },
                      {
                        "name": "z_ui_reply_00_filetest.gno",
                        "body": "// Render comment/reply view.\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"test-board\"\n)\n\nvar (\n\tthreadID boards.ID\n\treplyID  boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\tthreadID = boards2.CreateThread(cross, boardID, \"Foo\", \"Body\")\n\n\t// Create two comments and a reply to a comment\n\tboards2.CreateReply(cross, boardID, threadID, 0, \"First comment\")\n\n\treplyID = boards2.CreateReply(cross, boardID, threadID, 0, \"Second comment\")\n\tboards2.CreateReply(cross, 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// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [2] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3)\n// \u003e Second comment\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=3\u0026threadID=1)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#4](/r/gnoland/boards2/v1:test-board/1/4)\n// \u003e \u003e Third comment\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/4/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/4/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/4/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=4\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_ui_reply_01_filetest.gno",
                        "body": "// Render comment/reply view of a deleted comment.\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"test-board\"\n)\n\nvar (\n\tthreadID boards.ID\n\treplyID  boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\tthreadID = boards2.CreateThread(cross, boardID, \"Foo\", \"Body\")\n\n\t// Create a comments with a reply\n\treplyID = boards2.CreateReply(cross, boardID, threadID, 0, \"Second comment\")\n\tboards2.CreateReply(cross, boardID, threadID, replyID, \"Third comment\")\n\n\t// Delete the comment\n\tboards2.DeleteReply(cross, 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// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2)\n// \u003e ⚠ This comment has been deleted\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/2/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=2\u0026threadID=1)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3)\n// \u003e \u003e Third comment\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=3\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_ui_reply_02_filetest.gno",
                        "body": "// Render comment/reply view of a flagged comment.\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"test-board\"\n)\n\nvar (\n\tthreadID boards.ID\n\treplyID  boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\tthreadID = boards2.CreateThread(cross, boardID, \"Foo\", \"Body\")\n\n\t// Create a comments with a reply\n\treplyID = boards2.CreateReply(cross, boardID, threadID, 0, \"Second comment\")\n\tboards2.CreateReply(cross, boardID, threadID, replyID, \"Third comment\")\n\n\t// Flag the comment\n\tboards2.FlagReply(cross, 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// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1) • [Show all Replies](/r/gnoland/boards2/v1:test-board/1)\n//\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2)\n// \u003e ⚠ Reply is hidden as it has been flagged as [inappropriate](/r/gnoland/boards2/v1:test-board/1/2/flagging-reasons)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3)\n// \u003e \u003e Third comment\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=3\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_ui_thread_00_filetest.gno",
                        "body": "// Render default thread view.\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"test-board\"\n)\n\nvar threadID boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\tthreadID = boards2.CreateThread(cross, boardID, \"Foo\", \"Body\")\n\n\t// Create two comments and a reply to a comment\n\tboards2.CreateReply(cross, boardID, threadID, 0, \"First comment\")\n\n\treplyID := boards2.CreateReply(cross, boardID, threadID, 0, \"Second comment\")\n\tboards2.CreateReply(cross, 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/v1) › [test\\-board](/r/gnoland/boards2/v1:test-board)\n// ## Foo\n//\n// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [2] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1)\n//\n// ---\n// Sort by: [oldest first](/r/gnoland/boards2/v1:test-board/1?order=desc)\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2)\n// \u003e First comment\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/2/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=2\u0026threadID=1)\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3)\n// \u003e Second comment\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=3\u0026threadID=1)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#4](/r/gnoland/boards2/v1:test-board/1/4)\n// \u003e \u003e Third comment\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/4/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/4/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/4/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=4\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_ui_thread_01_filetest.gno",
                        "body": "// Render thread sorting comments from newest to oldest.\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"test-board\"\n)\n\nvar threadID boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\tthreadID = boards2.CreateThread(cross, boardID, \"Foo\", \"Body\")\n\n\t// Create two comments and a reply to a comment\n\tboards2.CreateReply(cross, boardID, threadID, 0, \"First comment\")\n\n\treplyID := boards2.CreateReply(cross, boardID, threadID, 0, \"Second comment\")\n\tboards2.CreateReply(cross, 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/v1) › [test\\-board](/r/gnoland/boards2/v1:test-board)\n// ## Foo\n//\n// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) [2] • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1)\n//\n// ---\n// Sort by: [newest first](/r/gnoland/boards2/v1:test-board/1?order=asc)\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#3](/r/gnoland/boards2/v1:test-board/1/3)\n// \u003e Second comment\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/3/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/3/reply) [1] • [Edit](/r/gnoland/boards2/v1:test-board/1/3/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=3\u0026threadID=1)\n// \u003e\n// \u003e \u003e\n// \u003e \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#4](/r/gnoland/boards2/v1:test-board/1/4)\n// \u003e \u003e Third comment\n// \u003e \u003e\n// \u003e \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/4/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/4/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/4/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=4\u0026threadID=1)\n//\n// \u003e\n// \u003e **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC [#2](/r/gnoland/boards2/v1:test-board/1/2)\n// \u003e First comment\n// \u003e\n// \u003e ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/2/flag) • [Reply](/r/gnoland/boards2/v1:test-board/1/2/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/2/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteReply\u0026boardID=1\u0026replyID=2\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_ui_thread_02_filetest.gno",
                        "body": "// Render both original thread and thread repost view.\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner        address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tsrcBoardName         = \"test-board\"\n\tdstBoardName         = \"test-board-2\"\n)\n\nvar (\n\tthreadID       boards.ID\n\trepostThreadID boards.ID\n)\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross, srcBoardName, false, false)\n\tthreadID = boards2.CreateThread(cross, boardID, \"Foo\", \"Body\")\n\n\t// Repost thread into a different board\n\tdstBoardID := boards2.CreateBoard(cross, dstBoardName, false, false)\n\trepostThreadID = boards2.CreateRepost(cross, 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/v1) › [test\\-board](/r/gnoland/boards2/v1:test-board)\n// ## Foo\n//\n// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost) [1] • [Comment](/r/gnoland/boards2/v1:test-board/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=1\u0026threadID=1)\n//\n// \u003e\u003e\u003e\u003c\u003c\u003c\n//\n// # [Boards](/r/gnoland/boards2/v1) › [test\\-board\\-2](/r/gnoland/boards2/v1:test-board-2)\n// ## Bar\n//\n// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// \u003e [!INFO]- Thread Repost\n// \u003e Original thread is [Foo](/r/gnoland/boards2/v1:test-board/1)\n// \u003e Created by [g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq) on 2009-02-13 11:31pm UTC\n//\n// Body2\n//\n// \u003e Body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board-2/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board-2/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board-2/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board-2/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_ui_thread_03_filetest.gno",
                        "body": "// Render thread from a readonly board.\n// Rendered thread action links should be limited to readonly actions.\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"test-board\"\n)\n\nvar threadID boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a readonly board and then add a thread\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\tthreadID = boards2.CreateThread(cross, boardID, \"Foo\", \"Body\")\n\tboards2.FreezeBoard(cross, boardID)\n}\n\nfunc main() {\n\tpath := boardName + \"/\" + threadID.String()\n\tprintln(boards2.Render(path))\n}\n\n// Output:\n// # [Boards](/r/gnoland/boards2/v1) › [test\\-board](/r/gnoland/boards2/v1:test-board)\n// ## Foo\n//\n// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Body\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board/1/repost)\n"
                      },
                      {
                        "name": "z_ui_thread_04_filetest.gno",
                        "body": "// Render thread repost of a deleted thread.\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner        address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tsrcBoardName         = \"test-board\"\n\tdstBoardName         = \"test-board-2\"\n)\n\nvar repostThreadID boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross, srcBoardName, false, false)\n\tthreadID := boards2.CreateThread(cross, boardID, \"Foo\", \"Body\")\n\n\t// Repost thread into a different board\n\tdstBoardID := boards2.CreateBoard(cross, dstBoardName, false, false)\n\trepostThreadID = boards2.CreateRepost(cross, boardID, threadID, dstBoardID, \"Bar\", \"Body2\")\n\n\t// Remove the original thread\n\tboards2.DeleteThread(cross, 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/v1) › [test\\-board\\-2](/r/gnoland/boards2/v1:test-board-2)\n// ## Bar\n//\n// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Body2\n//\n// \u003e ⚠ Source post has been deleted\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board-2/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board-2/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board-2/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board-2/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_ui_thread_05_filetest.gno",
                        "body": "// Render thread which has been flagged.\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner     address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tboardName         = \"BoardName\"\n)\n\nvar threadID boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a readonly board and then add a thread\n\tboardID := boards2.CreateBoard(cross, boardName, false, false)\n\tthreadID = boards2.CreateThread(cross, boardID, \"Foo\", \"Body\")\n\n\t// Flag the thread\n\tboards2.SetFlaggingThreshold(cross, boardID, 1)\n\tboards2.FlagThread(cross, 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/v1:BoardName/1/flagging-reasons)\n"
                      },
                      {
                        "name": "z_ui_thread_06_filetest.gno",
                        "body": "// Render thread repost of a flagged thread.\npackage main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner        address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tsrcBoardName         = \"test-board\"\n\tdstBoardName         = \"test-board-2\"\n)\n\nvar repostThreadID boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\t// Create a board and a thread\n\tboardID := boards2.CreateBoard(cross, srcBoardName, false, false)\n\tthreadID := boards2.CreateThread(cross, boardID, \"Foo\", \"Body\")\n\n\t// Repost thread into a different board\n\tdstBoardID := boards2.CreateBoard(cross, dstBoardName, false, false)\n\trepostThreadID = boards2.CreateRepost(cross, boardID, threadID, dstBoardID, \"Bar\", \"Body2\")\n\n\t// Flag original thread\n\tboards2.SetFlaggingThreshold(cross, boardID, 1)\n\tboards2.FlagThread(cross, 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/v1) › [test\\-board\\-2](/r/gnoland/boards2/v1:test-board-2)\n// ## Bar\n//\n// **[g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq](/u/g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq)** `owner` · 2009-02-13 11:31pm UTC\n// Body2\n//\n// \u003e ⚠ Source post has been flagged as inappropriate\n//\n// ↳ [Flag](/r/gnoland/boards2/v1:test-board-2/1/flag) • [Repost](/r/gnoland/boards2/v1:test-board-2/1/repost) • [Comment](/r/gnoland/boards2/v1:test-board-2/1/reply) • [Edit](/r/gnoland/boards2/v1:test-board-2/1/edit) • [Delete](/r/gnoland/boards2/v1$help\u0026func=DeleteThread\u0026boardID=2\u0026threadID=1)\n"
                      },
                      {
                        "name": "z_unban_00_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\tboards2.Ban(cross, bid, user, boards2.BanDay, \"Unpolite behavior\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.Unban(cross, bid, user, \"\")\n\n\tprintln(boards2.IsBanned(bid, user))\n}\n\n// Output:\n// false\n"
                      },
                      {
                        "name": "z_unban_01_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst owner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\n\tboards2.Unban(cross, bid, \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\", \"\")\n}\n\n// Error:\n// user is not banned\n"
                      },
                      {
                        "name": "z_unban_02_filetest.gno",
                        "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/gnoland/boards\"\n\n\tboards2 \"gno.land/r/gnoland/boards2/v1\"\n)\n\nconst (\n\towner address = \"g16jpf0puufcpcjkph5nxueec8etpcldz7zwgydq\"\n\tuser  address = \"g1vh7krmmzfua5xjmkatvmx09z37w34lsvd2mxa5\"\n)\n\nvar bid boards.ID\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(owner))\n\tbid = boards2.CreateBoard(cross, \"test123\", false, false)\n\tboards2.Ban(cross, bid, user, boards2.BanDay, \"Unpolite behavior\")\n}\n\nfunc main() {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"))\n\n\t// Try to unban without unbanning permissions\n\tboards2.Unban(cross, bid, user, \"\")\n}\n\n// Error:\n// unauthorized\n"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "8QM9XEa73qUggaJtVwqw9fp0x5ony5naxk7Gl2Lc+Pg8DAhPNJ6zvySoFDb13EPVxsinHgO5YRHQD2uJ0DBiTw=="
                }
              ],
              "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\"\n\t\"gno.land/p/leon/ctg\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nvar router *mux.Router\n\nfunc init() {\n\trouter = mux.NewRouter()\n\n\trouter.HandleFunc(\"\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(renderHomepage())\n\t})\n\n\trouter.HandleFunc(\"balances\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(renderBalances(req))\n\t})\n\n\trouter.HandleFunc(\"convert/{address}\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\tres.Write(renderConvertedAddress(req.GetVar(\"address\")))\n\t})\n\n\t// Coin info\n\trouter.HandleFunc(\"supply/{denom}\", func(res *mux.ResponseWriter, req *mux.Request) {\n\t\t// banker := std.NewBanker(std.BankerTypeReadonly)\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.NewBanker(banker.BankerTypeReadonly)\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)\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\nfunc renderSingleCoinBalance(denom, addr, origInput string, wasConverted bool) string {\n\tout := \"# Coin balance\\n\\n\"\n\tbanker_ := banker.NewBanker(banker.BankerTypeReadonly)\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, banker_.GetCoins(address(addr)).AmountOf(denom), 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\"\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\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "6yUhI//ErcH4VjSS2mYeeB94NpToW5DpA4gyax7Ojbow2h/rsN/jgWBanZutbLY9szx/FRs/72gF2xHNjFfLkg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "wugnot",
                    "path": "gno.land/r/gnoland/wugnot",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/gnoland/wugnot\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "wugnot.gno",
                        "body": "package wugnot\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/demo/defi/grc20reg\"\n)\n\nvar Token, adm = grc20.NewToken(\"wrapped GNOT\", \"wugnot\", 0)\n\nconst (\n\tugnotMinDeposit  int64 = 1000\n\twugnotMinDeposit int64 = 1\n)\n\nfunc init() {\n\tgrc20reg.Register(cross, 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 := runtime.PreviousRealm().Address()\n\tsent := banker.OriginSend()\n\tamount := sent.AmountOf(\"ugnot\")\n\n\trequire(int64(amount) \u003e= ugnotMinDeposit, ufmt.Sprintf(\"Deposit below minimum: %d/%d ugnot.\", amount, ugnotMinDeposit))\n\n\tcheckErr(adm.Mint(caller, int64(amount)))\n}\n\nfunc Withdraw(cur realm, amount int64) {\n\truntime.AssertOriginCall()\n\trequire(amount \u003e= wugnotMinDeposit, ufmt.Sprintf(\"Deposit below minimum: %d/%d wugnot.\", amount, wugnotMinDeposit))\n\n\tcaller := runtime.PreviousRealm().Address()\n\tpkgaddr := runtime.CurrentRealm().Address()\n\tcallerBal := Token.BalanceOf(caller)\n\trequire(amount \u003c= callerBal, ufmt.Sprintf(\"Insufficient balance: %d available, %d needed.\", callerBal, amount))\n\n\t// send swapped ugnots to qcaller\n\tstdBanker := banker.NewBanker(banker.BankerTypeRealmSend)\n\tsend := chain.Coins{{\"ugnot\", int64(amount)}}\n\tstdBanker.SendCoins(pkgaddr, caller, send)\n\tcheckErr(adm.Burn(caller, amount))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn Token.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := address(parts[1])\n\t\tbalance := Token.BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\", balance)\n\tdefault:\n\t\treturn \"404\"\n\t}\n}\n\nfunc TotalSupply() int64 {\n\treturn Token.TotalSupply()\n}\n\nfunc BalanceOf(owner address) int64 {\n\treturn Token.BalanceOf(owner)\n}\n\nfunc Allowance(owner, spender address) int64 {\n\treturn Token.Allowance(owner, spender)\n}\n\nfunc Transfer(cur realm, to address, amount int64) {\n\tuserTeller := Token.CallerTeller()\n\tcheckErr(userTeller.Transfer(to, amount))\n}\n\nfunc Approve(cur realm, spender address, amount int64) {\n\tuserTeller := Token.CallerTeller()\n\tcheckErr(userTeller.Approve(spender, amount))\n}\n\nfunc TransferFrom(cur realm, from, to address, amount int64) {\n\tuserTeller := Token.CallerTeller()\n\tcheckErr(userTeller.TransferFrom(from, to, amount))\n}\n\nfunc require(condition bool, msg string) {\n\tif !condition {\n\t\tpanic(msg)\n\t}\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
                      },
                      {
                        "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() {\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).SendCoins(addr1, addrc, coins)\n\twugnot.Deposit(cross)\n\tprintBalances()\n\n\t// withdraw of 4242ugnot to addr1\n\twugnot.Withdraw(cross, 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.NewBanker(banker.BankerTypeReadonly)\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"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "ZRrlvaDGI7o/K3Wc4zQ+xqhiAN+em7VseqORQWVShBAN2qqR7d1GbsVp/wUjc9FM/LRueBklSnmQaPMmr4zi3g=="
                }
              ],
              "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\"chain\"\n\n\t\"gno.land/p/moul/authz\"\n)\n\nvar auth *authz.Authorizer\n\nfunc Auth() *authz.Authorizer {\n\treturn auth\n}\n\nfunc updateInstructions(newInstructions string) {\n\terr := auth.DoByCurrent(\"update-instructions\", func() error {\n\t\tinstructions = newInstructions\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc updateMinFee(newMinFee int64) {\n\terr := auth.DoByCurrent(\"update-min-fee\", func() error {\n\t\tminFee = chain.NewCoin(\"ugnot\", newMinFee)\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc NewInstructionsProposalCallback(newInstructions string) func(realm) error {\n\tcb := func(cur realm) error {\n\t\tupdateInstructions(newInstructions)\n\t\treturn nil\n\t}\n\n\treturn cb\n}\n\nfunc NewMinFeeProposalCallback(newMinFee int64) func(realm) error {\n\tcb := func(cur realm) error {\n\t\tupdateMinFee(newMinFee)\n\t\treturn nil\n\t}\n\n\treturn cb\n}\n"
                      },
                      {
                        "name": "admin_test.gno",
                        "body": "package valopers\n\nimport (\n\t\"chain\"\n\t\"testing\"\n\n\t\"gno.land/p/moul/authz\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\n\nfunc TestUpdateInstructions(t *testing.T) {\n\tauth = authz.NewWithAuthority(\n\t\tauthz.NewContractAuthority(\n\t\t\t\"gno.land/r/gov/dao\",\n\t\t\tfunc(title string, action authz.PrivilegedAction) error {\n\t\t\t\treturn action()\n\t\t\t},\n\t\t),\n\t)\n\n\tnewInstructions := \"new instructions\"\n\n\tuassert.PanicsWithMessage(t, \"action can only be executed by the contract\", func() {\n\t\tupdateInstructions(newInstructions)\n\t})\n\n\ttesting.SetOriginCaller(chain.PackageAddress(\"gno.land/r/gov/dao\"))\n\n\tuassert.NotPanics(t, func() {\n\t\tupdateInstructions(newInstructions)\n\t})\n\n\tuassert.Equal(t, newInstructions, instructions)\n}\n\nfunc TestUpdateMinFee(t *testing.T) {\n\tauth = authz.NewWithAuthority(\n\t\tauthz.NewContractAuthority(\n\t\t\t\"gno.land/r/gov/dao\",\n\t\t\tfunc(title string, action authz.PrivilegedAction) error {\n\t\t\t\treturn action()\n\t\t\t},\n\t\t),\n\t)\n\n\tnewMinFee := int64(100)\n\n\tuassert.PanicsWithMessage(t, \"action can only be executed by the contract\", func() {\n\t\tupdateMinFee(newMinFee)\n\t})\n\n\ttesting.SetOriginCaller(chain.PackageAddress(\"gno.land/r/gov/dao\"))\n\n\tuassert.NotPanics(t, func() {\n\t\tupdateMinFee(newMinFee)\n\t})\n\n\tuassert.Equal(t, newMinFee, minFee.Amount)\n}\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/gnops/valopers\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"\n"
                      },
                      {
                        "name": "init.gno",
                        "body": "package valopers\n\nimport (\n\t\"gno.land/p/moul/authz\"\n\t\"gno.land/p/moul/txlink\"\n\t\"gno.land/p/nt/avl/v0\"\n)\n\nfunc init() {\n\tvalopers = avl.NewTree()\n\n\tauth = authz.NewWithAuthority(\n\t\tauthz.NewContractAuthority(\n\t\t\t\"gno.land/r/gnops/valopers\",\n\t\t\tfunc(_ string, action authz.PrivilegedAction) error {\n\t\t\t\treturn action()\n\t\t\t},\n\t\t),\n\t)\n\n\tinstructions = `\n# Welcome to the **Valopers** realm\n\n## 📌 Purpose of this Contract\n\nThe **Valopers** contract is designed to maintain a registry of **validator profiles**. This registry provides essential information to **GovDAO members**, enabling them to make informed decisions when voting on the inclusion of new validators into the **valset**.\n\nBy registering your validator profile, you contribute to a transparent and well-informed governance process within **gno.land**.\n\n---\n\n## 📝 How to Register Your Validator Node\n\nTo add your validator node to the registry, use the [**Register**](` + txlink.Call(\"Register\") + `) function with the following parameters:\n\n- **Moniker** (Validator Name)\n  - Must be **human-readable**\n  - **Max length**: **32 characters**\n  - **Allowed characters**: Letters, numbers, spaces, hyphens (**-**), and underscores (**_**)\n  - **No special characters** at the beginning or end\n\n- **Description** (Introduction \u0026 Validator Details)\n  - **Max length**: **2048 characters**\n  - Must include answers to the questions listed below\n\n- **Server Type** (Infrastructure Type)\n  - Must be one of the following values:\n    - **cloud**: For validators running on cloud infrastructure (AWS, GCP, Azure, etc.)\n    - **on-prem**: For validators running on on-premises infrastructure\n    - **data-center**: For validators running in dedicated data centers\n\n- **Validator Address**\n  - Your validator node's address\n\n- **Validator Public Key**\n  - Your validator node's public 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 testnet validator](https://gnops.io/articles/guides/become-testnet-validator/) \u003c!-- XXX: replace with a r/gnops/blog:xxx link --\u003e\n\nDisclaimer: Please note, registering your validator profile and/or validating on testnets does not guarantee a validator slot on the gno.land beta mainnet. However, active participation and contributions to testnets will help establish credibility and may improve your chances for future validator acceptance. The initial validator amount and valset will ultimately be selected through GovDAO governance proposals and acceptance.\n\n---\n\n`\n}\n"
                      },
                      {
                        "name": "valopers.gno",
                        "body": "// Package valopers is designed around the permissionless lifecycle of valoper profiles.\npackage valopers\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"crypto/bech32\"\n\t\"errors\"\n\t\"regexp\"\n\n\t\"gno.land/p/moul/realmpath\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/avl/v0/pager\"\n\t\"gno.land/p/nt/combinederr/v0\"\n\t\"gno.land/p/nt/ownable/v0\"\n\t\"gno.land/p/nt/ownable/v0/exts/authorizable\"\n\t\"gno.land/p/nt/ufmt/v0\"\n)\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)\n\nvar (\n\tvalopers     *avl.Tree                              // valopers keeps track of all the valoper profiles. Address -\u003e Valoper\n\tinstructions string                                 // markdown instructions for valoper's registration\n\tminFee       = chain.NewCoin(\"ugnot\", 20*1_000_000) // minimum gnot must be paid to register.\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// 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\tAddress     address // The bech32 gno address of the validator\n\tPubKey      string  // The bech32 public key of the validator\n\tKeepRunning bool    // Flag indicating if the owner wants to keep the validator running\n\n\tauth *authorizable.Authorizable // The authorizer system for the valoper\n}\n\nfunc (v Valoper) Auth() *authorizable.Authorizable {\n\treturn v.auth\n}\n\nfunc AddToAuthList(cur realm, address_XXX address, member address) {\n\tv := GetByAddr(address_XXX)\n\tif err := v.Auth().AddToAuthList(member); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc DeleteFromAuthList(cur realm, address_XXX address, member address) {\n\tv := GetByAddr(address_XXX)\n\tif err := v.Auth().DeleteFromAuthList(member); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Register registers a new valoper\nfunc Register(cur realm, moniker string, description string, serverType string, address_XXX address, pubKey string) {\n\t// Check if a fee is enforced\n\tif !minFee.IsZero() {\n\t\tsentCoins := banker.OriginSend()\n\n\t\t// Coins must be sent and cover the min fee\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(address_XXX) {\n\t\tpanic(ErrValoperExists)\n\t}\n\n\tv := Valoper{\n\t\tMoniker:     moniker,\n\t\tDescription: description,\n\t\tServerType:  serverType,\n\t\tAddress:     address_XXX,\n\t\tPubKey:      pubKey,\n\t\tKeepRunning: true,\n\t\tauth:        authorizable.New(ownable.NewWithOrigin()),\n\t}\n\n\tif err := v.Validate(); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// TODO add address derivation from public key\n\t// (when the laws of gno make it possible)\n\n\t// Save the valoper to the set\n\tvalopers.Set(v.Address.String(), v)\n}\n\n// UpdateMoniker updates an existing valoper's moniker\nfunc UpdateMoniker(cur realm, address_XXX 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(address_XXX)\n\n\t// Check that the caller has permissions\n\tv.Auth().AssertPreviousOnAuthList()\n\n\t// Update the moniker\n\tv.Moniker = moniker\n\n\t// Save the valoper info\n\tvalopers.Set(address_XXX.String(), v)\n}\n\n// UpdateDescription updates an existing valoper's description\nfunc UpdateDescription(cur realm, address_XXX 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(address_XXX)\n\n\t// Check that the caller has permissions\n\tv.Auth().AssertPreviousOnAuthList()\n\n\t// Update the description\n\tv.Description = description\n\n\t// Save the valoper info\n\tvalopers.Set(address_XXX.String(), v)\n}\n\n// UpdateKeepRunning updates an existing valoper's active status\nfunc UpdateKeepRunning(cur realm, address_XXX address, keepRunning bool) {\n\tv := GetByAddr(address_XXX)\n\n\t// Check that the caller has permissions\n\tv.Auth().AssertPreviousOnAuthList()\n\n\t// Update status\n\tv.KeepRunning = keepRunning\n\n\t// Save the valoper info\n\tvalopers.Set(address_XXX.String(), v)\n}\n\n// UpdateServerType updates an existing valoper's server type\nfunc UpdateServerType(cur realm, address_XXX 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(address_XXX)\n\n\t// Check that the caller has permissions\n\tv.Auth().AssertPreviousOnAuthList()\n\n\t// Update server type\n\tv.ServerType = serverType\n\n\t// Save the valoper info\n\tvalopers.Set(address_XXX.String(), v)\n}\n\n// GetByAddr fetches the valoper using the address, if present\nfunc GetByAddr(address_XXX address) Valoper {\n\tvaloperRaw, exists := valopers.Get(address_XXX.String())\n\tif !exists {\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, exists := valopers.Get(addr)\n\t\tif !exists {\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.Address, v.Address)\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.Address))\n\terrs.Add(validatePubKey(v.PubKey))\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(\"- Address: %s\\n\", v.Address.String())\n\toutput += ufmt.Sprintf(\"- PubKey: %s\\n\", v.PubKey)\n\toutput += ufmt.Sprintf(\"- Server Type: %s\\n\\n\", v.ServerType)\n\toutput += ufmt.Sprintf(\"[Profile link](/r/demo/profile:u/%s)\\n\", v.Address)\n\n\treturn output\n}\n\n// isValoper checks if the valoper exists\nfunc isValoper(address_XXX address) bool {\n\t_, exists := valopers.Get(address_XXX.String())\n\n\treturn exists\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(address_XXX address) error {\n\tif !address_XXX.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// 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/ownable/v0/exts/authorizable\"\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 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(t *testing.T) {\n\ttest1 := testutils.TestAddress(\"test1\")\n\ttesting.SetRealm(testing.NewUserRealm(test1))\n\n\tt.Run(\"already a valoper\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\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\tAddress:     info.Address,\n\t\t\tPubKey:      info.PubKey,\n\t\t\tKeepRunning: true,\n\t\t}\n\n\t\t// Add the valoper\n\t\tvalopers.Set(v.Address.String(), v)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.AbortsWithMessage(t, ErrValoperExists.Error(), func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\t})\n\n\tt.Run(\"no coins deposited\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Send no coins\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"ugnot\", 0)})\n\n\t\tuassert.AbortsWithMessage(t, ufmt.Sprintf(\"payment must not be less than %d%s\", minFee.Amount, minFee.Denom), func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\t})\n\n\tt.Run(\"insufficient coins amount deposited\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\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, ufmt.Sprintf(\"payment must not be less than %d%s\", minFee.Amount, minFee.Denom), func() {\n\t\t\tRegister(cross, 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(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Send invalid coins\n\t\ttesting.SetOriginSend(chain.Coins{chain.NewCoin(\"gnogno\", minFee.Amount)})\n\n\t\tuassert.AbortsWithMessage(t, \"incompatible coin denominations: gnogno, ugnot\", func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\t})\n\n\tt.Run(\"successful registration\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\tuassert.NotAborts(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.NotPanics(t, 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.Address)\n\t\t\tuassert.Equal(t, info.PubKey, valoper.PubKey)\n\t\t\tuassert.Equal(t, true, valoper.KeepRunning)\n\t\t})\n\t})\n}\n\nfunc TestValopers_UpdateAuthMembers(t *testing.T) {\n\ttest1Address := testutils.TestAddress(\"test1\")\n\ttest2Address := testutils.TestAddress(\"test2\")\n\n\tt.Run(\"unauthorized member adds member\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(test1Address))\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\t\t// try to add member without being authorized\n\t\tuassert.AbortsWithMessage(t, authorizable.ErrNotSuperuser.Error(), func() {\n\t\t\tAddToAuthList(cross, info.Address, test2Address)\n\t\t})\n\t})\n\n\tt.Run(\"unauthorized member deletes member\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(test1Address))\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\t// XXX this panics.\n\t\t\tAddToAuthList(cross, info.Address, test2Address)\n\t\t})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(info.Address))\n\n\t\t// try to add member without being authorized\n\t\tuassert.AbortsWithMessage(t, authorizable.ErrNotSuperuser.Error(), func() {\n\t\t\tDeleteFromAuthList(cross, info.Address, test2Address)\n\t\t})\n\t})\n\n\tt.Run(\"authorized member adds member\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(test1Address))\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tAddToAuthList(cross, info.Address, test2Address)\n\t\t})\n\n\t\ttesting.SetRealm(testing.NewUserRealm(test2Address))\n\n\t\tnewMoniker := \"new moniker\"\n\t\t// Update the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tUpdateMoniker(cross, info.Address, newMoniker)\n\t\t})\n\n\t\tuassert.NotPanics(t, 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(t *testing.T) {\n\ttest1Address := testutils.TestAddress(\"test1\")\n\ttest2Address := testutils.TestAddress(\"test2\")\n\n\tt.Run(\"non-existing valoper\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateMoniker(cross, info.Address, \"new moniker\")\n\t\t})\n\t})\n\n\tt.Run(\"invalid caller\", func(t *testing.T) {\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Change the origin caller\n\t\ttesting.SetOriginCaller(test2Address)\n\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateMoniker(cross, info.Address, \"new moniker\")\n\t\t})\n\t})\n\n\tt.Run(\"invalid moniker\", func(t *testing.T) {\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\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tfor _, invalidMoniker := range invalidMonikers {\n\t\t\t// Update the valoper\n\t\t\tuassert.AbortsWithMessage(t, ErrInvalidMoniker.Error(), func() {\n\t\t\t\tUpdateMoniker(cross, info.Address, invalidMoniker)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"too long moniker\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, ErrInvalidMoniker.Error(), func() {\n\t\t\tUpdateMoniker(cross, info.Address, strings.Repeat(\"a\", MonikerMaxLength+1))\n\t\t})\n\t})\n\n\tt.Run(\"successful update\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tnewMoniker := \"new moniker\"\n\t\t// Update the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tUpdateMoniker(cross, info.Address, newMoniker)\n\t\t})\n\n\t\t// Make sure the valoper is updated\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\n\t\t\tuassert.Equal(t, newMoniker, valoper.Moniker)\n\t\t})\n\t})\n}\n\nfunc TestValopers_UpdateDescription(t *testing.T) {\n\ttest1Address := testutils.TestAddress(\"test1\")\n\ttest2Address := testutils.TestAddress(\"test2\")\n\n\tt.Run(\"non-existing valoper\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateDescription(cross, validValidatorInfo(t).Address, \"new description\")\n\t\t})\n\t})\n\n\tt.Run(\"invalid caller\", func(t *testing.T) {\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Change the origin caller\n\t\ttesting.SetOriginCaller(test2Address)\n\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateDescription(cross, info.Address, \"new description\")\n\t\t})\n\t})\n\n\tt.Run(\"empty description\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\temptyDescription := \"\"\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, ErrInvalidDescription.Error(), func() {\n\t\t\tUpdateDescription(cross, info.Address, emptyDescription)\n\t\t})\n\t})\n\n\tt.Run(\"too long description\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, ErrInvalidDescription.Error(), func() {\n\t\t\tUpdateDescription(cross, info.Address, strings.Repeat(\"a\", DescriptionMaxLength+1))\n\t\t})\n\t})\n\n\tt.Run(\"successful update\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\tnewDescription := \"new description\"\n\t\t// Update the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tUpdateDescription(cross, info.Address, newDescription)\n\t\t})\n\n\t\t// Make sure the valoper is updated\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\n\t\t\tuassert.Equal(t, newDescription, valoper.Description)\n\t\t})\n\t})\n}\n\nfunc TestValopers_UpdateKeepRunning(t *testing.T) {\n\ttest1Address := testutils.TestAddress(\"test1\")\n\ttest2Address := testutils.TestAddress(\"test2\")\n\n\tt.Run(\"non-existing valoper\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateKeepRunning(cross, validValidatorInfo(t).Address, false)\n\t\t})\n\t})\n\n\tt.Run(\"invalid caller\", func(t *testing.T) {\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Change the origin caller\n\t\ttesting.SetOriginCaller(test2Address)\n\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateKeepRunning(cross, info.Address, false)\n\t\t})\n\t})\n\n\tt.Run(\"successful update\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Update the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tUpdateKeepRunning(cross, info.Address, false)\n\t\t})\n\n\t\t// Make sure the valoper is updated\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\n\t\t\tuassert.Equal(t, false, valoper.KeepRunning)\n\t\t})\n\t})\n}\n\nfunc TestValopers_UpdateServerType(t *testing.T) {\n\ttest1Address := testutils.TestAddress(\"test1\")\n\ttest2Address := testutils.TestAddress(\"test2\")\n\n\tt.Run(\"non-existing valoper\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateServerType(cross, validValidatorInfo(t).Address, ServerTypeCloud)\n\t\t})\n\t})\n\n\tt.Run(\"invalid caller\", func(t *testing.T) {\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Change the origin caller\n\t\ttesting.SetOriginCaller(test2Address)\n\n\t\t// Update the valoper\n\t\tuassert.AbortsWithMessage(t, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateServerType(cross, info.Address, ServerTypeCloud)\n\t\t})\n\t})\n\n\tt.Run(\"invalid server type\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, 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\t// Update the valoper with invalid server type\n\t\t\tuassert.AbortsWithMessage(t, ErrInvalidServerType.Error(), func() {\n\t\t\t\tUpdateServerType(cross, info.Address, invalidType)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"successful update\", func(t *testing.T) {\n\t\t// Clear the set for the test\n\t\tvalopers = avl.NewTree()\n\n\t\tinfo := validValidatorInfo(t)\n\n\t\t// Set the origin caller\n\t\ttesting.SetOriginCaller(test1Address)\n\n\t\t// Send coins\n\t\ttesting.SetOriginSend(chain.Coins{minFee})\n\n\t\t// Add the valoper with on-prem server type\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(cross, info.Moniker, info.Description, info.ServerType, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Update the valoper to cloud\n\t\tuassert.NotPanics(t, func() {\n\t\t\tUpdateServerType(cross, info.Address, ServerTypeCloud)\n\t\t})\n\n\t\t// Make sure the valoper is updated\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper := GetByAddr(info.Address)\n\t\t\tuassert.Equal(t, ServerTypeCloud, valoper.ServerType)\n\t\t})\n\n\t\t// Update the valoper to data-center\n\t\tuassert.NotPanics(t, func() {\n\t\t\tUpdateServerType(cross, info.Address, ServerTypeDataCenter)\n\t\t})\n\n\t\t// Make sure the valoper is updated\n\t\tuassert.NotPanics(t, 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_test\n// SEND: 20000000ugnot\n\npackage valopers_test\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() {\n\ttesting.SetOriginCaller(g1user)\n\n\t// Register a validator and add the proposal\n\tvalopers.Register(cross, 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// - **Validator Address**\n//   - Your validator node's address\n//\n// - **Validator Public Key**\n//   - Your validator node's public 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 testnet validator](https://gnops.io/articles/guides/become-testnet-validator/) \u003c!-- XXX: replace with a r/gnops/blog:xxx link --\u003e\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_test\n// SEND: 20000000ugnot\n\npackage valopers_test\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() {\n\ttesting.SetOriginCaller(g1user)\n\n\t// Register a validator and add the proposal\n\tvalopers.Register(cross, 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// - Address: g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\n// - PubKey: gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\n// - Server Type: on-prem\n//\n// [Profile link](/r/demo/profile:u/g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h)\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "J3G5PZovip53lzyQi/iw8d9R5dNjie7ZHVcFEvM0ouMxdtxkvKbEWXfTVNeI4UE5jZEnOJC0SHwYQzQ1pdxnlA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da",
                  "package": {
                    "name": "memberstore",
                    "path": "gno.land/r/gov/dao/v3/memberstore",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/gov/dao/v3/memberstore\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"
                      },
                      {
                        "name": "memberstore.gno",
                        "body": "package memberstore\n\nimport (\n\t\"chain/runtime\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/svg\"\n\t\"gno.land/p/moul/md\"\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\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{avl.NewTree()}\n\ttiers.Set(T1, Tier{\n\t\tInvitationPoints: 3,\n\t\tMinSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn 70\n\t\t},\n\t\tMaxSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn 0\n\t\t},\n\t\tBasePower: 3,\n\t\tPowerHandler: func(membersByTier MembersByTier, tiersByName TiersByName) float64 {\n\t\t\treturn 3\n\t\t},\n\t})\n\n\ttiers.Set(T2, Tier{\n\t\tInvitationPoints: 2,\n\t\tMaxSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn membersByTier.GetTierSize(T1) * 2\n\t\t},\n\t\tMinSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn membersByTier.GetTierSize(T1) / 4\n\t\t},\n\t\tBasePower: 2,\n\t\tPowerHandler: func(membersByTier MembersByTier, tiersByName TiersByName) float64 {\n\t\t\tt1ms := float64(membersByTier.GetTierSize(T1))\n\t\t\tt1, _ := tiersByName.GetTier(T1)\n\t\t\tt2ms := float64(membersByTier.GetTierSize(T2))\n\t\t\tt2, _ := tiersByName.GetTier(T2)\n\n\t\t\tt1p := t1.BasePower * t1ms\n\t\t\tt2p := t2.BasePower * t2ms\n\n\t\t\t// capped to 2/3 of tier 1\n\t\t\tt1ptreshold := t1p * (2.0 / 3.0)\n\t\t\tif t2p \u003e t1ptreshold {\n\t\t\t\treturn t1ptreshold / t2ms\n\t\t\t}\n\n\t\t\treturn t2.BasePower\n\t\t},\n\t})\n\n\ttiers.Set(T3, Tier{\n\t\tInvitationPoints: 1,\n\t\tMaxSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn 0\n\t\t},\n\t\tMinSize: func(membersByTier MembersByTier, tiersByName TiersByName) int {\n\t\t\treturn 0\n\t\t},\n\t\tBasePower: 1,\n\t\tPowerHandler: func(membersByTier MembersByTier, tiersByName TiersByName) float64 {\n\t\t\tt1ms := float64(membersByTier.GetTierSize(T1))\n\t\t\tt1, _ := tiersByName.GetTier(T1)\n\t\t\tt3ms := float64(membersByTier.GetTierSize(T3))\n\t\t\tt3, _ := tiersByName.GetTier(T3)\n\n\t\t\tt1p := t1.BasePower * t1ms\n\t\t\tt3p := t3.BasePower * t3ms\n\n\t\t\t// capped to 1/3 of tier 1\n\t\t\tt1ptreshold := t1p * (1.0 / 3.0)\n\t\t\tif t3p \u003e t1ptreshold {\n\t\t\t\treturn t1ptreshold / t3ms\n\t\t\t}\n\n\t\t\treturn t3.BasePower\n\t\t},\n\t})\n\n\tinitRouter()\n}\n\n// initRouter initializes the router for the memberstore.\nfunc initRouter() {\n\trouter = mux.NewRouter()\n\trouter.HandleFunc(\"\", renderHome)\n\trouter.HandleFunc(\"members\", renderMembers)\n\trouter.NotFoundHandler = renderNotFound\n}\n\n// renderHome displays the tiers data (Number of members and powers) and tiers charts.\nfunc renderHome(res *mux.ResponseWriter, req *mux.Request) {\n\tvar sb strings.Builder\n\tsb.WriteString(md.Link(\"\u003e Go to Members list \u003c\", \"/r/gov/dao/v3/memberstore:members\") + \"\\n\")\n\n\tmembers.Iterate(\"\", \"\", func(tn string, ti interface{}) bool {\n\t\ttree, ok := ti.(*avl.Tree)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\ttier, ok := tiers.GetTier(tn)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\ttp := (tier.PowerHandler(members, tiers) * float64(members.GetTierSize(tn)))\n\n\t\tsb.WriteString(ufmt.Sprintf(\"- %v Tier %v contains %v members with power: %v\\n\", tierColoredChip(tn), tn, tree.Size(), tp))\n\n\t\treturn false\n\t})\n\n\tsb.WriteString(\"\\n\" + RenderCharts(members))\n\tres.Write(sb.String())\n}\n\n// renderMembers displays the members list.\nfunc renderMembers(res *mux.ResponseWriter, req *mux.Request) {\n\tpath := strings.Replace(req.RawPath, \"members\", \"\", 1) // We have to clean the path\n\tres.Write(RenderMembers(path, members))\n}\n\nfunc renderNotFound(res *mux.ResponseWriter, req *mux.Request) {\n\tres.Write(\"# 404\\n\\nThat page was not found. Would you like to [**go home**?](/r/gov/dao/v3/memberstore)\")\n}\n\nfunc tierColor(tn string) string {\n\tswitch tn {\n\tcase T1:\n\t\treturn \"#329175\"\n\tcase T2:\n\t\treturn \"#21577A\"\n\tcase T3:\n\t\treturn \"#F3D3BC\"\n\tdefault:\n\t\treturn \"#FFF\"\n\t}\n}\n\n// tierColoredChip returns a colored chip svg for the given tier name.\nfunc tierColoredChip(tn string) string {\n\tcanvas := svg.NewCanvas(16, 16)\n\tcanvas.Append(svg.NewRectangle(0, 0, 16, 16, tierColor(tn)))\n\treturn canvas.Render(tn + \" colored chip\")\n}\n\nfunc Render(path string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(md.H1(\"Memberstore Govdao v3\"))\n\tsb.WriteString(router.Render(path))\n\treturn sb.String()\n}\n\n// Get gets the Members store\nfunc Get() MembersByTier {\n\tcurrealm := runtime.CurrentRealm().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"
                      },
                      {
                        "name": "prop_requests.gno",
                        "body": "package memberstore\n\nimport (\n\t\"chain/runtime\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\n\t\"gno.land/r/gov/dao\"\n)\n\nfunc NewChangeTiersRequest(tiers map[string]Tier) dao.ProposalRequest {\n\tif len(tiers) == 0 {\n\t\tpanic(\"tiers list is empty\")\n\t}\n\n\tmember, _ := Get().GetMember(runtime.OriginCaller())\n\tif member == nil {\n\t\tpanic(\"proposer is not a member\")\n\t}\n\n\tnewTiers := TiersByName{avl.NewTree()}\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(callback, \"New set of tiers proposed.\")\n\n\treturn dao.NewProposalRequest(\"Change Tiers Proposal\", \"This proposal is looking to change the existing Tiers in memberstore\", e)\n}\n"
                      },
                      {
                        "name": "rendercharts.gno",
                        "body": "package memberstore\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/samcrew/piechart\"\n)\n\n// RenderCharts generates two pie charts for member tiers:\n// 1) distribution of member counts per tier\n// 2) distribution of power per tier\nfunc RenderCharts(members MembersByTier) string {\n\tvar sb strings.Builder\n\n\ttierNames := []string{T1, T2, T3}\n\tpieSlicesTs := make([]piechart.PieSlice, 0, len(tierNames))\n\tpieSlicesTp := make([]piechart.PieSlice, 0, len(tierNames))\n\n\tfor _, tn := range tierNames {\n\t\ttier, ok := tiers.GetTier(tn)\n\t\tif !ok {\n\t\t\treturn \"\"\n\t\t}\n\n\t\tts := float64(members.GetTierSize(tn))\n\t\ttp := tier.PowerHandler(members, tiers) * ts\n\n\t\tpieSlicesTs = append(pieSlicesTs, piechart.PieSlice{\n\t\t\tValue: ts,\n\t\t\tColor: tierColor(tn),\n\t\t\tLabel: tn,\n\t\t})\n\t\tpieSlicesTp = append(pieSlicesTp, piechart.PieSlice{\n\t\t\tValue: tp,\n\t\t\tColor: tierColor(tn),\n\t\t\tLabel: tn,\n\t\t})\n\t}\n\n\t// Render pie charts for members count and power distribution\n\tresultPieChartTs := piechart.Render(pieSlicesTs, \"Members distribution:\")\n\tresultPieChartTp := piechart.Render(pieSlicesTp, \"Power distribution:\")\n\n\tsb.WriteString(resultPieChartTs + \"\\n\")\n\tsb.WriteString(resultPieChartTp + \"\\n\")\n\n\treturn sb.String()\n}\n"
                      },
                      {
                        "name": "rendermembers.gno",
                        "body": "package memberstore\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/avl/v0/pager\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/samcrew/tablesort\"\n\t\"gno.land/p/samcrew/urlfilter\"\n)\n\n// RenderMembers returns the members list with tier filters and pagination.\nfunc RenderMembers(path string, members MembersByTier) string {\n\tu, _ := url.Parse(path)\n\tmdFilters, items := urlfilter.ApplyFilters(u, members.Tree, \"filter\")\n\tvar sb strings.Builder\n\n\tsb.WriteString(md.Link(\"\u003e Go to Tiers summary \u003c\", \"/r/gov/dao/v3/memberstore\") + \"\\n\\n\")\n\tsb.WriteString(md.Bold(\"Filter members by tiers:\"))\n\tsb.WriteString(mdFilters + \"\\n\")\n\n\tconst pageSize = 14\n\tpager := pager.NewPager(items, pageSize, false)\n\tpage := pager.MustGetPageByPath(path)\n\n\tsb.WriteString(renderMembersPages(u, page, items) + \"\\n\")\n\tsb.WriteString(renderPagination(u, page))\n\n\treturn sb.String()\n}\n\n// renderMembersPages returns the members of each page.\nfunc renderMembersPages(u *url.URL, page *pager.Page, members *avl.Tree) 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/avl/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 (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*avl.Tree // tier name -\u003e address -\u003e member\n}\n\nfunc NewMembersByTier() MembersByTier {\n\treturn MembersByTier{Tree: avl.NewTree()}\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, avl.NewTree())\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, ok := mbt.Get(tn)\n\tif !ok {\n\t\treturn 0\n\t}\n\n\ttree, ok := tv.(*avl.Tree)\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.(*avl.Tree)\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.(*avl.Tree)\n\t\tif !ok {\n\t\t\tpanic(\"MembersByTier values can only be avl.Tree\")\n\t\t}\n\n\t\tmv, ok := mst.Get(string(addr))\n\t\tif !ok {\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.(*avl.Tree)\n\t\tif !ok {\n\t\t\tpanic(\"MembersByTier values can only be avl.Tree\")\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*avl.Tree // *avl.Tree[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, ok := tbn.Get(tn)\n\tif !ok {\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/v3/memberstore\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "W3y8eSccTdAraYLPLk2vY6YizxL68bobtjqBiCoCwNQNO31PrUwKHtuc4XgwNoT0aZb1tFCOhvJwO5zOVe4vLA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "treasury",
                    "path": "gno.land/r/gov/dao/v3/treasury",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/gov/dao/v3/treasury\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "treasury.gno",
                        "body": "package treasury\n\nimport (\n\t\"chain/banker\"\n\t\"chain/runtime\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\tt \"gno.land/p/nt/treasury/v0\"\n\n\t\"gno.land/r/demo/defi/grc20reg\"\n\t\"gno.land/r/gov/dao\"\n)\n\nvar (\n\ttreasury  *t.Treasury\n\ttokenKeys = []string{\n\t\t// TODO: Add the default GRC20 tokens we want to support here.\n\t}\n)\n\nfunc init() {\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.NewCoinsBanker(banker.NewBanker(banker.BankerTypeRealmSend))\n\tif err != nil {\n\t\tpanic(\"failed to create CoinsBanker: \" + err.Error())\n\t}\n\tgrc20Banker, err := t.NewGRC20Banker(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.\n\ttreasury, err = t.New(bankers)\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(_ realm, keys []string) {\n\tcaller := runtime.PreviousRealm().PkgPath()\n\n\t// Check if the caller realm is allowed to set token keys.\n\tif !dao.InAllowedDAOs(caller) {\n\t\tpanic(\"this Realm is not allowed to send payment: \" + caller)\n\t}\n\n\ttokenKeys = keys\n}\n\n// Send sends a payment using the treasury instance.\nfunc Send(_ realm, payment t.Payment) {\n\tcaller := runtime.PreviousRealm().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.\n\tif err := treasury.Send(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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "dbdCcCrJ9hFv1B6AYGcqxL08Zp82KIKWiqxd36aEmAkiWe9ByOC/t0QUpBUrs7N7PCudPqxHtNkz2dXe8VUBmw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da",
                  "package": {
                    "name": "impl",
                    "path": "gno.land/r/gov/dao/v3/impl",
                    "files": [
                      {
                        "name": "filter.gno",
                        "body": "package impl\n\ntype FilterByTier struct {\n\tTier string\n}\n"
                      },
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/gov/dao/v3/impl\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"
                      },
                      {
                        "name": "govdao.gno",
                        "body": "package impl\n\nimport (\n\t\"chain\"\n\t\"chain/runtime\"\n\t\"errors\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\nvar ErrMemberNotFound = errors.New(\"member not found\")\n\ntype GovDAO struct {\n\tpss    ProposalsStatuses\n\trender *render\n}\n\nfunc NewGovDAO() *GovDAO {\n\tpss := NewProposalsStatuses()\n\td := \u0026GovDAO{\n\t\tpss: pss,\n\t}\n\n\td.render = NewRender(d)\n\n\t// There was no realm, from main(), so it succeeded, And\n\t// when returning, there was no finalization.  We don't\n\t// finalize anyways because there wasn't a realm boundary.\n\t// XXX make filetest main package a realm.\n\t//\n\t// filetest.init() -\u003e\n\t//   v3/init.Init() -\u003e\n\t//     NewGovDAO() -\u003e\n\t//       returns an unsaved DAO NOTE NO REALM!\n\t//     dao.UpdateImpl =\u003e\n\t//       saves dao under\n\t//\n\t// r/gov/dao.CrossPropposal() -\u003e\n\t//   proposals.SetProposal(),\n\t//     that proposal lives in r/gov/dao.\n\t// r/gov/dao.ExecuteProposal() -\u003e\n\t//   g.PreExecuteProposal() -\u003e\n\t//     XXX g.test = 1 fails, owned by gov/dao.\n\t//\n\t//\n\tfunc(cur realm) {\n\t\t// TODO: replace with future attach()\n\t\t_govdao = d\n\t}(cross)\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(r dao.ProposalRequest) (address, error) {\n\tif !g.isValidCall() {\n\t\treturn \"\", errors.New(ufmt.Sprintf(\"proposal creation must be done directly by a user or through the r/gov/dao proxy. current realm: %v; previous realm: %v\",\n\t\t\truntime.CurrentRealm(), runtime.PreviousRealm()))\n\t}\n\n\t// Verify that the one creating the proposal is a member.\n\tcaller := runtime.OriginCaller()\n\tmem, _ := getMembers(cross).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(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(r dao.VoteRequest) error {\n\tif !g.isValidCall() {\n\t\treturn errors.New(\"proposal voting must be done directly by a user\")\n\t}\n\n\tcaller := runtime.OriginCaller()\n\tmem, tie := getMembers(cross).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\tdefault:\n\t\treturn errors.New(\"voting can only be YES or NO\")\n\t}\n\n\treturn nil\n}\n\nfunc (g *GovDAO) PreGetProposal(pid dao.ProposalID) error {\n\treturn nil\n}\n\nfunc (g *GovDAO) PostGetProposal(pid dao.ProposalID, p *dao.Proposal) error {\n\treturn nil\n}\n\nfunc (g *GovDAO) PreExecuteProposal(pid dao.ProposalID) (bool, error) {\n\tif !g.isValidCall() {\n\t\treturn false, errors.New(\"proposal execution must be done directly by a user\")\n\t}\n\tstatus := g.pss.GetStatus(pid)\n\tif status.Denied || status.Accepted {\n\t\treturn false, errors.New(ufmt.Sprintf(\"proposal already executed. Accepted: %v\", status.Accepted))\n\t}\n\n\tif status.YesPercent() \u003e= law.Supermajority {\n\t\tstatus.Accepted = true\n\t\treturn true, nil\n\t}\n\n\tif status.NoPercent() \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) Render(pkgPath string, path string) string {\n\treturn g.render.Render(pkgPath, path)\n}\n\nfunc (g *GovDAO) isValidCall() bool {\n\t// We need to verify two cases:\n\t// 1: r/gov/dao (proxy) functions called directly by an user\n\t// 2: r/gov/dao/v3/impl methods called directly by an user\n\n\t// case 1\n\tif runtime.CurrentRealm().PkgPath() == \"gno.land/r/gov/dao\" {\n\t\t// called directly by an user through MsgCall\n\t\tif runtime.PreviousRealm().IsUser() {\n\t\t\treturn true\n\t\t}\n\t\tisMsgRun := chain.PackageAddress(runtime.PreviousRealm().PkgPath()) == runtime.OriginCaller()\n\t\t// called directly by an user through MsgRun\n\t\tif isMsgRun {\n\t\t\treturn true\n\t\t}\n\t}\n\n\t// case 2\n\tif runtime.CurrentRealm().IsUser() {\n\t\treturn true\n\t}\n\n\treturn false\n}\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/v3/memberstore\"\n)\n\nfunc init() {\n\tloadMembers()\n\n\tdao.UpdateImpl(cross, dao.UpdateRequest{\n\t\tDAO:         govDAO,\n\t\tAllowedDAOs: []string{\"gno.land/r/gov/dao/v3/impl\"},\n\t})\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() {\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()\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()\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/v3/impl\"))\n\n\tnm1 := testutils.TestAddress(\"nm1\")\n\n\turequire.AbortsWithMessage(t, \"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, NewAddMemberRequest(cur, nm1, memberstore.T3, portfolio))\n\t})\n\n\turequire.AbortsWithMessage(t, \"proposer is not a member\", func(cur realm) {\n\t\tdao.MustCreateProposal(cross, NewAddMemberRequest(cur, nm1, memberstore.T2, portfolio))\n\t})\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/v3/impl\"))\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, 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, dao.VoteRequest{\n\t\tOption:     dao.YesVote,\n\t\tProposalID: dao.ProposalID(0),\n\t})\n\n\ttesting.SetOriginCaller(m11)\n\n\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\tOption:     dao.NoVote,\n\t\tProposalID: dao.ProposalID(0),\n\t})\n\n\ttesting.SetOriginCaller(m2)\n\n\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\tOption:     dao.NoVote,\n\t\tProposalID: dao.ProposalID(0),\n\t})\n\n\ttesting.SetOriginCaller(m3)\n\n\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\tOption:     dao.NoVote,\n\t\tProposalID: dao.ProposalID(0),\n\t})\n\n\ttesting.SetOriginCaller(m4)\n\n\turequire.AbortsWithMessage(t, \"member on specified tier is not allowed to vote on this proposal\", func() {\n\t\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\t\tOption:     dao.NoVote,\n\t\t\tProposalID: dao.ProposalID(0),\n\t\t})\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, 0, \"NO\")\n\n\turequire.Equal(t, true, strings.Contains(dao.Render(\"\"), \"Prop #0 - New T2 Member Proposal\"))\n\t// urequire.Equal(t, true, strings.Contains(dao.Render(\"\"), \"Author: \"+m1.String()))\n\n\turequire.AbortsWithMessage(t, \"proposal didn't reach supermajority yet: 66.66\", func() {\n\t\tdao.ExecuteProposal(cross, dao.ProposalID(0))\n\t})\n\n\ttesting.SetOriginCaller(m1111)\n\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\tOption:     dao.NoVote,\n\t\tProposalID: dao.ProposalID(0),\n\t})\n\n\taccepted := dao.ExecuteProposal(cross, dao.ProposalID(0))\n\turequire.Equal(t, false, accepted)\n\n\turequire.Equal(t, true, contains(dao.Render(\"0\"), \"**PROPOSAL HAS BEEN DENIED**\"))\n\turequire.Equal(t, true, contains(dao.Render(\"0\"), \"NO PERCENT: 68.42105263157895%\"))\n}\n\nfunc TestExecutorCreationRealm(cur realm, t *testing.T) {\n\tloadMembers()\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(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, proposalRequest)\n\n\t// Get the proposal\n\tprop := dao.MustGetProposal(cross, 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(pid.String())\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()\n\tportfolio := \"### This is my portfolio:\\n\\n- THINGS\"\n\n\ttesting.SetOriginCaller(m1)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/v3/impl\"))\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, 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, proposalRequest)\n\turequire.Equal(t, 3, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross, proposalRequest)\n\turequire.Equal(t, 4, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross, proposalRequest)\n\turequire.Equal(t, 5, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross, proposalRequest)\n\turequire.Equal(t, 6, int(pid))\n\n\ttesting.SetRealm(testing.NewUserRealm(m1))\n\tpid = dao.MustCreateProposal(cross, proposalRequest)\n\turequire.Equal(t, 7, int(pid))\n\n\tfmt.Println(dao.Render(\"\"))\n\turequire.Equal(t, true, contains(dao.Render(\"\"), \"### [Prop #7 - New T2 Member Proposal](/r/gov/dao:7)\"))\n\turequire.Equal(t, true, contains(dao.Render(\"\"), \"### [Prop #6 - New T2 Member Proposal](/r/gov/dao:6)\"))\n\turequire.Equal(t, true, contains(dao.Render(\"\"), \"### [Prop #5 - New T2 Member Proposal](/r/gov/dao:5)\"))\n\turequire.Equal(t, true, contains(dao.Render(\"\"), \"### [Prop #4 - New T2 Member Proposal](/r/gov/dao:4)\"))\n\turequire.Equal(t, true, contains(dao.Render(\"\"), \"### [Prop #3 - New T2 Member Proposal](/r/gov/dao:3)\"))\n\n\turequire.Equal(t, true, contains(dao.Render(\"?page=2\"), \"### [Prop #2 - New T2 Member Proposal](/r/gov/dao:2)\"))\n\turequire.Equal(t, true, contains(dao.Render(\"?page=2\"), \"### [Prop #1 - Test Proposal](/r/gov/dao:1)\"))\n\turequire.Equal(t, true, contains(dao.Render(\"?page=2\"), \"### [Prop #0 - New T2 Member Proposal](/r/gov/dao:0)\"))\n}\n\nfunc TestUpgradeDaoImplementation(t *testing.T) {\n\tloadMembers()\n\n\ttesting.SetOriginCaller(noMember)\n\ttesting.SetRealm(testing.NewCodeRealm(\"gno.land/r/gov/dao/v3/impl\"))\n\n\turequire.PanicsWithMessage(t, \"proposer is not a member\", func() {\n\t\tNewUpgradeDaoImplRequest(govDAO, \"gno.land/r/gov/dao/v4/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/v3/impl\"))\n\n\tpreq := NewUpgradeDaoImplRequest(govDAO, \"gno.land/r/gov/dao/v4/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, 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, dao.VoteRequest{\n\t\tOption:     dao.YesVote,\n\t\tProposalID: dao.ProposalID(pid),\n\t})\n\n\ttesting.SetOriginCaller(m11)\n\n\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\tOption:     dao.YesVote,\n\t\tProposalID: dao.ProposalID(pid),\n\t})\n\n\ttesting.SetOriginCaller(m2)\n\n\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\tOption:     dao.YesVote,\n\t\tProposalID: dao.ProposalID(pid),\n\t})\n\n\ttesting.SetOriginCaller(m3)\n\n\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\tOption:     dao.YesVote,\n\t\tProposalID: dao.ProposalID(pid),\n\t})\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, int64(pid), \"YES\")\n\n\turequire.Equal(t, true, contains(dao.Render(\"8\"), \"**Proposal is open for votes**\"))\n\turequire.Equal(t, true, contains(dao.Render(\"8\"), \"68.42105263157895%\"))\n\turequire.Equal(t, true, contains(dao.Render(\"8\"), \"0%\"))\n\n\taccepted := dao.ExecuteProposal(cross, dao.ProposalID(pid))\n\turequire.Equal(t, true, accepted)\n\turequire.Equal(t, true, contains(dao.Render(\"8\"), \"**PROPOSAL HAS BEEN ACCEPTED**\"))\n\turequire.Equal(t, true, contains(dao.Render(\"8\"), \"YES PERCENT: 68.42105263157895%\"))\n}\n\nfunc contains(s, substr string) bool {\n\treturn strings.Index(s, substr) \u003e= 0\n}\n"
                      },
                      {
                        "name": "impl.gno",
                        "body": "package impl\n\nimport (\n\t\"chain/runtime\"\n\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\nvar (\n\tlaw    *Law\n\tgovDAO *GovDAO = NewGovDAO()\n\tgRealm         = runtime.CurrentRealm()\n)\n\nfunc init() {\n\tlaw = \u0026Law{\n\t\tSupermajority: 66.66, // Two thirds\n\t}\n}\n\nfunc Render(in string) string {\n\treturn govDAO.Render(gRealm.PkgPath(), in)\n}\n\n// AddMember allows T1 and T2 members to freely add T3 members using their invitation points.\nfunc AddMember(cur realm, addr address) {\n\tcaller := runtime.PreviousRealm()\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().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().SetMember(memberstore.T3, addr, memberByTier(memberstore.T3)); err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\nfunc GetInstance() *GovDAO {\n\tif runtime.CurrentRealm().PkgPath() != \"gno.land/r/gov/dao/v3/loader\" {\n\t\tpanic(\"not allowed\")\n\t}\n\n\treturn govDAO\n}\n"
                      },
                      {
                        "name": "prop_requests.gno",
                        "body": "package impl\n\nimport (\n\t\"chain/runtime\"\n\t\"strings\"\n\n\t\"gno.land/p/aeddi/panictoerr\"\n\t\"gno.land/p/moul/md\"\n\ttrs_pkg \"gno.land/p/nt/treasury/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n\t\"gno.land/r/gov/dao/v3/treasury\"\n)\n\nfunc NewChangeLawRequest(_ realm, newLaw Law) dao.ProposalRequest {\n\tmember, _ := memberstore.Get().GetMember(runtime.OriginCaller())\n\tif member == nil {\n\t\tpanic(\"proposer is not a member\")\n\t}\n\n\tcb := func(_ realm) error {\n\t\tlaw = \u0026newLaw\n\t\treturn nil\n\t}\n\n\te := dao.NewSimpleExecutor(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(newDao dao.DAO, realmPkg, reason string) dao.ProposalRequest {\n\tmember, _ := memberstore.Get().GetMember(runtime.OriginCaller())\n\tif member == nil {\n\t\tpanic(\"proposer is not a member\")\n\t}\n\n\tcb := func(_ realm) error {\n\t\t// dao.UpdateImpl() must be cross-called from v3/impl but\n\t\t// what calls this cb function is r/gov/dao.\n\t\t// therefore we must cross back into v3/impl and then\n\t\t// cross call dao.UpdateRequest().\n\t\tdao.UpdateImpl(cross, dao.UpdateRequest{\n\t\t\tDAO:         newDao,\n\t\t\tAllowedDAOs: []string{\"gno.land/r/gov/dao/v3/impl\", realmPkg}, // keeping previous realm just in case something went wrong\n\t\t})\n\t\treturn nil\n\t}\n\n\te := dao.NewSimpleExecutor(cb, \"\")\n\n\treturn dao.NewProposalRequest(\"Change DAO implementation\", \"This proposal is looking to change the actual govDAO implementation. Reason: \"+reason, e)\n}\n\nfunc NewAddMemberRequest(_ realm, addr address, tier string, portfolio string) dao.ProposalRequest {\n\t_, ok := memberstore.GetTier(tier)\n\tif !ok {\n\t\tpanic(\"provided tier does not exists\")\n\t}\n\n\tif tier != memberstore.T1 \u0026\u0026 tier != memberstore.T2 {\n\t\tpanic(\"Only T1 and T2 members can be added by proposal. To add a T3 member use AddMember function directly.\")\n\t}\n\n\tif portfolio == \"\" {\n\t\tpanic(\"A portfolio for the proposed member is required\")\n\t}\n\n\tmember, _ := memberstore.Get().GetMember(runtime.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(_ realm) error {\n\t\tmember.RemoveInvitationPoint()\n\t\terr := memberstore.Get().SetMember(tier, addr, memberByTier(tier))\n\n\t\treturn err\n\t}\n\n\te := dao.NewSimpleExecutor(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(_ realm, addr address, reason string) dao.ProposalRequest {\n\tmember, tier := memberstore.Get().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(_ realm) error {\n\t\tmemberstore.Get().RemoveMember(addr)\n\t\treturn nil\n\t}\n\n\te := dao.NewSimpleExecutor(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(addr address, fromTier string, toTier string) dao.ProposalRequest {\n\tcb := func(_ realm) error {\n\t\tprevTier := memberstore.Get().RemoveMember(addr)\n\t\tif prevTier == \"\" {\n\t\t\tpanic(\"member not found, so cannot be promoted\")\n\t\t}\n\n\t\tif prevTier != fromTier {\n\t\t\tpanic(\"previous tier changed from the one indicated in the proposal\")\n\t\t}\n\n\t\terr := memberstore.Get().SetMember(toTier, addr, memberByTier(toTier))\n\n\t\treturn err\n\t}\n\n\te := dao.NewSimpleExecutor(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(payment trs_pkg.Payment, reason string) dao.ProposalRequest {\n\tif !treasury.HasBanker(payment.BankerID()) {\n\t\tpanic(\"banker not registered in treasury with ID: \" + payment.BankerID())\n\t}\n\n\treason = strings.TrimSpace(reason)\n\tif reason == \"\" {\n\t\tpanic(\"treasury payment request requires a reason\")\n\t}\n\n\tcb := func(_ realm) error {\n\t\treturn panictoerr.PanicToError(func() {\n\t\t\ttreasury.Send(cross, payment)\n\t\t})\n\t}\n\n\te := dao.NewSimpleExecutor(\n\t\tcb,\n\t\tufmt.Sprintf(\n\t\t\t\"A payment will be sent by the GovDAO treasury.\\n\\nReason: %s\\n\\nPayment: %s.\",\n\t\t\treason,\n\t\t\tpayment.String(),\n\t\t),\n\t)\n\n\treturn dao.NewProposalRequest(\n\t\t\"Treasury Payment\",\n\t\tufmt.Sprintf(\n\t\t\t\"This proposal is looking to send a payment using the treasury.\\n\\nReason: %s\\n\\nPayment: %s\",\n\t\t\treason,\n\t\t\tpayment.String(),\n\t\t),\n\t\te,\n\t)\n}\n\n// NewTreasuryGRC20TokensUpdate creates a proposal request to update the list of GRC20 tokens registry\n// keys used by the treasury. The new list, if voted and accepted, will overwrite the current one.\nfunc NewTreasuryGRC20TokensUpdate(newTokenKeys []string) dao.ProposalRequest {\n\tif len(newTokenKeys) == 0 {\n\t\tpanic(\"the list of new tokens is empty\")\n\t}\n\n\tcb := func(_ realm) error {\n\t\treturn panictoerr.PanicToError(func() {\n\t\t\t// NOTE:: Consider checking if the newTokenKeys are already registered\n\t\t\t// in the grc20reg before updating the treasury tokens keys.\n\t\t\ttreasury.SetTokenKeys(cross, newTokenKeys)\n\t\t})\n\t}\n\n\tbulletList := md.BulletList(newTokenKeys)\n\n\te := dao.NewSimpleExecutor(\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 \u0026memberstore.Member{\n\t\t\tInvitationPoints: t.InvitationPoints,\n\t\t}\n\tcase memberstore.T2:\n\t\tt, _ := memberstore.GetTier(memberstore.T2)\n\t\treturn \u0026memberstore.Member{\n\t\t\tInvitationPoints: t.InvitationPoints,\n\t\t}\n\tcase memberstore.T3:\n\t\tt, _ := memberstore.GetTier(memberstore.T3)\n\t\treturn \u0026memberstore.Member{\n\t\t\tInvitationPoints: t.InvitationPoints,\n\t\t}\n\tdefault:\n\t\tpanic(\"member not found by the specified tier\")\n\t}\n}\n"
                      },
                      {
                        "name": "render.gno",
                        "body": "package impl\n\nimport (\n\t\"chain/runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/helplink\"\n\t\"gno.land/p/nt/avl/v0/pager\"\n\t\"gno.land/p/nt/mux/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/sys/users\"\n)\n\ntype render struct {\n\trelativeRealmPath string\n\trouter            *mux.Router\n\tpssPager          *pager.Pager\n}\n\nfunc NewRender(d *GovDAO) *render {\n\tren := \u0026render{\n\t\tpssPager: pager.NewPager(d.pss.Tree, 5, true),\n\t}\n\n\tr := mux.NewRouter()\n\n\tr.HandleFunc(\"\", func(rw *mux.ResponseWriter, req *mux.Request) {\n\t\trw.Write(ren.renderActiveProposals(req.RawPath, d))\n\t})\n\n\tr.HandleFunc(\"{pid}\", func(rw *mux.ResponseWriter, req *mux.Request) {\n\t\trw.Write(ren.renderProposalPage(req.GetVar(\"pid\"), d))\n\t})\n\n\tr.HandleFunc(\"{pid}/votes\", func(rw *mux.ResponseWriter, req *mux.Request) {\n\t\trw.Write(ren.renderVotesForProposal(req.GetVar(\"pid\"), d))\n\t})\n\n\tren.router = r\n\n\treturn ren\n}\n\nfunc (ren *render) Render(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.Render(path)\n}\n\nfunc (ren *render) renderActiveProposals(url string, d *GovDAO) string {\n\tout := \"# GovDAO\\n\"\n\tout += \"## Members\\n\"\n\tout += \"[\u003e Go to Memberstore \u003c](/r/gov/dao/v3/memberstore)\\n\"\n\tout += \"## Proposals\\n\"\n\tpage := ren.pssPager.MustGetPageByPath(url)\n\tif len(page.Items) == 0 {\n\t\tout += \"\\nNo proposals yet.\\n\\n\"\n\t\treturn out\n\t}\n\n\tfor _, item := range page.Items {\n\t\tseqpid, err := seqid.FromString(item.Key)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tout += ren.renderProposalListItem(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(sPid string, d *GovDAO) string {\n\tpid, err := strconv.ParseInt(sPid, 10, 64)\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", err.Error())\n\t}\n\n\tp, err := dao.GetProposal(cross, dao.ProposalID(pid))\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"# Proposal not found\\n\\n%s\", err.Error())\n\t}\n\n\tps := d.pss.GetStatus(dao.ProposalID(pid))\n\tout := ufmt.Sprintf(\"## Prop #%v - %v\\n\", pid, p.Title())\n\tout += \"Author: \" + tryResolveAddr(p.Author()) + \"\\n\\n\"\n\n\tout += p.Description()\n\tout += \"\\n\\n\"\n\n\t// Add executor metadata if available\n\tif p.ExecutorString() != \"\" {\n\t\tout += ufmt.Sprintf(`This proposal contains the following metadata:\n\n%s\n\nExecutor created in: %s\n`, p.ExecutorString(), p.ExecutorCreationRealm())\n\t\tout += \"\\n\\n\"\n\t}\n\n\tout += \"\\n\\n---\\n\\n\"\n\tout += ps.String()\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(sPid string, d *GovDAO) string {\n\tpid, err := strconv.ParseInt(sPid, 10, 64)\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", err.Error())\n\t}\n\n\tp, err := dao.GetProposal(cross, 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, p.Title(), ren.relativeRealmPath, pid)\n\tout += ufmt.Sprintf(\"Author: %s\\n\\n\", tryResolveAddr(p.Author()))\n\n\tout += \"Status: \" + getPropStatus(ps)\n\tout += \"\\n\\n\"\n\n\tout += \"Tiers eligible to vote: \"\n\tout += strings.Join(ps.TiersAllowedToVote, \", \")\n\n\tout += \"\\n\\n\"\n\treturn out\n}\n\nfunc (ren *render) renderVotesForProposal(sPid string, d *GovDAO) string {\n\tpid, err := strconv.ParseInt(sPid, 10, 64)\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"# Error: Invalid proposal ID format.\\n\\n\\n%s\\n\\n\", err.Error())\n\t}\n\n\tps := d.pss.GetStatus(dao.ProposalID(pid))\n\tif ps == nil {\n\t\treturn ufmt.Sprintf(\"# Proposal not found\\n\\nProposal %v does not exist.\", pid)\n\t}\n\n\tout := \"\"\n\tout += ufmt.Sprintf(\"# Proposal #%v - Vote List\\n\\n\", pid)\n\tout += StringifyVotes(ps)\n\n\treturn out\n}\n\nfunc isPropActive(ps *proposalStatus) bool {\n\treturn !ps.Accepted \u0026\u0026 !ps.Denied\n}\n\nfunc getPropStatus(ps *proposalStatus) string {\n\tif ps == nil {\n\t\treturn \"UNKNOWN\"\n\t}\n\tif ps.Accepted {\n\t\treturn \"ACCEPTED\"\n\t} else if ps.Denied {\n\t\treturn \"REJECTED\"\n\t}\n\treturn \"ACTIVE\"\n}\n\nfunc renderActionBar(sPid string) string {\n\tout := \"### Actions\\n\"\n\n\tproxy := helplink.Realm(\"gno.land/r/gov/dao\")\n\tout += proxy.Func(\"Vote YES\", \"MustVoteOnProposalSimple\", \"pid\", sPid, \"option\", \"YES\") + \" | \"\n\tout += proxy.Func(\"Vote NO\", \"MustVoteOnProposalSimple\", \"pid\", sPid, \"option\", \"NO\") + \" | \"\n\tout += proxy.Func(\"Vote ABSTAIN\", \"MustVoteOnProposalSimple\", \"pid\", sPid, \"option\", \"ABSTAIN\")\n\n\tout += \"\\n\\n\"\n\tout += \"WARNING: Please double check transaction data before voting.\"\n\treturn out\n}\n\nfunc tryResolveAddr(addr address) string {\n\tuserData := users.ResolveAddress(addr)\n\tif userData == nil {\n\t\treturn addr.String()\n\t}\n\treturn userData.RenderLink(\"\")\n}\n"
                      },
                      {
                        "name": "types.gno",
                        "body": "package impl\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/avl/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\ntype Law struct {\n\tSupermajority float64\n}\n\nfunc (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*avl.Tree // map[int]*proposalStatus\n}\n\nfunc NewProposalsStatuses() ProposalsStatuses {\n\treturn ProposalsStatuses{avl.NewTree()}\n}\n\nfunc (pss ProposalsStatuses) GetStatus(id dao.ProposalID) *proposalStatus {\n\tif pss.Tree == nil {\n\t\treturn nil\n\t}\n\n\tpids := id.String()\n\tpsv, ok := pss.Get(pids)\n\tif !ok {\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\tAllVotes memberstore.MembersByTier\n\n\tAccepted bool\n\tDenied   bool\n\n\tDeniedReason string\n\n\tTiersAllowedToVote []string\n\n\tTotalPower float64 // TotalPower is the power of all the members existing when this proposal was created.\n}\n\nfunc getMembers(cur realm) memberstore.MembersByTier {\n\treturn memberstore.Get()\n}\n\nfunc newProposalStatus(allowedToVote []string) *proposalStatus {\n\tyv := memberstore.NewMembersByTier()\n\tyv.SetTier(memberstore.T1)\n\tyv.SetTier(memberstore.T2)\n\tyv.SetTier(memberstore.T3)\n\tnv := memberstore.NewMembersByTier()\n\tnv.SetTier(memberstore.T1)\n\tnv.SetTier(memberstore.T2)\n\tnv.SetTier(memberstore.T3)\n\tav := memberstore.NewMembersByTier()\n\tav.SetTier(memberstore.T1)\n\tav.SetTier(memberstore.T2)\n\tav.SetTier(memberstore.T3)\n\n\treturn \u0026proposalStatus{\n\t\tYesVotes: yv,\n\t\tNoVotes:  nv,\n\t\tAllVotes: av,\n\n\t\tTiersAllowedToVote: allowedToVote,\n\n\t\tTotalPower: getMembers(cross).GetTotalPower(),\n\t}\n}\n\nfunc (ps *proposalStatus) YesPercent() float64 {\n\tvar yp float64\n\n\tmemberstore.IterateTiers(func(tn string, tier memberstore.Tier) bool {\n\t\tpower := memberstore.GetTierPower(tn, getMembers(cross))\n\t\tts := ps.YesVotes.GetTierSize(tn)\n\n\t\typ = yp + (power * float64(ts))\n\n\t\treturn false\n\t})\n\n\treturn (yp / ps.TotalPower) * 100\n}\n\nfunc (ps *proposalStatus) NoPercent() float64 {\n\tvar np float64\n\n\tmemberstore.IterateTiers(func(tn string, tier memberstore.Tier) bool {\n\t\tpower := memberstore.GetTierPower(tn, getMembers(cross))\n\t\tts := ps.NoVotes.GetTierSize(tn)\n\t\tnp = np + (power * float64(ts))\n\n\t\treturn false\n\t})\n\n\treturn (np / ps.TotalPower) * 100\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\nfunc (ps *proposalStatus) String() string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"### Stats\\n\")\n\n\tif ps.Accepted {\n\t\tsb.WriteString(\"- **PROPOSAL HAS BEEN ACCEPTED**\\n\")\n\t} else if ps.Denied {\n\t\tsb.WriteString(\"- **PROPOSAL HAS BEEN DENIED**\\n\")\n\t\tif ps.DeniedReason != \"\" {\n\t\t\tsb.WriteString(\"REASON: \")\n\t\t\tsb.WriteString(ps.DeniedReason)\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\t} else {\n\t\tsb.WriteString(\"- **Proposal is open for votes**\\n\")\n\t}\n\n\tsb.WriteString(\"- Tiers eligible to vote: \")\n\tsb.WriteString(strings.Join(ps.TiersAllowedToVote, \", \"))\n\tsb.WriteString(\"\\n\")\n\n\tsb.WriteString(ufmt.Sprintf(\"- YES PERCENT: %v%%\\n\", ps.YesPercent()))\n\tsb.WriteString(ufmt.Sprintf(\"- NO PERCENT: %v%%\\n\", ps.NoPercent()))\n\n\treturn sb.String()\n}\n\nfunc StringifyVotes(ps *proposalStatus) string {\n\tvar sb strings.Builder\n\n\twriteVotes(\u0026sb, ps.YesVotes, \"YES\")\n\twriteVotes(\u0026sb, ps.NoVotes, \"NO\")\n\n\tif sb.String() == \"\" {\n\t\treturn \"No one voted yet.\"\n\t}\n\n\treturn sb.String()\n}\n\nfunc writeVotes(sb *strings.Builder, t memberstore.MembersByTier, title string) {\n\tif t.Size() == 0 {\n\t\treturn\n\t}\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, getMembers(cross))\n\n\t\tsb.WriteString(ufmt.Sprintf(\"%v from %v (VPPM %v):\\n\\n\", title, tn, power))\n\t\tms, _ := value.(*avl.Tree)\n\t\tms.Iterate(\"\", \"\", func(addr string, _ interface{}) bool {\n\t\t\tsb.WriteString(\"- \" + tryResolveAddr(address(addr)) + \"\\n\")\n\t\t\treturn false\n\t\t})\n\n\t\tsb.WriteString(\"\\n\")\n\n\t\treturn false\n\t})\n}\n\nfunc StringifyProposal(p *dao.Proposal) string {\n\tout := ufmt.Sprintf(`\n### Title: %s\n\n### Proposed by: %s\n\n%s\n`, p.Title(), p.Author(), p.Description())\n\n\tif p.ExecutorString() != \"\" {\n\t\tout += ufmt.Sprintf(`\nThis proposal contains the following metadata:\n\n%s\n\nExecutor created in: %s\n`, p.ExecutorString(), p.ExecutorCreationRealm())\n\t}\n\n\treturn out\n}\n"
                      },
                      {
                        "name": "z_stringify_proposal_filetest.gno",
                        "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\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/v3/impl\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\nvar (\n\ttestUser = testutils.TestAddress(\"test\")\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 \u0026memberstore.Member{\n\t\t\tInvitationPoints: t.InvitationPoints,\n\t\t}\n\tdefault:\n\t\tpanic(\"unsupported tier: \" + tier)\n\t}\n}\n\nfunc init() {\n\t// Load members for testing\n\tmstore := memberstore.Get()\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, dao.UpdateRequest{\n\t\tDAO:         govDAO,\n\t\tAllowedDAOs: []string{\"gno.land/r/test\", \"gno.land/r/gov/dao/v3/impl\"},\n\t})\n}\n\nfunc main() {\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(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, proposalRequest)\n\n\t// Get the proposal and test the core functionality\n\tprop := dao.MustGetProposal(cross, 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\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"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "AI2xSRI79YjI1tV3S0XKb+4B1mZPHeOlMEGO9W6DzRZi5STRnNZZBmQBm1DMD8AVGhnB1Qb0Nbd3SVyali43LA=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da",
                  "package": {
                    "name": "init",
                    "path": "gno.land/r/gov/dao/v3/init",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/gov/dao/v3/init\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"
                      },
                      {
                        "name": "init.gno",
                        "body": "package init\n\nimport (\n\t\"chain/runtime\"\n\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/impl\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\nfunc Init() {\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().DeleteAll()\n\tdao.UpdateImpl(cross, dao.UpdateRequest{\n\t\tDAO:         impl.NewGovDAO(),\n\t\tAllowedDAOs: []string{\"gno.land/r/gov/dao/v3/impl\"},\n\t})\n}\n\nfunc InitWithUsers(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().DeleteAll()\n\tmemberstore.Get().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().SetMember(memberstore.T1, a, \u0026memberstore.Member{InvitationPoints: 3})\n\t}\n\n\tdao.UpdateImpl(cross, dao.UpdateRequest{\n\t\tDAO:         impl.NewGovDAO(),\n\t\tAllowedDAOs: []string{\"gno.land/r/gov/dao/v3/impl\"},\n\t})\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "Cfk7Vtdf6n2Nm+WGoQfoaczTVIH47KPyEohlHGf413FkN0H7QjlsuiudeYzOnRbz0yoU9kvHe4ZXmt6uqIuozg=="
                }
              ],
              "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\"gno.land/p/sys/validators\"\n)\n\n// GetChanges returns the validator changes stored on the realm, since the given block number.\n// This function is intended to be called by gno.land through the GnoSDK\nfunc GetChanges(from int64) []validators.Validator {\n\tvalsetChanges := make([]validators.Validator, 0)\n\n\t// Gather the changes from the specified block\n\tchanges.Iterate(getBlockID(from), \"\", 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/avl/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 = avl.NewTree()\n}\n"
                      },
                      {
                        "name": "poc.gno",
                        "body": "package validators\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sys/validators\"\n\t\"gno.land/r/gov/dao\"\n)\n\n// NewPropRequest creates a new proposal request that wraps a changes closure\n// proposal. This wrapper is required to ensure the GovDAO Realm actually\n// executed the callback.\nfunc NewPropRequest(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(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/avl/v0\"\n\t\"gno.land/p/nt/seqid/v0\"\n\t\"gno.land/p/nt/ufmt/v0\"\n\t\"gno.land/p/sys/validators\"\n)\n\nvar (\n\tvp      validators.ValsetProtocol // p is the underlying validator set protocol\n\tchanges *avl.Tree                 // 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, exists := changes.Get(id)\n\tif !exists {\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(size-maxDisplay, 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\"testing\"\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/p/nt/ufmt/v0\"\n\t\"gno.land/p/sys/validators\"\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: 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 = avl.NewTree()\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)\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 = avl.NewTree()\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)\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"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "LDdPPlAuJvyUns1OyL0MNmBdZ8/ZiZC2KKzcWnkYW+BZlnu7n5y0CUVBjnZcrGwRKOM2+nq3s/0JCE122W+7Mg=="
                }
              ],
              "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\tpVals \"gno.land/p/sys/validators\"\n\tvalopers \"gno.land/r/gnops/valopers\"\n\t\"gno.land/r/gov/dao\"\n\tvalidators \"gno.land/r/sys/validators/v2\"\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 the given valoper to the validator set.\nfunc NewValidatorProposalRequest(cur realm, address_XXX address) dao.ProposalRequest {\n\tvar (\n\t\tvaloper     = valopers.GetByAddr(address_XXX)\n\t\tvotingPower = uint64(1)\n\t)\n\n\texist := validators.IsValidator(address_XXX)\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(address_XXX)\n\t\tif validator.VotingPower == votingPower \u0026\u0026 validator.PubKey == valoper.PubKey {\n\t\t\tpanic(ErrSameValues)\n\t\t}\n\t}\n\n\tchangesFn := func() []pVals.Validator {\n\t\treturn []pVals.Validator{\n\t\t\t{\n\t\t\t\tAddress:     valoper.Address,\n\t\t\t\tPubKey:      valoper.PubKey,\n\t\t\t\tVotingPower: votingPower,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Craft the proposal title\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.Address,\n\t\tvaloper.Render(),\n\t)\n\n\t// Create the request\n\treturn validators.NewPropRequest(changesFn, title, description)\n}\n\n// ProposeNewInstructionsProposalRequest creates a proposal to the GovDAO\n// for updating the realm instructions.\nfunc ProposeNewInstructionsProposalRequest(cur realm, newInstructions string) dao.ProposalRequest {\n\tcb := valopers.NewInstructionsProposalCallback(newInstructions)\n\t// Create a proposal\n\ttitle := \"/p/gnops/valopers: Update instructions\"\n\tdescription := ufmt.Sprintf(\"Update the instructions to: \\n\\n%s\", newInstructions)\n\n\te := dao.NewSimpleExecutor(cb, \"\")\n\n\treturn dao.NewProposalRequest(title, description, e)\n}\n\n// ProposeNewMinFeeProposalRequest creates a proposal to the GovDAO\n// for updating the minimum fee to register a new valoper.\nfunc ProposeNewMinFeeProposalRequest(cur realm, newMinFee int64) dao.ProposalRequest {\n\tcb := valopers.NewMinFeeProposalCallback(newMinFee)\n\t// Create a proposal\n\ttitle := \"/p/gnops/valopers: Update minFee\"\n\tdescription := ufmt.Sprintf(\"Update the minimum register fee to: %d ugnot\", newMinFee)\n\n\te := dao.NewSimpleExecutor(cb, \"\")\n\n\treturn dao.NewProposalRequest(title, description, e)\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/v3/init\" // so that the govdao initializer is executed\n)\n\nvar g1user = testutils.TestAddress(\"g1user\")\n\nfunc init() {\n\tdaoinit.InitWithUsers(g1user)\n}\n\nfunc TestValopers_ProposeNewValidator(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// Set origin caller\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\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, func() {\n\t\t\tvalopers.Register(cross, moniker, description, serverType, g1user, pubKey)\n\t\t\tvalopers.UpdateKeepRunning(cross, g1user, false)\n\t\t})\n\n\t\turequire.NotPanics(t, func() {\n\t\t\tvalopers.GetByAddr(g1user)\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, ErrValidatorMissing.Error(), func(cur realm) {\n\t\t\tpr := NewValidatorProposalRequest(cur, g1user)\n\n\t\t\tdao.MustCreateProposal(cross, 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, func() {\n\t\t\tvalopers.UpdateKeepRunning(cross, g1user, true)\n\t\t})\n\n\t\tvar valoper valopers.Valoper\n\n\t\turequire.NotPanics(t, func() {\n\t\t\tvaloper = valopers.GetByAddr(g1user)\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, func(cur realm) {\n\t\t\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\t\t\tpr := NewValidatorProposalRequest(cur, g1user)\n\n\t\t\tpid = dao.MustCreateProposal(cross, pr)\n\t\t})\n\n\t\tproposal, err := dao.GetProposal(cross, pid) // index starts from 0\n\t\turequire.NoError(t, err, \"proposal not found\")\n\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\\n\",\n\t\t\tvaloper.Moniker,\n\t\t\tvaloper.Address,\n\t\t\tvaloper.Render(),\n\t\t\tvaloper.Address,\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, func() {\n\t\t\tvalopers.GetByAddr(g1user)\n\t\t})\n\n\t\turequire.NotPanics(t, func() {\n\t\t\t// Vote the proposal created in the previous test\n\t\t\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\t\t\tOption:     dao.YesVote,\n\t\t\t\tProposalID: dao.ProposalID(0),\n\t\t\t})\n\n\t\t\t// Execute the proposal\n\t\t\tdao.ExecuteProposal(cross, dao.ProposalID(0))\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, ErrSameValues.Error(), func() {\n\t\t\tpr := NewValidatorProposalRequest(cross, g1user)\n\t\t\tdao.MustCreateProposal(cross, pr)\n\t\t})\n\t})\n}\n\nfunc TestValopers_ProposeNewInstructions(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, func() {\n\t\tpr := ProposeNewInstructionsProposalRequest(cross, newInstructions)\n\n\t\tpid = dao.MustCreateProposal(cross, pr)\n\t})\n\n\tproposal, err := dao.GetProposal(cross, 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(t *testing.T) {\n\tconst proposalMinFee int64 = 100 * 1_000_000\n\tnewMinFee := int64(10)\n\tdescription := ufmt.Sprintf(\"Update the minimum register fee to: %d ugnot\", newMinFee)\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, func() {\n\t\tpr := ProposeNewMinFeeProposalRequest(cross, newMinFee)\n\n\t\tpid = dao.MustCreateProposal(cross, pr)\n\t})\n\n\tproposal, err := dao.GetProposal(cross, 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(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, func() {\n\t\t\t_ = valopers.MustGetValoper(g1user)\n\t\t})\n\n\t\t// Create the proposal\n\t\turequire.NotPanics(t, 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, 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, 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, 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, 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, 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, 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/gnops/valopers/proposal_test\n// SEND: 20000000ugnot\n\npackage proposal_test\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/v3/init\"\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() {\n\ttesting.SetOriginCaller(g1user)\n\tdaoinit.InitWithUsers(g1user)\n}\n\nfunc main() {\n\ttesting.SetOriginCaller(g1user)\n\t// Register a validator\n\tvalopers.Register(cross, validMoniker, validDescription, validServerType, validAddress, validPubKey)\n\t// Try to make a proposal for a non-existing validator\n\n\tif err := revive(func() {\n\t\tpr := proposal.NewValidatorProposalRequest(cross, otherAddress)\n\t\tdao.MustCreateProposal(cross, 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/gnops/valopers/proposal_test\n// SEND: 100000000ugnot\n\npackage proposal_test\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/v3/init\" // 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() {\n\ttesting.SetOriginCaller(g1user)\n\tdaoinit.InitWithUsers(g1user)\n\n\t// Register a validator and add the proposal\n\tvalopers.Register(cross, validMoniker, validDescription, validServerType, validAddress, validPubKey)\n\n\tif err := revive(func() {\n\t\tpr := proposal.NewValidatorProposalRequest(cross, validAddress)\n\t\tdao.MustCreateProposal(cross, pr)\n\t}); err != nil {\n\t\tprintln(\"r: \", err)\n\t} else {\n\t\tprintln(\"OK\")\n\t}\n}\n\nfunc main() {\n\tprintln(dao.Render(\"\"))\n}\n\n// Output:\n// OK\n// # GovDAO\n// ## Members\n// [\u003e Go to Memberstore \u003c](/r/gov/dao/v3/memberstore)\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"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "UXcTQJBdoeqZrUAeYxRcJ8N+QovPrrpbkF3Nbufa571gVagS+b/0/v+ywgQODV1tACEdrAuXz+FURW16RODkxw=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da",
                  "package": {
                    "name": "loader",
                    "path": "gno.land/r/gov/dao/v3/loader",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/gov/dao/v3/loader\"\ngno = \"0.9\"\n\n[addpkg]\n  creator = \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"\n"
                      },
                      {
                        "name": "loader.gno",
                        "body": "// loader.gno initialises the govDAO v3 implementation and tier structure.\n//\n// It intentionally does NOT add any members or set AllowedDAOs.  When the\n// allowedDAOs list in the DAO proxy is empty, InAllowedDAOs() returns true\n// for any caller (see r/gov/dao/proxy.gno), which lets a subsequent MsgRun\n// bootstrap the member set and then lock things down.\n//\n// Bootstrap flow (official network genesis or local dev):\n//\n//  1. All packages — including this loader — are deployed via MsgAddPackage.\n//     The loader sets up tier entries and the DAO implementation.\n//  2. A MsgRun executes a setup script (e.g. govdao_prop1.gno) which:\n//     a. Adds a temporary deployer as T1 member (for supermajority).\n//     b. Creates a governance proposal to register validators, votes YES,\n//     and executes it.\n//     c. Adds the real govDAO members directly via memberstore.Get().\n//     d. Removes the temporary deployer.\n//     e. Calls dao.UpdateImpl to set AllowedDAOs, locking down access.\n//\n// See misc/deployments/ for concrete genesis generation examples.\npackage loader\n\nimport (\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/impl\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n)\n\nfunc init() {\n\t// Create tier entries in the members tree (required before any SetMember).\n\tmemberstore.Get().SetTier(memberstore.T1)\n\tmemberstore.Get().SetTier(memberstore.T2)\n\tmemberstore.Get().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, dao.UpdateRequest{\n\t\tDAO: impl.GetInstance(),\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "GcpiNIOTRhMbkehNRknix3im2+nFNoP0Uj7W2fVL53V6W2uLZbfL/ztgdwrxgAJu3GYlZnRBhuodYKElrvBMTQ=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "test",
                    "path": "gno.land/r/gov/dao/v3/treasury/test",
                    "files": [
                      {
                        "name": "gnomod.toml",
                        "body": "module = \"gno.land/r/gov/dao/v3/treasury/test\"\ngno = \"0.9\"\n"
                      },
                      {
                        "name": "treasury_test.gno",
                        "body": "package test\n\nimport (\n\t\"chain\"\n\t\"chain/banker\"\n\t\"chain/runtime\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/tokens/grc20\"\n\t\"gno.land/p/nt/fqname/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/demo/defi/grc20reg\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/impl\"\n\t\"gno.land/r/gov/dao/v3/treasury\"\n)\n\nvar (\n\tuser1Addr       = testutils.TestAddress(\"g1user1\")\n\tuser2Addr       = testutils.TestAddress(\"g1user2\")\n\ttreasuryAddr    = chain.PackageAddress(\"gno.land/r/gov/dao/v3/treasury\")\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() {\n\t// Register allowed Realm path.\n\tdao.UpdateImpl(cross, dao.UpdateRequest{\n\t\tDAO:         impl.NewGovDAO(),\n\t\tAllowedDAOs: []string{allowedRealm.PkgPath()},\n\t})\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.NewBanker(banker.BankerTypeReadonly)\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(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)\n\n\t\t// Register the token.\n\t\tgrc20reg.Register(cross, 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(runtime.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, keys)\n\n\treturn keyedTokens\n}\n\nfunc TestAllowedDAOs(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,\n\t\t\"this Realm is not allowed to send payment: \"+notAllowedRealm.PkgPath(),\n\t\tfunc() { treasury.Send(cross, 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,\n\t\t\"banker not found: \"+dummyP.BankerID(),\n\t\tfunc() { treasury.Send(cross, 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\tregisteredBankerIDs := treasury.ListBankerIDs()\n\tuassert.Equal(t, len(registeredBankerIDs), len(expectedBankerIDs))\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(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,\n\t\t\"failed to send payment: GRC20 token not found: UNKNOW\",\n\t\tfunc() {\n\t\t\ttreasury.Send(cross, 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\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, func() {\n\t\t\t\ttreasury.Send(\n\t\t\t\t\tcross,\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,\n\t\t\"failed to send payment: invalid payment type\",\n\t\tfunc() {\n\t\t\ttreasury.Send(cross, \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,\n\t\t\"failed to send payment: insufficient balance\",\n\t\tfunc() {\n\t\t\ttreasury.Send(\n\t\t\t\tcross,\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(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, func() {\n\t\t\ttreasury.Send(\n\t\t\t\tcross,\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,\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"
                      },
                      {
                        "name": "workaround.gno",
                        "body": "package test\n\n// This package exists solely to circumvent a limitation associated with the\n// suffixed test package (a test package sharing the same folder as the main\n// package to be tested but having the suffix _test in its name).\n// Currently, the GnoVM no longer differentiates between the dependencies of a\n// package and its test package, which causes circular dependencies issues.\n"
                      }
                    ],
                    "type": {
                      "@type": "/gno.MemPackageType",
                      "value": "MPUserAll"
                    }
                  },
                  "send": "",
                  "max_deposit": ""
                }
              ],
              "fee": {
                "gas_wanted": "50000",
                "gas_fee": "1ugnot"
              },
              "signatures": [
                {
                  "pub_key": {
                    "@type": "/tm.PubKeySecp256k1",
                    "value": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "GeBOAgrWWIRVuu3pikp8frk8VmFtOC0DWetLMRcDuF1iuICVwYoylX52j0dG9a0cDu4EQ+tyitVcKRU0Qj54Pg=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "package": {
                    "name": "cla",
                    "path": "gno.land/r/sys/cla",
                    "files": [
                      {
                        "name": "admin.gno",
                        "body": "package cla\n\nimport (\n\t\"chain\"\n\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/p/moul/helplink\"\n\t\"gno.land/r/gov/dao\"\n)\n\nconst RequiredHashChangedEvent = \"CLARequiredHashChanged\"\n\n// ProposeNewCLA creates a govdao proposal to update the CLA document hash and URL.\n// When executed, it resets all existing signatures.\n// Propose an empty hash to disable CLA enforcement.\nfunc ProposeNewCLA(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(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\t\"chain/runtime\"\n\n\t\"gno.land/p/moul/addrset\"\n)\n\nconst SignedEvent = \"CLASigned\"\n\nvar (\n\trequiredHash string // SHA256 hash of the CLA document; empty = enforcement disabled\n\tclaURL       string // URL where the CLA document can be found\n\tsignatures   addrset.Set\n)\n\n// Sign records a CLA signature for the caller.\n// The hash must match the current required hash.\nfunc Sign(cur realm, hash string) {\n\tif hash != requiredHash {\n\t\tpanic(\"hash does not match required CLA hash\")\n\t}\n\n\tcaller := runtime.PreviousRealm().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\"\n\t\"gno.land/p/nt/uassert/v0\"\n)\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(t *testing.T) {\n\tresetState()\n\n\tsetRequiredHash(testHash1)\n\n\ttesting.SetRealm(testing.NewUserRealm(testUser1))\n\tSign(cross, testHash1)\n\n\tuassert.True(t, HasValidSignature(address(testUser1)))\n}\n\nfunc TestSign_WrongHash(t *testing.T) {\n\tresetState()\n\n\tsetRequiredHash(testHash1)\n\n\ttesting.SetRealm(testing.NewUserRealm(testUser1))\n\tuassert.AbortsWithMessage(t, \"hash does not match required CLA hash\", func() {\n\t\tSign(cross, 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(t *testing.T) {\n\tresetState()\n\n\tsetRequiredHash(testHash1)\n\n\ttesting.SetRealm(testing.NewUserRealm(testUser1))\n\tSign(cross, 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(t *testing.T) {\n\tresetState()\n\n\tsetRequiredHash(testHash1)\n\n\ttesting.SetRealm(testing.NewUserRealm(testUser1))\n\tSign(cross, 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 \"gno.land/p/moul/helplink\"\n\nfunc Render(path string) string {\n\tif requiredHash == \"\" {\n\t\treturn \"# Gno CLA Registry\\n\\n**Status:** CLA enforcement is DISABLED\\n\"\n\t}\n\n\toutput := \"# Gno CLA Registry\\n\\n**Status:** CLA enforcement is ENABLED\\n\\n\"\n\toutput += \"**Required Hash:** \" + requiredHash + \"\\n\"\n\tif claURL != \"\" {\n\t\toutput += \"**CLA Document:** \" + claURL + \"\\n\"\n\t}\n\toutput += \"\\n### Actions\\n\\n\"\n\toutput += helplink.Func(\"Sign CLA\", \"Sign\", \"hash\", requiredHash) + \"\\n\"\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "HGIq5TB1WQxTvSnoyJvNIACtknxCDSVYAZMcfO9Qk9c7yEA0pA0hmEkCDhmKLJTDQ4zYS3n3LDRvNIo5ktlIeA=="
                }
              ],
              "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// Only address-prefix (PA) namespaces are allowed.\npackage names\n\nimport \"gno.land/p/nt/ownable/v0\"\n\nvar (\n\tOwnable = ownable.NewWithAddressByPrevious(\"g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p\") // genesis deployer — dropped in genesis via Enable.\n\tenabled = false\n)\n\n// IsAuthorizedAddressForNamespace checks if the given address can deploy to the given namespace.\n// Only the address's own PA namespace is permitted.\nfunc IsAuthorizedAddressForNamespace(address_XXX address, namespace string) bool {\n\treturn verifier(enabled, address_XXX, namespace)\n}\n\n// Enable enables the namespace check and drops centralized ownership of 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 err := Ownable.DropOwnership(); err != nil {\n\t\tpanic(err)\n\t}\n\tenabled = true\n}\n\nfunc IsEnabled() bool {\n\treturn enabled\n}\n\n// verifier checks namespace deployment permissions.\n// An address matching the namespace is the only allowed case.\nfunc verifier(isEnabled bool, address_XXX address, namespace string) bool {\n\tif !isEnabled {\n\t\treturn true // only in pre-genesis cases\n\t}\n\n\tif namespace == \"\" || !address_XXX.IsValid() {\n\t\treturn false\n\t}\n\n\t// Allow user with their own address as namespace\n\t// ie gno.land/{p,r}/{ADDRESS}/**\n\treturn address_XXX.String() == namespace\n}\n"
                      },
                      {
                        "name": "verifier_test.gno",
                        "body": "package names\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 alice = testutils.TestAddress(\"alice\")\n\nfunc TestDefaultVerifier(t *testing.T) {\n\t// Disabled: any case is true\n\tuassert.True(t, verifier(false, alice, alice.String()))\n\tuassert.True(t, verifier(false, \"\", alice.String()))\n\tuassert.True(t, verifier(false, alice, \"somerandomusername\"))\n\n\t// Enabled: PA namespace check\n\tuassert.True(t, verifier(true, alice, alice.String()))\n\n\t// Enabled: non-PA namespaces denied\n\tuassert.False(t, verifier(true, alice, \"notregistered\"))\n\tuassert.False(t, verifier(true, alice, \"alice\"))\n\n\t// Enabled: empty name/address\n\tuassert.False(t, verifier(true, address(\"\"), \"\"))\n\tuassert.False(t, verifier(true, alice, \"\"))\n\tuassert.False(t, verifier(true, address(\"\"), \"something\"))\n}\n\nfunc TestEnable(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(\"g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p\"))\n\n\tuassert.NotPanics(t, func() {\n\t\tEnable(cross)\n\t})\n\n\t// Confirm enable drops ownership\n\tuassert.Equal(t, Ownable.Owner().String(), \"\")\n\tuassert.AbortsWithMessage(t, ownable.ErrUnauthorized.Error(), func() {\n\t\tEnable(cross)\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "Ygj2aw4nUPnvUVixXHLaYiL+SY0oHkGByjXFpXs/+dZl5i0AqjOvkE/uej2upRzSxJCN4LV3zLASM87jvqmyng=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_addpkg",
                  "creator": "g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l",
                  "package": {
                    "name": "params",
                    "path": "gno.land/r/sys/params",
                    "files": [
                      {
                        "name": "fee_collector.gno",
                        "body": "package params\n\nimport (\n\t\"gno.land/r/gov/dao\"\n)\n\nfunc NewSetFeeCollectorRequest(addr address) dao.ProposalRequest {\n\treturn NewSysParamStringPropRequest(\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\nfunc TestSetFeeCollector(t *testing.T) {\n\tuserRealm := testing.NewUserRealm(g1user)\n\ttesting.SetRealm(userRealm)\n\n\tpr := NewSetFeeCollectorRequest(userRealm.Address())\n\tid := dao.MustCreateProposal(cross, pr)\n\t_, err := dao.GetProposal(cross, id)\n\turequire.NoError(t, err)\n\n\turequire.NotPanics(\n\t\tt,\n\t\tfunc() {\n\t\t\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\t\t\tOption:     dao.YesVote,\n\t\t\t\tProposalID: dao.ProposalID(id),\n\t\t\t})\n\t\t},\n\t)\n\n\turequire.NotPanics(\n\t\tt,\n\t\tfunc() {\n\t\t\tdao.ExecuteProposal(cross, 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": "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\nfunc NewSysParamStringPropRequest(module, submodule, name, value string) dao.ProposalRequest {\n\treturn newPropRequest(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamString(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamInt64PropRequest(module, submodule, name string, value int64) dao.ProposalRequest {\n\treturn newPropRequest(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamInt64(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamUint64PropRequest(module, submodule, name string, value uint64) dao.ProposalRequest {\n\treturn newPropRequest(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamUint64(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamBoolPropRequest(module, submodule, name string, value bool) dao.ProposalRequest {\n\treturn newPropRequest(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamBool(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamBytesPropRequest(module, submodule, name string, value []byte) dao.ProposalRequest {\n\treturn newPropRequest(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamBytes(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamStringsPropRequest(module, submodule, name string, value []string) dao.ProposalRequest {\n\treturn newPropRequest(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamStrings(module, submodule, name, value) },\n\t\t\"\",\n\t)\n}\n\nfunc NewSysParamStringsPropRequestWithTitle(module, submodule, name, title string, value []string) dao.ProposalRequest {\n\treturn newPropRequest(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamStrings(module, submodule, name, value) },\n\t\ttitle,\n\t)\n}\nfunc NewSysParamStringsPropRequestAddWithTitle(module, submodule, name, title string, value []string) dao.ProposalRequest {\n\treturn newPropRequest(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.UpdateSysParamStrings(module, submodule, name, value, true) },\n\t\ttitle,\n\t)\n}\nfunc NewSysParamStringsPropRequestRemoveWithTitle(module, submodule, name, title string, value []string) dao.ProposalRequest {\n\treturn newPropRequest(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.UpdateSysParamStrings(module, submodule, name, value, false) },\n\t\ttitle,\n\t)\n}\nfunc newPropRequest(key string, fn func(), title string) dao.ProposalRequest {\n\tcallback := func(cur realm) error {\n\t\tfn()\n\t\tchain.Emit(\"set\", \"key\", key) // TODO document, make const, make consistent. 'k'??\n\t\treturn nil\n\t}\n\n\tif title == \"\" {\n\t\ttitle = \"Set new sys/params key\"\n\t}\n\n\te := dao.NewSimpleExecutor(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// Testing this package is limited because it only contains an `std.Set` method\n// without a corresponding `std.Get` method. For comprehensive testing, refer to\n// the tests located in the r/gov/dao/ directory, specifically in one of the\n// propX_filetest.gno files.\n\nfunc TestNewStringPropRequest(t *testing.T) {\n\tpr := NewSysParamStringPropRequest(\"foo\", \"bar\", \"baz\", \"qux\")\n\tif pr.Title() == \"\" {\n\t\tt.Errorf(\"executor shouldn't be nil\")\n\t}\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() dao.ProposalRequest {\n\treturn NewSysParamStringsPropRequestWithTitle(bankModulePrefix, \"p\", restrictedDenomsKey, unlockTransferTitle, []string{})\n}\n\nfunc ProposeLockTransferRequest() dao.ProposalRequest {\n\treturn NewSysParamStringsPropRequestWithTitle(bankModulePrefix, \"p\", restrictedDenomsKey, lockTransferTitle, []string{\"ugnot\"})\n}\n\nfunc ProposeAddUnrestrictedAcctsRequest(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(authModulePrefix, \"p\", unrestrictedAddrsKey, \"Add unrestricted transfer accounts\", addrStrings)\n}\n\nfunc ProposeRemoveUnrestrictedAcctsRequest(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(authModulePrefix, \"p\", unrestrictedAddrsKey, \"Add 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/v3/init\"\n)\n\nvar g1user = testutils.TestAddress(\"g1user\")\n\nfunc init() {\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\tini.InitWithUsers(g1user)\n}\n\nfunc TestProUnlockTransfer(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\n\tpr := ProposeUnlockTransferRequest()\n\tid := dao.MustCreateProposal(cross, pr)\n\tp, err := dao.GetProposal(cross, id)\n\turequire.NoError(t, err)\n\turequire.Equal(t, unlockTransferTitle, p.Title())\n}\n\nfunc TestFailUnlockTransfer(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\n\tpr := ProposeUnlockTransferRequest()\n\tid := dao.MustCreateProposal(cross, pr)\n\turequire.AbortsWithMessage(\n\t\tt,\n\t\t\"proposal didn't reach supermajority yet: 66.66\",\n\t\tfunc() {\n\t\t\tdao.ExecuteProposal(cross, id)\n\t\t},\n\t)\n}\n\nfunc TestExeUnlockTransfer(t *testing.T) {\n\ttesting.SetRealm(testing.NewUserRealm(g1user))\n\n\tpr := ProposeUnlockTransferRequest()\n\tid := dao.MustCreateProposal(cross, pr)\n\t_, err := dao.GetProposal(cross, id)\n\turequire.NoError(t, err)\n\t// urequire.True(t, dao.Active == p.Status()) // TODO\n\n\turequire.NotPanics(\n\t\tt,\n\t\tfunc() {\n\t\t\tdao.MustVoteOnProposal(cross, dao.VoteRequest{\n\t\t\t\tOption:     dao.YesVote,\n\t\t\t\tProposalID: dao.ProposalID(id),\n\t\t\t})\n\t\t},\n\t)\n\n\turequire.NotPanics(\n\t\tt,\n\t\tfunc() {\n\t\t\tdao.ExecuteProposal(cross, id)\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "NTNdn94Sp7kKdJrgtLkWNy2/T02ARUOYyqOFC+jle2tT5nl2YJ+ohpNbrx4xx83Zj9Qx6bnrUeqOoWkVngHiJA=="
                }
              ],
              "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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "B9W/LAp3HlpUmYjkuRRshaAQlEEkCEYSXg52hXTPnDoFCwQngVjlTidJKrRCMsEbGBq0SobTtkoJ2TvLXRQLYQ=="
                }
              ],
              "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\"chain/runtime\"\n\t\"strings\"\n)\n\nfunc Render(path string) string {\n\tbanker_ := banker.NewBanker(banker.BankerTypeReadonly)\n\trealmAddr := runtime.CurrentRealm().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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "ZiwLB7FMMKOZEX9e6eXe2IKy0iKYIVu2t+ccd7AbWdtNTr3KH3zASrt7j0i835dL7h+5Rtapc/i5Zgfm9AJ0PQ=="
                }
              ],
              "memo": ""
            }
          },
          {
            "tx": {
              "msg": [
                {
                  "@type": "/vm.m_run",
                  "caller": "g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p",
                  "send": "",
                  "max_deposit": "",
                  "package": {
                    "name": "main",
                    "path": "",
                    "files": [
                      {
                        "name": "govdao_prop1.gno",
                        "body": "// govdao_prop1.gno is executed via MsgRun during genesis to bootstrap the\n// test12 governance state.\n//\n// At this point the loader has initialised the DAO and tier structure but\n// has NOT added any members and has NOT set AllowedDAOs (the empty-list\n// corner case in proxy.gno:InAllowedDAOs lets any caller through).\n//\n// NOTE: Temporarily, aeddi is the sole govDAO T1 member. He will\n// craft proposals to add validators and other members, executing them alone\n// until the DAO is large enough to enable real multi-party voting.\n//\n// Steps:\n//  1. Add a temporary deployer as T1 member (100% supermajority).\n//  2. Register the initial validator set via governance proposal.\n//  3. Set chain params (unrestricted addrs, restricted denoms) via proposals.\n//  4. Enable namespace enforcement (r/sys/names).\n//  5. Add the real govDAO T1 member (aeddi) directly to the memberstore.\n//  6. Remove the temporary deployer from the memberstore.\n//  7. Lock down memberstore access by setting AllowedDAOs.\npackage main\n\nimport (\n\t\"gno.land/p/sys/validators\"\n\t\"gno.land/r/gov/dao\"\n\t\"gno.land/r/gov/dao/v3/memberstore\"\n\t\"gno.land/r/sys/names\"\n\t\"gno.land/r/sys/params\"\n\tvalr \"gno.land/r/sys/validators/v2\"\n)\n\n// genesisDeployerAddr is the address derived from the genesis deployer\n// mnemonic.  See loader.gno for the full mnemonic.\nconst genesisDeployerAddr = address(\"g1edq4dugw0sgat4zxcw9xardvuydqf6cgleuc8p\")\n\nfunc main() {\n\tms := memberstore.Get()\n\n\t// ---- 1. Add temporary deployer as sole T1 member ----\n\t// This gives the deployer 100% supermajority so proposals pass.\n\tmust(ms.SetMember(memberstore.T1, genesisDeployerAddr, \u0026memberstore.Member{InvitationPoints: 0}))\n\n\t// ---- 2. Register the initial validator set via governance proposal ----\n\t// 7 validators — BFT \u003e2/3 threshold (floor(2n/3)+1) means 5 nodes must be up for consensus.\n\t// Additional validators can be added post-genesis via govDAO proposals\n\t// (see govdao-scripts/add-validator.sh).\n\tgovExec(valr.NewPropRequest(\n\t\tfunc() []validators.Validator {\n\t\t\treturn []validators.Validator{\n\t\t\t\t// aeddi-2\n\t\t\t\t{\n\t\t\t\t\tAddress:     address(\"g1qtxq5t3acclx03cznwusddg5mv4pfm2knjcmsg\"),\n\t\t\t\t\tPubKey:      \"gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq23rkn8tsqufv67v8rwwl4kfz3jfx4ykhvszpd09j6l9f6sykc0w6zqghnv\",\n\t\t\t\t\tVotingPower: 1,\n\t\t\t\t},\n\t\t\t\t// gfanton-2\n\t\t\t\t{\n\t\t\t\t\tAddress:     address(\"g15atj32de45nqgm68298aua8ayy4aujwyewegvd\"),\n\t\t\t\t\tPubKey:      \"gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqw93xyutkn4wy3gnuwxsu7ak963kv2ztepzxjxyrwuhwkc0wzh2hy7cpm39\",\n\t\t\t\t\tVotingPower: 1,\n\t\t\t\t},\n\t\t\t\t// gnocore-val-01\n\t\t\t\t{\n\t\t\t\t\tAddress:     address(\"g18kp360plxkmh3yal3juzffjuqa0ugys0963a80\"),\n\t\t\t\t\tPubKey:      \"gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqdxm8w984fdgm92pnc406z8cv2lugu48ekfdhytvmcqx5s8sccfskhh6j7r\",\n\t\t\t\t\tVotingPower: 1,\n\t\t\t\t},\n\t\t\t\t// samourai-crew-1\n\t\t\t\t{\n\t\t\t\t\tAddress:     address(\"g1z9eedz4qfru6ggdsyj7yn85s5ewvdr5gr39c7r\"),\n\t\t\t\t\tPubKey:      \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zq83n9f9xfpg2ut0p6hsxlu7f9fwuhm9wwt5y8ez0rv4s6xrzsalva7j2pu\",\n\t\t\t\t\tVotingPower: 1,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\t\"Add initial validator set\",\n\t\t\"Bootstrap the test12 initial validator set\",\n\t))\n\n\t// ---- 3. Set chain parameters via governance proposals ----\n\n\t// Unrestricted addresses (can transfer ugnot even when bank is locked).\n\tgovExec(params.ProposeAddUnrestrictedAcctsRequest(\n\t\taddress(\"g1rp7cmetn27eqlpjpc4vuusf8kaj746tysc0qgh\"), // GovDAO T1 multisig\n\t\taddress(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"), // test1\n\t))\n\n\t// Lock ugnot transfers (restricted denom).\n\tgovExec(params.ProposeLockTransferRequest())\n\n\t// ---- 4. Enable namespace enforcement (r/sys/names) ----\n\t// This drops ownership and enables the namespace permission check.\n\t// Must be called during genesis; after this, package paths require registration.\n\tnames.Enable(cross)\n\n\t// ---- 5. Add govDAO T1 member ----\n\t// Temporarily aeddi is the sole govDAO member. He will manually\n\t// craft and execute proposals to onboard validators and additional members\n\t// until the DAO is large enough for real multi-party governance.\n\tmust(ms.SetMember(memberstore.T1, address(\"g1aeddlftlfk27ret5rf750d7w5dume3kcsm8r8m\"), \u0026memberstore.Member{InvitationPoints: 3})) // aeddi\n\n\t// ---- 6. Remove the genesis deployer ----\n\tms.RemoveMember(genesisDeployerAddr)\n\n\t// ---- 7. Lock down memberstore access ----\n\t// From this point on, only gno.land/r/gov/dao/v3/impl can call\n\t// memberstore.Get() — matching the production AllowedDAOs configuration.\n\tdao.UpdateImpl(cross, dao.UpdateRequest{\n\t\tAllowedDAOs: []string{\"gno.land/r/gov/dao/v3/impl\"},\n\t})\n}\n\n// govExec creates a proposal, votes YES, and executes it immediately.\n// Works because the deployer is the sole T1 member (100% supermajority).\nfunc govExec(r dao.ProposalRequest) {\n\tpid := dao.MustCreateProposal(cross, r)\n\tdao.MustVoteOnProposal(cross, dao.VoteRequest{Option: dao.YesVote, ProposalID: pid})\n\tdao.ExecuteProposal(cross, pid)\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": "AwM2Mzk4a+TI01XuhcWlm7OFVux8jIbw6hE7QfatTyA/"
                  },
                  "signature": "G2wmUoFhlCxMr21IJVIkGm3L3Nb5xjsToDuMHvHw6ZoxhHDmpImXRMhvZM6HE8ahPh41OAzzt4RVHbtMXMGZMw=="
                }
              ],
              "memo": ""
            }
          }
        ],
        "auth": {
          "params": {
            "max_memo_bytes": "65536",
            "tx_sig_limit": "7",
            "tx_size_cost_per_byte": "10",
            "sig_verify_cost_ed25519": "590",
            "sig_verify_cost_secp256k1": "1000",
            "gas_price_change_compressor": "10",
            "target_gas_ratio": "70",
            "initial_gasprice": {
              "gas": "1000",
              "price": "1ugnot"
            },
            "unrestricted_addrs": null,
            "fee_collector": "g17xpfvakm2amg962yls6f84z3kell8c5lr9lr2e"
          }
        },
        "bank": {
          "params": {
            "restricted_denoms": []
          }
        },
        "vm": {
          "params": {
            "sysnames_pkgpath": "gno.land/r/sys/names",
            "syscla_pkgpath": "gno.land/r/sys/cla",
            "chain_domain": "gno.land",
            "default_deposit": "600000000ugnot",
            "storage_price": "100ugnot",
            "storage_fee_collector": "g1c9stkafpvcwez2efq3qtfuezw4zpaux3tvxggk"
          },
          "realm_params": null
        }
      }
    }
  }
}