From 984a88965488a486051453a4e821a27c4c038677 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 6 Mar 2025 22:37:46 +0100 Subject: [PATCH 01/67] feat: backup command Signed-off-by: Norman --- WORKLOG.md | 33 ++++ gno.land/cmd/gnoland/backup.go | 155 ++++++++++++++++++ gno.land/cmd/gnoland/root.go | 1 + go.mod | 2 + go.sum | 4 + tm2/Makefile | 4 + tm2/pkg/bft/node/backup_svc.go | 58 +++++++ tm2/pkg/bft/node/backuppb/backup.proto | 18 ++ .../backuppb/backupconnect/backup.connect.go | 109 ++++++++++++ tm2/pkg/bft/node/buf.gen.yaml | 9 + tm2/pkg/bft/node/buf.yaml | 10 ++ tm2/pkg/bft/node/node.go | 14 ++ tm2/tools/tools.go | 6 + 13 files changed, 423 insertions(+) create mode 100644 WORKLOG.md create mode 100644 gno.land/cmd/gnoland/backup.go create mode 100644 tm2/pkg/bft/node/backup_svc.go create mode 100644 tm2/pkg/bft/node/backuppb/backup.proto create mode 100644 tm2/pkg/bft/node/backuppb/backupconnect/backup.connect.go create mode 100644 tm2/pkg/bft/node/buf.gen.yaml create mode 100644 tm2/pkg/bft/node/buf.yaml create mode 100644 tm2/tools/tools.go diff --git a/WORKLOG.md b/WORKLOG.md new file mode 100644 index 00000000000..38766eb3713 --- /dev/null +++ b/WORKLOG.md @@ -0,0 +1,33 @@ +# Backup/restore feature + +## Initial assignment + +This effort covers the implementation of a proper backup / restore functionality for the Gno node. + +Currently, there is no block backup / restore functionality, but only transaction exports, and transaction replays. + +This can be cumbersome, considering users want to be able to quickly restore Gno chains from their trusted backup without having to sync with other peers. + +This functionality should work with direct node access (not over JSON-RPC), for example through special commands that utilize gRPC, as there is no need for users to back up remote node states. + +Successful outcome of this effort: + + The Gno node implements an efficient backup / restore functionality that a node operator can use to save chain state + +## Logs + +### Init + +The storage layer of a gnoland node is initialized in the gnoland command so this seems like the correct place for these features so I'm adding `gnoland backup create` and `gnoland backup restore` commands. + +The only stable database backend is goleveldb. After searching for `backup` on the goleveldb repo I didn't see any official way to backup the database. There is [an issue asking for it opened in 2016](https://github.com/syndtr/goleveldb/issues/135) but no response. + +We could backup every entry in the database to be agnostic of the backend but it would probably be much less efficient than working with the filesystem directly. + +The output format will be a single file compressed archive. It would be better to not depend on system tools and use pure a pure go library for that. + +The only lib I found that seem serious and to offer this functionality is https://github.com/mholt/archives + +After examination of the data needed to be saved, it seems only the "db" and "wal" dirs are needed + +Tar + Xz was chosen for the archive format but this could become options \ No newline at end of file diff --git a/gno.land/cmd/gnoland/backup.go b/gno.land/cmd/gnoland/backup.go new file mode 100644 index 00000000000..43571adb9b6 --- /dev/null +++ b/gno.land/cmd/gnoland/backup.go @@ -0,0 +1,155 @@ +package main + +import ( + "archive/tar" + "context" + "flag" + "fmt" + "net/http" + "os" + "path/filepath" + + "connectrpc.com/connect" + backup "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb" + "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb/backupconnect" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/ulikunitz/xz" +) + +type backupCfg struct { + remote string +} + +func newBackupCmd(io commands.IO) *commands.Command { + cfg := &backupCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "backup", + ShortUsage: "backup [flags]", + ShortHelp: "backups the Gnoland blockchain node", + LongHelp: "Backups the Gnoland blockchain node, with accompanying setup", + }, + cfg, + func(ctx context.Context, _ []string) error { + return execBackup(ctx, cfg, io) + }, + ) +} + +func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.remote, + "remote", + "http://localhost:4242", + "backup service remote", + ) + +} + +func execBackup(ctx context.Context, c *backupCfg, io commands.IO) error { + client := backupconnect.NewBackupServiceClient( + http.DefaultClient, + c.remote, + connect.WithGRPC(), + ) + res, err := client.StreamBlocks( + ctx, + connect.NewRequest(&backup.StreamBlocksRequest{}), + ) + if err != nil { + return err + } + + const chunkSize = 100 + + outdir := "blocks-backup" + if err := os.MkdirAll(outdir, 0o775); err != nil { + return err + } + + height := 1 + chunkStart := height + + latestFP := filepath.Join(outdir, "latest.tar.xz") + outFile, err := os.OpenFile(latestFP, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o664) + if err != nil { + return err + } + xzw, err := xz.NewWriter(outFile) + if err != nil { + return err + } + w := tar.NewWriter(xzw) + + finalizeChunk := func(last bool) error { + if err := w.Close(); err != nil { + return err + } + if err := xzw.Close(); err != nil { + return err + } + if err := outFile.Close(); err != nil { + return err + } + + if err := os.Rename(latestFP, filepath.Join(outdir, fmt.Sprintf("%d-%d.tm2blocks.tar.xz", chunkStart, height-1))); err != nil { + return err + } + + io.ErrPrintln(height - 1) + + if last { + return nil + } + + outFile, err = os.OpenFile(latestFP, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o664) + if err != nil { + return err + } + xzw, err = xz.NewWriter(outFile) + if err != nil { + return err + } + w = tar.NewWriter(xzw) + + chunkStart = height + + return nil + } + + for { + ok := res.Receive() + if !ok { + if res.Err() != nil { + return err + } + + return finalizeChunk(true) + } + msg := res.Msg() + + header := &tar.Header{ + Name: fmt.Sprintf("%d", msg.Height), + Size: int64(len(msg.Data)), + Mode: 0o664, + } + if err := w.WriteHeader(header); err != nil { + return err + } + + _, err := w.Write(msg.Data) + if err != nil { + return err + } + + height += 1 + if height%chunkSize != 1 { + continue + } + + if err := finalizeChunk(false); err != nil { + return err + } + } +} diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index c6143ab9cd3..c2701082c95 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -27,6 +27,7 @@ func newRootCmd(io commands.IO) *commands.Command { newStartCmd(io), newSecretsCmd(io), newConfigCmd(io), + newBackupCmd(io), ) return cmd diff --git a/go.mod b/go.mod index e9d61b9fa08..0a2568470fe 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23 toolchain go1.23.6 require ( + connectrpc.com/connect v1.18.1 dario.cat/mergo v1.0.1 github.com/alecthomas/chroma/v2 v2.15.0 github.com/btcsuite/btcd/btcec/v2 v2.3.4 @@ -27,6 +28,7 @@ require ( github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 + github.com/ulikunitz/xz v0.5.12 github.com/valyala/bytebufferpool v1.0.0 github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc diff --git a/go.sum b/go.sum index 5fd4cddd627..610bee3b9f8 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -148,6 +150,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= +github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/tm2/Makefile b/tm2/Makefile index fd3aede0d4c..a72af3a056f 100644 --- a/tm2/Makefile +++ b/tm2/Makefile @@ -68,3 +68,7 @@ _test.pkg.db:; go test $(GOTEST_FLAGS) ./pkg/db/... ./pkg/iavl/benchmarks/. generate: go generate -x ./... $(MAKE) fmt + +.PHONY: generate.grpc +generate.grpc: + cd pkg/bft/node && buf generate \ No newline at end of file diff --git a/tm2/pkg/bft/node/backup_svc.go b/tm2/pkg/bft/node/backup_svc.go new file mode 100644 index 00000000000..610ce955eb0 --- /dev/null +++ b/tm2/pkg/bft/node/backup_svc.go @@ -0,0 +1,58 @@ +package node + +import ( + "context" + "fmt" + + "connectrpc.com/connect" + "github.com/gnolang/gno/tm2/pkg/amino" + backup "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb" + "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb/backupconnect" + "github.com/gnolang/gno/tm2/pkg/bft/store" +) + +type backupServer struct { + store *store.BlockStore +} + +// StreamBlocks implements backupconnect.BackupServiceHandler. +func (b *backupServer) StreamBlocks(_ context.Context, req *connect.Request[backup.StreamBlocksRequest], stream *connect.ServerStream[backup.StreamBlocksResponse]) error { + startHeight := req.Msg.StartHeight + if startHeight == 0 { + startHeight = 1 + } + if startHeight < 1 { + return fmt.Errorf("start height must be >= 1, got %d", startHeight) + } + + endHeight := req.Msg.EndHeight + blockStoreHeight := b.store.Height() + if endHeight == 0 { + endHeight = blockStoreHeight + } else if endHeight > blockStoreHeight { + return fmt.Errorf("end height must be <= %d", blockStoreHeight) + } + + if startHeight > endHeight { + return fmt.Errorf("end height must be >= than start height") + } + + for height := startHeight; height <= endHeight; height++ { + block := b.store.LoadBlock(height) + data, err := amino.Marshal(block) + if err != nil { + return err + } + + if err := stream.Send(&backup.StreamBlocksResponse{ + Height: height, + Data: data, + }); err != nil { + return err + } + } + + return nil +} + +var _ backupconnect.BackupServiceHandler = (*backupServer)(nil) diff --git a/tm2/pkg/bft/node/backuppb/backup.proto b/tm2/pkg/bft/node/backuppb/backup.proto new file mode 100644 index 00000000000..417a8c03cea --- /dev/null +++ b/tm2/pkg/bft/node/backuppb/backup.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +package tm; + +option go_package = "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb;backup"; + +service BackupService { + rpc StreamBlocks (StreamBlocksRequest) returns (stream StreamBlocksResponse); +} + +message StreamBlocksRequest { + int64 start_height = 1; + int64 end_height = 2; +} + +message StreamBlocksResponse { + int64 height = 1; + bytes data = 2; +} \ No newline at end of file diff --git a/tm2/pkg/bft/node/backuppb/backupconnect/backup.connect.go b/tm2/pkg/bft/node/backuppb/backupconnect/backup.connect.go new file mode 100644 index 00000000000..9e150faf45c --- /dev/null +++ b/tm2/pkg/bft/node/backuppb/backupconnect/backup.connect.go @@ -0,0 +1,109 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: backuppb/backup.proto + +package backupconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + backuppb "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // BackupServiceName is the fully-qualified name of the BackupService service. + BackupServiceName = "tm.BackupService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // BackupServiceStreamBlocksProcedure is the fully-qualified name of the BackupService's + // StreamBlocks RPC. + BackupServiceStreamBlocksProcedure = "/tm.BackupService/StreamBlocks" +) + +// BackupServiceClient is a client for the tm.BackupService service. +type BackupServiceClient interface { + StreamBlocks(context.Context, *connect.Request[backuppb.StreamBlocksRequest]) (*connect.ServerStreamForClient[backuppb.StreamBlocksResponse], error) +} + +// NewBackupServiceClient constructs a client for the tm.BackupService service. By default, it uses +// the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends +// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewBackupServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) BackupServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + backupServiceMethods := backuppb.File_backuppb_backup_proto.Services().ByName("BackupService").Methods() + return &backupServiceClient{ + streamBlocks: connect.NewClient[backuppb.StreamBlocksRequest, backuppb.StreamBlocksResponse]( + httpClient, + baseURL+BackupServiceStreamBlocksProcedure, + connect.WithSchema(backupServiceMethods.ByName("StreamBlocks")), + connect.WithClientOptions(opts...), + ), + } +} + +// backupServiceClient implements BackupServiceClient. +type backupServiceClient struct { + streamBlocks *connect.Client[backuppb.StreamBlocksRequest, backuppb.StreamBlocksResponse] +} + +// StreamBlocks calls tm.BackupService.StreamBlocks. +func (c *backupServiceClient) StreamBlocks(ctx context.Context, req *connect.Request[backuppb.StreamBlocksRequest]) (*connect.ServerStreamForClient[backuppb.StreamBlocksResponse], error) { + return c.streamBlocks.CallServerStream(ctx, req) +} + +// BackupServiceHandler is an implementation of the tm.BackupService service. +type BackupServiceHandler interface { + StreamBlocks(context.Context, *connect.Request[backuppb.StreamBlocksRequest], *connect.ServerStream[backuppb.StreamBlocksResponse]) error +} + +// NewBackupServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewBackupServiceHandler(svc BackupServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + backupServiceMethods := backuppb.File_backuppb_backup_proto.Services().ByName("BackupService").Methods() + backupServiceStreamBlocksHandler := connect.NewServerStreamHandler( + BackupServiceStreamBlocksProcedure, + svc.StreamBlocks, + connect.WithSchema(backupServiceMethods.ByName("StreamBlocks")), + connect.WithHandlerOptions(opts...), + ) + return "/tm.BackupService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case BackupServiceStreamBlocksProcedure: + backupServiceStreamBlocksHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedBackupServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedBackupServiceHandler struct{} + +func (UnimplementedBackupServiceHandler) StreamBlocks(context.Context, *connect.Request[backuppb.StreamBlocksRequest], *connect.ServerStream[backuppb.StreamBlocksResponse]) error { + return connect.NewError(connect.CodeUnimplemented, errors.New("tm.BackupService.StreamBlocks is not implemented")) +} diff --git a/tm2/pkg/bft/node/buf.gen.yaml b/tm2/pkg/bft/node/buf.gen.yaml new file mode 100644 index 00000000000..fce3ed533a8 --- /dev/null +++ b/tm2/pkg/bft/node/buf.gen.yaml @@ -0,0 +1,9 @@ +version: v2 +plugins: + - local: [go, run, -modfile, ../../../../go.mod, google.golang.org/protobuf/cmd/protoc-gen-go] + out: . + opt: paths=source_relative + + - local: [go, run, -modfile, ../../../../go.mod, connectrpc.com/connect/cmd/protoc-gen-connect-go] + out: . + opt: paths=source_relative \ No newline at end of file diff --git a/tm2/pkg/bft/node/buf.yaml b/tm2/pkg/bft/node/buf.yaml new file mode 100644 index 00000000000..5a15800e540 --- /dev/null +++ b/tm2/pkg/bft/node/buf.yaml @@ -0,0 +1,10 @@ +# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml +version: v2 +modules: + - path: . +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index 7f16d6780c7..495cc9dc92e 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -13,11 +13,14 @@ import ( "time" "github.com/gnolang/gno/tm2/pkg/bft/appconn" + "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb/backupconnect" "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" "github.com/gnolang/gno/tm2/pkg/p2p/conn" "github.com/gnolang/gno/tm2/pkg/p2p/discovery" p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/rs/cors" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" "github.com/gnolang/gno/tm2/pkg/amino" bc "github.com/gnolang/gno/tm2/pkg/bft/blockchain" @@ -601,6 +604,17 @@ func (n *Node) OnStart() error { n.rpcListeners = listeners } + // start backup service + backupServ := &backupServer{store: n.blockStore} + mux := http.NewServeMux() + path, handler := backupconnect.NewBackupServiceHandler(backupServ) + mux.Handle(path, handler) + go http.ListenAndServe( + "localhost:4242", + // Use h2c so we can serve HTTP/2 without TLS. + h2c.NewHandler(mux, &http2.Server{}), + ) + // Start the transport. // The listen address for the transport needs to be an address within reach of the machine NIC listenAddress := p2pTypes.NetAddressString(n.nodeKey.ID(), n.config.P2P.ListenAddress) diff --git a/tm2/tools/tools.go b/tm2/tools/tools.go new file mode 100644 index 00000000000..437deac4755 --- /dev/null +++ b/tm2/tools/tools.go @@ -0,0 +1,6 @@ +package tools + +import ( + _ "connectrpc.com/connect/cmd/protoc-gen-connect-go" + _ "google.golang.org/protobuf/cmd/protoc-gen-go" +) From f1a52c81276b939381c89399416f43fc824e45df Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 13 Mar 2025 15:48:41 +0100 Subject: [PATCH 02/67] feat: use zstd instead of xz Signed-off-by: Norman --- gno.land/cmd/gnoland/backup.go | 17 +++++++++-------- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/gno.land/cmd/gnoland/backup.go b/gno.land/cmd/gnoland/backup.go index 43571adb9b6..149b90217b8 100644 --- a/gno.land/cmd/gnoland/backup.go +++ b/gno.land/cmd/gnoland/backup.go @@ -13,7 +13,7 @@ import ( backup "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb" "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb/backupconnect" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/ulikunitz/xz" + "github.com/klauspost/compress/zstd" ) type backupCfg struct { @@ -71,29 +71,30 @@ func execBackup(ctx context.Context, c *backupCfg, io commands.IO) error { height := 1 chunkStart := height - latestFP := filepath.Join(outdir, "latest.tar.xz") + latestFP := filepath.Join(outdir, "latest.tar.zst") outFile, err := os.OpenFile(latestFP, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o664) if err != nil { return err } - xzw, err := xz.NewWriter(outFile) + zstw, err := zstd.NewWriter(outFile) if err != nil { return err } - w := tar.NewWriter(xzw) + w := tar.NewWriter(zstw) finalizeChunk := func(last bool) error { if err := w.Close(); err != nil { return err } - if err := xzw.Close(); err != nil { + if err := zstw.Close(); err != nil { return err } if err := outFile.Close(); err != nil { return err } - if err := os.Rename(latestFP, filepath.Join(outdir, fmt.Sprintf("%d-%d.tm2blocks.tar.xz", chunkStart, height-1))); err != nil { + chunkFP := filepath.Join(outdir, fmt.Sprintf("%019d-%019d.tm2blocks.tar.zst", chunkStart, height-1)) + if err := os.Rename(latestFP, chunkFP); err != nil { return err } @@ -107,11 +108,11 @@ func execBackup(ctx context.Context, c *backupCfg, io commands.IO) error { if err != nil { return err } - xzw, err = xz.NewWriter(outFile) + zstw, err = zstd.NewWriter(outFile) if err != nil { return err } - w = tar.NewWriter(xzw) + w = tar.NewWriter(zstw) chunkStart = height diff --git a/go.mod b/go.mod index 0a2568470fe..e3539ae1b41 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/gofuzz v1.2.0 github.com/gorilla/websocket v1.5.3 + github.com/klauspost/compress v1.18.0 github.com/libp2p/go-buffer-pool v0.1.0 github.com/pelletier/go-toml v1.9.5 github.com/peterbourgon/ff/v3 v3.4.0 @@ -28,7 +29,6 @@ require ( github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 - github.com/ulikunitz/xz v0.5.12 github.com/valyala/bytebufferpool v1.0.0 github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc diff --git a/go.sum b/go.sum index 610bee3b9f8..b78d1dcf1ae 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,8 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -150,8 +152,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= From ddcb2746f5021c6bf24d39c7880526693831fff5 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 13 Mar 2025 20:08:15 +0100 Subject: [PATCH 03/67] chore: move to contribs Signed-off-by: Norman --- contribs/gnomd/go.mod | 4 +- contribs/tm2backup/.gitignore | 1 + contribs/tm2backup/go.mod | 21 +++ contribs/tm2backup/go.sum | 28 +++ contribs/tm2backup/main.go | 303 +++++++++++++++++++++++++++++++++ gno.land/cmd/gnoland/backup.go | 156 ----------------- gno.land/cmd/gnoland/root.go | 1 - go.mod | 1 - go.sum | 2 - 9 files changed, 356 insertions(+), 161 deletions(-) create mode 100644 contribs/tm2backup/.gitignore create mode 100644 contribs/tm2backup/go.mod create mode 100644 contribs/tm2backup/go.sum create mode 100644 contribs/tm2backup/main.go delete mode 100644 gno.land/cmd/gnoland/backup.go diff --git a/contribs/gnomd/go.mod b/contribs/gnomd/go.mod index d8f01bf82e3..083d223feeb 100644 --- a/contribs/gnomd/go.mod +++ b/contribs/gnomd/go.mod @@ -1,4 +1,4 @@ -module github.com/gnolang/gno/contribs/gnomd +module github.com/gnolang/gno/contribs/tm2backup go 1.22 @@ -24,3 +24,5 @@ require ( golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect ) + +replace github.com/gnolang/gno => ../.. diff --git a/contribs/tm2backup/.gitignore b/contribs/tm2backup/.gitignore new file mode 100644 index 00000000000..d2e59036fc6 --- /dev/null +++ b/contribs/tm2backup/.gitignore @@ -0,0 +1 @@ +blocks-backup \ No newline at end of file diff --git a/contribs/tm2backup/go.mod b/contribs/tm2backup/go.mod new file mode 100644 index 00000000000..9addf0cae72 --- /dev/null +++ b/contribs/tm2backup/go.mod @@ -0,0 +1,21 @@ +module github.com/gnolang/gno/contribs/tm2backup + +go 1.23 + +toolchain go1.23.6 + +replace github.com/gnolang/gno => ../.. + +require ( + connectrpc.com/connect v1.18.1 + github.com/gnolang/gno v0.0.0-00010101000000-000000000000 + github.com/gofrs/flock v0.12.1 + github.com/klauspost/compress v1.18.0 +) + +require ( + github.com/peterbourgon/ff/v3 v3.4.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/term v0.28.0 // indirect + google.golang.org/protobuf v1.36.3 // indirect +) diff --git a/contribs/tm2backup/go.sum b/contribs/tm2backup/go.sum new file mode 100644 index 00000000000..998db821f7c --- /dev/null +++ b/contribs/tm2backup/go.sum @@ -0,0 +1,28 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= +github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contribs/tm2backup/main.go b/contribs/tm2backup/main.go new file mode 100644 index 00000000000..83cf6249428 --- /dev/null +++ b/contribs/tm2backup/main.go @@ -0,0 +1,303 @@ +package main + +import ( + "archive/tar" + "context" + "errors" + "flag" + "fmt" + "math" + "net/http" + "os" + "path/filepath" + "strconv" + + "connectrpc.com/connect" + backup "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb" + "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb/backupconnect" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gofrs/flock" + "github.com/klauspost/compress/zstd" +) + +func main() { + cmd := newRootCmd(commands.NewDefaultIO()) + + cmd.Execute(context.Background(), os.Args[1:]) +} + +func newRootCmd(io commands.IO) *commands.Command { + cfg := &backupCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: "[flags]", + ShortHelp: "efficiently backup tm2 blocks", + LongHelp: "Efficiently backup tendermint2 blocks", + }, + cfg, + func(ctx context.Context, _ []string) error { + return execBackup(ctx, cfg, io) + }, + ) + + return cmd +} + +type backupCfg struct { + remote string + outDir string + startHeight uint64 + endHeight uint64 +} + +func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.remote, + "remote", + "http://localhost:4242", + "Backup service remote.", + ) + fs.StringVar( + &c.outDir, + "o", + "blocks-backup", + "Output directory.", + ) + fs.Uint64Var( + &c.startHeight, + "start", + 0, + fmt.Sprintf("Start height. Will be aligned at a multiple of %d blocks. This can't be used when resuming from an existing output directory.", chunkSize), + ) + fs.Uint64Var( + &c.endHeight, + "end", + 0, + "End height, inclusive. Use 0 for latest height.", + ) +} + +// XXX: versioning of output directory + +const nextHeightFilename = "next-height.txt" +const chunkSize = 100 + +func execBackup(ctx context.Context, c *backupCfg, io commands.IO) error { + if err := validateInput(c); err != nil { + return fmt.Errorf("invalid input: %w", err) + } + + unlock, err := lockOutputDir(c, io) + if err != nil { + return fmt.Errorf("failed to lock output directory: %w", err) + } + defer unlock() + + nextHeightFP := filepath.Join(c.outDir, nextHeightFilename) + nextHeight, err := readNextHeight(nextHeightFP) + if err != nil { + return fmt.Errorf("failed to read next height: %w", err) + } + + height, err := getStartHeight(int64(c.startHeight), nextHeight) + if err != nil { + return fmt.Errorf("failed to decide start height: %w", err) + } + + if c.endHeight != 0 && int64(c.endHeight) < nextHeight { + return fmt.Errorf("invalid input: requested end height is smaller than the next height in output directory (%d), use a different output directory or a valid end height", nextHeight) + } + + prefix := "starting" + if nextHeight != -1 { + prefix = "resuming" + } + io.Println(prefix, "at height", height) + + client := backupconnect.NewBackupServiceClient( + http.DefaultClient, + c.remote, + connect.WithGRPC(), + ) + res, err := client.StreamBlocks( + ctx, + connect.NewRequest(&backup.StreamBlocksRequest{ + StartHeight: height, + EndHeight: int64(c.endHeight), + }), + ) + if err != nil { + return err + } + + chunkStart := height + + nextChunkFP := filepath.Join(c.outDir, "next-chunk.tar.zst") + outFile, err := os.OpenFile(nextChunkFP, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o664) + if err != nil { + return err + } + zstw, err := zstd.NewWriter(outFile) + if err != nil { + return err + } + w := tar.NewWriter(zstw) + + finalizeChunk := func(last bool) error { + if err := w.Close(); err != nil { + return err + } + if err := zstw.Close(); err != nil { + return err + } + if err := outFile.Close(); err != nil { + return err + } + + // using padding for filename to match chunk order and lexicographical order + chunkFP := filepath.Join(c.outDir, fmt.Sprintf("%019d.tm2blocks.tar.zst", chunkStart)) + if err := os.Rename(nextChunkFP, chunkFP); err != nil { + return err + } + + if err := os.WriteFile(nextHeightFP, []byte(strconv.FormatInt(height, 10)), 0o664); err != nil { + return err + } + + io.Println("wrote blocks", chunkStart, "to", height-1, "at", chunkFP) + + if last { + return nil + } + + outFile, err = os.OpenFile(nextChunkFP, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o664) + if err != nil { + return err + } + zstw, err = zstd.NewWriter(outFile) + if err != nil { + return err + } + w = tar.NewWriter(zstw) + + chunkStart = height + + return nil + } + + for { + ok := res.Receive() + if !ok { + if res.Err() != nil { + return err + } + + return finalizeChunk(true) + } + msg := res.Msg() + + header := &tar.Header{ + Name: fmt.Sprintf("%d", msg.Height), + Size: int64(len(msg.Data)), + Mode: 0o664, + } + if err := w.WriteHeader(header); err != nil { + return err + } + + _, err := w.Write(msg.Data) + if err != nil { + return err + } + + height += 1 + if height%chunkSize != 1 { + continue + } + + if err := finalizeChunk(false); err != nil { + return err + } + + // don't finalize twice if the requested endHeight is a multiple of chunkSize + if height-1 == int64(c.endHeight) { + return nil + } + } +} + +func validateInput(c *backupCfg) error { + if c.startHeight > math.MaxInt64 { + return fmt.Errorf("start must be <= %d", math.MaxInt64) + } + + if c.endHeight > math.MaxInt64 { + return fmt.Errorf("end must be <= %d", math.MaxInt64) + } + + return nil +} + +func lockOutputDir(c *backupCfg, io commands.IO) (func(), error) { + if err := os.MkdirAll(c.outDir, 0o775); err != nil { + return nil, fmt.Errorf("failed to ensure output directory exists: %w", err) + } + + fileLock := flock.New(filepath.Join(c.outDir, "blocks.lock")) + locked, err := fileLock.TryLock() + if err != nil { + return nil, err + } + if !locked { + return nil, errors.New("failed to acquire lock on output directory") + } + return func() { + if err := fileLock.Unlock(); err != nil { + io.ErrPrintln(err) + } + }, nil +} + +func getStartHeight(requestedStartHeight int64, outputDirNextHeight int64) (int64, error) { + height := int64(1) + + if requestedStartHeight != 0 && outputDirNextHeight != -1 { + return 0, errors.New("can't request a start height when resuming, use a different output directory or no start height") + } + + if requestedStartHeight != 0 { + height = requestedStartHeight + } else { + height = outputDirNextHeight + } + + // align: 4 -> 1, 100 -> 1, 101 -> 101, 150 -> 101 + // we simply overwrite the latest chunk if it is partial because it's not expensive + height -= (height - 1) % chunkSize + + if height < 1 || height%100 != 1 { + return 0, fmt.Errorf("unexpected start height %d", height) + } + + return height, nil +} + +func readNextHeight(nextHeightFP string) (int64, error) { + nextHeightBz, err := os.ReadFile(nextHeightFP) + switch { + case os.IsNotExist(err): + return -1, nil + case err != nil: + return 0, err + default: + nextHeight, err := strconv.ParseInt(string(nextHeightBz), 10, 64) + if err != nil { + return 0, err + } + if nextHeight < 1 { + return 0, fmt.Errorf("unexpected next height %d", nextHeight) + } + return nextHeight, nil + } +} diff --git a/gno.land/cmd/gnoland/backup.go b/gno.land/cmd/gnoland/backup.go deleted file mode 100644 index 149b90217b8..00000000000 --- a/gno.land/cmd/gnoland/backup.go +++ /dev/null @@ -1,156 +0,0 @@ -package main - -import ( - "archive/tar" - "context" - "flag" - "fmt" - "net/http" - "os" - "path/filepath" - - "connectrpc.com/connect" - backup "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb" - "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb/backupconnect" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/klauspost/compress/zstd" -) - -type backupCfg struct { - remote string -} - -func newBackupCmd(io commands.IO) *commands.Command { - cfg := &backupCfg{} - - return commands.NewCommand( - commands.Metadata{ - Name: "backup", - ShortUsage: "backup [flags]", - ShortHelp: "backups the Gnoland blockchain node", - LongHelp: "Backups the Gnoland blockchain node, with accompanying setup", - }, - cfg, - func(ctx context.Context, _ []string) error { - return execBackup(ctx, cfg, io) - }, - ) -} - -func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.remote, - "remote", - "http://localhost:4242", - "backup service remote", - ) - -} - -func execBackup(ctx context.Context, c *backupCfg, io commands.IO) error { - client := backupconnect.NewBackupServiceClient( - http.DefaultClient, - c.remote, - connect.WithGRPC(), - ) - res, err := client.StreamBlocks( - ctx, - connect.NewRequest(&backup.StreamBlocksRequest{}), - ) - if err != nil { - return err - } - - const chunkSize = 100 - - outdir := "blocks-backup" - if err := os.MkdirAll(outdir, 0o775); err != nil { - return err - } - - height := 1 - chunkStart := height - - latestFP := filepath.Join(outdir, "latest.tar.zst") - outFile, err := os.OpenFile(latestFP, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o664) - if err != nil { - return err - } - zstw, err := zstd.NewWriter(outFile) - if err != nil { - return err - } - w := tar.NewWriter(zstw) - - finalizeChunk := func(last bool) error { - if err := w.Close(); err != nil { - return err - } - if err := zstw.Close(); err != nil { - return err - } - if err := outFile.Close(); err != nil { - return err - } - - chunkFP := filepath.Join(outdir, fmt.Sprintf("%019d-%019d.tm2blocks.tar.zst", chunkStart, height-1)) - if err := os.Rename(latestFP, chunkFP); err != nil { - return err - } - - io.ErrPrintln(height - 1) - - if last { - return nil - } - - outFile, err = os.OpenFile(latestFP, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o664) - if err != nil { - return err - } - zstw, err = zstd.NewWriter(outFile) - if err != nil { - return err - } - w = tar.NewWriter(zstw) - - chunkStart = height - - return nil - } - - for { - ok := res.Receive() - if !ok { - if res.Err() != nil { - return err - } - - return finalizeChunk(true) - } - msg := res.Msg() - - header := &tar.Header{ - Name: fmt.Sprintf("%d", msg.Height), - Size: int64(len(msg.Data)), - Mode: 0o664, - } - if err := w.WriteHeader(header); err != nil { - return err - } - - _, err := w.Write(msg.Data) - if err != nil { - return err - } - - height += 1 - if height%chunkSize != 1 { - continue - } - - if err := finalizeChunk(false); err != nil { - return err - } - } -} diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index c2701082c95..c6143ab9cd3 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -27,7 +27,6 @@ func newRootCmd(io commands.IO) *commands.Command { newStartCmd(io), newSecretsCmd(io), newConfigCmd(io), - newBackupCmd(io), ) return cmd diff --git a/go.mod b/go.mod index e3539ae1b41..78d351c7bb4 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,6 @@ require ( github.com/google/go-cmp v0.6.0 github.com/google/gofuzz v1.2.0 github.com/gorilla/websocket v1.5.3 - github.com/klauspost/compress v1.18.0 github.com/libp2p/go-buffer-pool v0.1.0 github.com/pelletier/go-toml v1.9.5 github.com/peterbourgon/ff/v3 v3.4.0 diff --git a/go.sum b/go.sum index b78d1dcf1ae..fd24668db28 100644 --- a/go.sum +++ b/go.sum @@ -102,8 +102,6 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= From 600f085c947c47c93a71912e088e61e734eb74e5 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 13 Mar 2025 20:09:03 +0100 Subject: [PATCH 04/67] chore: remove worklog Signed-off-by: Norman --- WORKLOG.md | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 WORKLOG.md diff --git a/WORKLOG.md b/WORKLOG.md deleted file mode 100644 index 38766eb3713..00000000000 --- a/WORKLOG.md +++ /dev/null @@ -1,33 +0,0 @@ -# Backup/restore feature - -## Initial assignment - -This effort covers the implementation of a proper backup / restore functionality for the Gno node. - -Currently, there is no block backup / restore functionality, but only transaction exports, and transaction replays. - -This can be cumbersome, considering users want to be able to quickly restore Gno chains from their trusted backup without having to sync with other peers. - -This functionality should work with direct node access (not over JSON-RPC), for example through special commands that utilize gRPC, as there is no need for users to back up remote node states. - -Successful outcome of this effort: - - The Gno node implements an efficient backup / restore functionality that a node operator can use to save chain state - -## Logs - -### Init - -The storage layer of a gnoland node is initialized in the gnoland command so this seems like the correct place for these features so I'm adding `gnoland backup create` and `gnoland backup restore` commands. - -The only stable database backend is goleveldb. After searching for `backup` on the goleveldb repo I didn't see any official way to backup the database. There is [an issue asking for it opened in 2016](https://github.com/syndtr/goleveldb/issues/135) but no response. - -We could backup every entry in the database to be agnostic of the backend but it would probably be much less efficient than working with the filesystem directly. - -The output format will be a single file compressed archive. It would be better to not depend on system tools and use pure a pure go library for that. - -The only lib I found that seem serious and to offer this functionality is https://github.com/mholt/archives - -After examination of the data needed to be saved, it seems only the "db" and "wal" dirs are needed - -Tar + Xz was chosen for the archive format but this could become options \ No newline at end of file From 47b5677e368a5e171d61647e90f15a644646bd58 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 13 Mar 2025 20:09:30 +0100 Subject: [PATCH 05/67] chore: revert mischange Signed-off-by: Norman --- contribs/gnomd/go.mod | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contribs/gnomd/go.mod b/contribs/gnomd/go.mod index 083d223feeb..d8f01bf82e3 100644 --- a/contribs/gnomd/go.mod +++ b/contribs/gnomd/go.mod @@ -1,4 +1,4 @@ -module github.com/gnolang/gno/contribs/tm2backup +module github.com/gnolang/gno/contribs/gnomd go 1.22 @@ -24,5 +24,3 @@ require ( golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect ) - -replace github.com/gnolang/gno => ../.. From ad2c1466f9f3971125cd1701b41e20d6be21c533 Mon Sep 17 00:00:00 2001 From: Norman Date: Thu, 13 Mar 2025 20:13:49 +0100 Subject: [PATCH 06/67] chore: improve backupsvc files Signed-off-by: Norman --- tm2/pkg/bft/node/backuppb/.gitignore | 1 + tm2/pkg/bft/node/backuppb/backup.pb.go | 204 +++++++++++++++++++++++++ tm2/{ => pkg/bft/node}/tools/tools.go | 2 + 3 files changed, 207 insertions(+) create mode 100644 tm2/pkg/bft/node/backuppb/.gitignore create mode 100644 tm2/pkg/bft/node/backuppb/backup.pb.go rename tm2/{ => pkg/bft/node}/tools/tools.go (87%) diff --git a/tm2/pkg/bft/node/backuppb/.gitignore b/tm2/pkg/bft/node/backuppb/.gitignore new file mode 100644 index 00000000000..fefa436400e --- /dev/null +++ b/tm2/pkg/bft/node/backuppb/.gitignore @@ -0,0 +1 @@ +!*.pb.go \ No newline at end of file diff --git a/tm2/pkg/bft/node/backuppb/backup.pb.go b/tm2/pkg/bft/node/backuppb/backup.pb.go new file mode 100644 index 00000000000..a6a34750e88 --- /dev/null +++ b/tm2/pkg/bft/node/backuppb/backup.pb.go @@ -0,0 +1,204 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.3 +// protoc (unknown) +// source: backuppb/backup.proto + +package backup + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type StreamBlocksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + StartHeight int64 `protobuf:"varint,1,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"` + EndHeight int64 `protobuf:"varint,2,opt,name=end_height,json=endHeight,proto3" json:"end_height,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamBlocksRequest) Reset() { + *x = StreamBlocksRequest{} + mi := &file_backuppb_backup_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamBlocksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamBlocksRequest) ProtoMessage() {} + +func (x *StreamBlocksRequest) ProtoReflect() protoreflect.Message { + mi := &file_backuppb_backup_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamBlocksRequest.ProtoReflect.Descriptor instead. +func (*StreamBlocksRequest) Descriptor() ([]byte, []int) { + return file_backuppb_backup_proto_rawDescGZIP(), []int{0} +} + +func (x *StreamBlocksRequest) GetStartHeight() int64 { + if x != nil { + return x.StartHeight + } + return 0 +} + +func (x *StreamBlocksRequest) GetEndHeight() int64 { + if x != nil { + return x.EndHeight + } + return 0 +} + +type StreamBlocksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Height int64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StreamBlocksResponse) Reset() { + *x = StreamBlocksResponse{} + mi := &file_backuppb_backup_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StreamBlocksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StreamBlocksResponse) ProtoMessage() {} + +func (x *StreamBlocksResponse) ProtoReflect() protoreflect.Message { + mi := &file_backuppb_backup_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StreamBlocksResponse.ProtoReflect.Descriptor instead. +func (*StreamBlocksResponse) Descriptor() ([]byte, []int) { + return file_backuppb_backup_proto_rawDescGZIP(), []int{1} +} + +func (x *StreamBlocksResponse) GetHeight() int64 { + if x != nil { + return x.Height + } + return 0 +} + +func (x *StreamBlocksResponse) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +var File_backuppb_backup_proto protoreflect.FileDescriptor + +var file_backuppb_backup_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x70, 0x62, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, + 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x74, 0x6d, 0x22, 0x57, 0x0a, 0x13, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, + 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, + 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69, + 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65, + 0x69, 0x67, 0x68, 0x74, 0x22, 0x42, 0x0a, 0x14, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x68, 0x65, + 0x69, 0x67, 0x68, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0x54, 0x0a, 0x0d, 0x42, 0x61, 0x63, 0x6b, + 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x43, 0x0a, 0x0c, 0x53, 0x74, 0x72, + 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x17, 0x2e, 0x74, 0x6d, 0x2e, 0x53, + 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x74, 0x6d, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x39, + 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6e, 0x6f, + 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x67, 0x6e, 0x6f, 0x2f, 0x74, 0x6d, 0x32, 0x2f, 0x70, 0x6b, 0x67, + 0x2f, 0x62, 0x66, 0x74, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x70, 0x62, 0x3b, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_backuppb_backup_proto_rawDescOnce sync.Once + file_backuppb_backup_proto_rawDescData = file_backuppb_backup_proto_rawDesc +) + +func file_backuppb_backup_proto_rawDescGZIP() []byte { + file_backuppb_backup_proto_rawDescOnce.Do(func() { + file_backuppb_backup_proto_rawDescData = protoimpl.X.CompressGZIP(file_backuppb_backup_proto_rawDescData) + }) + return file_backuppb_backup_proto_rawDescData +} + +var file_backuppb_backup_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_backuppb_backup_proto_goTypes = []any{ + (*StreamBlocksRequest)(nil), // 0: tm.StreamBlocksRequest + (*StreamBlocksResponse)(nil), // 1: tm.StreamBlocksResponse +} +var file_backuppb_backup_proto_depIdxs = []int32{ + 0, // 0: tm.BackupService.StreamBlocks:input_type -> tm.StreamBlocksRequest + 1, // 1: tm.BackupService.StreamBlocks:output_type -> tm.StreamBlocksResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_backuppb_backup_proto_init() } +func file_backuppb_backup_proto_init() { + if File_backuppb_backup_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_backuppb_backup_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_backuppb_backup_proto_goTypes, + DependencyIndexes: file_backuppb_backup_proto_depIdxs, + MessageInfos: file_backuppb_backup_proto_msgTypes, + }.Build() + File_backuppb_backup_proto = out.File + file_backuppb_backup_proto_rawDesc = nil + file_backuppb_backup_proto_goTypes = nil + file_backuppb_backup_proto_depIdxs = nil +} diff --git a/tm2/tools/tools.go b/tm2/pkg/bft/node/tools/tools.go similarity index 87% rename from tm2/tools/tools.go rename to tm2/pkg/bft/node/tools/tools.go index 437deac4755..c3d7578484a 100644 --- a/tm2/tools/tools.go +++ b/tm2/pkg/bft/node/tools/tools.go @@ -1,3 +1,5 @@ +//go:build tools + package tools import ( From e3ffb38e9eb1d251216d7b4df5526b23bd9b8817 Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 14 Mar 2025 21:27:58 +0100 Subject: [PATCH 07/67] chore: refacto backup service setup Signed-off-by: Norman --- tm2/Makefile | 6 +- .../bft/{node/backuppb => backup}/.gitignore | 0 tm2/pkg/bft/backup/backup_svc.go | 79 +++++++++++++++++++ .../{node => backup}/backuppb/backup.pb.go | 12 +-- .../{node => backup}/backuppb/backup.proto | 2 +- .../backuppbconnect}/backup.connect.go | 4 +- tm2/pkg/bft/{node => backup}/buf.gen.yaml | 0 tm2/pkg/bft/{node => backup}/buf.yaml | 0 tm2/pkg/bft/{node => backup}/tools/tools.go | 0 tm2/pkg/bft/config/config.go | 2 + tm2/pkg/bft/node/backup_svc.go | 58 -------------- tm2/pkg/bft/node/node.go | 34 +++++--- 12 files changed, 114 insertions(+), 83 deletions(-) rename tm2/pkg/bft/{node/backuppb => backup}/.gitignore (100%) create mode 100644 tm2/pkg/bft/backup/backup_svc.go rename tm2/pkg/bft/{node => backup}/backuppb/backup.pb.go (95%) rename tm2/pkg/bft/{node => backup}/backuppb/backup.proto (78%) rename tm2/pkg/bft/{node/backuppb/backupconnect => backup/backuppb/backuppbconnect}/backup.connect.go (98%) rename tm2/pkg/bft/{node => backup}/buf.gen.yaml (100%) rename tm2/pkg/bft/{node => backup}/buf.yaml (100%) rename tm2/pkg/bft/{node => backup}/tools/tools.go (100%) delete mode 100644 tm2/pkg/bft/node/backup_svc.go diff --git a/tm2/Makefile b/tm2/Makefile index a72af3a056f..443f0e13d07 100644 --- a/tm2/Makefile +++ b/tm2/Makefile @@ -69,6 +69,6 @@ generate: go generate -x ./... $(MAKE) fmt -.PHONY: generate.grpc -generate.grpc: - cd pkg/bft/node && buf generate \ No newline at end of file +.PHONY: generate.backup-grpc +generate.backup-grpc: + cd pkg/bft/backup && buf generate \ No newline at end of file diff --git a/tm2/pkg/bft/node/backuppb/.gitignore b/tm2/pkg/bft/backup/.gitignore similarity index 100% rename from tm2/pkg/bft/node/backuppb/.gitignore rename to tm2/pkg/bft/backup/.gitignore diff --git a/tm2/pkg/bft/backup/backup_svc.go b/tm2/pkg/bft/backup/backup_svc.go new file mode 100644 index 00000000000..f6cec074a44 --- /dev/null +++ b/tm2/pkg/bft/backup/backup_svc.go @@ -0,0 +1,79 @@ +package backup + +import ( + "context" + "fmt" + "net/http" + + "connectrpc.com/connect" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" + "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb/backuppbconnect" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +type Config struct { + // Address for the backup server to listen on. Empty means disabled. + ListenAddress string `json:"laddr" toml:"laddr" comment:"Address for the backup server to listen on. Empty means disabled."` +} + +type blockStore interface { + Height() int64 + LoadBlock(height int64) *types.Block +} + +func NewServer(conf *Config, store blockStore) *http.Server { + backupServ := &backupServer{store: store} + mux := http.NewServeMux() + path, handler := backuppbconnect.NewBackupServiceHandler(backupServ) + mux.Handle(path, handler) + return &http.Server{Addr: conf.ListenAddress, Handler: h2c.NewHandler(mux, &http2.Server{})} +} + +type backupServer struct { + store blockStore +} + +// StreamBlocks implements backupconnect.BackupServiceHandler. +func (b *backupServer) StreamBlocks(_ context.Context, req *connect.Request[backuppb.StreamBlocksRequest], stream *connect.ServerStream[backuppb.StreamBlocksResponse]) error { + startHeight := req.Msg.StartHeight + if startHeight == 0 { + startHeight = 1 + } + if startHeight < 1 { + return fmt.Errorf("start height must be >= 1, got %d", startHeight) + } + + endHeight := req.Msg.EndHeight + blockStoreHeight := b.store.Height() + if endHeight == 0 { + endHeight = blockStoreHeight + } else if endHeight > blockStoreHeight { + return fmt.Errorf("end height must be <= %d", blockStoreHeight) + } + + if startHeight > endHeight { + return fmt.Errorf("end height must be >= than start height") + } + + for height := startHeight; height <= endHeight; height++ { + block := b.store.LoadBlock(height) + data, err := amino.Marshal(block) + if err != nil { + return err + } + + if err := stream.Send(&backuppb.StreamBlocksResponse{ + Height: height, + Data: data, + }); err != nil { + return err + } + } + + return nil +} + +var _ backuppbconnect.BackupServiceHandler = (*backupServer)(nil) diff --git a/tm2/pkg/bft/node/backuppb/backup.pb.go b/tm2/pkg/bft/backup/backuppb/backup.pb.go similarity index 95% rename from tm2/pkg/bft/node/backuppb/backup.pb.go rename to tm2/pkg/bft/backup/backuppb/backup.pb.go index a6a34750e88..91625ed444b 100644 --- a/tm2/pkg/bft/node/backuppb/backup.pb.go +++ b/tm2/pkg/bft/backup/backuppb/backup.pb.go @@ -4,7 +4,7 @@ // protoc (unknown) // source: backuppb/backup.proto -package backup +package backuppb import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" @@ -143,12 +143,12 @@ var file_backuppb_backup_proto_rawDesc = []byte{ 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x17, 0x2e, 0x74, 0x6d, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x74, 0x6d, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, - 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x39, - 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6e, 0x6f, + 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x3d, + 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6e, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x67, 0x6e, 0x6f, 0x2f, 0x74, 0x6d, 0x32, 0x2f, 0x70, 0x6b, 0x67, - 0x2f, 0x62, 0x66, 0x74, 0x2f, 0x6e, 0x6f, 0x64, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, - 0x70, 0x62, 0x3b, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x2f, 0x62, 0x66, 0x74, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x2f, 0x62, 0x61, 0x63, 0x6b, + 0x75, 0x70, 0x70, 0x62, 0x3b, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x70, 0x62, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/tm2/pkg/bft/node/backuppb/backup.proto b/tm2/pkg/bft/backup/backuppb/backup.proto similarity index 78% rename from tm2/pkg/bft/node/backuppb/backup.proto rename to tm2/pkg/bft/backup/backuppb/backup.proto index 417a8c03cea..2d0296d5d5b 100644 --- a/tm2/pkg/bft/node/backuppb/backup.proto +++ b/tm2/pkg/bft/backup/backuppb/backup.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package tm; -option go_package = "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb;backup"; +option go_package = "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb;backuppb"; service BackupService { rpc StreamBlocks (StreamBlocksRequest) returns (stream StreamBlocksResponse); diff --git a/tm2/pkg/bft/node/backuppb/backupconnect/backup.connect.go b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go similarity index 98% rename from tm2/pkg/bft/node/backuppb/backupconnect/backup.connect.go rename to tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go index 9e150faf45c..f0afc1fe508 100644 --- a/tm2/pkg/bft/node/backuppb/backupconnect/backup.connect.go +++ b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go @@ -2,13 +2,13 @@ // // Source: backuppb/backup.proto -package backupconnect +package backuppbconnect import ( connect "connectrpc.com/connect" context "context" errors "errors" - backuppb "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb" + backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" http "net/http" strings "strings" ) diff --git a/tm2/pkg/bft/node/buf.gen.yaml b/tm2/pkg/bft/backup/buf.gen.yaml similarity index 100% rename from tm2/pkg/bft/node/buf.gen.yaml rename to tm2/pkg/bft/backup/buf.gen.yaml diff --git a/tm2/pkg/bft/node/buf.yaml b/tm2/pkg/bft/backup/buf.yaml similarity index 100% rename from tm2/pkg/bft/node/buf.yaml rename to tm2/pkg/bft/backup/buf.yaml diff --git a/tm2/pkg/bft/node/tools/tools.go b/tm2/pkg/bft/backup/tools/tools.go similarity index 100% rename from tm2/pkg/bft/node/tools/tools.go rename to tm2/pkg/bft/backup/tools/tools.go diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go index 1a01686f4bd..836ee7b945d 100644 --- a/tm2/pkg/bft/config/config.go +++ b/tm2/pkg/bft/config/config.go @@ -10,6 +10,7 @@ import ( "dario.cat/mergo" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/bft/backup" cns "github.com/gnolang/gno/tm2/pkg/bft/consensus/config" mem "github.com/gnolang/gno/tm2/pkg/bft/mempool/config" rpc "github.com/gnolang/gno/tm2/pkg/bft/rpc/config" @@ -57,6 +58,7 @@ type Config struct { TxEventStore *eventstore.Config `json:"tx_event_store" toml:"tx_event_store" comment:"##### event store #####"` Telemetry *telemetry.Config `json:"telemetry" toml:"telemetry" comment:"##### node telemetry #####"` Application *sdk.AppConfig `json:"application" toml:"application" comment:"##### app settings #####"` + Backup *backup.Config `json:"backup" toml:"backup" comment:"##### backup server configuration options #####"` } // DefaultConfig returns a default configuration for a Tendermint node diff --git a/tm2/pkg/bft/node/backup_svc.go b/tm2/pkg/bft/node/backup_svc.go deleted file mode 100644 index 610ce955eb0..00000000000 --- a/tm2/pkg/bft/node/backup_svc.go +++ /dev/null @@ -1,58 +0,0 @@ -package node - -import ( - "context" - "fmt" - - "connectrpc.com/connect" - "github.com/gnolang/gno/tm2/pkg/amino" - backup "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb" - "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb/backupconnect" - "github.com/gnolang/gno/tm2/pkg/bft/store" -) - -type backupServer struct { - store *store.BlockStore -} - -// StreamBlocks implements backupconnect.BackupServiceHandler. -func (b *backupServer) StreamBlocks(_ context.Context, req *connect.Request[backup.StreamBlocksRequest], stream *connect.ServerStream[backup.StreamBlocksResponse]) error { - startHeight := req.Msg.StartHeight - if startHeight == 0 { - startHeight = 1 - } - if startHeight < 1 { - return fmt.Errorf("start height must be >= 1, got %d", startHeight) - } - - endHeight := req.Msg.EndHeight - blockStoreHeight := b.store.Height() - if endHeight == 0 { - endHeight = blockStoreHeight - } else if endHeight > blockStoreHeight { - return fmt.Errorf("end height must be <= %d", blockStoreHeight) - } - - if startHeight > endHeight { - return fmt.Errorf("end height must be >= than start height") - } - - for height := startHeight; height <= endHeight; height++ { - block := b.store.LoadBlock(height) - data, err := amino.Marshal(block) - if err != nil { - return err - } - - if err := stream.Send(&backup.StreamBlocksResponse{ - Height: height, - Data: data, - }); err != nil { - return err - } - } - - return nil -} - -var _ backupconnect.BackupServiceHandler = (*backupServer)(nil) diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index 495cc9dc92e..ae569d2c32b 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -4,6 +4,7 @@ package node // is enabled by the user by setting a profiling address import ( + "context" "fmt" "log/slog" "net" @@ -13,14 +14,12 @@ import ( "time" "github.com/gnolang/gno/tm2/pkg/bft/appconn" - "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb/backupconnect" + "github.com/gnolang/gno/tm2/pkg/bft/backup" "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" "github.com/gnolang/gno/tm2/pkg/p2p/conn" "github.com/gnolang/gno/tm2/pkg/p2p/discovery" p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/rs/cors" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" "github.com/gnolang/gno/tm2/pkg/amino" bc "github.com/gnolang/gno/tm2/pkg/bft/blockchain" @@ -174,6 +173,7 @@ type Node struct { txEventStore eventstore.TxEventStore eventStoreService *eventstore.Service firstBlockSignal <-chan struct{} + backupServer *http.Server } func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *store.BlockStore, stateDB dbm.DB, err error) { @@ -604,16 +604,15 @@ func (n *Node) OnStart() error { n.rpcListeners = listeners } - // start backup service - backupServ := &backupServer{store: n.blockStore} - mux := http.NewServeMux() - path, handler := backupconnect.NewBackupServiceHandler(backupServ) - mux.Handle(path, handler) - go http.ListenAndServe( - "localhost:4242", - // Use h2c so we can serve HTTP/2 without TLS. - h2c.NewHandler(mux, &http2.Server{}), - ) + // start backup server if requested + if n.config.Backup.ListenAddress != "" { + n.backupServer = backup.NewServer(n.config.Backup, n.blockStore) + go func() { + if err := n.backupServer.ListenAndServe(); err != nil { + n.Logger.Error("Backup server", "err", err) + } + }() + } // Start the transport. // The listen address for the transport needs to be an address within reach of the machine NIC @@ -686,6 +685,15 @@ func (n *Node) OnStop() { n.isListening = false + // stop the backup server if started + if n.backupServer != nil { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + if err := n.backupServer.Shutdown(ctx); err != nil { + n.Logger.Error("Error closing backup server", "err", err) + } + } + // finally stop the listeners / external services for _, l := range n.rpcListeners { n.Logger.Info("Closing rpc listener", "listener", l) From 6bc40e9edae7884f41e90751129c68956c1a9655 Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 14 Mar 2025 21:45:06 +0100 Subject: [PATCH 08/67] fix: changed paths and go mod tidy Signed-off-by: Norman --- contribs/gnodev/go.mod | 1 + contribs/gnodev/go.sum | 2 ++ contribs/gnofaucet/go.sum | 2 ++ contribs/gnogenesis/go.mod | 1 + contribs/gnogenesis/go.sum | 2 ++ contribs/gnohealth/go.sum | 2 ++ contribs/gnokeykc/go.sum | 2 ++ contribs/gnomigrate/go.mod | 1 + contribs/gnomigrate/go.sum | 2 ++ contribs/tm2backup/main.go | 6 +++--- misc/autocounterd/go.sum | 2 ++ misc/loop/go.mod | 1 + misc/loop/go.sum | 2 ++ tm2/pkg/bft/backup/backup_svc.go | 2 +- 14 files changed, 24 insertions(+), 4 deletions(-) diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index f520e2fbb79..756c37e4732 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -27,6 +27,7 @@ require ( ) require ( + connectrpc.com/connect v1.18.1 // indirect dario.cat/mergo v1.0.1 // indirect github.com/alecthomas/chroma/v2 v2.15.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index f4bf32aafd5..65c9c1b9da4 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum index e6743b75960..ec456a2bebc 100644 --- a/contribs/gnofaucet/go.sum +++ b/contribs/gnofaucet/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod index e03ad1dde42..03efe6b430a 100644 --- a/contribs/gnogenesis/go.mod +++ b/contribs/gnogenesis/go.mod @@ -10,6 +10,7 @@ require ( replace github.com/gnolang/gno => ../.. require ( + connectrpc.com/connect v1.18.1 // indirect dario.cat/mergo v1.0.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/btcsuite/btcd/btcutil v1.1.6 // indirect diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum index e3462f9c431..19617527ad2 100644 --- a/contribs/gnogenesis/go.sum +++ b/contribs/gnogenesis/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= diff --git a/contribs/gnohealth/go.sum b/contribs/gnohealth/go.sum index 3c8b5de45f2..886c854981a 100644 --- a/contribs/gnohealth/go.sum +++ b/contribs/gnohealth/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum index 6b4f81dfcf5..345ca9f106a 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod index 0381c6acd01..f47cd18b10a 100644 --- a/contribs/gnomigrate/go.mod +++ b/contribs/gnomigrate/go.mod @@ -10,6 +10,7 @@ require ( replace github.com/gnolang/gno => ../.. require ( + connectrpc.com/connect v1.18.1 // indirect dario.cat/mergo v1.0.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/btcsuite/btcd/btcutil v1.1.6 // indirect diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum index e3462f9c431..19617527ad2 100644 --- a/contribs/gnomigrate/go.sum +++ b/contribs/gnomigrate/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= diff --git a/contribs/tm2backup/main.go b/contribs/tm2backup/main.go index 83cf6249428..f36f441dd52 100644 --- a/contribs/tm2backup/main.go +++ b/contribs/tm2backup/main.go @@ -13,8 +13,8 @@ import ( "strconv" "connectrpc.com/connect" - backup "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb" - "github.com/gnolang/gno/tm2/pkg/bft/node/backuppb/backupconnect" + backup "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" + "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb/backuppbconnect" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gofrs/flock" "github.com/klauspost/compress/zstd" @@ -115,7 +115,7 @@ func execBackup(ctx context.Context, c *backupCfg, io commands.IO) error { } io.Println(prefix, "at height", height) - client := backupconnect.NewBackupServiceClient( + client := backuppbconnect.NewBackupServiceClient( http.DefaultClient, c.remote, connect.WithGRPC(), diff --git a/misc/autocounterd/go.sum b/misc/autocounterd/go.sum index 6d6d87fa01a..3140fe4ff78 100644 --- a/misc/autocounterd/go.sum +++ b/misc/autocounterd/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= diff --git a/misc/loop/go.mod b/misc/loop/go.mod index 7d0252da82c..6aa299b222a 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -14,6 +14,7 @@ require ( replace github.com/gnolang/gno => ../.. require ( + connectrpc.com/connect v1.18.1 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/misc/loop/go.sum b/misc/loop/go.sum index c5aed820f5e..85a697bc7d8 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -1,3 +1,5 @@ +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= diff --git a/tm2/pkg/bft/backup/backup_svc.go b/tm2/pkg/bft/backup/backup_svc.go index f6cec074a44..28e6aa734cf 100644 --- a/tm2/pkg/bft/backup/backup_svc.go +++ b/tm2/pkg/bft/backup/backup_svc.go @@ -36,7 +36,7 @@ type backupServer struct { store blockStore } -// StreamBlocks implements backupconnect.BackupServiceHandler. +// StreamBlocks implements backuppbconnect.BackupServiceHandler. func (b *backupServer) StreamBlocks(_ context.Context, req *connect.Request[backuppb.StreamBlocksRequest], stream *connect.ServerStream[backuppb.StreamBlocksResponse]) error { startHeight := req.Msg.StartHeight if startHeight == 0 { From 43cc3104a0bc48fabb3ecefeaa0a11f80a65bf42 Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 14 Mar 2025 22:06:11 +0100 Subject: [PATCH 09/67] chore: fail earlier Signed-off-by: Norman --- contribs/tm2backup/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contribs/tm2backup/main.go b/contribs/tm2backup/main.go index f36f441dd52..56ffdfffedb 100644 --- a/contribs/tm2backup/main.go +++ b/contribs/tm2backup/main.go @@ -100,15 +100,15 @@ func execBackup(ctx context.Context, c *backupCfg, io commands.IO) error { return fmt.Errorf("failed to read next height: %w", err) } + if c.endHeight != 0 && int64(c.endHeight) < nextHeight { + return fmt.Errorf("invalid input: requested end height is smaller than the next height in output directory (%d), use a different output directory or a valid end height", nextHeight) + } + height, err := getStartHeight(int64(c.startHeight), nextHeight) if err != nil { return fmt.Errorf("failed to decide start height: %w", err) } - if c.endHeight != 0 && int64(c.endHeight) < nextHeight { - return fmt.Errorf("invalid input: requested end height is smaller than the next height in output directory (%d), use a different output directory or a valid end height", nextHeight) - } - prefix := "starting" if nextHeight != -1 { prefix = "resuming" From 79605c6ab39a5b501d1a888ebba6b7ebb0267f39 Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 14 Mar 2025 22:06:25 +0100 Subject: [PATCH 10/67] =?UTF-8?q?chore:=20don't=20crash=20if=20backup=20co?= =?UTF-8?q?nfig=20does=20not=20exists=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Norman --- tm2/pkg/bft/node/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index ae569d2c32b..7f4ccbcf78b 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -605,7 +605,7 @@ func (n *Node) OnStart() error { } // start backup server if requested - if n.config.Backup.ListenAddress != "" { + if n.config.Backup != nil && n.config.Backup.ListenAddress != "" { n.backupServer = backup.NewServer(n.config.Backup, n.blockStore) go func() { if err := n.backupServer.ListenAndServe(); err != nil { From a99603f0ec5b836eea250e2d19c2498a3de59936 Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 14 Mar 2025 22:20:15 +0100 Subject: [PATCH 11/67] fix: prevent slowloris Signed-off-by: Norman --- tm2/pkg/bft/backup/backup_svc.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tm2/pkg/bft/backup/backup_svc.go b/tm2/pkg/bft/backup/backup_svc.go index 28e6aa734cf..ec6cad3354a 100644 --- a/tm2/pkg/bft/backup/backup_svc.go +++ b/tm2/pkg/bft/backup/backup_svc.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "time" "connectrpc.com/connect" "github.com/gnolang/gno/tm2/pkg/amino" @@ -29,7 +30,7 @@ func NewServer(conf *Config, store blockStore) *http.Server { mux := http.NewServeMux() path, handler := backuppbconnect.NewBackupServiceHandler(backupServ) mux.Handle(path, handler) - return &http.Server{Addr: conf.ListenAddress, Handler: h2c.NewHandler(mux, &http2.Server{})} + return &http.Server{Addr: conf.ListenAddress, Handler: h2c.NewHandler(mux, &http2.Server{}), ReadHeaderTimeout: time.Second * 5} } type backupServer struct { From aae47a3083a25917bc6d2aa85532d0ad806e8e9e Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 14 Mar 2025 22:23:46 +0100 Subject: [PATCH 12/67] chore: format Signed-off-by: Norman --- tm2/Makefile | 4 +++- tm2/pkg/bft/backup/backuppb/backup.pb.go | 5 +++-- .../bft/backup/backuppb/backuppbconnect/backup.connect.go | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/tm2/Makefile b/tm2/Makefile index 443f0e13d07..7cdec8c37aa 100644 --- a/tm2/Makefile +++ b/tm2/Makefile @@ -71,4 +71,6 @@ generate: .PHONY: generate.backup-grpc generate.backup-grpc: - cd pkg/bft/backup && buf generate \ No newline at end of file + cd pkg/bft/backup && buf generate + go run -modfile ../misc/devdeps/go.mod mvdan.cc/gofumpt -w pkg/bft/backup + go run -modfile ../misc/devdeps/go.mod golang.org/x/tools/cmd/goimports -w pkg/bft/backup \ No newline at end of file diff --git a/tm2/pkg/bft/backup/backuppb/backup.pb.go b/tm2/pkg/bft/backup/backuppb/backup.pb.go index 91625ed444b..ca23c4cce23 100644 --- a/tm2/pkg/bft/backup/backuppb/backup.pb.go +++ b/tm2/pkg/bft/backup/backuppb/backup.pb.go @@ -7,10 +7,11 @@ package backuppb import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go index f0afc1fe508..81ec75cdaa0 100644 --- a/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go +++ b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go @@ -5,12 +5,13 @@ package backuppbconnect import ( - connect "connectrpc.com/connect" context "context" errors "errors" - backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" http "net/http" strings "strings" + + connect "connectrpc.com/connect" + backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" ) // This is a compile-time assertion to ensure that this generated file and the connect package are From 69a2e6414ff04a2871332310eac1495b1195f53b Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 14 Mar 2025 22:30:14 +0100 Subject: [PATCH 13/67] fix: populate backup section default Signed-off-by: Norman --- tm2/pkg/bft/backup/backup_svc.go | 4 ++++ tm2/pkg/bft/config/config.go | 1 + 2 files changed, 5 insertions(+) diff --git a/tm2/pkg/bft/backup/backup_svc.go b/tm2/pkg/bft/backup/backup_svc.go index ec6cad3354a..78c7583976c 100644 --- a/tm2/pkg/bft/backup/backup_svc.go +++ b/tm2/pkg/bft/backup/backup_svc.go @@ -20,6 +20,10 @@ type Config struct { ListenAddress string `json:"laddr" toml:"laddr" comment:"Address for the backup server to listen on. Empty means disabled."` } +func DefaultConfig() *Config { + return &Config{} +} + type blockStore interface { Height() int64 LoadBlock(height int64) *types.Block diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go index 836ee7b945d..82c04a0a67c 100644 --- a/tm2/pkg/bft/config/config.go +++ b/tm2/pkg/bft/config/config.go @@ -72,6 +72,7 @@ func DefaultConfig() *Config { TxEventStore: eventstore.DefaultEventStoreConfig(), Telemetry: telemetry.DefaultTelemetryConfig(), Application: sdk.DefaultAppConfig(), + Backup: backup.DefaultConfig(), } } From f102c59cf116f16d5a555271992e33bf1da1137a Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 14 Mar 2025 22:33:04 +0100 Subject: [PATCH 14/67] chore: format Signed-off-by: Norman --- contribs/tm2backup/main.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contribs/tm2backup/main.go b/contribs/tm2backup/main.go index 56ffdfffedb..e1a6d659e4a 100644 --- a/contribs/tm2backup/main.go +++ b/contribs/tm2backup/main.go @@ -80,8 +80,10 @@ func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { // XXX: versioning of output directory -const nextHeightFilename = "next-height.txt" -const chunkSize = 100 +const ( + nextHeightFilename = "next-height.txt" + chunkSize = 100 +) func execBackup(ctx context.Context, c *backupCfg, io commands.IO) error { if err := validateInput(c); err != nil { From 381bdd8d82d47469a99c0312a31e803878d0e8f0 Mon Sep 17 00:00:00 2001 From: Norman Date: Fri, 14 Mar 2025 23:09:03 +0100 Subject: [PATCH 15/67] chore: better log for backup server stop Signed-off-by: Norman --- tm2/pkg/bft/node/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index 7f4ccbcf78b..5151a582b54 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -609,7 +609,7 @@ func (n *Node) OnStart() error { n.backupServer = backup.NewServer(n.config.Backup, n.blockStore) go func() { if err := n.backupServer.ListenAndServe(); err != nil { - n.Logger.Error("Backup server", "err", err) + n.Logger.Info("Backup server stopped", "err", err) } }() } From 82982d9510edfef7b98a041417712019770c11ff Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 15 Mar 2025 08:18:31 +0100 Subject: [PATCH 16/67] chore: use constant Signed-off-by: Norman --- contribs/tm2backup/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contribs/tm2backup/main.go b/contribs/tm2backup/main.go index e1a6d659e4a..d6cb2ba2c6e 100644 --- a/contribs/tm2backup/main.go +++ b/contribs/tm2backup/main.go @@ -274,11 +274,11 @@ func getStartHeight(requestedStartHeight int64, outputDirNextHeight int64) (int6 height = outputDirNextHeight } - // align: 4 -> 1, 100 -> 1, 101 -> 101, 150 -> 101 + // align: 4 -> 1, 100 -> 1, 101 -> 101, 150 -> 101 (with chunkSize == 100) // we simply overwrite the latest chunk if it is partial because it's not expensive height -= (height - 1) % chunkSize - if height < 1 || height%100 != 1 { + if height < 1 || height%chunkSize != 1 { return 0, fmt.Errorf("unexpected start height %d", height) } From 585d637491db68e05b712b6f6c9b50eb94e4f8e5 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 19 Mar 2025 08:21:49 +0100 Subject: [PATCH 17/67] feat: restore command Signed-off-by: Norman --- contribs/tm2backup/go.mod | 17 +- contribs/tm2backup/go.sum | 130 ++++++++++++++ contribs/tm2backup/main.go | 274 +++++------------------------- contribs/tm2backup/testfile | 0 gno.land/cmd/gnoland/restore.go | 73 ++++++++ gno.land/cmd/gnoland/root.go | 1 + gno.land/cmd/gnoland/start.go | 97 ++++++----- go.mod | 2 + go.sum | 4 + tm2/pkg/bft/backup/v1/reader.go | 153 +++++++++++++++++ tm2/pkg/bft/backup/v1/util.go | 88 ++++++++++ tm2/pkg/bft/backup/v1/writer.go | 239 ++++++++++++++++++++++++++ tm2/pkg/bft/blockchain/reactor.go | 45 +++++ tm2/pkg/bft/node/node.go | 13 +- 14 files changed, 857 insertions(+), 279 deletions(-) create mode 100644 contribs/tm2backup/testfile create mode 100644 gno.land/cmd/gnoland/restore.go create mode 100644 tm2/pkg/bft/backup/v1/reader.go create mode 100644 tm2/pkg/bft/backup/v1/util.go create mode 100644 tm2/pkg/bft/backup/v1/writer.go diff --git a/contribs/tm2backup/go.mod b/contribs/tm2backup/go.mod index 9addf0cae72..d34a2b35e23 100644 --- a/contribs/tm2backup/go.mod +++ b/contribs/tm2backup/go.mod @@ -9,13 +9,26 @@ replace github.com/gnolang/gno => ../.. require ( connectrpc.com/connect v1.18.1 github.com/gnolang/gno v0.0.0-00010101000000-000000000000 - github.com/gofrs/flock v0.12.1 - github.com/klauspost/compress v1.18.0 + go.uber.org/zap v1.27.0 ) require ( + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/btcsuite/btcd/btcutil v1.1.6 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/gofrs/flock v0.12.1 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.36.3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contribs/tm2backup/go.sum b/contribs/tm2backup/go.sum index 998db821f7c..bc4c1307a67 100644 --- a/contribs/tm2backup/go.sum +++ b/contribs/tm2backup/go.sum @@ -1,28 +1,158 @@ connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= +github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= +github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contribs/tm2backup/main.go b/contribs/tm2backup/main.go index d6cb2ba2c6e..0abffa16811 100644 --- a/contribs/tm2backup/main.go +++ b/contribs/tm2backup/main.go @@ -1,23 +1,21 @@ package main import ( - "archive/tar" "context" - "errors" "flag" "fmt" - "math" "net/http" "os" - "path/filepath" - "strconv" "connectrpc.com/connect" - backup "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb/backuppbconnect" + "github.com/gnolang/gno/tm2/pkg/bft/backup/v1" + "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gofrs/flock" - "github.com/klauspost/compress/zstd" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" ) func main() { @@ -47,8 +45,8 @@ func newRootCmd(io commands.IO) *commands.Command { type backupCfg struct { remote string outDir string - startHeight uint64 - endHeight uint64 + startHeight int64 + endHeight int64 } func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { @@ -64,13 +62,13 @@ func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { "blocks-backup", "Output directory.", ) - fs.Uint64Var( + fs.Int64Var( &c.startHeight, "start", 0, - fmt.Sprintf("Start height. Will be aligned at a multiple of %d blocks. This can't be used when resuming from an existing output directory.", chunkSize), + fmt.Sprintf("Start height. Will be aligned at a multiple of %d blocks. This can't be used when resuming from an existing output directory.", backup.ChunkSize), ) - fs.Uint64Var( + fs.Int64Var( &c.endHeight, "end", 0, @@ -78,228 +76,46 @@ func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { ) } -// XXX: versioning of output directory - -const ( - nextHeightFilename = "next-height.txt" - chunkSize = 100 -) - -func execBackup(ctx context.Context, c *backupCfg, io commands.IO) error { - if err := validateInput(c); err != nil { - return fmt.Errorf("invalid input: %w", err) - } - - unlock, err := lockOutputDir(c, io) - if err != nil { - return fmt.Errorf("failed to lock output directory: %w", err) - } - defer unlock() - - nextHeightFP := filepath.Join(c.outDir, nextHeightFilename) - nextHeight, err := readNextHeight(nextHeightFP) - if err != nil { - return fmt.Errorf("failed to read next height: %w", err) - } - - if c.endHeight != 0 && int64(c.endHeight) < nextHeight { - return fmt.Errorf("invalid input: requested end height is smaller than the next height in output directory (%d), use a different output directory or a valid end height", nextHeight) - } - - height, err := getStartHeight(int64(c.startHeight), nextHeight) - if err != nil { - return fmt.Errorf("failed to decide start height: %w", err) - } - - prefix := "starting" - if nextHeight != -1 { - prefix = "resuming" - } - io.Println(prefix, "at height", height) - - client := backuppbconnect.NewBackupServiceClient( - http.DefaultClient, - c.remote, - connect.WithGRPC(), - ) - res, err := client.StreamBlocks( - ctx, - connect.NewRequest(&backup.StreamBlocksRequest{ - StartHeight: height, - EndHeight: int64(c.endHeight), - }), - ) - if err != nil { - return err - } - - chunkStart := height - - nextChunkFP := filepath.Join(c.outDir, "next-chunk.tar.zst") - outFile, err := os.OpenFile(nextChunkFP, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o664) - if err != nil { - return err - } - zstw, err := zstd.NewWriter(outFile) - if err != nil { - return err - } - w := tar.NewWriter(zstw) - - finalizeChunk := func(last bool) error { - if err := w.Close(); err != nil { - return err - } - if err := zstw.Close(); err != nil { - return err - } - if err := outFile.Close(); err != nil { - return err - } - - // using padding for filename to match chunk order and lexicographical order - chunkFP := filepath.Join(c.outDir, fmt.Sprintf("%019d.tm2blocks.tar.zst", chunkStart)) - if err := os.Rename(nextChunkFP, chunkFP); err != nil { - return err - } - - if err := os.WriteFile(nextHeightFP, []byte(strconv.FormatInt(height, 10)), 0o664); err != nil { - return err - } - - io.Println("wrote blocks", chunkStart, "to", height-1, "at", chunkFP) - - if last { - return nil - } - - outFile, err = os.OpenFile(nextChunkFP, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o664) - if err != nil { - return err - } - zstw, err = zstd.NewWriter(outFile) +func execBackup(ctx context.Context, c *backupCfg, io commands.IO) (resErr error) { + config := zap.NewDevelopmentConfig() + config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + core := zapcore.NewCore(zapcore.NewConsoleEncoder(config.EncoderConfig), zapcore.AddSync(io.Out()), config.Level) + logger := zap.New(core) + + return backup.WithWriter(c.outDir, c.startHeight, c.endHeight, logger, func(startHeight int64, write func(block *types.Block) error) error { + client := backuppbconnect.NewBackupServiceClient( + http.DefaultClient, + c.remote, + connect.WithGRPC(), + ) + res, err := client.StreamBlocks( + ctx, + connect.NewRequest(&backuppb.StreamBlocksRequest{ + StartHeight: startHeight, + EndHeight: int64(c.endHeight), + }), + ) if err != nil { - return err + return fmt.Errorf("open blocks stream: %w", err) } - w = tar.NewWriter(zstw) - - chunkStart = height - return nil - } + for { + ok := res.Receive() + if !ok { + return res.Err() + } - for { - ok := res.Receive() - if !ok { - if res.Err() != nil { + // XXX: we unmarshal the block which is remarshalled in the backup writer just after. + // This choice was made to have a more stable interface than bytes. + // If this proves to be a bottleneck, we should revist. + block := &types.Block{} + if err := amino.Unmarshal(res.Msg().Data, block); err != nil { return err } - return finalizeChunk(true) - } - msg := res.Msg() - - header := &tar.Header{ - Name: fmt.Sprintf("%d", msg.Height), - Size: int64(len(msg.Data)), - Mode: 0o664, - } - if err := w.WriteHeader(header); err != nil { - return err - } - - _, err := w.Write(msg.Data) - if err != nil { - return err - } - - height += 1 - if height%chunkSize != 1 { - continue - } - - if err := finalizeChunk(false); err != nil { - return err - } - - // don't finalize twice if the requested endHeight is a multiple of chunkSize - if height-1 == int64(c.endHeight) { - return nil - } - } -} - -func validateInput(c *backupCfg) error { - if c.startHeight > math.MaxInt64 { - return fmt.Errorf("start must be <= %d", math.MaxInt64) - } - - if c.endHeight > math.MaxInt64 { - return fmt.Errorf("end must be <= %d", math.MaxInt64) - } - - return nil -} - -func lockOutputDir(c *backupCfg, io commands.IO) (func(), error) { - if err := os.MkdirAll(c.outDir, 0o775); err != nil { - return nil, fmt.Errorf("failed to ensure output directory exists: %w", err) - } - - fileLock := flock.New(filepath.Join(c.outDir, "blocks.lock")) - locked, err := fileLock.TryLock() - if err != nil { - return nil, err - } - if !locked { - return nil, errors.New("failed to acquire lock on output directory") - } - return func() { - if err := fileLock.Unlock(); err != nil { - io.ErrPrintln(err) - } - }, nil -} - -func getStartHeight(requestedStartHeight int64, outputDirNextHeight int64) (int64, error) { - height := int64(1) - - if requestedStartHeight != 0 && outputDirNextHeight != -1 { - return 0, errors.New("can't request a start height when resuming, use a different output directory or no start height") - } - - if requestedStartHeight != 0 { - height = requestedStartHeight - } else { - height = outputDirNextHeight - } - - // align: 4 -> 1, 100 -> 1, 101 -> 101, 150 -> 101 (with chunkSize == 100) - // we simply overwrite the latest chunk if it is partial because it's not expensive - height -= (height - 1) % chunkSize - - if height < 1 || height%chunkSize != 1 { - return 0, fmt.Errorf("unexpected start height %d", height) - } - - return height, nil -} - -func readNextHeight(nextHeightFP string) (int64, error) { - nextHeightBz, err := os.ReadFile(nextHeightFP) - switch { - case os.IsNotExist(err): - return -1, nil - case err != nil: - return 0, err - default: - nextHeight, err := strconv.ParseInt(string(nextHeightBz), 10, 64) - if err != nil { - return 0, err - } - if nextHeight < 1 { - return 0, fmt.Errorf("unexpected next height %d", nextHeight) + if err := write(block); err != nil { + return err + } } - return nextHeight, nil - } + }) } diff --git a/contribs/tm2backup/testfile b/contribs/tm2backup/testfile new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gno.land/cmd/gnoland/restore.go b/gno.land/cmd/gnoland/restore.go new file mode 100644 index 00000000000..da3ca948ae8 --- /dev/null +++ b/gno.land/cmd/gnoland/restore.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/backup/v1" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type restoreCfg struct { + nodeCfg + backupDir string + endHeight int64 +} + +func newRestoreCmd(io commands.IO) *commands.Command { + cfg := &restoreCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "restore", + ShortUsage: "restore [flags]", + ShortHelp: "restore the Gnoland blockchain node", + LongHelp: "Restores the Gnoland blockchain node, with accompanying setup", + }, + cfg, + func(ctx context.Context, _ []string) error { + return execRestore(ctx, cfg, io) + }, + ) +} + +func (c *restoreCfg) RegisterFlags(fs *flag.FlagSet) { + c.nodeCfg.RegisterFlags(fs) + + fs.StringVar( + &c.backupDir, + "backup-dir", + "blocks-backup", + "directory where the backup files are", + ) + + fs.Int64Var( + &c.endHeight, + "end-height", + 0, + "height at which the restore process should stop", + ) +} + +func execRestore(ctx context.Context, c *restoreCfg, io commands.IO) error { + gnoNode, err := createNode(&c.nodeCfg, io) + if err != nil { + return err + } + + // need block n+1 to commit block n + endHeight := c.endHeight + if endHeight != 0 { + endHeight += 1 + } + + startHeight := gnoNode.BlockStore().Height() + 1 + if c.endHeight != 0 && c.endHeight < startHeight { + return fmt.Errorf("invalid input: requested end height (#%d) is smaller than next chain height (#%d)", c.endHeight, startHeight) + } + + return backup.WithReader(c.backupDir, startHeight, endHeight, func(reader backup.Reader) error { + return gnoNode.Restore(ctx, reader) + }) +} diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index c6143ab9cd3..339920a67fd 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -27,6 +27,7 @@ func newRootCmd(io commands.IO) *commands.Command { newStartCmd(io), newSecretsCmd(io), newConfigCmd(io), + newRestoreCmd(io), ) return cmd diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 4f380031be4..4042e13b903 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -26,9 +26,9 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/events" osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/telemetry" "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/gno/tm2/pkg/telemetry" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -48,7 +48,7 @@ var startGraphic = strings.ReplaceAll(` // Keep in sync with contribs/gnogenesis/internal/txs/txs_add_packages.go var genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) -type startCfg struct { +type nodeCfg struct { gnoRootDir string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 skipFailingGenesisTxs bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 skipGenesisSigVerification bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 @@ -65,7 +65,7 @@ type startCfg struct { } func newStartCmd(io commands.IO) *commands.Command { - cfg := &startCfg{} + cfg := &nodeCfg{} return commands.NewCommand( commands.Metadata{ @@ -81,7 +81,7 @@ func newStartCmd(io commands.IO) *commands.Command { ) } -func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { +func (c *nodeCfg) RegisterFlags(fs *flag.FlagSet) { gnoroot := gnoenv.RootDir() defaultGenesisBalancesFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt") @@ -170,23 +170,58 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execStart(ctx context.Context, c *startCfg, io commands.IO) error { +func execStart(ctx context.Context, c *nodeCfg, io commands.IO) error { + gnoNode, err := createNode(c, io) + if err != nil { + return err + } + + // Start the node (async) + if err := gnoNode.Start(); err != nil { + return fmt.Errorf("unable to start the Gnoland node, %w", err) + } + + // Set up the wait context + nodeCtx, _ := signal.NotifyContext( + ctx, + os.Interrupt, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, + ) + + // Wait for the exit signal + <-nodeCtx.Done() + + if !gnoNode.IsRunning() { + return nil + } + + // Gracefully stop the gno node + if err := gnoNode.Stop(); err != nil { + return fmt.Errorf("unable to gracefully stop the Gnoland node, %w", err) + } + + return nil +} + +func createNode(c *nodeCfg, io commands.IO) (*node.Node, error) { // Get the absolute path to the node's data directory nodeDir, err := filepath.Abs(c.dataDir) if err != nil { - return fmt.Errorf("unable to get absolute path for data directory, %w", err) + return nil, fmt.Errorf("unable to get absolute path for data directory, %w", err) } // Get the absolute path to the node's genesis.json genesisPath, err := filepath.Abs(c.genesisFile) if err != nil { - return fmt.Errorf("unable to get absolute path for the genesis.json, %w", err) + return nil, fmt.Errorf("unable to get absolute path for the genesis.json, %w", err) } // Initialize the logger zapLogger, err := initializeLogger(io.Out(), c.logLevel, c.logFormat) if err != nil { - return fmt.Errorf("unable to initialize zap logger, %w", err) + return nil, fmt.Errorf("unable to initialize zap logger, %w", err) } defer func() { @@ -199,20 +234,20 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { if c.lazyInit { if err := lazyInitNodeDir(io, nodeDir); err != nil { - return fmt.Errorf("unable to lazy-init the node directory, %w", err) + return nil, fmt.Errorf("unable to lazy-init the node directory, %w", err) } } // Load the configuration cfg, err := config.LoadConfig(nodeDir) if err != nil { - return fmt.Errorf("%s, %w", tryConfigInit, err) + return nil, fmt.Errorf("%s, %w", tryConfigInit, err) } // Check if the genesis.json exists if !osm.FileExists(genesisPath) { if !c.lazyInit { - return errMissingGenesis + return nil, errMissingGenesis } // Load the private validator secrets @@ -223,13 +258,13 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { // Init a new genesis.json if err := lazyInitGenesis(io, c, genesisPath, privateKey.Key.PrivKey); err != nil { - return fmt.Errorf("unable to initialize genesis.json, %w", err) + return nil, fmt.Errorf("unable to initialize genesis.json, %w", err) } } // Initialize telemetry if err := telemetry.Init(*cfg.Telemetry); err != nil { - return fmt.Errorf("unable to initialize telemetry, %w", err) + return nil, fmt.Errorf("unable to initialize telemetry, %w", err) } // Print the starting graphic @@ -253,42 +288,16 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { minGasPrices, ) if err != nil { - return fmt.Errorf("unable to create the Gnoland app, %w", err) + return nil, fmt.Errorf("unable to create the Gnoland app, %w", err) } // Create a default node, with the given setup gnoNode, err := node.DefaultNewNode(cfg, genesisPath, evsw, logger) if err != nil { - return fmt.Errorf("unable to create the Gnoland node, %w", err) - } - - // Start the node (async) - if err := gnoNode.Start(); err != nil { - return fmt.Errorf("unable to start the Gnoland node, %w", err) - } - - // Set up the wait context - nodeCtx, _ := signal.NotifyContext( - ctx, - os.Interrupt, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - ) - - // Wait for the exit signal - <-nodeCtx.Done() - - if !gnoNode.IsRunning() { - return nil + return nil, fmt.Errorf("unable to create the Gnoland node, %w", err) } - // Gracefully stop the gno node - if err := gnoNode.Stop(); err != nil { - return fmt.Errorf("unable to gracefully stop the Gnoland node, %w", err) - } - - return nil + return gnoNode, err } // lazyInitNodeDir initializes new secrets, and a default configuration @@ -345,7 +354,7 @@ func lazyInitNodeDir(io commands.IO, nodeDir string) error { // lazyInitGenesis a new genesis.json file, with a signle validator func lazyInitGenesis( io commands.IO, - c *startCfg, + c *nodeCfg, genesisPath string, privateKey crypto.PrivKey, ) error { @@ -380,7 +389,7 @@ func initializeLogger(io io.WriteCloser, logLevel, logFormat string) (*zap.Logge return log.GetZapLoggerFn(format)(io, level), nil } -func generateGenesisFile(genesisFile string, privKey crypto.PrivKey, c *startCfg) error { +func generateGenesisFile(genesisFile string, privKey crypto.PrivKey, c *nodeCfg) error { var ( pubKey = privKey.PubKey() // There is an active constraint for gno.land transactions: diff --git a/go.mod b/go.mod index 78d351c7bb4..627684b8457 100644 --- a/go.mod +++ b/go.mod @@ -15,9 +15,11 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fortytw2/leaktest v1.3.0 + github.com/gofrs/flock v0.12.1 github.com/google/go-cmp v0.6.0 github.com/google/gofuzz v1.2.0 github.com/gorilla/websocket v1.5.3 + github.com/klauspost/compress v1.18.0 github.com/libp2p/go-buffer-pool v0.1.0 github.com/pelletier/go-toml v1.9.5 github.com/peterbourgon/ff/v3 v3.4.0 diff --git a/go.sum b/go.sum index fd24668db28..82e35c19c99 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -102,6 +104,8 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/tm2/pkg/bft/backup/v1/reader.go b/tm2/pkg/bft/backup/v1/reader.go new file mode 100644 index 00000000000..93cb84fe8e7 --- /dev/null +++ b/tm2/pkg/bft/backup/v1/reader.go @@ -0,0 +1,153 @@ +package backup + +import ( + "archive/tar" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/klauspost/compress/zstd" +) + +type Reader = func(yield func(block *types.Block) error) error + +// WithReader creates a backup reader and pass it to the provided cb. +// It is process-safe but not thread-safe. +func WithReader(dir string, startHeight int64, endHeight int64, cb func(reader Reader) error) (resErr error) { + dir = filepath.Clean(dir) + + if endHeight != 0 && endHeight < startHeight { + return fmt.Errorf("requested end height (%d) is smaller than requested start height (%d)", endHeight, startHeight) + } + + unlock, err := lockDir(dir) + if err != nil { + return fmt.Errorf("lock output directory %q: %w", dir, err) + } + defer func() { + resErr = errors.Join(unlock(), resErr) + }() + + state, err := readState(dir) + if err != nil { + return err + } + + if state.StartHeight < 1 || state.EndHeight < 1 { + return errors.New("invalid backup state") + } + + if startHeight < state.StartHeight { + return fmt.Errorf("requested start height (#%d) is smaller than backup start height (#%d)", startHeight, state.StartHeight) + } + + if endHeight == 0 { + endHeight = state.EndHeight + } else if endHeight > state.EndHeight { + return fmt.Errorf("requested end height (#%d) is greater than backup end height (#%d)", endHeight, state.EndHeight) + } + + chunkHeight := startHeight - (startHeight-1)%ChunkSize + + reader := &backupReader{ + dir: dir, + startHeight: startHeight, + endHeight: endHeight, + chunkHeight: chunkHeight, + height: startHeight, + } + + return cb(reader.read) +} + +type backupReader struct { + dir string + startHeight int64 + endHeight int64 + chunkHeight int64 + height int64 +} + +func (c *backupReader) read(yield func(block *types.Block) error) error { + for { + err := c.readChunk(yield) + switch { + case errors.Is(err, errReadDone): + return nil + case err != nil: + return err + } + } +} + +func (c *backupReader) readChunk(yield func(block *types.Block) error) (retErr error) { + chunkFP := getChunkFP(c.dir, c.chunkHeight) + + chunk, err := os.Open(chunkFP) + if err != nil { + return err + } + defer func() { + retErr = errors.Join(chunk.Close(), retErr) + }() + + zstr, err := zstd.NewReader(chunk) + if err != nil { + return err + } + defer zstr.Close() + + r := tar.NewReader(zstr) + for { + h, err := r.Next() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return err + } + + blockHeight, err := strconv.ParseInt(h.Name, 10, 64) + if err != nil { + return err + } + + if blockHeight < c.startHeight { + continue + } + + if blockHeight != c.height { + return fmt.Errorf("unexpected block height: wanted %d, got %d", c.height, blockHeight) + } + + blockBz := make([]byte, h.Size) + if _, err := r.Read(blockBz); err != nil && !errors.Is(err, io.EOF) { + return err + } + block := &types.Block{} + if err := amino.Unmarshal(blockBz, block); err != nil { + return err + } + + if err := yield(block); err != nil { + return err + } + + if c.height == c.endHeight { + return errReadDone + } + + c.height += 1 + } + + c.chunkHeight += ChunkSize + + return nil +} + +var errReadDone = errors.New("read done") diff --git a/tm2/pkg/bft/backup/v1/util.go b/tm2/pkg/bft/backup/v1/util.go new file mode 100644 index 00000000000..46fc050d5ad --- /dev/null +++ b/tm2/pkg/bft/backup/v1/util.go @@ -0,0 +1,88 @@ +package backup + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/gofrs/flock" +) + +const ( + ChunkSize = 100 + heightFilename = "next-height.txt" + backupStateFilename = "info.json" + backupVersion = "v1" +) + +func lockDir(dir string) (func() error, error) { + if err := os.MkdirAll(dir, 0o775); err != nil { + return nil, fmt.Errorf("failed to ensure output directory exists: %w", err) + } + + fileLock := flock.New(filepath.Join(dir, "blocks.lock")) + locked, err := fileLock.TryLock() + if err != nil { + return nil, err + } + if !locked { + return nil, errors.New("failed to acquire lock on output directory") + } + return fileLock.Unlock, nil +} + +func getChunkFP(dir string, chunkHeight int64) string { + return filepath.Join(dir, fmt.Sprintf("%019d.tm2blocks"+archiveSuffix, chunkHeight)) +} + +type backupState struct { + Version string + StartHeight int64 + EndHeight int64 + + filepath string +} + +func (s *backupState) save() error { + bz, err := json.Marshal(s) + if err != nil { + return err + } + if err := os.WriteFile(s.filepath, bz, 0o664); err != nil { + return err + } + + return nil +} + +func readState(dir string) (*backupState, error) { + fp := filepath.Join(dir, backupStateFilename) + + stateBz, err := os.ReadFile(fp) + switch { + case os.IsNotExist(err): + return &backupState{ + StartHeight: -1, + EndHeight: -1, + Version: backupVersion, + filepath: fp, + }, nil + case err != nil: + return nil, err + } + + state := backupState{} + if err := json.Unmarshal(stateBz, &state); err != nil { + return nil, err + } + + if state.Version != backupVersion { + return nil, fmt.Errorf("backup version mismatch, expected %q (binary), got %q (directory)", backupVersion, state.Version) + } + + state.filepath = fp + + return &state, nil +} diff --git a/tm2/pkg/bft/backup/v1/writer.go b/tm2/pkg/bft/backup/v1/writer.go new file mode 100644 index 00000000000..ab23c458be1 --- /dev/null +++ b/tm2/pkg/bft/backup/v1/writer.go @@ -0,0 +1,239 @@ +package backup + +import ( + "archive/tar" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/klauspost/compress/zstd" + "go.uber.org/zap" +) + +const ( + archiveSuffix = ".tar.zst" + nextChunkFilename = "next-chunk" + archiveSuffix +) + +type Writer = func(block *types.Block) error + +// WithWriter creates a backup writer and pass it to the provided cb. +// It is process-safe but not thread-safe. +func WithWriter(dir string, startHeightReq int64, endHeight int64, logger *zap.Logger, cb func(startHeight int64, write Writer) error) (retErr error) { + if startHeightReq < 0 { + return errors.New("start height request must be >= 0") + } + if endHeight < 0 { + return errors.New("end height must be >= 0") + } + + dir = filepath.Clean(dir) + + unlock, err := lockDir(dir) + if err != nil { + return fmt.Errorf("lock output directory %q: %w", dir, err) + } + defer func() { + retErr = errors.Join(unlock(), retErr) + }() + + state, err := readState(dir) + if err != nil { + return err + } + + if endHeight != 0 && int64(endHeight) == state.EndHeight { + logger.Info("Nothing to do, backup is already at requested end height") + return nil + } + + if endHeight != 0 && int64(endHeight) <= state.EndHeight { + return fmt.Errorf("invalid input: requested end height is smaller or equal to the existing backup height (#%d), use a different output directory or a valid end height", state.EndHeight) + } + + height, err := getStartHeight(int64(startHeightReq), state.EndHeight) + if err != nil { + return fmt.Errorf("decide start height: %w", err) + } + + state.StartHeight = height + + prefix := "Starting backup" + if state.EndHeight != -1 { + prefix = "Resuming backup" + } + logger.Info(prefix, zap.Int64("height", height), zap.String("dir", dir), zap.Int64("end", endHeight)) + + writer := writerImpl{ + dir: dir, + chunkStart: height, + nextHeight: height, + logger: logger, + state: state, + } + + if err := writer.openChunk(); err != nil { + return fmt.Errorf("open first chunk: %w", err) + } + defer func() { + retErr = errors.Join(writer.finalizeChunk(), retErr) + }() + + return cb(height, writer.write) +} + +type writerImpl struct { + dir string + nextHeight int64 + chunkStart int64 + outFile *os.File + logger *zap.Logger + zstw *zstd.Encoder + w *tar.Writer + poisoned bool + state *backupState +} + +func (b *writerImpl) write(block *types.Block) error { + if b.poisoned { + return errors.New("poisoned") + } + + if block.Height != b.nextHeight { + return fmt.Errorf("non-contiguous block received, expected height #%d, got #%d", b.nextHeight, block.Height) + } + + blockBz, err := amino.Marshal(block) + if err != nil { + return fmt.Errorf("marshal block: %w", err) + } + + header := tar.Header{ + Name: fmt.Sprintf("%d", b.nextHeight), + Size: int64(len(blockBz)), + Mode: 0o664, + } + if err := b.w.WriteHeader(&header); err != nil { + b.poisoned = true + return fmt.Errorf("write tar file header: %w", err) + } + + if _, err := b.w.Write(blockBz); err != nil { + b.poisoned = true + return fmt.Errorf("write block body: %w", err) + } + + b.nextHeight += 1 + if b.nextHeight%ChunkSize != 1 { + return nil + } + + if err := b.finalizeChunk(); err != nil { + b.poisoned = true + return fmt.Errorf("finalize chunk: %w", err) + } + + b.chunkStart = b.nextHeight + + if err := b.openChunk(); err != nil { + b.poisoned = true + return fmt.Errorf("open chunk: %w", err) + } + + return nil +} + +func (b *writerImpl) openChunk() error { + var err error + + nextChunkFP := filepath.Join(b.dir, nextChunkFilename) + + b.outFile, err = os.OpenFile(nextChunkFP, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o664) + if err != nil { + return err + } + + b.zstw, err = zstd.NewWriter(b.outFile) + if err != nil { + return errors.Join(b.outFile.Close(), err) + } + + b.w = tar.NewWriter(b.zstw) + + return nil +} + +func (b *writerImpl) finalizeChunk() error { + if err := b.closeWriters(); err != nil { + return err + } + + if b.nextHeight == b.chunkStart || b.poisoned { + return nil + } + + nextChunkFP := filepath.Join(b.dir, nextChunkFilename) + + // using padding for filename to match chunk order and lexicographical order + chunkFP := getChunkFP(b.dir, b.chunkStart) + if err := os.Rename(nextChunkFP, chunkFP); err != nil { + return err + } + + b.state.EndHeight = b.nextHeight - 1 + if err := b.state.save(); err != nil { + return err + } + + b.logger.Debug("Wrote chunk", zap.Int64("start", b.chunkStart), zap.Int64("end", b.nextHeight-1), zap.String("file", chunkFP)) + + return nil +} + +func (b *writerImpl) closeWriters() error { + errs := make([]error, 0, 3) + + if b.w != nil { + errs = append(errs, b.w.Close()) + b.w = nil + } + + if b.zstw != nil { + errs = append(errs, b.zstw.Close()) + b.zstw = nil + } + + if b.outFile != nil { + errs = append(errs, b.outFile.Close()) + b.outFile = nil + } + + return errors.Join(errs...) +} + +func getStartHeight(requestedStartHeight int64, backupHeight int64) (int64, error) { + height := int64(1) + + if requestedStartHeight != 0 && backupHeight != -1 { + return 0, errors.New("can't request a start height when resuming, use a different output directory or no start height") + } + + if requestedStartHeight != 0 { + height = requestedStartHeight + } else { + height = backupHeight + 1 + } + + // align: 4 -> 1, 100 -> 1, 101 -> 101, 150 -> 101 (with ChunkSize == 100) + // we simply overwrite the latest chunk if it is partial because it's not expensive + height -= (height - 1) % ChunkSize + + if height < 1 || height%ChunkSize != 1 { + return 0, fmt.Errorf("unexpected start height %d", height) + } + + return height, nil +} diff --git a/tm2/pkg/bft/blockchain/reactor.go b/tm2/pkg/bft/blockchain/reactor.go index 417e96ad383..c2eb2fbd319 100644 --- a/tm2/pkg/bft/blockchain/reactor.go +++ b/tm2/pkg/bft/blockchain/reactor.go @@ -1,6 +1,7 @@ package blockchain import ( + "context" "errors" "fmt" "log/slog" @@ -361,6 +362,50 @@ FOR_LOOP: } } +func (bcR *BlockchainReactor) Restore(ctx context.Context, blocksIterator func(yield func(block *types.Block) error) error) error { + var ( + first *types.Block + second *types.Block + chainID = bcR.initialState.ChainID + state = bcR.initialState + err error + ) + + return blocksIterator(func(block *types.Block) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + if first == nil { + first = block + return nil + } + + second = block + + firstParts := first.MakePartSet(types.BlockPartSizeBytes) + firstPartsHeader := firstParts.Header() + firstID := types.BlockID{Hash: first.Hash(), PartsHeader: firstPartsHeader} + + if err := state.Validators.VerifyCommit( + chainID, firstID, first.Height, second.LastCommit); err != nil { + return fmt.Errorf("invalid commit (%d:%X): %w", first.Height, first.Hash(), err) + } + + bcR.store.SaveBlock(first, firstParts, second.LastCommit) + + state, err = bcR.blockExec.ApplyBlock(state, firstID, first) + if err != nil { + return fmt.Errorf("failed to process committed block (%d:%X): %w", first.Height, first.Hash(), err) + } + + first = second + return nil + }) +} + // BroadcastStatusRequest broadcasts `BlockStore` height. func (bcR *BlockchainReactor) BroadcastStatusRequest() error { msgBytes := amino.MustMarshalAny(&bcStatusRequestMessage{bcR.store.Height()}) diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index 5151a582b54..31a3d6284ec 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -15,6 +15,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/appconn" "github.com/gnolang/gno/tm2/pkg/bft/backup" + "github.com/gnolang/gno/tm2/pkg/bft/blockchain" "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" "github.com/gnolang/gno/tm2/pkg/p2p/conn" "github.com/gnolang/gno/tm2/pkg/p2p/discovery" @@ -162,9 +163,9 @@ type Node struct { // services evsw events.EventSwitch stateDB dbm.DB - blockStore *store.BlockStore // store the blockchain to disk - bcReactor p2p.Reactor // for fast-syncing - mempoolReactor *mempl.Reactor // for gossipping transactions + blockStore *store.BlockStore // store the blockchain to disk + bcReactor *blockchain.BlockchainReactor // for fast-syncing and restoring from backup + mempoolReactor *mempl.Reactor // for gossipping transactions mempool mempl.Mempool consensusState *cs.ConsensusState // latest consensus state consensusReactor *cs.ConsensusReactor // for participating in the consensus @@ -295,7 +296,7 @@ func createBlockchainReactor( fastSync bool, switchToConsensusFn bc.SwitchToConsensusFn, logger *slog.Logger, -) (bcReactor p2p.Reactor, err error) { +) (bcReactor *bc.BlockchainReactor, err error) { bcReactor = bc.NewBlockchainReactor( state.Copy(), blockExec, @@ -575,6 +576,10 @@ func NewNode(config *cfg.Config, return node, nil } +func (n *Node) Restore(ctx context.Context, readBlocks func(yield func(block *types.Block) error) error) error { + return n.bcReactor.Restore(ctx, readBlocks) +} + // OnStart starts the Node. It implements service.Service. func (n *Node) OnStart() error { now := tmtime.Now() From f587f3050be16ffb36206f9a188ad981d970f8f3 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 19 Mar 2025 08:38:23 +0100 Subject: [PATCH 18/67] chore: lint + refacto Signed-off-by: Norman --- tm2/pkg/bft/backup/v1/writer.go | 6 +++--- tm2/pkg/bft/blockchain/reactor.go | 4 +++- tm2/pkg/bft/node/node.go | 11 +++++------ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tm2/pkg/bft/backup/v1/writer.go b/tm2/pkg/bft/backup/v1/writer.go index ab23c458be1..91b0ad9aa7b 100644 --- a/tm2/pkg/bft/backup/v1/writer.go +++ b/tm2/pkg/bft/backup/v1/writer.go @@ -45,16 +45,16 @@ func WithWriter(dir string, startHeightReq int64, endHeight int64, logger *zap.L return err } - if endHeight != 0 && int64(endHeight) == state.EndHeight { + if endHeight != 0 && endHeight == state.EndHeight { logger.Info("Nothing to do, backup is already at requested end height") return nil } - if endHeight != 0 && int64(endHeight) <= state.EndHeight { + if endHeight != 0 && endHeight <= state.EndHeight { return fmt.Errorf("invalid input: requested end height is smaller or equal to the existing backup height (#%d), use a different output directory or a valid end height", state.EndHeight) } - height, err := getStartHeight(int64(startHeightReq), state.EndHeight) + height, err := getStartHeight(startHeightReq, state.EndHeight) if err != nil { return fmt.Errorf("decide start height: %w", err) } diff --git a/tm2/pkg/bft/blockchain/reactor.go b/tm2/pkg/bft/blockchain/reactor.go index c2eb2fbd319..919591ebcd7 100644 --- a/tm2/pkg/bft/blockchain/reactor.go +++ b/tm2/pkg/bft/blockchain/reactor.go @@ -362,7 +362,9 @@ FOR_LOOP: } } -func (bcR *BlockchainReactor) Restore(ctx context.Context, blocksIterator func(yield func(block *types.Block) error) error) error { +type BlocksIterator func(yield func(block *types.Block) error) error + +func (bcR *BlockchainReactor) Restore(ctx context.Context, blocksIterator BlocksIterator) error { var ( first *types.Block second *types.Block diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index 31a3d6284ec..37cd94b00fb 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -15,7 +15,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/appconn" "github.com/gnolang/gno/tm2/pkg/bft/backup" - "github.com/gnolang/gno/tm2/pkg/bft/blockchain" "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" "github.com/gnolang/gno/tm2/pkg/p2p/conn" "github.com/gnolang/gno/tm2/pkg/p2p/discovery" @@ -163,9 +162,9 @@ type Node struct { // services evsw events.EventSwitch stateDB dbm.DB - blockStore *store.BlockStore // store the blockchain to disk - bcReactor *blockchain.BlockchainReactor // for fast-syncing and restoring from backup - mempoolReactor *mempl.Reactor // for gossipping transactions + blockStore *store.BlockStore // store the blockchain to disk + bcReactor *bc.BlockchainReactor // for fast-syncing and restoring from backup + mempoolReactor *mempl.Reactor // for gossipping transactions mempool mempl.Mempool consensusState *cs.ConsensusState // latest consensus state consensusReactor *cs.ConsensusReactor // for participating in the consensus @@ -576,8 +575,8 @@ func NewNode(config *cfg.Config, return node, nil } -func (n *Node) Restore(ctx context.Context, readBlocks func(yield func(block *types.Block) error) error) error { - return n.bcReactor.Restore(ctx, readBlocks) +func (n *Node) Restore(ctx context.Context, blocksIterator bc.BlocksIterator) error { + return n.bcReactor.Restore(ctx, blocksIterator) } // OnStart starts the Node. It implements service.Service. From 6524c5b12d7a9a51ca59d214e9860f01129c158f Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 19 Mar 2025 08:46:44 +0100 Subject: [PATCH 19/67] chore: add tm2backup Makefile and move dev tools to devdeps Signed-off-by: Norman --- contribs/tm2backup/.gitignore | 2 +- contribs/tm2backup/Makefile | 18 ++ misc/devdeps/deps.go | 5 + misc/devdeps/go.mod | 103 ++++++++- misc/devdeps/go.sum | 260 +++++++++++++++++++++-- tm2/Makefile | 2 +- tm2/pkg/bft/backup/backuppb/backup.pb.go | 14 +- tm2/pkg/bft/backup/buf.gen.yaml | 4 +- tm2/pkg/bft/backup/tools/tools.go | 8 - 9 files changed, 370 insertions(+), 46 deletions(-) create mode 100644 contribs/tm2backup/Makefile delete mode 100644 tm2/pkg/bft/backup/tools/tools.go diff --git a/contribs/tm2backup/.gitignore b/contribs/tm2backup/.gitignore index d2e59036fc6..e2a1939bf60 100644 --- a/contribs/tm2backup/.gitignore +++ b/contribs/tm2backup/.gitignore @@ -1 +1 @@ -blocks-backup \ No newline at end of file +blocks-backup* \ No newline at end of file diff --git a/contribs/tm2backup/Makefile b/contribs/tm2backup/Makefile new file mode 100644 index 00000000000..155fc997012 --- /dev/null +++ b/contribs/tm2backup/Makefile @@ -0,0 +1,18 @@ +rundep := go run -modfile ../../misc/devdeps/go.mod +golangci_lint := $(rundep) github.com/golangci/golangci-lint/cmd/golangci-lint + + +.PHONY: install +install: + go install . + +.PHONY: build +build: + go build -o build/gnomigrate . + +lint: + $(golangci_lint) --config ../../.github/golangci.yml run ./... + +test: + go test $(GOTEST_FLAGS) -v ./... + diff --git a/misc/devdeps/deps.go b/misc/devdeps/deps.go index f7da2b10c12..fd309422c5e 100644 --- a/misc/devdeps/deps.go +++ b/misc/devdeps/deps.go @@ -28,4 +28,9 @@ import ( // embedmd _ "github.com/campoy/embedmd/embedmd" + + // backup grpc + _ "connectrpc.com/connect/cmd/protoc-gen-connect-go" + _ "github.com/bufbuild/buf/cmd/buf" + _ "google.golang.org/protobuf/cmd/protoc-gen-go" ) diff --git a/misc/devdeps/go.mod b/misc/devdeps/go.mod index 74ceda0abf0..a83ccf9b3b9 100644 --- a/misc/devdeps/go.mod +++ b/misc/devdeps/go.mod @@ -7,31 +7,49 @@ toolchain go1.23.2 require ( github.com/campoy/embedmd v1.0.0 github.com/golangci/golangci-lint v1.64.5 // sync with github action - golang.org/x/tools v0.30.0 + golang.org/x/tools v0.31.0 google.golang.org/protobuf v1.36.5 moul.io/testman v1.5.0 mvdan.cc/gofumpt v0.7.0 ) +require ( + connectrpc.com/connect v1.18.1 + github.com/bufbuild/buf v1.50.1 +) + require ( 4d63.com/gocheckcompilerdirectives v1.2.1 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect + buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.4-20250121211742-6d880cc6cc8d.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.4-20241127180247-a33202765966.1 // indirect + buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 // indirect + buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.4-20250116203702-1c024d64352b.1 // indirect + buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1 // indirect + buf.build/go/bufplugin v0.7.0 // indirect + buf.build/go/protoyaml v0.3.1 // indirect + buf.build/go/spdx v0.2.0 // indirect + cel.dev/expr v0.19.2 // indirect + connectrpc.com/otelconnect v0.7.1 // indirect github.com/4meepo/tagalign v1.4.2 // indirect github.com/Abirdcfly/dupword v0.1.3 // indirect github.com/Antonboom/errname v1.0.0 // indirect github.com/Antonboom/nilnil v1.0.1 // indirect github.com/Antonboom/testifylint v1.5.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/Crocmagnon/fatcontext v0.7.1 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect github.com/alexkohler/nakedret/v2 v2.0.5 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/nilnesserr v0.1.2 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/ashanbrown/forbidigo v1.6.0 // indirect github.com/ashanbrown/makezero v1.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -40,6 +58,9 @@ require ( github.com/bombsimon/wsl/v4 v4.5.0 // indirect github.com/breml/bidichk v0.3.2 // indirect github.com/breml/errchkjson v0.4.0 // indirect + github.com/bufbuild/protocompile v0.14.1 // indirect + github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9 // indirect + github.com/bufbuild/protovalidate-go v0.8.2 // indirect github.com/butuzov/ireturn v0.3.1 // indirect github.com/butuzov/mirror v1.3.0 // indirect github.com/catenacyber/perfsprint v0.8.1 // indirect @@ -48,18 +69,34 @@ require ( github.com/charithe/durationcheck v0.0.10 // indirect github.com/chavacava/garif v0.1.0 // indirect github.com/ckaznocha/intrange v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/daixiang0/gci v0.13.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v27.5.1+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v28.0.0+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.2 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect + github.com/felixge/fgprof v0.9.5 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/firefart/nonamedreturns v1.0.5 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.3.9 // indirect + github.com/go-chi/chi/v5 v5.2.1 // indirect github.com/go-critic/go-critic v0.12.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -71,6 +108,7 @@ require ( github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gofrs/flock v0.12.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect github.com/golangci/go-printf-func-name v0.1.0 // indirect github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect @@ -78,18 +116,24 @@ require ( github.com/golangci/plugin-module-register v0.1.1 // indirect github.com/golangci/revgrep v0.8.0 // indirect github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/cel-go v0.24.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.20.3 // indirect + github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gordonklaus/ineffassign v0.1.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect github.com/gostaticanalysis/nilerr v0.1.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jdx/go-netrc v1.0.0 // indirect github.com/jgautheron/goconst v1.7.1 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jjti/go-spancheck v0.6.4 // indirect @@ -97,6 +141,8 @@ require ( github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect github.com/kisielk/errcheck v1.8.0 // indirect github.com/kkHAIKE/contextcheck v1.1.5 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect github.com/kulti/thelper v0.6.3 // indirect github.com/kunwardeep/paralleltest v1.0.10 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect @@ -117,15 +163,32 @@ require ( github.com/mgechev/revive v1.7.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/mount v0.3.4 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/sys/reexec v0.1.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/moricho/tparallel v0.3.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.19.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/profile v1.7.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polyfloyd/go-errorlint v1.7.1 // indirect github.com/prometheus/client_golang v1.20.5 // indirect @@ -137,9 +200,13 @@ require ( github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.50.0 // indirect github.com/raeperd/recvcheck v0.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryancurrah/gomodguard v1.3.5 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect @@ -149,6 +216,8 @@ require ( github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect github.com/securego/gosec/v2 v2.22.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/segmentio/encoding v0.4.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sivchari/containedctx v1.0.3 // indirect github.com/sivchari/tenv v1.12.1 // indirect @@ -162,11 +231,13 @@ require ( github.com/spf13/viper v1.19.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tdakkota/asciicheck v0.4.1 // indirect github.com/tetafro/godot v1.5.0 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.10.1 // indirect github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect @@ -175,6 +246,7 @@ require ( github.com/ultraware/whitespace v0.2.0 // indirect github.com/uudashr/gocognit v1.2.0 // indirect github.com/uudashr/iface v1.3.1 // indirect + github.com/vbatts/tar-split v0.12.1 // indirect github.com/xen0n/gosmopolitan v1.2.2 // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect @@ -183,15 +255,31 @@ require ( gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.13.0 // indirect go-simpler.org/sloglint v0.9.0 // indirect + go.lsp.dev/jsonrpc2 v0.10.0 // indirect + go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect + go.lsp.dev/protocol v0.12.0 // indirect + go.lsp.dev/uri v0.3.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect + go.uber.org/zap/exp v0.3.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 // indirect golang.org/x/exp/typeparams v0.0.0-20250215185904-eff6e970281f // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -200,4 +288,5 @@ require ( moul.io/motd v1.0.0 // indirect moul.io/u v1.27.0 // indirect mvdan.cc/unparam v0.0.0-20250211232406-0e51248738fc // indirect + pluginrpc.com/pluginrpc v0.5.0 // indirect ) diff --git a/misc/devdeps/go.sum b/misc/devdeps/go.sum index 6a720aaf84d..3e457072d6f 100644 --- a/misc/devdeps/go.sum +++ b/misc/devdeps/go.sum @@ -2,16 +2,42 @@ 4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs= 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.4-20250121211742-6d880cc6cc8d.1 h1:p5SFT60M93aMQhOz81VH3kPg8t1pp/Litae/1eSxie4= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.4-20250121211742-6d880cc6cc8d.1/go.mod h1:umI0o7WWHv8lCbLjYUMzfjHKjyaIt2D89sIj1D9fqy0= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.4-20241127180247-a33202765966.1 h1:yeaeyw0RQUe009ebxBQ3TsqBPptiNEGsiS10t+8Htuo= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.4-20241127180247-a33202765966.1/go.mod h1:novQBstnxcGpfKf8qGRATqn1anQKwMJIbH5Q581jibU= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1 h1:1SDs5tEGoWWv2vmKLx2B0Bp+yfhlxiU4DaZUII8+Pvs= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250116203702-1c024d64352b.1/go.mod h1:o2AgVM1j3MczvxnMqfZTpiqGwK1VD4JbEagseY0QcjE= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.4-20250116203702-1c024d64352b.1 h1:uKJgSNHvwQUZ6+0dSnx9MtkZ+h/ORbkKym0rlzIjUSI= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.4-20250116203702-1c024d64352b.1/go.mod h1:Ua59W2s7uwPS5sGNgW08QewjBaPnUxOdpkWsuDvJ36Q= +buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1 h1:XmYgi9W/9oST2ZrfT3ucGWkzD9+Vd0ls9yhyZ8ae0KQ= +buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.4-20241007202033-cf42259fcbfc.1/go.mod h1:cxFpqWIC80Wm8YNo1038ocBmrF84uQ0IfL0uVdAu9ZY= +buf.build/go/bufplugin v0.7.0 h1:Tq8FXBVfpMxhl3QR6P/gMQHROg1Ss7WhpyD4QVV61ds= +buf.build/go/bufplugin v0.7.0/go.mod h1:LuQzv36Ezu2zQIQUtwg4WJJFe58tXn1anL1IosAh6ik= +buf.build/go/protoyaml v0.3.1 h1:ucyzE7DRnjX+mQ6AH4JzN0Kg50ByHHu+yrSKbgQn2D4= +buf.build/go/protoyaml v0.3.1/go.mod h1:0TzNpFQDXhwbkXb/ajLvxIijqbve+vMQvWY/b3/Dzxg= +buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw= +buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8= +cel.dev/expr v0.19.2 h1:V354PbqIXr9IQdwy4SYA4xa0HXaWq1BUPAGzugBY5V4= +cel.dev/expr v0.19.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +connectrpc.com/otelconnect v0.7.1 h1:scO5pOb0i4yUE66CnNrHeK1x51yq0bE0ehPg6WvzXJY= +connectrpc.com/otelconnect v0.7.1/go.mod h1:dh3bFgHBTb2bkqGCeVVOtHJreSns7uu9wwL2Tbz17ms= github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E= github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA= github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI= github.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs= github.com/Antonboom/nilnil v1.0.1/go.mod h1:CH7pW2JsRNFgEh8B2UaPZTEPhCMuFowP/e8Udp9Nnb0= github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk= github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -23,6 +49,8 @@ github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0 h1:/fTUt5vmbkAcMBt4YQiuC github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.0/go.mod h1:ONJg5sxcbsdQQ4pOW8TGdTidT2TMAUy/2Xhr8mrYaao= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/OpenPeeDeeP/depguard/v2 v2.2.0 h1:vDfG60vDtIuf0MEOhmLlLLSzqaRM8EMcgJPdp74zmpA= github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+ClszLIuVocRiCYFQ= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= @@ -39,6 +67,8 @@ github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQ github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/alingse/nilnesserr v0.1.2 h1:Yf8Iwm3z2hUUrP4muWfW83DF4nE3r1xZ26fGWUKCZlo= github.com/alingse/nilnesserr v0.1.2/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU= @@ -55,6 +85,14 @@ github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs= github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos= github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk= github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8= +github.com/bufbuild/buf v1.50.1 h1:3sEaWLw6g7bSIJ+yKo6ERF3qpkaLNGd8SzImFpA5gUI= +github.com/bufbuild/buf v1.50.1/go.mod h1:LqTlfsFs4RD3L+VoBudEWJzWi12Pa0+Q2vDQnY0YQv0= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9 h1:kAWER21DzhzU7ys8LL1WkSfbGkwXv+tM30hyEsYrW2k= +github.com/bufbuild/protoplugin v0.0.0-20250106231243-3a819552c9d9/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= +github.com/bufbuild/protovalidate-go v0.8.2 h1:sgzXHkHYP6HnAsL2Rd3I1JxkYUyEQUv9awU1PduMxbM= +github.com/bufbuild/protovalidate-go v0.8.2/go.mod h1:K6w8iPNAXBoIivVueSELbUeUl+MmeTQfCDSug85pn3M= github.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY= github.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M= github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= @@ -65,16 +103,36 @@ github.com/catenacyber/perfsprint v0.8.1 h1:bGOHuzHe0IkoGeY831RW4aSlt1lPRd3WRASc github.com/catenacyber/perfsprint v0.8.1/go.mod h1:/wclWYompEyjUD2FuIIDVKNkqz7IgBIWXIH3V0Zol50= github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd5ZY= github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= @@ -85,14 +143,35 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY= +github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM= +github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -103,10 +182,15 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ= github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w= github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -136,9 +220,14 @@ github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUW github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= @@ -155,15 +244,24 @@ github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2 github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= +github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI= +github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= +github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= @@ -179,6 +277,8 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= @@ -192,26 +292,36 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ= +github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8= github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88= +github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc= github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI= github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.8.0 h1:ZX/URYa7ilESY19ik/vBmCn6zdGQLxACwjAcWbHlYlg= github.com/kisielk/errcheck v1.8.0/go.mod h1:1kLL+jV4e+CFfueBmI1dSK2ADDyQnlrnrY/FqKluHJQ= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.5 h1:CdnJh63tcDe53vG+RebdpdXJTc9atMgGqdx8LXxiilg= github.com/kkHAIKE/contextcheck v1.1.5/go.mod h1:O930cpht4xb1YQpK+1+AgoM3mFsvxr7uyFptcnWTYUA= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -236,12 +346,14 @@ github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORI github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= github.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA= github.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= @@ -263,8 +375,30 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww= +github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/reexec v0.1.0 h1:RrBi8e0EBTLEgfruBOFcxtElzRGTEUkeIFaVXgU7wok= +github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= @@ -281,6 +415,13 @@ github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -294,8 +435,14 @@ github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xl github.com/peterbourgon/ff/v3 v3.0.0/go.mod h1:UILIFjRH5a/ar8TjXYLTkIvSvekZqPm5Eb/qbGk6CT0= github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -321,6 +468,10 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo= +github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -328,6 +479,9 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM= github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= @@ -347,6 +501,10 @@ github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxX github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= github.com/securego/gosec/v2 v2.22.1 h1:IcBt3TpI5Y9VN1YlwjSpM2cHu0i3Iw52QM+PQeg7jN8= github.com/securego/gosec/v2 v2.22.1/go.mod h1:4bb95X4Jz7VSEPdVjC0hD7C/yR6kdeUBvCPOy9gDQ0g= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/encoding v0.4.1 h1:KLGaLSW0jrmhB58Nn4+98spfvPvmo4Ci1P/WIQ9wn7w= +github.com/segmentio/encoding v0.4.1/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= @@ -377,6 +535,8 @@ github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YE github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -389,6 +549,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -403,6 +564,8 @@ github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpR github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw= github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg= @@ -419,6 +582,8 @@ github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYR github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U= github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= +github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= @@ -428,6 +593,7 @@ github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+ github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -443,25 +609,59 @@ go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= go-simpler.org/sloglint v0.9.0 h1:/40NQtjRx9txvsB/RN022KsUJU+zaaSb/9q9BSefSrE= go-simpler.org/sloglint v0.9.0/go.mod h1:G/OrAF6uxj48sHahCzrbarVMptL2kjWTaUeC8+fOGww= +go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI= +go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac= +go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE= +go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw= +go.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg= +go.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ= +go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo= +go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= +go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg= -golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 h1:aWwlzYV971S4BXRS9AmqwDLAD85ouC6X+pocatKY58c= +golang.org/x/exp v0.0.0-20250228200357-dead58393ab7/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20250215185904-eff6e970281f h1:lwUSxjTFq2sP4q5JdTtCEuDDSl3udvTn2UEksv8OHFY= @@ -479,8 +679,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -497,8 +697,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -508,8 +708,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -521,19 +721,23 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -542,6 +746,8 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -552,8 +758,10 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -561,10 +769,12 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= @@ -577,12 +787,18 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -600,6 +816,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.6.0 h1:TAODvD3knlq75WCp2nyGJtT4LeRV/o7NN9nYPeVJXf8= honnef.co/go/tools v0.6.0/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= moul.io/banner v1.0.1 h1:+WsemGLhj2pOajw2eR5VYjLhOIqs0XhIRYchzTyMLk0= @@ -616,3 +834,5 @@ mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= mvdan.cc/unparam v0.0.0-20250211232406-0e51248738fc h1:mEpjEutR7Qjdis+HqGQNdsJY/uRbH/MnyGXzLKMhDFo= mvdan.cc/unparam v0.0.0-20250211232406-0e51248738fc/go.mod h1:rthT7OuvRbaGcd5ginj6dA2oLE7YNlta9qhBNNdCaLE= +pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo= +pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o= diff --git a/tm2/Makefile b/tm2/Makefile index 7cdec8c37aa..46db85ad547 100644 --- a/tm2/Makefile +++ b/tm2/Makefile @@ -71,6 +71,6 @@ generate: .PHONY: generate.backup-grpc generate.backup-grpc: - cd pkg/bft/backup && buf generate + cd pkg/bft/backup && go run -modfile ../../../../misc/devdeps/go.mod github.com/bufbuild/buf/cmd/buf generate go run -modfile ../misc/devdeps/go.mod mvdan.cc/gofumpt -w pkg/bft/backup go run -modfile ../misc/devdeps/go.mod golang.org/x/tools/cmd/goimports -w pkg/bft/backup \ No newline at end of file diff --git a/tm2/pkg/bft/backup/backuppb/backup.pb.go b/tm2/pkg/bft/backup/backuppb/backup.pb.go index ca23c4cce23..c90088a6c4f 100644 --- a/tm2/pkg/bft/backup/backuppb/backup.pb.go +++ b/tm2/pkg/bft/backup/backuppb/backup.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.3 +// protoc-gen-go v1.36.5 // protoc (unknown) // source: backuppb/backup.proto @@ -9,6 +9,7 @@ package backuppb import ( reflect "reflect" sync "sync" + unsafe "unsafe" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -127,7 +128,7 @@ func (x *StreamBlocksResponse) GetData() []byte { var File_backuppb_backup_proto protoreflect.FileDescriptor -var file_backuppb_backup_proto_rawDesc = []byte{ +var file_backuppb_backup_proto_rawDesc = string([]byte{ 0x0a, 0x15, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x70, 0x62, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x02, 0x74, 0x6d, 0x22, 0x57, 0x0a, 0x13, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, @@ -150,16 +151,16 @@ var file_backuppb_backup_proto_rawDesc = []byte{ 0x2f, 0x62, 0x66, 0x74, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x70, 0x62, 0x3b, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} +}) var ( file_backuppb_backup_proto_rawDescOnce sync.Once - file_backuppb_backup_proto_rawDescData = file_backuppb_backup_proto_rawDesc + file_backuppb_backup_proto_rawDescData []byte ) func file_backuppb_backup_proto_rawDescGZIP() []byte { file_backuppb_backup_proto_rawDescOnce.Do(func() { - file_backuppb_backup_proto_rawDescData = protoimpl.X.CompressGZIP(file_backuppb_backup_proto_rawDescData) + file_backuppb_backup_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_backuppb_backup_proto_rawDesc), len(file_backuppb_backup_proto_rawDesc))) }) return file_backuppb_backup_proto_rawDescData } @@ -188,7 +189,7 @@ func file_backuppb_backup_proto_init() { out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_backuppb_backup_proto_rawDesc, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_backuppb_backup_proto_rawDesc), len(file_backuppb_backup_proto_rawDesc)), NumEnums: 0, NumMessages: 2, NumExtensions: 0, @@ -199,7 +200,6 @@ func file_backuppb_backup_proto_init() { MessageInfos: file_backuppb_backup_proto_msgTypes, }.Build() File_backuppb_backup_proto = out.File - file_backuppb_backup_proto_rawDesc = nil file_backuppb_backup_proto_goTypes = nil file_backuppb_backup_proto_depIdxs = nil } diff --git a/tm2/pkg/bft/backup/buf.gen.yaml b/tm2/pkg/bft/backup/buf.gen.yaml index fce3ed533a8..64c1cf12c92 100644 --- a/tm2/pkg/bft/backup/buf.gen.yaml +++ b/tm2/pkg/bft/backup/buf.gen.yaml @@ -1,9 +1,9 @@ version: v2 plugins: - - local: [go, run, -modfile, ../../../../go.mod, google.golang.org/protobuf/cmd/protoc-gen-go] + - local: [go, run, -modfile, ../../../../misc/devdeps/go.mod, google.golang.org/protobuf/cmd/protoc-gen-go] out: . opt: paths=source_relative - - local: [go, run, -modfile, ../../../../go.mod, connectrpc.com/connect/cmd/protoc-gen-connect-go] + - local: [go, run, -modfile, ../../../../misc/devdeps/go.mod, connectrpc.com/connect/cmd/protoc-gen-connect-go] out: . opt: paths=source_relative \ No newline at end of file diff --git a/tm2/pkg/bft/backup/tools/tools.go b/tm2/pkg/bft/backup/tools/tools.go deleted file mode 100644 index c3d7578484a..00000000000 --- a/tm2/pkg/bft/backup/tools/tools.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build tools - -package tools - -import ( - _ "connectrpc.com/connect/cmd/protoc-gen-connect-go" - _ "google.golang.org/protobuf/cmd/protoc-gen-go" -) From 4e3a9def43d8603f08c6afea271f910d5e77ae29 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 19 Mar 2025 08:48:12 +0100 Subject: [PATCH 20/67] chore: tidy Signed-off-by: Norman --- contribs/tm2backup/go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/contribs/tm2backup/go.mod b/contribs/tm2backup/go.mod index d34a2b35e23..1784cf899e4 100644 --- a/contribs/tm2backup/go.mod +++ b/contribs/tm2backup/go.mod @@ -25,10 +25,8 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.32.0 // indirect - golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.36.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) From e55fc15156946f709989109e27b98109169c1f74 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 19 Mar 2025 09:04:44 +0100 Subject: [PATCH 21/67] chore: lint Signed-off-by: Norman --- contribs/tm2backup/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contribs/tm2backup/main.go b/contribs/tm2backup/main.go index 0abffa16811..1d83e438ff7 100644 --- a/contribs/tm2backup/main.go +++ b/contribs/tm2backup/main.go @@ -92,7 +92,7 @@ func execBackup(ctx context.Context, c *backupCfg, io commands.IO) (resErr error ctx, connect.NewRequest(&backuppb.StreamBlocksRequest{ StartHeight: startHeight, - EndHeight: int64(c.endHeight), + EndHeight: c.endHeight, }), ) if err != nil { From 1a0fae48186d0dc1c37e7837fdeb068b05eeb605 Mon Sep 17 00:00:00 2001 From: Norman Date: Tue, 1 Apr 2025 10:47:47 +0200 Subject: [PATCH 22/67] chore: remove unused constant Signed-off-by: Norman --- tm2/pkg/bft/backup/v1/util.go | 1 - 1 file changed, 1 deletion(-) diff --git a/tm2/pkg/bft/backup/v1/util.go b/tm2/pkg/bft/backup/v1/util.go index 46fc050d5ad..16edc6a4fc3 100644 --- a/tm2/pkg/bft/backup/v1/util.go +++ b/tm2/pkg/bft/backup/v1/util.go @@ -12,7 +12,6 @@ import ( const ( ChunkSize = 100 - heightFilename = "next-height.txt" backupStateFilename = "info.json" backupVersion = "v1" ) From ede2468abf7bfc5d84cc28118590c8bd896131f6 Mon Sep 17 00:00:00 2001 From: Norman Date: Tue, 1 Apr 2025 11:51:30 +0200 Subject: [PATCH 23/67] chore: add backup svc tests Signed-off-by: Norman --- tm2/pkg/bft/backup/backup_svc.go | 10 +- tm2/pkg/bft/backup/backup_svc_test.go | 218 ++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 tm2/pkg/bft/backup/backup_svc_test.go diff --git a/tm2/pkg/bft/backup/backup_svc.go b/tm2/pkg/bft/backup/backup_svc.go index 78c7583976c..086cbc6aab2 100644 --- a/tm2/pkg/bft/backup/backup_svc.go +++ b/tm2/pkg/bft/backup/backup_svc.go @@ -51,8 +51,12 @@ func (b *backupServer) StreamBlocks(_ context.Context, req *connect.Request[back return fmt.Errorf("start height must be >= 1, got %d", startHeight) } - endHeight := req.Msg.EndHeight blockStoreHeight := b.store.Height() + if blockStoreHeight < 1 { + return fmt.Errorf("block store returned invalid max height (%d)", blockStoreHeight) + } + + endHeight := req.Msg.EndHeight if endHeight == 0 { endHeight = blockStoreHeight } else if endHeight > blockStoreHeight { @@ -65,6 +69,10 @@ func (b *backupServer) StreamBlocks(_ context.Context, req *connect.Request[back for height := startHeight; height <= endHeight; height++ { block := b.store.LoadBlock(height) + if block == nil { + return fmt.Errorf("block store returned nil block for height %d", height) + } + data, err := amino.Marshal(block) if err != nil { return err diff --git a/tm2/pkg/bft/backup/backup_svc_test.go b/tm2/pkg/bft/backup/backup_svc_test.go new file mode 100644 index 00000000000..a1435d81388 --- /dev/null +++ b/tm2/pkg/bft/backup/backup_svc_test.go @@ -0,0 +1,218 @@ +package backup + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "connectrpc.com/connect" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" + "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb/backuppbconnect" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/stretchr/testify/require" +) + +func TestStreamBlocks(t *testing.T) { + tcs := []struct { + name string + initStore func() *mockBlockStore + start int64 + end int64 + expectedResult []*types.Block + errContains string + }{ + { + name: "one block store", + initStore: func() *mockBlockStore { + return &mockBlockStore{height: 1, blocks: map[int64]*types.Block{ + 1: {Header: types.Header{Height: 1}}, + }} + }, + expectedResult: []*types.Block{ + {Header: types.Header{Height: 1}}, + }, + }, + { + name: "range", + start: 2, + end: 4, + initStore: func() *mockBlockStore { + return &mockBlockStore{height: 5, blocks: map[int64]*types.Block{ + 1: {Header: types.Header{Height: 1}}, + 2: {Header: types.Header{Height: 2}}, + 3: {Header: types.Header{Height: 3}}, + 4: {Header: types.Header{Height: 4}}, + 5: {Header: types.Header{Height: 5}}, + }} + }, + expectedResult: []*types.Block{ + {Header: types.Header{Height: 2}}, + {Header: types.Header{Height: 3}}, + {Header: types.Header{Height: 4}}, + }, + }, + { + name: "range no start", + end: 4, + initStore: func() *mockBlockStore { + return &mockBlockStore{height: 5, blocks: map[int64]*types.Block{ + 1: {Header: types.Header{Height: 1}}, + 2: {Header: types.Header{Height: 2}}, + 3: {Header: types.Header{Height: 3}}, + 4: {Header: types.Header{Height: 4}}, + 5: {Header: types.Header{Height: 5}}, + }} + }, + expectedResult: []*types.Block{ + {Header: types.Header{Height: 1}}, + {Header: types.Header{Height: 2}}, + {Header: types.Header{Height: 3}}, + {Header: types.Header{Height: 4}}, + }, + }, + { + name: "range no end", + start: 2, + initStore: func() *mockBlockStore { + return &mockBlockStore{height: 5, blocks: map[int64]*types.Block{ + 1: {Header: types.Header{Height: 1}}, + 2: {Header: types.Header{Height: 2}}, + 3: {Header: types.Header{Height: 3}}, + 4: {Header: types.Header{Height: 4}}, + 5: {Header: types.Header{Height: 5}}, + }} + }, + expectedResult: []*types.Block{ + {Header: types.Header{Height: 2}}, + {Header: types.Header{Height: 3}}, + {Header: types.Header{Height: 4}}, + {Header: types.Header{Height: 5}}, + }, + }, + { + name: "range no params", + initStore: func() *mockBlockStore { + return &mockBlockStore{height: 5, blocks: map[int64]*types.Block{ + 1: {Header: types.Header{Height: 1}}, + 2: {Header: types.Header{Height: 2}}, + 3: {Header: types.Header{Height: 3}}, + 4: {Header: types.Header{Height: 4}}, + 5: {Header: types.Header{Height: 5}}, + }} + }, + expectedResult: []*types.Block{ + {Header: types.Header{Height: 1}}, + {Header: types.Header{Height: 2}}, + {Header: types.Header{Height: 3}}, + {Header: types.Header{Height: 4}}, + {Header: types.Header{Height: 5}}, + }, + }, + { + name: "err nil block", + initStore: func() *mockBlockStore { + return &mockBlockStore{height: 3, blocks: map[int64]*types.Block{ + 1: {Header: types.Header{Height: 1}}, + 2: nil, + 3: {Header: types.Header{Height: 3}}, + }} + }, + errContains: "block store returned nil block for height 2", + expectedResult: []*types.Block{ + {Header: types.Header{Height: 1}}, + }, + }, + { + name: "err empty store", + initStore: func() *mockBlockStore { + return &mockBlockStore{} + }, + errContains: "block store returned invalid max height (0)", + }, + { + name: "err reverse range", + start: 2, + end: 1, + initStore: func() *mockBlockStore { + return &mockBlockStore{height: 1, blocks: map[int64]*types.Block{1: {}, 2: {}}} + }, + errContains: "end height must be >= than start height", + }, + { + name: "err invalid start", + start: -42, + initStore: func() *mockBlockStore { + return &mockBlockStore{height: 1, blocks: map[int64]*types.Block{1: {Header: types.Header{Height: 1}}}} + }, + errContains: "start height must be >= 1, got -42", + }, + { + name: "err invalid end", + end: 42, + initStore: func() *mockBlockStore { + return &mockBlockStore{height: 1, blocks: map[int64]*types.Block{1: {}}} + }, + errContains: "end height must be <= 1", + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + store := tc.initStore() + svc := &backupServer{store: store} + mux := http.NewServeMux() + mux.Handle(backuppbconnect.NewBackupServiceHandler(svc)) + srv := httptest.NewServer(mux) + httpClient := srv.Client() + client := backuppbconnect.NewBackupServiceClient(httpClient, srv.URL) + + stream, err := client.StreamBlocks(context.Background(), &connect.Request[backuppb.StreamBlocksRequest]{Msg: &backuppb.StreamBlocksRequest{ + StartHeight: tc.start, + EndHeight: tc.end, + }}) + require.NoError(t, err) + + data := []*types.Block(nil) + for { + if !stream.Receive() { + err := stream.Err() + if tc.errContains == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tc.errContains) + } + break + } + msg := stream.Msg() + block := &types.Block{} + require.NoError(t, amino.Unmarshal(msg.Data, block)) + data = append(data, block) + } + require.Equal(t, tc.expectedResult, data) + }) + } +} + +func TestNewServer(t *testing.T) { + require.NotPanics(t, func() { + serv := NewServer(DefaultConfig(), &mockBlockStore{}) + require.NotNil(t, serv) + }) +} + +type mockBlockStore struct { + height int64 + blocks map[int64]*types.Block +} + +// Height implements blockStore. +func (m *mockBlockStore) Height() int64 { + return m.height +} + +// LoadBlock implements blockStore. +func (m *mockBlockStore) LoadBlock(height int64) *types.Block { + return m.blocks[height] +} From a6403e06a396e4f28ed489270bdfa01ce110fc3f Mon Sep 17 00:00:00 2001 From: Norman Date: Tue, 1 Apr 2025 11:53:13 +0200 Subject: [PATCH 24/67] chore: close test server Signed-off-by: Norman --- tm2/pkg/bft/backup/backup_svc_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tm2/pkg/bft/backup/backup_svc_test.go b/tm2/pkg/bft/backup/backup_svc_test.go index a1435d81388..29ca7183147 100644 --- a/tm2/pkg/bft/backup/backup_svc_test.go +++ b/tm2/pkg/bft/backup/backup_svc_test.go @@ -165,6 +165,7 @@ func TestStreamBlocks(t *testing.T) { mux := http.NewServeMux() mux.Handle(backuppbconnect.NewBackupServiceHandler(svc)) srv := httptest.NewServer(mux) + defer srv.Close() httpClient := srv.Client() client := backuppbconnect.NewBackupServiceClient(httpClient, srv.URL) From d516d29f71d1ee1a5efe4913b0c66e6b8a310825 Mon Sep 17 00:00:00 2001 From: Norman Date: Tue, 1 Apr 2025 11:57:08 +0200 Subject: [PATCH 25/67] chore: close test stream Signed-off-by: Norman --- tm2/pkg/bft/backup/backup_svc_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tm2/pkg/bft/backup/backup_svc_test.go b/tm2/pkg/bft/backup/backup_svc_test.go index 29ca7183147..b7ad1098588 100644 --- a/tm2/pkg/bft/backup/backup_svc_test.go +++ b/tm2/pkg/bft/backup/backup_svc_test.go @@ -174,6 +174,9 @@ func TestStreamBlocks(t *testing.T) { EndHeight: tc.end, }}) require.NoError(t, err) + defer func() { + require.NoError(t, stream.Close()) + }() data := []*types.Block(nil) for { From ad4907ac0f5adc6aed5cd71ab24974e5fdaf8491 Mon Sep 17 00:00:00 2001 From: Norman Date: Tue, 1 Apr 2025 12:34:55 +0200 Subject: [PATCH 26/67] chore: test backup service init Signed-off-by: Norman --- tm2/pkg/bft/config/toml.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tm2/pkg/bft/config/toml.go b/tm2/pkg/bft/config/toml.go index ce61ffed5a2..6a0db7193fa 100644 --- a/tm2/pkg/bft/config/toml.go +++ b/tm2/pkg/bft/config/toml.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" + "github.com/gnolang/gno/tm2/pkg/bft/backup" osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/pelletier/go-toml" ) @@ -106,6 +107,11 @@ func ResetTestRoot(testName string) (*Config, string) { config := TestConfig().SetRootDir(rootDir) + if config.Backup == nil { + config.Backup = &backup.Config{} + } + config.Backup.ListenAddress = "unix://" + filepath.Join(rootDir, "backup.sock") + return config, genesisFilePath } From 4002838e0577143c2ed944628d4fb53063f1afd3 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 12:05:37 +0200 Subject: [PATCH 27/67] chore: remove unneeded field Signed-off-by: Norman --- tm2/Makefile | 8 +--- tm2/pkg/bft/backup/backup_svc.go | 5 +-- tm2/pkg/bft/backup/backuppb/backup.pb.go | 41 +++++++------------ tm2/pkg/bft/backup/backuppb/backup.proto | 1 - .../backuppbconnect/backup.connect.go | 5 +-- tm2/pkg/bft/backup/gen.go | 3 ++ 6 files changed, 22 insertions(+), 41 deletions(-) create mode 100644 tm2/pkg/bft/backup/gen.go diff --git a/tm2/Makefile b/tm2/Makefile index 46db85ad547..f2222aa5092 100644 --- a/tm2/Makefile +++ b/tm2/Makefile @@ -67,10 +67,4 @@ _test.pkg.db:; go test $(GOTEST_FLAGS) ./pkg/db/... ./pkg/iavl/benchmarks/. .PHONY: generate generate: go generate -x ./... - $(MAKE) fmt - -.PHONY: generate.backup-grpc -generate.backup-grpc: - cd pkg/bft/backup && go run -modfile ../../../../misc/devdeps/go.mod github.com/bufbuild/buf/cmd/buf generate - go run -modfile ../misc/devdeps/go.mod mvdan.cc/gofumpt -w pkg/bft/backup - go run -modfile ../misc/devdeps/go.mod golang.org/x/tools/cmd/goimports -w pkg/bft/backup \ No newline at end of file + $(MAKE) fmt \ No newline at end of file diff --git a/tm2/pkg/bft/backup/backup_svc.go b/tm2/pkg/bft/backup/backup_svc.go index 086cbc6aab2..c4da1a0339e 100644 --- a/tm2/pkg/bft/backup/backup_svc.go +++ b/tm2/pkg/bft/backup/backup_svc.go @@ -78,10 +78,7 @@ func (b *backupServer) StreamBlocks(_ context.Context, req *connect.Request[back return err } - if err := stream.Send(&backuppb.StreamBlocksResponse{ - Height: height, - Data: data, - }); err != nil { + if err := stream.Send(&backuppb.StreamBlocksResponse{Data: data}); err != nil { return err } } diff --git a/tm2/pkg/bft/backup/backuppb/backup.pb.go b/tm2/pkg/bft/backup/backuppb/backup.pb.go index c90088a6c4f..84c67a40d1d 100644 --- a/tm2/pkg/bft/backup/backuppb/backup.pb.go +++ b/tm2/pkg/bft/backup/backuppb/backup.pb.go @@ -7,12 +7,11 @@ package backuppb import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" - - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( @@ -76,7 +75,6 @@ func (x *StreamBlocksRequest) GetEndHeight() int64 { type StreamBlocksResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Height int64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -112,13 +110,6 @@ func (*StreamBlocksResponse) Descriptor() ([]byte, []int) { return file_backuppb_backup_proto_rawDescGZIP(), []int{1} } -func (x *StreamBlocksResponse) GetHeight() int64 { - if x != nil { - return x.Height - } - return 0 -} - func (x *StreamBlocksResponse) GetData() []byte { if x != nil { return x.Data @@ -136,21 +127,19 @@ var file_backuppb_backup_proto_rawDesc = string([]byte{ 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65, - 0x69, 0x67, 0x68, 0x74, 0x22, 0x42, 0x0a, 0x14, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, - 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, - 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x68, 0x65, - 0x69, 0x67, 0x68, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x32, 0x54, 0x0a, 0x0d, 0x42, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x43, 0x0a, 0x0c, 0x53, 0x74, 0x72, - 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x17, 0x2e, 0x74, 0x6d, 0x2e, 0x53, - 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x74, 0x6d, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, - 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x3d, - 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6e, 0x6f, - 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x67, 0x6e, 0x6f, 0x2f, 0x74, 0x6d, 0x32, 0x2f, 0x70, 0x6b, 0x67, - 0x2f, 0x62, 0x66, 0x74, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x2f, 0x62, 0x61, 0x63, 0x6b, - 0x75, 0x70, 0x70, 0x62, 0x3b, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x70, 0x62, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x67, 0x68, 0x74, 0x22, 0x2a, 0x0a, 0x14, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x32, 0x54, 0x0a, 0x0d, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0x43, 0x0a, 0x0c, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, + 0x73, 0x12, 0x17, 0x2e, 0x74, 0x6d, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x74, 0x6d, 0x2e, + 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6e, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x67, 0x6e, 0x6f, + 0x2f, 0x74, 0x6d, 0x32, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x62, 0x66, 0x74, 0x2f, 0x62, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x70, 0x62, 0x3b, 0x62, 0x61, 0x63, + 0x6b, 0x75, 0x70, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/tm2/pkg/bft/backup/backuppb/backup.proto b/tm2/pkg/bft/backup/backuppb/backup.proto index 2d0296d5d5b..7b257c08bfc 100644 --- a/tm2/pkg/bft/backup/backuppb/backup.proto +++ b/tm2/pkg/bft/backup/backuppb/backup.proto @@ -13,6 +13,5 @@ message StreamBlocksRequest { } message StreamBlocksResponse { - int64 height = 1; bytes data = 2; } \ No newline at end of file diff --git a/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go index 81ec75cdaa0..f0afc1fe508 100644 --- a/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go +++ b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go @@ -5,13 +5,12 @@ package backuppbconnect import ( + connect "connectrpc.com/connect" context "context" errors "errors" + backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" http "net/http" strings "strings" - - connect "connectrpc.com/connect" - backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" ) // This is a compile-time assertion to ensure that this generated file and the connect package are diff --git a/tm2/pkg/bft/backup/gen.go b/tm2/pkg/bft/backup/gen.go new file mode 100644 index 00000000000..1213c9ab049 --- /dev/null +++ b/tm2/pkg/bft/backup/gen.go @@ -0,0 +1,3 @@ +package backup + +//go:generate go run -modfile ../../../../misc/devdeps/go.mod github.com/bufbuild/buf/cmd/buf generate From 7115e80cdff2c1e06e5f1638ba7943691013f007 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 12:35:30 +0200 Subject: [PATCH 28/67] chore: better comment Signed-off-by: Norman --- contribs/tm2backup/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contribs/tm2backup/main.go b/contribs/tm2backup/main.go index 1d83e438ff7..0c2df72cb23 100644 --- a/contribs/tm2backup/main.go +++ b/contribs/tm2backup/main.go @@ -66,7 +66,7 @@ func (c *backupCfg) RegisterFlags(fs *flag.FlagSet) { &c.startHeight, "start", 0, - fmt.Sprintf("Start height. Will be aligned at a multiple of %d blocks. This can't be used when resuming from an existing output directory.", backup.ChunkSize), + fmt.Sprintf("Start height. Will be aligned at a multiple of %d blocks. This option can't be used when resuming from an existing output directory.", backup.ChunkSize), ) fs.Int64Var( &c.endHeight, From c209a20d5fbdc413e187b7533a45d62559a0dbf7 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 13:00:48 +0200 Subject: [PATCH 29/67] chore: add gnoland test Signed-off-by: Norman --- .gitignore | 3 +++ gno.land/cmd/gnoland/restore_test.go | 25 ++++++++++++++++++ .../0000000000000000001.tm2blocks.tar.zst | Bin 0 -> 526 bytes .../gnoland/testdata/backup-2blocks/info.json | 1 + 4 files changed, 29 insertions(+) create mode 100644 gno.land/cmd/gnoland/restore_test.go create mode 100644 gno.land/cmd/gnoland/testdata/backup-2blocks/0000000000000000001.tm2blocks.tar.zst create mode 100644 gno.land/cmd/gnoland/testdata/backup-2blocks/info.json diff --git a/.gitignore b/.gitignore index 82cc109887e..0c604c5e3e4 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ coverage.out .tmp/ wal/ + +# Filelocks +*.lock \ No newline at end of file diff --git a/gno.land/cmd/gnoland/restore_test.go b/gno.land/cmd/gnoland/restore_test.go new file mode 100644 index 00000000000..1e66cf888bf --- /dev/null +++ b/gno.land/cmd/gnoland/restore_test.go @@ -0,0 +1,25 @@ +package main + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/require" +) + +func TestRestore(t *testing.T) { + tmpDir := t.TempDir() + io := commands.NewTestIO() + io.SetOut(os.Stdout) + io.SetErr(os.Stderr) + err := newRestoreCmd(io).ParseAndRun(context.Background(), []string{ + "--data-dir", filepath.Join(tmpDir, "chain-data"), + "--backup-dir", filepath.FromSlash("testdata/backup-2blocks"), + "--genesis", filepath.FromSlash("testdata/backup-2blocks/genesis.json"), + "--lazy", + }) + require.NoError(t, err) +} diff --git a/gno.land/cmd/gnoland/testdata/backup-2blocks/0000000000000000001.tm2blocks.tar.zst b/gno.land/cmd/gnoland/testdata/backup-2blocks/0000000000000000001.tm2blocks.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..6ce94067a12d57d83578602d69df49e7b90daba7 GIT binary patch literal 526 zcmV+p0`dJQwJ-f-01X8Y0F)Xr05C8xFg7+cFfcGMGB!5=H!?Rj03a{`004D!bYXG; zFaQd&0SXItF)lDJFfDRpE-(@UWMy_30wN0tho`i^1`xQG>ONW^D|#fE)?RXIQ{%KN zs)dOZ>`*3tKE4DQ_E&tFJFw^cVrn3`nVLWOkImfX16z0htocD>g%>$^zg`frFt3jC zXlJm30VroNaC$N{b$D!LV{2?QH+Xh5GXLA}d05LH$ zG%~>g1R@Lwl<=^$odZHh3Lx$TNV3)oWsC%k&c#N4L<}eIV!zuu0OWMD(?4dHwU81d z2m%ryzN-GgYm7G1Ft?V_`~1wQ2rOBV#c(68s$SiWiN+1N;b3oi{Q={>4!RA=f z4aDW$BJqFC-OEy!B^cMzPYZG&-7r^OS5WHEeu8F{EAfmLeJ#>%AR=4s54LTd?e4dW zl>!P9$N>lf5CS49GD1N0OPj!mhn!m7)FduC(3 zyF=YXTohTmqDVsopYCbn`9wSl0g)O=790Zr9RM_(;RgWH6AXZQpp}p~({YVLlv@c! zhms_l9}hTkGC+7BX=FeZz@q_j0|wH8kv~A0Qwm$uIf@#C1c>owK!bzdkdSxGw4Dop QrB`sQT;`b^7Oy&yQXLZ3RR910 literal 0 HcmV?d00001 diff --git a/gno.land/cmd/gnoland/testdata/backup-2blocks/info.json b/gno.land/cmd/gnoland/testdata/backup-2blocks/info.json new file mode 100644 index 00000000000..2d7ec37952c --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/backup-2blocks/info.json @@ -0,0 +1 @@ +{"Version":"v1","StartHeight":1,"EndHeight":2} \ No newline at end of file From 534189c96929a6e9cff9b926b7b829b53eb433a4 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 13:06:34 +0200 Subject: [PATCH 30/67] fix: add genesis.json Signed-off-by: Norman --- .gitignore | 3 - gno.land/cmd/gnoland/testdata/.gitignore | 2 + .../testdata/backup-2blocks/genesis.json | 10148 ++++++++++++++++ 3 files changed, 10150 insertions(+), 3 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/.gitignore create mode 100644 gno.land/cmd/gnoland/testdata/backup-2blocks/genesis.json diff --git a/.gitignore b/.gitignore index 0c604c5e3e4..82cc109887e 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,3 @@ coverage.out .tmp/ wal/ - -# Filelocks -*.lock \ No newline at end of file diff --git a/gno.land/cmd/gnoland/testdata/.gitignore b/gno.land/cmd/gnoland/testdata/.gitignore new file mode 100644 index 00000000000..69172b687c4 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/.gitignore @@ -0,0 +1,2 @@ +!genesis.json +*.lock \ No newline at end of file diff --git a/gno.land/cmd/gnoland/testdata/backup-2blocks/genesis.json b/gno.land/cmd/gnoland/testdata/backup-2blocks/genesis.json new file mode 100644 index 00000000000..c352e754016 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/backup-2blocks/genesis.json @@ -0,0 +1,10148 @@ +{ + "genesis_time": "2025-04-02T10:37:59.131763Z", + "chain_id": "dev", + "consensus_params": { + "Block": { + "MaxTxBytes": "1000000", + "MaxDataBytes": "2000000", + "MaxBlockBytes": "0", + "MaxGas": "3000000000", + "TimeIotaMS": "100" + }, + "Validator": null + }, + "validators": [ + { + "address": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "power": "10", + "name": "testvalidator" + } + ], + "app_hash": null, + "app_state": { + "@type": "/gno.GenesisState", + "balances": [ + "g13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6=10000000000ugnot", + "g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa=1000000000000ugnot", + "g1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz=10000000000ugnot", + "g1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7=10000000000ugnot", + "g1qpymzwx4l4cy6cerdyajp9ksvjsf20rk5y9rtt=10000000000ugnot", + "g1f4v282mwyhu29afke4vq5r2xzcm6z3ftnugcnv=1000000000000ugnot", + "g1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6=10000000000ugnot", + "g1z629z04f85k4t5gnkk5egpxw9tqxeec435esap=10000000000ugnot", + "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5=10000000000000ugnot", + "g13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8=10000000000ugnot", + "g1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq=10000000000ugnot", + "g1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3=10000000000ugnot", + "g1q6jrp203fq0239pv38sdq3y3urvd6vt5azacpv=1000000000000ugnot", + "g1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64=10000000000ugnot", + "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj=10000000000000ugnot", + "g1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz=10000000000ugnot", + "g1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv=10000000000ugnot", + "g18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0=10000000000ugnot", + "g14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3=10000000000ugnot", + "g1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac=10000000000ugnot", + "g1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6=10000000000ugnot", + "g1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp=10000000000ugnot", + "g19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w=10000000000ugnot", + "g19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd=10000000000ugnot", + "g1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw=10000000000ugnot", + "g19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a=10000000000ugnot", + "g1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea=10000000000ugnot", + "g14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5=10000000000ugnot", + "g1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r=10000000000ugnot", + "g1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su=10000000000ugnot", + "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq=10000000000ugnot", + "g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s=10000000000ugnot", + "g1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj=10000000000ugnot", + "g1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69=10000000000ugnot", + "g1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0=10000000000ugnot", + "g152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv=10000000000ugnot", + "g1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q=10000000000ugnot", + "g1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc=10000000000ugnot", + "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs=2270000000ugnot", + "g1manfred47kzduec920z88wfr64ylksmdcedlf5=10000000000ugnot", + "g1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q=10000000000ugnot", + "g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5=10000000000ugnot", + "g13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll=10000000000ugnot", + "g19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf=10000000000ugnot", + "g1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g=10000000000ugnot", + "g1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6=10000000000ugnot", + "g13d7jc32adhc39erm5me38w5v7ej7lpvlnqjk73=1000000000000ugnot", + "g18l9us6trqaljw39j94wzf5ftxmd9qqkvrxghd2=1000000000000ugnot", + "g14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa=10000000000ugnot", + "g19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz=10000000000ugnot", + "g1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr=10000000000ugnot", + "g15gdm49ktawvkrl88jadqpucng37yxutucuwaef=10000000000ugnot", + "g187982000zsc493znqt828s90cmp6hcp2erhu6m=10000000000ugnot", + "g1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl=10000000000ugnot", + "g15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n=10000000000ugnot", + "g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9=10000000000ugnot", + "g16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037=10000000000ugnot", + "g1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k=10000000000ugnot" + ], + "txs": [ + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "ufmt", + "path": "gno.land/p/demo/ufmt", + "files": [ + { + "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\tverb := sTor[i+1]\n\t\tif verb == '%' {\n\t\t\tp.buf.writeRune('%')\n\t\t\ti += 2\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\twriteString(p, verb, arg)\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\twriteFloat(p, verb, arg)\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 += 2\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// writeString handles %s formatting\nfunc writeString(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase (interface{ String() string }):\n\t\tp.buf.writeString(v.String())\n\tcase error:\n\t\tp.buf.writeString(v.Error())\n\tcase string:\n\t\tp.buf.writeString(v)\n\tdefault:\n\t\tp.buf.writeString(fallback(verb, v))\n\t}\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// writeFloat handles floating-point formatting verbs\nfunc writeFloat(p *printer, verb rune, arg any) {\n\tswitch v := arg.(type) {\n\tcase float64:\n\t\tswitch verb {\n\t\tcase 'e':\n\t\t\tp.buf.writeString(strconv.FormatFloat(v, 'e', -1, 64))\n\t\tcase 'E':\n\t\t\tp.buf.writeString(strconv.FormatFloat(v, 'E', -1, 64))\n\t\tcase 'f', 'F':\n\t\t\tp.buf.writeString(strconv.FormatFloat(v, 'f', 6, 64))\n\t\tcase 'g':\n\t\t\tp.buf.writeString(strconv.FormatFloat(v, 'g', -1, 64))\n\t\tcase 'G':\n\t\t\tp.buf.writeString(strconv.FormatFloat(v, 'G', -1, 64))\n\t\t}\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\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "owrnuzDKFq1IFj4ksuYl9jtSrK4GabE4eDiZU3O3dIO9SpJyTw9nKn8s3opsMG6wWbqfF0OhRIbPJQ1BwxSKBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "json", + "path": "gno.land/p/demo/json", + "files": [ + { + "name": "LICENSE", + "body": "# MIT License\n\nCopyright (c) 2019 Pyzhov Stepan\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "README.md", + "body": "# JSON Parser\n\nThe JSON parser is a package that provides functionality for parsing and processing JSON strings. This package accepts JSON strings as byte slices.\n\nCurrently, gno does not [support the `reflect` package](https://docs.gno.land/concepts/effective-gno#reflection-is-never-clear), so it cannot retrieve type information at runtime. Therefore, it is designed to infer and handle type information when parsing JSON strings using a state machine approach.\n\nAfter passing through the state machine, JSON strings are represented as the `Node` type. The `Node` type represents nodes for JSON data, including various types such as `ObjectNode`, `ArrayNode`, `StringNode`, `NumberNode`, `BoolNode`, and `NullNode`.\n\nThis package provides methods for manipulating, searching, and extracting the Node type.\n\n## State Machine\n\nTo parse JSON strings, a [finite state machine](https://en.wikipedia.org/wiki/Finite-state_machine) approach is used. The state machine transitions to the next state based on the current state and the input character while parsing the JSON string. Through this method, type information can be inferred and processed without reflect, and the amount of parser code can be significantly reduced.\n\nThe image below shows the state transitions of the state machine according to the states and input characters.\n\n```mermaid\nstateDiagram-v2\n [*] --\u003e __: Start\n __ --\u003e ST: String\n __ --\u003e MI: Number\n __ --\u003e ZE: Zero\n __ --\u003e IN: Integer\n __ --\u003e T1: Boolean (true)\n __ --\u003e F1: Boolean (false)\n __ --\u003e N1: Null\n __ --\u003e ec: Empty Object End\n __ --\u003e cc: Object End\n __ --\u003e bc: Array End\n __ --\u003e co: Object Begin\n __ --\u003e bo: Array Begin\n __ --\u003e cm: Comma\n __ --\u003e cl: Colon\n __ --\u003e OK: Success/End\n ST --\u003e OK: String Complete\n MI --\u003e OK: Number Complete\n ZE --\u003e OK: Zero Complete\n IN --\u003e OK: Integer Complete\n T1 --\u003e OK: True Complete\n F1 --\u003e OK: False Complete\n N1 --\u003e OK: Null Complete\n ec --\u003e OK: Empty Object Complete\n cc --\u003e OK: Object Complete\n bc --\u003e OK: Array Complete\n co --\u003e OB: Inside Object\n bo --\u003e AR: Inside Array\n cm --\u003e KE: Expecting New Key\n cm --\u003e VA: Expecting New Value\n cl --\u003e VA: Expecting Value\n OB --\u003e ST: String in Object (Key)\n OB --\u003e ec: Empty Object\n OB --\u003e cc: End Object\n AR --\u003e ST: String in Array\n AR --\u003e bc: End Array\n KE --\u003e ST: String as Key\n VA --\u003e ST: String as Value\n VA --\u003e MI: Number as Value\n VA --\u003e T1: True as Value\n VA --\u003e F1: False as Value\n VA --\u003e N1: Null as Value\n OK --\u003e [*]: End\n```\n\n## Examples\n\nThis package provides parsing functionality along with encoding and decoding functionality. The following examples demonstrate how to use this package.\n\n### Decoding\n\nDecoding (or Unmarshaling) is the functionality that converts an input byte slice JSON string into a `Node` type.\n\nThe converted `Node` type allows you to modify the JSON data or search and extract data that meets specific conditions.\n\n```go\npackage main\n\nimport (\n \"gno.land/p/demo/json\"\n \"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n node, err := json.Unmarshal([]byte(`{\"foo\": \"var\"}`))\n if err != nil {\n ufmt.Errorf(\"error: %v\", err)\n }\n\n ufmt.Sprintf(\"node: %v\", node)\n}\n```\n\n### Encoding\n\nEncoding (or Marshaling) is the functionality that converts JSON data represented as a Node type into a byte slice JSON string.\n\n\u003e ⚠️ Caution: Converting a large `Node` type into a JSON string may _impact performance_. or might be cause _unexpected behavior_.\n\n```go\npackage main\n\nimport (\n \"gno.land/p/demo/json\"\n \"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n node := ObjectNode(\"\", map[string]*Node{\n \"foo\": StringNode(\"foo\", \"bar\"),\n \"baz\": NumberNode(\"baz\", 100500),\n \"qux\": NullNode(\"qux\"),\n })\n\n b, err := json.Marshal(node)\n if err != nil {\n ufmt.Errorf(\"error: %v\", err)\n }\n\n ufmt.Sprintf(\"json: %s\", string(b))\n}\n```\n\n### Searching\n\nOnce the JSON data converted into a `Node` type, you can **search** and **extract** data that satisfy specific conditions. For example, you can find data with a specific type or data with a specific key.\n\nTo use this functionality, you can use methods in the `GetXXX` prefixed methods. The `MustXXX` methods also provide the same functionality as the former methods, but they will **panic** if data doesn't satisfies the condition.\n\nHere is an example of finding data with a specific key. For more examples, please refer to the [node.gno](node.gno) file.\n\n```go\npackage main\n\nimport (\n \"gno.land/p/demo/json\"\n \"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n root, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null}`))\n if err != nil {\n ufmt.Errorf(\"error: %v\", err)\n }\n\n value, err := root.GetKey(\"foo\")\n if err != nil {\n ufmt.Errorf(\"error occurred while getting key, %s\", err)\n }\n\n if value.MustBool() != true {\n ufmt.Errorf(\"value is not true\")\n }\n\n value, err = root.GetKey(\"bar\")\n if err != nil {\n t.Errorf(\"error occurred while getting key, %s\", err)\n }\n\n _, err = root.GetKey(\"baz\")\n if err == nil {\n t.Errorf(\"key baz is not exist. must be failed\")\n }\n}\n```\n\n## Contributing\n\nPlease submit any issues or pull requests for this package through the GitHub repository at [gnolang/gno](\u003chttps://github.com/gnolang/gno\u003e).\n" + }, + { + "name": "buffer.gno", + "body": "package json\n\nimport (\n\t\"errors\"\n\t\"io\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype buffer struct {\n\tdata []byte\n\tlength int\n\tindex int\n\n\tlast States\n\tstate States\n\tclass Classes\n}\n\n// newBuffer creates a new buffer with the given data\nfunc newBuffer(data []byte) *buffer {\n\treturn \u0026buffer{\n\t\tdata: data,\n\t\tlength: len(data),\n\t\tlast: GO,\n\t\tstate: GO,\n\t}\n}\n\n// first retrieves the first non-whitespace (or other escaped) character in the buffer.\nfunc (b *buffer) first() (byte, error) {\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tc := b.data[b.index]\n\n\t\tif !(c == whiteSpace || c == carriageReturn || c == newLine || c == tab) {\n\t\t\treturn c, nil\n\t\t}\n\t}\n\n\treturn 0, io.EOF\n}\n\n// current returns the byte of the current index.\nfunc (b *buffer) current() (byte, error) {\n\tif b.index \u003e= b.length {\n\t\treturn 0, io.EOF\n\t}\n\n\treturn b.data[b.index], nil\n}\n\n// next moves to the next byte and returns it.\nfunc (b *buffer) next() (byte, error) {\n\tb.index++\n\treturn b.current()\n}\n\n// step just moves to the next position.\nfunc (b *buffer) step() error {\n\t_, err := b.next()\n\treturn err\n}\n\n// move moves the index by the given position.\nfunc (b *buffer) move(pos int) error {\n\tnewIndex := b.index + pos\n\n\tif newIndex \u003e b.length {\n\t\treturn io.EOF\n\t}\n\n\tb.index = newIndex\n\n\treturn nil\n}\n\n// slice returns the slice from the current index to the given position.\nfunc (b *buffer) slice(pos int) ([]byte, error) {\n\tend := b.index + pos\n\n\tif end \u003e b.length {\n\t\treturn nil, io.EOF\n\t}\n\n\treturn b.data[b.index:end], nil\n}\n\n// sliceFromIndices returns a slice of the buffer's data starting from 'start' up to (but not including) 'stop'.\nfunc (b *buffer) sliceFromIndices(start, stop int) []byte {\n\tif start \u003e b.length {\n\t\tstart = b.length\n\t}\n\n\tif stop \u003e b.length {\n\t\tstop = b.length\n\t}\n\n\treturn b.data[start:stop]\n}\n\n// skip moves the index to skip the given byte.\nfunc (b *buffer) skip(bs byte) error {\n\tfor b.index \u003c b.length {\n\t\tif b.data[b.index] == bs \u0026\u0026 !b.backslash() {\n\t\t\treturn nil\n\t\t}\n\n\t\tb.index++\n\t}\n\n\treturn io.EOF\n}\n\n// skipAndReturnIndex moves the buffer index forward by one and returns the new index.\nfunc (b *buffer) skipAndReturnIndex() (int, error) {\n\terr := b.step()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn b.index, nil\n}\n\n// skipUntil moves the buffer index forward until it encounters a byte contained in the endTokens set.\nfunc (b *buffer) skipUntil(endTokens map[byte]bool) (int, error) {\n\tfor b.index \u003c b.length {\n\t\tcurrentByte, err := b.current()\n\t\tif err != nil {\n\t\t\treturn b.index, err\n\t\t}\n\n\t\t// Check if the current byte is in the set of end tokens.\n\t\tif _, exists := endTokens[currentByte]; exists {\n\t\t\treturn b.index, nil\n\t\t}\n\n\t\tb.index++\n\t}\n\n\treturn b.index, io.EOF\n}\n\n// significantTokens is a map where the keys are the significant characters in a JSON path.\n// The values in the map are all true, which allows us to use the map as a set for quick lookups.\nvar significantTokens = [256]bool{\n\tdot: true, // access properties of an object\n\tdollarSign: true, // root object\n\tatSign: true, // current object\n\tbracketOpen: true, // start of an array index or filter expression\n\tbracketClose: true, // end of an array index or filter expression\n}\n\n// filterTokens stores the filter expression tokens.\nvar filterTokens = [256]bool{\n\taesterisk: true, // wildcard\n\tandSign: true,\n\torSign: true,\n}\n\n// skipToNextSignificantToken advances the buffer index to the next significant character.\n// Significant characters are defined based on the JSON path syntax.\nfunc (b *buffer) skipToNextSignificantToken() {\n\tfor b.index \u003c b.length {\n\t\tcurrent := b.data[b.index]\n\n\t\tif significantTokens[current] {\n\t\t\tbreak\n\t\t}\n\n\t\tb.index++\n\t}\n}\n\n// backslash checks to see if the number of backslashes before the current index is odd.\n//\n// This is used to check if the current character is escaped. However, unlike the \"unescape\" function,\n// \"backslash\" only serves to check the number of backslashes.\nfunc (b *buffer) backslash() bool {\n\tif b.index == 0 {\n\t\treturn false\n\t}\n\n\tcount := 0\n\tfor i := b.index - 1; ; i-- {\n\t\tif b.data[i] != backSlash {\n\t\t\tbreak\n\t\t}\n\n\t\tcount++\n\n\t\tif i == 0 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn count%2 != 0\n}\n\n// numIndex holds a map of valid numeric characters\nvar numIndex = [256]bool{\n\t'0': true,\n\t'1': true,\n\t'2': true,\n\t'3': true,\n\t'4': true,\n\t'5': true,\n\t'6': true,\n\t'7': true,\n\t'8': true,\n\t'9': true,\n\t'.': true,\n\t'e': true,\n\t'E': true,\n}\n\n// pathToken checks if the current token is a valid JSON path token.\nfunc (b *buffer) pathToken() error {\n\tvar stack []byte\n\n\tinToken := false\n\tinNumber := false\n\tfirst := b.index\n\n\tfor b.index \u003c b.length {\n\t\tc := b.data[b.index]\n\n\t\tswitch {\n\t\tcase c == doubleQuote || c == singleQuote:\n\t\t\tinToken = true\n\t\t\tif err := b.step(); err != nil {\n\t\t\t\treturn errors.New(\"error stepping through buffer\")\n\t\t\t}\n\n\t\t\tif err := b.skip(c); err != nil {\n\t\t\t\treturn errUnmatchedQuotePath\n\t\t\t}\n\n\t\t\tif b.index \u003e= b.length {\n\t\t\t\treturn errUnmatchedQuotePath\n\t\t\t}\n\n\t\tcase c == bracketOpen || c == parenOpen:\n\t\t\tinToken = true\n\t\t\tstack = append(stack, c)\n\n\t\tcase c == bracketClose || c == parenClose:\n\t\t\tinToken = true\n\t\t\tif len(stack) == 0 || (c == bracketClose \u0026\u0026 stack[len(stack)-1] != bracketOpen) || (c == parenClose \u0026\u0026 stack[len(stack)-1] != parenOpen) {\n\t\t\t\treturn errUnmatchedParenthesis\n\t\t\t}\n\n\t\t\tstack = stack[:len(stack)-1]\n\n\t\tcase pathStateContainsValidPathToken(c):\n\t\t\tinToken = true\n\n\t\tcase c == plus || c == minus:\n\t\t\tif inNumber || (b.index \u003e 0 \u0026\u0026 numIndex[b.data[b.index-1]]) {\n\t\t\t\tinToken = true\n\t\t\t} else if !inToken \u0026\u0026 (b.index+1 \u003c b.length \u0026\u0026 numIndex[b.data[b.index+1]]) {\n\t\t\t\tinToken = true\n\t\t\t\tinNumber = true\n\t\t\t} else if !inToken {\n\t\t\t\treturn errInvalidToken\n\t\t\t}\n\n\t\tdefault:\n\t\t\tif len(stack) != 0 || inToken {\n\t\t\t\tinToken = true\n\t\t\t} else {\n\t\t\t\tgoto end\n\t\t\t}\n\t\t}\n\n\t\tb.index++\n\t}\n\nend:\n\tif len(stack) != 0 {\n\t\treturn errUnmatchedParenthesis\n\t}\n\n\tif first == b.index {\n\t\treturn errors.New(\"no token found\")\n\t}\n\n\tif inNumber \u0026\u0026 !numIndex[b.data[b.index-1]] {\n\t\tinNumber = false\n\t}\n\n\treturn nil\n}\n\nfunc pathStateContainsValidPathToken(c byte) bool {\n\tif significantTokens[c] {\n\t\treturn true\n\t}\n\n\tif filterTokens[c] {\n\t\treturn true\n\t}\n\n\tif numIndex[c] {\n\t\treturn true\n\t}\n\n\tif 'A' \u003c= c \u0026\u0026 c \u003c= 'Z' || 'a' \u003c= c \u0026\u0026 c \u003c= 'z' {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (b *buffer) numeric(token bool) error {\n\tif token {\n\t\tb.last = GO\n\t}\n\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tb.class = b.getClasses(doubleQuote)\n\t\tif b.class == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tb.state = StateTransitionTable[b.last][b.class]\n\t\tif b.state == __ {\n\t\t\tif token {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tif b.state \u003c __ {\n\t\t\treturn nil\n\t\t}\n\n\t\tif b.state \u003c MI || b.state \u003e E3 {\n\t\t\treturn nil\n\t\t}\n\n\t\tb.last = b.state\n\t}\n\n\tif b.last != ZE \u0026\u0026 b.last != IN \u0026\u0026 b.last != FR \u0026\u0026 b.last != E3 {\n\t\treturn errInvalidToken\n\t}\n\n\treturn nil\n}\n\nfunc (b *buffer) getClasses(c byte) Classes {\n\tif b.data[b.index] \u003e= 128 {\n\t\treturn C_ETC\n\t}\n\n\tif c == singleQuote {\n\t\treturn QuoteAsciiClasses[b.data[b.index]]\n\t}\n\n\treturn AsciiClasses[b.data[b.index]]\n}\n\nfunc (b *buffer) getState() States {\n\tb.last = b.state\n\n\tb.class = b.getClasses(doubleQuote)\n\tif b.class == __ {\n\t\treturn __\n\t}\n\n\tb.state = StateTransitionTable[b.last][b.class]\n\n\treturn b.state\n}\n\n// string parses a string token from the buffer.\nfunc (b *buffer) string(search byte, token bool) error {\n\tif token {\n\t\tb.last = GO\n\t}\n\n\tfor ; b.index \u003c b.length; b.index++ {\n\t\tb.class = b.getClasses(search)\n\n\t\tif b.class == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tb.state = StateTransitionTable[b.last][b.class]\n\t\tif b.state == __ {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tif b.state \u003c __ {\n\t\t\tbreak\n\t\t}\n\n\t\tb.last = b.state\n\t}\n\n\treturn nil\n}\n\nfunc (b *buffer) word(bs []byte) error {\n\tvar c byte\n\n\tmax := len(bs)\n\tindex := 0\n\n\tfor ; b.index \u003c b.length \u0026\u0026 index \u003c max; b.index++ {\n\t\tc = b.data[b.index]\n\n\t\tif c != bs[index] {\n\t\t\treturn errInvalidToken\n\t\t}\n\n\t\tindex++\n\t\tif index \u003e= max {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != max {\n\t\treturn errInvalidToken\n\t}\n\n\treturn nil\n}\n\nfunc numberKind2f64(value any) (result float64, err error) {\n\tswitch typed := value.(type) {\n\tcase float64:\n\t\tresult = typed\n\tcase float32:\n\t\tresult = float64(typed)\n\tcase int:\n\t\tresult = float64(typed)\n\tcase int8:\n\t\tresult = float64(typed)\n\tcase int16:\n\t\tresult = float64(typed)\n\tcase int32:\n\t\tresult = float64(typed)\n\tcase int64:\n\t\tresult = float64(typed)\n\tcase uint:\n\t\tresult = float64(typed)\n\tcase uint8:\n\t\tresult = float64(typed)\n\tcase uint16:\n\t\tresult = float64(typed)\n\tcase uint32:\n\t\tresult = float64(typed)\n\tcase uint64:\n\t\tresult = float64(typed)\n\tdefault:\n\t\terr = ufmt.Errorf(\"invalid number type: %T\", value)\n\t}\n\n\treturn\n}\n" + }, + { + "name": "buffer_test.gno", + "body": "package json\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBufferCurrent(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\texpected byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid current byte\",\n\t\t\tbuffer: \u0026buffer{\n\t\t\t\tdata: []byte(\"test\"),\n\t\t\t\tlength: 4,\n\t\t\t\tindex: 1,\n\t\t\t},\n\t\t\texpected: 'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF\",\n\t\t\tbuffer: \u0026buffer{\n\t\t\t\tdata: []byte(\"test\"),\n\t\t\t\tlength: 4,\n\t\t\t\tindex: 4,\n\t\t\t},\n\t\t\texpected: 0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.current()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.current() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"buffer.current() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferStep(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid step\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF error\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.step()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.step() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferNext(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\twant byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid next byte\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\twant: 'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF error\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\twant: 0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.next()\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.next() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"buffer.next() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferSlice(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\tpos int\n\t\twant []byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid slice -- 0 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 0,\n\t\t\twant: nil,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 1 character\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 1,\n\t\t\twant: []byte(\"t\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 2 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos: 2,\n\t\t\twant: []byte(\"es\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 3 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 3,\n\t\t\twant: []byte(\"tes\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Valid slice -- 4 characters\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tpos: 4,\n\t\t\twant: []byte(\"test\"),\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"EOF error\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 3},\n\t\t\tpos: 2,\n\t\t\twant: nil,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := tt.buffer.slice(tt.pos)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.slice() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif string(got) != string(tt.want) {\n\t\t\t\tt.Errorf(\"buffer.slice() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferMove(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\tpos int\n\t\twantErr bool\n\t\twantIdx int\n\t}{\n\t\t{\n\t\t\tname: \"Valid move\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos: 2,\n\t\t\twantErr: false,\n\t\t\twantIdx: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"Move beyond length\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 1},\n\t\t\tpos: 4,\n\t\t\twantErr: true,\n\t\t\twantIdx: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.move(tt.pos)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.move() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t\tif tt.buffer.index != tt.wantIdx {\n\t\t\t\tt.Errorf(\"buffer.move() index = %v, want %v\", tt.buffer.index, tt.wantIdx)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferSkip(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuffer *buffer\n\t\tb byte\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Skip byte\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tb: 'e',\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Skip to EOF\",\n\t\t\tbuffer: \u0026buffer{data: []byte(\"test\"), length: 4, index: 0},\n\t\t\tb: 'x',\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.buffer.skip(tt.b)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"buffer.skip() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSkipToNextSignificantToken(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []byte\n\t\texpected int\n\t}{\n\t\t{\"No significant chars\", []byte(\"abc\"), 3},\n\t\t{\"One significant char at start\", []byte(\".abc\"), 0},\n\t\t{\"Significant char in middle\", []byte(\"ab.c\"), 2},\n\t\t{\"Multiple significant chars\", []byte(\"a$.c\"), 1},\n\t\t{\"Significant char at end\", []byte(\"abc$\"), 3},\n\t\t{\"Only significant chars\", []byte(\"$.\"), 0},\n\t\t{\"Empty string\", []byte(\"\"), 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb := newBuffer(tt.input)\n\t\t\tb.skipToNextSignificantToken()\n\t\t\tif b.index != tt.expected {\n\t\t\t\tt.Errorf(\"after skipToNextSignificantToken(), got index = %v, want %v\", b.index, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc mockBuffer(s string) *buffer {\n\treturn newBuffer([]byte(s))\n}\n\nfunc TestSkipAndReturnIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\texpected int\n\t}{\n\t\t{\"StartOfString\", \"\", 0},\n\t\t{\"MiddleOfString\", \"abcdef\", 1},\n\t\t{\"EndOfString\", \"abc\", 1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot, err := buf.skipAndReturnIndex()\n\t\t\tif err != nil \u0026\u0026 tt.input != \"\" { // Expect no error unless input is empty\n\t\t\t\tt.Errorf(\"skipAndReturnIndex() error = %v\", err)\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"skipAndReturnIndex() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSkipUntil(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\ttokens map[byte]bool\n\t\texpected int\n\t}{\n\t\t{\"SkipToToken\", \"abcdefg\", map[byte]bool{'c': true}, 2},\n\t\t{\"SkipToEnd\", \"abcdefg\", map[byte]bool{'h': true}, 7},\n\t\t{\"SkipNone\", \"abcdefg\", map[byte]bool{'a': true}, 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot, err := buf.skipUntil(tt.tokens)\n\t\t\tif err != nil \u0026\u0026 got != len(tt.input) { // Expect error only if reached end without finding token\n\t\t\t\tt.Errorf(\"skipUntil() error = %v\", err)\n\t\t\t}\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"skipUntil() = %v, want %v\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSliceFromIndices(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\tstart int\n\t\tend int\n\t\texpected string\n\t}{\n\t\t{\"FullString\", \"abcdefg\", 0, 7, \"abcdefg\"},\n\t\t{\"Substring\", \"abcdefg\", 2, 5, \"cde\"},\n\t\t{\"OutOfBounds\", \"abcdefg\", 5, 10, \"fg\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := mockBuffer(tt.input)\n\t\t\tgot := buf.sliceFromIndices(tt.start, tt.end)\n\t\t\tif string(got) != tt.expected {\n\t\t\t\tt.Errorf(\"sliceFromIndices() = %v, want %v\", string(got), tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferToken(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tpath string\n\t\tindex int\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Simple valid path\",\n\t\t\tpath: \"@.length\",\n\t\t\tindex: 8,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Path with array expr\",\n\t\t\tpath: \"@['foo'].0.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Path with array expr and simple fomula\",\n\t\t\tpath: \"@['foo'].[(@.length - 1)].*\",\n\t\t\tindex: 27,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Path with filter expr\",\n\t\t\tpath: \"@['foo'].[?(@.bar == 1 \u0026 @.baz \u003c @.length)].*\",\n\t\t\tindex: 45,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"addition of foo and bar\",\n\t\t\tpath: \"@.foo+@.bar\",\n\t\t\tindex: 11,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"logical AND of foo and bar\",\n\t\t\tpath: \"@.foo \u0026\u0026 @.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"logical OR of foo and bar\",\n\t\t\tpath: \"@.foo || @.bar\",\n\t\t\tindex: 14,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"accessing third element of foo\",\n\t\t\tpath: \"@.foo,3\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"accessing last element of array\",\n\t\t\tpath: \"@.length-1\",\n\t\t\tindex: 10,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"number 1\",\n\t\t\tpath: \"1\",\n\t\t\tindex: 1,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"float\",\n\t\t\tpath: \"3.1e4\",\n\t\t\tindex: 5,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"float with minus\",\n\t\t\tpath: \"3.1e-4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"float with plus\",\n\t\t\tpath: \"3.1e+4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative number\",\n\t\t\tpath: \"-12345\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative float\",\n\t\t\tpath: \"-3.1e4\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative float with minus\",\n\t\t\tpath: \"-3.1e-4\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negative float with plus\",\n\t\t\tpath: \"-3.1e+4\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"string number\",\n\t\t\tpath: \"'12345'\",\n\t\t\tindex: 7,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"string with backslash\",\n\t\t\tpath: \"'foo \\\\'bar '\",\n\t\t\tindex: 12,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"string with inner double quotes\",\n\t\t\tpath: \"'foo \\\"bar \\\"'\",\n\t\t\tindex: 12,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis 1\",\n\t\t\tpath: \"(@abc)\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis 2\",\n\t\t\tpath: \"[()]\",\n\t\t\tindex: 4,\n\t\t\tisErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis mismatch\",\n\t\t\tpath: \"[(])\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis mismatch 2\",\n\t\t\tpath: \"(\",\n\t\t\tindex: 1,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"parenthesis mismatch 3\",\n\t\t\tpath: \"())]\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"bracket mismatch\",\n\t\t\tpath: \"[()\",\n\t\t\tindex: 3,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"bracket mismatch 2\",\n\t\t\tpath: \"()]\",\n\t\t\tindex: 2,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"path does not close bracket\",\n\t\t\tpath: \"@.foo[)\",\n\t\t\tindex: 6,\n\t\t\tisErr: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tbuf := newBuffer([]byte(tt.path))\n\n\t\t\terr := buf.pathToken()\n\t\t\tif tt.isErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"Expected an error for path `%s`, but got none\", tt.path)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif err == nil \u0026\u0026 tt.isErr {\n\t\t\t\tt.Errorf(\"Expected an error for path `%s`, but got none\", tt.path)\n\t\t\t}\n\n\t\t\tif buf.index != tt.index {\n\t\t\t\tt.Errorf(\"Expected final index %d, got %d (token: `%s`) for path `%s`\", tt.index, buf.index, string(buf.data[buf.index]), tt.path)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferFirst(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t\texpected byte\n\t}{\n\t\t{\n\t\t\tname: \"Valid first byte\",\n\t\t\tdata: []byte(\"test\"),\n\t\t\texpected: 't',\n\t\t},\n\t\t{\n\t\t\tname: \"Empty buffer\",\n\t\t\tdata: []byte(\"\"),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"Whitespace buffer\",\n\t\t\tdata: []byte(\" \"),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"whitespace in middle\",\n\t\t\tdata: []byte(\"hello world\"),\n\t\t\texpected: 'h',\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb := newBuffer(tt.data)\n\n\t\t\tgot, err := b.first()\n\t\t\tif err != nil \u0026\u0026 tt.expected != 0 {\n\t\t\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t\t\t}\n\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"Expected first byte to be %q, got %q\", tt.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "builder.gno", + "body": "package json\n\ntype NodeBuilder struct {\n\tnode *Node\n}\n\nfunc Builder() *NodeBuilder {\n\treturn \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n}\n\nfunc (b *NodeBuilder) WriteString(key, value string) *NodeBuilder {\n\tb.node.AppendObject(key, StringNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteNumber(key string, value float64) *NodeBuilder {\n\tb.node.AppendObject(key, NumberNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteBool(key string, value bool) *NodeBuilder {\n\tb.node.AppendObject(key, BoolNode(\"\", value))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteNull(key string) *NodeBuilder {\n\tb.node.AppendObject(key, NullNode(\"\"))\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteObject(key string, fn func(*NodeBuilder)) *NodeBuilder {\n\tnestedBuilder := \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n\tfn(nestedBuilder)\n\tb.node.AppendObject(key, nestedBuilder.node)\n\treturn b\n}\n\nfunc (b *NodeBuilder) WriteArray(key string, fn func(*ArrayBuilder)) *NodeBuilder {\n\tarrayBuilder := \u0026ArrayBuilder{nodes: []*Node{}}\n\tfn(arrayBuilder)\n\tb.node.AppendObject(key, ArrayNode(\"\", arrayBuilder.nodes))\n\treturn b\n}\n\nfunc (b *NodeBuilder) Node() *Node {\n\treturn b.node\n}\n\ntype ArrayBuilder struct {\n\tnodes []*Node\n}\n\nfunc (ab *ArrayBuilder) WriteString(value string) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, StringNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteNumber(value float64) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, NumberNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteInt(value int) *ArrayBuilder {\n\treturn ab.WriteNumber(float64(value))\n}\n\nfunc (ab *ArrayBuilder) WriteBool(value bool) *ArrayBuilder {\n\tab.nodes = append(ab.nodes, BoolNode(\"\", value))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteNull() *ArrayBuilder {\n\tab.nodes = append(ab.nodes, NullNode(\"\"))\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteObject(fn func(*NodeBuilder)) *ArrayBuilder {\n\tnestedBuilder := \u0026NodeBuilder{node: ObjectNode(\"\", nil)}\n\tfn(nestedBuilder)\n\tab.nodes = append(ab.nodes, nestedBuilder.node)\n\treturn ab\n}\n\nfunc (ab *ArrayBuilder) WriteArray(fn func(*ArrayBuilder)) *ArrayBuilder {\n\tnestedArrayBuilder := \u0026ArrayBuilder{nodes: []*Node{}}\n\tfn(nestedArrayBuilder)\n\tab.nodes = append(ab.nodes, ArrayNode(\"\", nestedArrayBuilder.nodes))\n\treturn ab\n}\n" + }, + { + "name": "builder_test.gno", + "body": "package json\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNodeBuilder(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbuild func() *Node\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"plain object\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteString(\"name\", \"Alice\").\n\t\t\t\t\tWriteNumber(\"age\", 30).\n\t\t\t\t\tWriteBool(\"is_student\", false).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"name\":\"Alice\",\"age\":30,\"is_student\":false}`,\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteString(\"name\", \"Alice\").\n\t\t\t\t\tWriteObject(\"address\", func(b *NodeBuilder) {\n\t\t\t\t\t\tb.WriteString(\"city\", \"New York\").\n\t\t\t\t\t\t\tWriteNumber(\"zipcode\", 10001)\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"name\":\"Alice\",\"address\":{\"city\":\"New York\",\"zipcode\":10001}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"null node\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().WriteNull(\"foo\").Node()\n\t\t\t},\n\t\t\texpected: `{\"foo\":null}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array node\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"items\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteString(\"item1\").\n\t\t\t\t\t\t\tWriteString(\"item2\").\n\t\t\t\t\t\t\tWriteString(\"item3\")\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"items\":[\"item1\",\"item2\",\"item3\"]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array with objects\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"users\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteObject(func(b *NodeBuilder) {\n\t\t\t\t\t\t\tb.WriteString(\"name\", \"Bob\").\n\t\t\t\t\t\t\t\tWriteNumber(\"age\", 25)\n\t\t\t\t\t\t}).\n\t\t\t\t\t\t\tWriteObject(func(b *NodeBuilder) {\n\t\t\t\t\t\t\t\tb.WriteString(\"name\", \"Carol\").\n\t\t\t\t\t\t\t\t\tWriteNumber(\"age\", 27)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"users\":[{\"name\":\"Bob\",\"age\":25},{\"name\":\"Carol\",\"age\":27}]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"array with various types\",\n\t\t\tbuild: func() *Node {\n\t\t\t\treturn Builder().\n\t\t\t\t\tWriteArray(\"values\", func(ab *ArrayBuilder) {\n\t\t\t\t\t\tab.WriteString(\"item1\").\n\t\t\t\t\t\t\tWriteNumber(123).\n\t\t\t\t\t\t\tWriteBool(true).\n\t\t\t\t\t\t\tWriteNull()\n\t\t\t\t\t}).\n\t\t\t\t\tNode()\n\t\t\t},\n\t\t\texpected: `{\"values\":[\"item1\",123,true,null]}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode := tt.build()\n\t\t\tvalue, err := Marshal(node)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t\t}\n\t\t\tif string(value) != tt.expected {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", tt.expected, string(value))\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "decode.gno", + "body": "// ref: https://github.com/spyzhov/ajson/blob/master/decode.go\n\npackage json\n\nimport (\n\t\"errors\"\n\t\"io\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// This limits the max nesting depth to prevent stack overflow.\n// This is permitted by https://tools.ietf.org/html/rfc7159#section-9\nconst maxNestingDepth = 10000\n\n// Unmarshal parses the JSON-encoded data and returns a Node.\n// The data must be a valid JSON-encoded value.\n//\n// Usage:\n//\n//\tnode, err := json.Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tif err != nil {\n//\t\tufmt.Println(err)\n//\t}\n//\tprintln(node) // {\"key\": \"value\"}\nfunc Unmarshal(data []byte) (*Node, error) {\n\tbuf := newBuffer(data)\n\n\tvar (\n\t\tstate States\n\t\tkey *string\n\t\tcurrent *Node\n\t\tnesting int\n\t\tuseKey = func() **string {\n\t\t\ttmp := cptrs(key)\n\t\t\tkey = nil\n\t\t\treturn \u0026tmp\n\t\t}\n\t\terr error\n\t)\n\n\tif _, err = buf.first(); err != nil {\n\t\treturn nil, io.EOF\n\t}\n\n\tfor {\n\t\tstate = buf.getState()\n\t\tif state == __ {\n\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t}\n\n\t\t// region state machine\n\t\tif state \u003e= GO {\n\t\t\tswitch buf.state {\n\t\t\tcase ST: // string\n\t\t\t\tif current != nil \u0026\u0026 current.IsObject() \u0026\u0026 key == nil {\n\t\t\t\t\t// key detected\n\t\t\t\t\tif key, err = getString(buf); err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tbuf.state = CO\n\t\t\t\t} else {\n\t\t\t\t\tcurrent, nesting, err = createNestedNode(current, buf, String, nesting, useKey())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\terr = buf.string(doubleQuote, false)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tcurrent, nesting = updateNode(current, buf, nesting, true)\n\t\t\t\t\tbuf.state = OK\n\t\t\t\t}\n\n\t\t\tcase MI, ZE, IN: // number\n\t\t\t\tcurrent, err = processNumericNode(current, buf, useKey())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase T1, F1: // boolean\n\t\t\t\tliteral := falseLiteral\n\t\t\t\tif buf.state == T1 {\n\t\t\t\t\tliteral = trueLiteral\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = processLiteralNode(current, buf, Boolean, literal, useKey(), nesting)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase N1: // null\n\t\t\t\tcurrent, nesting, err = processLiteralNode(current, buf, Null, nullLiteral, useKey(), nesting)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// region action\n\t\t\tswitch state {\n\t\t\tcase ec, cc: // \u003cempty\u003e }\n\t\t\t\tif key != nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = updateNodeAndSetBufferState(current, buf, nesting, Object)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase bc: // ]\n\t\t\t\tcurrent, nesting, err = updateNodeAndSetBufferState(current, buf, nesting, Array)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\tcase co, bo: // { [\n\t\t\t\tvalTyp, bState := Object, OB\n\t\t\t\tif state == bo {\n\t\t\t\t\tvalTyp, bState = Array, AR\n\t\t\t\t}\n\n\t\t\t\tcurrent, nesting, err = createNestedNode(current, buf, valTyp, nesting, useKey())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.state = bState\n\n\t\t\tcase cm: // ,\n\t\t\t\tif current == nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tif !current.isContainer() {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tif current.IsObject() {\n\t\t\t\t\tbuf.state = KE // key expected\n\t\t\t\t} else {\n\t\t\t\t\tbuf.state = VA // value expected\n\t\t\t\t}\n\n\t\t\tcase cl: // :\n\t\t\t\tif current == nil || !current.IsObject() || key == nil {\n\t\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t\t}\n\n\t\t\t\tbuf.state = VA\n\n\t\t\tdefault:\n\t\t\t\treturn nil, unexpectedTokenError(buf.data, buf.index)\n\t\t\t}\n\t\t}\n\n\t\tif buf.step() != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif _, err = buf.first(); err != nil {\n\t\t\terr = nil\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif current == nil || buf.state != OK {\n\t\treturn nil, io.EOF\n\t}\n\n\troot := current.root()\n\tif !root.ready() {\n\t\treturn nil, io.EOF\n\t}\n\n\treturn root, err\n}\n\n// UnmarshalSafe parses the JSON-encoded data and returns a Node.\nfunc UnmarshalSafe(data []byte) (*Node, error) {\n\tvar safe []byte\n\tsafe = append(safe, data...)\n\treturn Unmarshal(safe)\n}\n\n// processNumericNode creates a new node, processes a numeric value,\n// sets the node's borders, and moves to the previous node.\nfunc processNumericNode(current *Node, buf *buffer, key **string) (*Node, error) {\n\tvar err error\n\tcurrent, err = createNode(current, buf, Number, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = buf.numeric(false); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcurrent.borders[1] = buf.index\n\tif current.prev != nil {\n\t\tcurrent = current.prev\n\t}\n\n\tbuf.index -= 1\n\tbuf.state = OK\n\n\treturn current, nil\n}\n\n// processLiteralNode creates a new node, processes a literal value,\n// sets the node's borders, and moves to the previous node.\nfunc processLiteralNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tliteralType ValueType,\n\tliteralValue []byte,\n\tuseKey **string,\n\tnesting int,\n) (*Node, int, error) {\n\tvar err error\n\tcurrent, nesting, err = createLiteralNode(current, buf, literalType, literalValue, useKey, nesting)\n\tif err != nil {\n\t\treturn nil, nesting, err\n\t}\n\treturn current, nesting, nil\n}\n\n// isValidContainerType checks if the current node is a valid container (object or array).\n// The container must satisfy the following conditions:\n// 1. The current node must not be nil.\n// 2. The current node must be an object or array.\n// 3. The current node must not be ready.\nfunc isValidContainerType(current *Node, nodeType ValueType) bool {\n\tswitch nodeType {\n\tcase Object:\n\t\treturn current != nil \u0026\u0026 current.IsObject() \u0026\u0026 !current.ready()\n\tcase Array:\n\t\treturn current != nil \u0026\u0026 current.IsArray() \u0026\u0026 !current.ready()\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// getString extracts a string from the buffer and advances the buffer index past the string.\nfunc getString(b *buffer) (*string, error) {\n\tstart := b.index\n\tif err := b.string(doubleQuote, false); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvalue, ok := Unquote(b.data[start:b.index+1], doubleQuote)\n\tif !ok {\n\t\treturn nil, unexpectedTokenError(b.data, start)\n\t}\n\n\treturn \u0026value, nil\n}\n\n// createNode creates a new node and sets the key if it is not nil.\nfunc createNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnodeType ValueType,\n\tkey **string,\n) (*Node, error) {\n\tvar err error\n\tcurrent, err = NewNode(current, buf, nodeType, key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn current, nil\n}\n\n// createNestedNode creates a new nested node (array or object) and sets the key if it is not nil.\nfunc createNestedNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnodeType ValueType,\n\tnesting int,\n\tkey **string,\n) (*Node, int, error) {\n\tvar err error\n\tif nesting, err = checkNestingDepth(nesting); err != nil {\n\t\treturn nil, nesting, err\n\t}\n\n\tif current, err = createNode(current, buf, nodeType, key); err != nil {\n\t\treturn nil, nesting, err\n\t}\n\n\treturn current, nesting, nil\n}\n\n// createLiteralNode creates a new literal node and sets the key if it is not nil.\n// The literal is a byte slice that represents a boolean or null value.\nfunc createLiteralNode(\n\tcurrent *Node,\n\tbuf *buffer,\n\tliteralType ValueType,\n\tliteral []byte,\n\tuseKey **string,\n\tnesting int,\n) (*Node, int, error) {\n\tvar err error\n\tif current, err = createNode(current, buf, literalType, useKey); err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tif err = buf.word(literal); err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tcurrent, nesting = updateNode(current, buf, nesting, false)\n\tbuf.state = OK\n\n\treturn current, nesting, nil\n}\n\n// updateNode updates the current node and returns the previous node.\nfunc updateNode(\n\tcurrent *Node, buf *buffer, nesting int, decreaseLevel bool,\n) (*Node, int) {\n\tcurrent.borders[1] = buf.index + 1\n\n\tprev := current.prev\n\tif prev == nil {\n\t\treturn current, nesting\n\t}\n\n\tcurrent = prev\n\tif decreaseLevel {\n\t\tnesting--\n\t}\n\n\treturn current, nesting\n}\n\n// updateNodeAndSetBufferState updates the current node and sets the buffer state to OK.\nfunc updateNodeAndSetBufferState(\n\tcurrent *Node,\n\tbuf *buffer,\n\tnesting int,\n\ttyp ValueType,\n) (*Node, int, error) {\n\tif !isValidContainerType(current, typ) {\n\t\treturn nil, nesting, unexpectedTokenError(buf.data, buf.index)\n\t}\n\n\tcurrent, nesting = updateNode(current, buf, nesting, true)\n\tbuf.state = OK\n\n\treturn current, nesting, nil\n}\n\n// checkNestingDepth checks if the nesting depth is within the maximum allowed depth.\nfunc checkNestingDepth(nesting int) (int, error) {\n\tif nesting \u003e= maxNestingDepth {\n\t\treturn nesting, errors.New(\"maximum nesting depth exceeded\")\n\t}\n\n\treturn nesting + 1, nil\n}\n\nfunc unexpectedTokenError(data []byte, index int) error {\n\treturn ufmt.Errorf(\"unexpected token at index %d. data %b\", index, data)\n}\n" + }, + { + "name": "decode_test.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\ntype testNode struct {\n\tname string\n\tinput []byte\n\tvalue []byte\n\t_type ValueType\n}\n\nfunc simpleValid(test *testNode, t *testing.T) {\n\troot, err := Unmarshal(test.input)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): %s\", test.input, err.Error())\n\t} else if root == nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): root is nil\", test.name)\n\t} else if root.nodeType != test._type {\n\t\tt.Errorf(\"Error on Unmarshal(%s): wrong type\", test.name)\n\t} else if !bytes.Equal(root.source(), test.value) {\n\t\tt.Errorf(\"Error on Unmarshal(%s): %s != %s\", test.name, root.source(), test.value)\n\t}\n}\n\nfunc simpleInvalid(test *testNode, t *testing.T) {\n\troot, err := Unmarshal(test.input)\n\tif err == nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): error expected, got '%s'\", test.name, root.source())\n\t} else if root != nil {\n\t\tt.Errorf(\"Error on Unmarshal(%s): root is not nil\", test.name)\n\t}\n}\n\nfunc simpleCorrupted(name string) *testNode {\n\treturn \u0026testNode{name: name, input: []byte(name)}\n}\n\nfunc TestUnmarshal_StringSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"blank\", input: []byte(\"\\\"\\\"\"), _type: String, value: []byte(\"\\\"\\\"\")},\n\t\t{name: \"char\", input: []byte(\"\\\"c\\\"\"), _type: String, value: []byte(\"\\\"c\\\"\")},\n\t\t{name: \"word\", input: []byte(\"\\\"cat\\\"\"), _type: String, value: []byte(\"\\\"cat\\\"\")},\n\t\t{name: \"spaces\", input: []byte(\" \\\"good cat or dog\\\"\\r\\n \"), _type: String, value: []byte(\"\\\"good cat or dog\\\"\")},\n\t\t{name: \"backslash\", input: []byte(\"\\\"good \\\\\\\"cat\\\\\\\"\\\"\"), _type: String, value: []byte(\"\\\"good \\\\\\\"cat\\\\\\\"\\\"\")},\n\t\t{name: \"backslash 2\", input: []byte(\"\\\"good \\\\\\\\\\\\\\\"cat\\\\\\\"\\\"\"), _type: String, value: []byte(\"\\\"good \\\\\\\\\\\\\\\"cat\\\\\\\"\\\"\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_NumericSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"1\", input: []byte(\"1\"), _type: Number, value: []byte(\"1\")},\n\t\t{name: \"-1\", input: []byte(\"-1\"), _type: Number, value: []byte(\"-1\")},\n\n\t\t{name: \"1234567890\", input: []byte(\"1234567890\"), _type: Number, value: []byte(\"1234567890\")},\n\t\t{name: \"-123\", input: []byte(\"-123\"), _type: Number, value: []byte(\"-123\")},\n\n\t\t{name: \"123.456\", input: []byte(\"123.456\"), _type: Number, value: []byte(\"123.456\")},\n\t\t{name: \"-123.456\", input: []byte(\"-123.456\"), _type: Number, value: []byte(\"-123.456\")},\n\n\t\t{name: \"1e3\", input: []byte(\"1e3\"), _type: Number, value: []byte(\"1e3\")},\n\t\t{name: \"1e+3\", input: []byte(\"1e+3\"), _type: Number, value: []byte(\"1e+3\")},\n\t\t{name: \"1e-3\", input: []byte(\"1e-3\"), _type: Number, value: []byte(\"1e-3\")},\n\t\t{name: \"-1e3\", input: []byte(\"-1e3\"), _type: Number, value: []byte(\"-1e3\")},\n\t\t{name: \"-1e-3\", input: []byte(\"-1e-3\"), _type: Number, value: []byte(\"-1e-3\")},\n\n\t\t{name: \"1.123e3456\", input: []byte(\"1.123e3456\"), _type: Number, value: []byte(\"1.123e3456\")},\n\t\t{name: \"1.123e-3456\", input: []byte(\"1.123e-3456\"), _type: Number, value: []byte(\"1.123e-3456\")},\n\t\t{name: \"-1.123e3456\", input: []byte(\"-1.123e3456\"), _type: Number, value: []byte(\"-1.123e3456\")},\n\t\t{name: \"-1.123e-3456\", input: []byte(\"-1.123e-3456\"), _type: Number, value: []byte(\"-1.123e-3456\")},\n\n\t\t{name: \"1E3\", input: []byte(\"1E3\"), _type: Number, value: []byte(\"1E3\")},\n\t\t{name: \"1E-3\", input: []byte(\"1E-3\"), _type: Number, value: []byte(\"1E-3\")},\n\t\t{name: \"-1E3\", input: []byte(\"-1E3\"), _type: Number, value: []byte(\"-1E3\")},\n\t\t{name: \"-1E-3\", input: []byte(\"-1E-3\"), _type: Number, value: []byte(\"-1E-3\")},\n\n\t\t{name: \"1.123E3456\", input: []byte(\"1.123E3456\"), _type: Number, value: []byte(\"1.123E3456\")},\n\t\t{name: \"1.123E-3456\", input: []byte(\"1.123E-3456\"), _type: Number, value: []byte(\"1.123E-3456\")},\n\t\t{name: \"-1.123E3456\", input: []byte(\"-1.123E3456\"), _type: Number, value: []byte(\"-1.123E3456\")},\n\t\t{name: \"-1.123E-3456\", input: []byte(\"-1.123E-3456\"), _type: Number, value: []byte(\"-1.123E-3456\")},\n\n\t\t{name: \"-1.123E-3456 with spaces\", input: []byte(\" \\r -1.123E-3456 \\t\\n\"), _type: Number, value: []byte(\"-1.123E-3456\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(test.input)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): %s\", test.name, err.Error())\n\t\t\t} else if root == nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): root is nil\", test.name)\n\t\t\t} else if root.nodeType != test._type {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): wrong type\", test.name)\n\t\t\t} else if !bytes.Equal(root.source(), test.value) {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(%s): %s != %s\", test.name, root.source(), test.value)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_StringSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"white NL\", input: []byte(\"\\\"foo\\nbar\\\"\")},\n\t\t{name: \"white R\", input: []byte(\"\\\"foo\\rbar\\\"\")},\n\t\t{name: \"white Tab\", input: []byte(\"\\\"foo\\tbar\\\"\")},\n\t\t{name: \"wrong quotes\", input: []byte(\"'cat'\")},\n\t\t{name: \"double string\", input: []byte(\"\\\"Hello\\\" \\\"World\\\"\")},\n\t\t{name: \"quotes in quotes\", input: []byte(\"\\\"good \\\"cat\\\"\\\"\")},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ObjectSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"{}\", input: []byte(\"{}\"), _type: Object, value: []byte(\"{}\")},\n\t\t{name: `{ \\r\\n }`, input: []byte(\"{ \\r\\n }\"), _type: Object, value: []byte(\"{ \\r\\n }\")},\n\t\t{name: `{\"key\":1}`, input: []byte(`{\"key\":1}`), _type: Object, value: []byte(`{\"key\":1}`)},\n\t\t{name: `{\"key\":true}`, input: []byte(`{\"key\":true}`), _type: Object, value: []byte(`{\"key\":true}`)},\n\t\t{name: `{\"key\":\"value\"}`, input: []byte(`{\"key\":\"value\"}`), _type: Object, value: []byte(`{\"key\":\"value\"}`)},\n\t\t{name: `{\"foo\":\"bar\",\"baz\":\"foo\"}`, input: []byte(`{\"foo\":\"bar\", \"baz\":\"foo\"}`), _type: Object, value: []byte(`{\"foo\":\"bar\", \"baz\":\"foo\"}`)},\n\t\t{name: \"spaces\", input: []byte(` { \"foo\" : \"bar\" , \"baz\" : \"foo\" } `), _type: Object, value: []byte(`{ \"foo\" : \"bar\" , \"baz\" : \"foo\" }`)},\n\t\t{name: \"nested\", input: []byte(`{\"foo\":{\"bar\":{\"baz\":{}}}}`), _type: Object, value: []byte(`{\"foo\":{\"bar\":{\"baz\":{}}}}`)},\n\t\t{name: \"array\", input: []byte(`{\"array\":[{},{},{\"foo\":[{\"bar\":[\"baz\"]}]}]}`), _type: Object, value: []byte(`{\"array\":[{},{},{\"foo\":[{\"bar\":[\"baz\"]}]}]}`)},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ObjectSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"{{{\\\"key\\\": \\\"foo\\\"{{{{\"),\n\t\tsimpleCorrupted(\"}\"),\n\t\tsimpleCorrupted(\"{ }}}}}}}\"),\n\t\tsimpleCorrupted(\" }\"),\n\t\tsimpleCorrupted(\"{,}\"),\n\t\tsimpleCorrupted(\"{:}\"),\n\t\tsimpleCorrupted(\"{100000}\"),\n\t\tsimpleCorrupted(\"{1:1}\"),\n\t\tsimpleCorrupted(\"{'1:2,3:4'}\"),\n\t\tsimpleCorrupted(`{\"d\"}`),\n\t\tsimpleCorrupted(`{\"foo\"}`),\n\t\tsimpleCorrupted(`{\"foo\":}`),\n\t\tsimpleCorrupted(`{:\"foo\"}`),\n\t\tsimpleCorrupted(`{\"foo\":bar}`),\n\t\tsimpleCorrupted(`{\"foo\":\"bar\",}`),\n\t\tsimpleCorrupted(`{}{}`),\n\t\tsimpleCorrupted(`{},{}`),\n\t\tsimpleCorrupted(`{[},{]}`),\n\t\tsimpleCorrupted(`{[,]}`),\n\t\tsimpleCorrupted(`{[]}`),\n\t\tsimpleCorrupted(`{}1`),\n\t\tsimpleCorrupted(`1{}`),\n\t\tsimpleCorrupted(`{\"x\"::1}`),\n\t\tsimpleCorrupted(`{null:null}`),\n\t\tsimpleCorrupted(`{\"foo:\"bar\"}`),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_NullSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"nul\", input: []byte(\"nul\")},\n\t\t{name: \"nil\", input: []byte(\"nil\")},\n\t\t{name: \"nill\", input: []byte(\"nill\")},\n\t\t{name: \"NILL\", input: []byte(\"NILL\")},\n\t\t{name: \"Null\", input: []byte(\"Null\")},\n\t\t{name: \"NULL\", input: []byte(\"NULL\")},\n\t\t{name: \"spaces\", input: []byte(\"Nu ll\")},\n\t\t{name: \"null1\", input: []byte(\"null1\")},\n\t\t{name: \"double\", input: []byte(\"null null\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_BoolSimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"lower true\", input: []byte(\"true\"), _type: Boolean, value: []byte(\"true\")},\n\t\t{name: \"lower false\", input: []byte(\"false\"), _type: Boolean, value: []byte(\"false\")},\n\t\t{name: \"spaces true\", input: []byte(\" true\\r\\n \"), _type: Boolean, value: []byte(\"true\")},\n\t\t{name: \"spaces false\", input: []byte(\" false\\r\\n \"), _type: Boolean, value: []byte(\"false\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_BoolSimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"tru\"),\n\t\tsimpleCorrupted(\"fals\"),\n\t\tsimpleCorrupted(\"tre\"),\n\t\tsimpleCorrupted(\"fal se\"),\n\t\tsimpleCorrupted(\"true false\"),\n\t\tsimpleCorrupted(\"True\"),\n\t\tsimpleCorrupted(\"TRUE\"),\n\t\tsimpleCorrupted(\"False\"),\n\t\tsimpleCorrupted(\"FALSE\"),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ArraySimpleSuccess(t *testing.T) {\n\ttests := []*testNode{\n\t\t{name: \"[]\", input: []byte(\"[]\"), _type: Array, value: []byte(\"[]\")},\n\t\t{name: \"[1]\", input: []byte(\"[1]\"), _type: Array, value: []byte(\"[1]\")},\n\t\t{name: \"[1,2,3]\", input: []byte(\"[1,2,3]\"), _type: Array, value: []byte(\"[1,2,3]\")},\n\t\t{name: \"[1, 2, 3]\", input: []byte(\"[1, 2, 3]\"), _type: Array, value: []byte(\"[1, 2, 3]\")},\n\t\t{name: \"[1,[2],3]\", input: []byte(\"[1,[2],3]\"), _type: Array, value: []byte(\"[1,[2],3]\")},\n\t\t{name: \"[[],[],[]]\", input: []byte(\"[[],[],[]]\"), _type: Array, value: []byte(\"[[],[],[]]\")},\n\t\t{name: \"[[[[[]]]]]\", input: []byte(\"[[[[[]]]]]\"), _type: Array, value: []byte(\"[[[[[]]]]]\")},\n\t\t{name: \"[true,null,1,\\\"foo\\\",[]]\", input: []byte(\"[true,null,1,\\\"foo\\\",[]]\"), _type: Array, value: []byte(\"[true,null,1,\\\"foo\\\",[]]\")},\n\t\t{name: \"spaces\", input: []byte(\"\\n\\r [\\n1\\n ]\\r\\n\"), _type: Array, value: []byte(\"[\\n1\\n ]\")},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleValid(test, t)\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_ArraySimpleCorrupted(t *testing.T) {\n\ttests := []*testNode{\n\t\tsimpleCorrupted(\"[,]\"),\n\t\tsimpleCorrupted(\"[]\\\\\"),\n\t\tsimpleCorrupted(\"[1,]\"),\n\t\tsimpleCorrupted(\"[[]\"),\n\t\tsimpleCorrupted(\"[]]\"),\n\t\tsimpleCorrupted(\"1[]\"),\n\t\tsimpleCorrupted(\"[]1\"),\n\t\tsimpleCorrupted(\"[[]1]\"),\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsimpleInvalid(test, t)\n\t\t})\n\t}\n}\n\n// Examples from https://json.org/example.html\nfunc TestUnmarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tvalue string\n\t}{\n\t\t{\n\t\t\tname: \"glossary\",\n\t\t\tvalue: `{\n\t\t\t\t\"glossary\": {\n\t\t\t\t\t\"title\": \"example glossary\",\n\t\t\t\t\t\"GlossDiv\": {\n\t\t\t\t\t\t\"title\": \"S\",\n\t\t\t\t\t\t\"GlossList\": {\n\t\t\t\t\t\t\t\"GlossEntry\": {\n\t\t\t\t\t\t\t\t\"ID\": \"SGML\",\n\t\t\t\t\t\t\t\t\"SortAs\": \"SGML\",\n\t\t\t\t\t\t\t\t\"GlossTerm\": \"Standard Generalized Markup Language\",\n\t\t\t\t\t\t\t\t\"Acronym\": \"SGML\",\n\t\t\t\t\t\t\t\t\"Abbrev\": \"ISO 8879:1986\",\n\t\t\t\t\t\t\t\t\"GlossDef\": {\n\t\t\t\t\t\t\t\t\t\"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\n\t\t\t\t\t\t\t\t\t\"GlossSeeAlso\": [\"GML\", \"XML\"]\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\"GlossSee\": \"markup\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"menu\",\n\t\t\tvalue: `{\"menu\": {\n\t\t\t\t\"id\": \"file\",\n\t\t\t\t\"value\": \"File\",\n\t\t\t\t\"popup\": {\n\t\t\t\t \"menuitem\": [\n\t\t\t\t\t{\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n\t\t\t\t\t{\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n\t\t\t\t\t{\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n\t\t\t\t ]\n\t\t\t\t}\n\t\t\t}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"widget\",\n\t\t\tvalue: `{\"widget\": {\n\t\t\t\t\"debug\": \"on\",\n\t\t\t\t\"window\": {\n\t\t\t\t\t\"title\": \"Sample Konfabulator Widget\",\n\t\t\t\t\t\"name\": \"main_window\",\n\t\t\t\t\t\"width\": 500,\n\t\t\t\t\t\"height\": 500\n\t\t\t\t},\n\t\t\t\t\"image\": { \n\t\t\t\t\t\"src\": \"Images/Sun.png\",\n\t\t\t\t\t\"name\": \"sun1\",\n\t\t\t\t\t\"hOffset\": 250,\n\t\t\t\t\t\"vOffset\": 250,\n\t\t\t\t\t\"alignment\": \"center\"\n\t\t\t\t},\n\t\t\t\t\"text\": {\n\t\t\t\t\t\"data\": \"Click Here\",\n\t\t\t\t\t\"size\": 36,\n\t\t\t\t\t\"style\": \"bold\",\n\t\t\t\t\t\"name\": \"text1\",\n\t\t\t\t\t\"hOffset\": 250,\n\t\t\t\t\t\"vOffset\": 100,\n\t\t\t\t\t\"alignment\": \"center\",\n\t\t\t\t\t\"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\"\n\t\t\t\t}\n\t\t\t}} `,\n\t\t},\n\t\t{\n\t\t\tname: \"web-app\",\n\t\t\tvalue: `{\"web-app\": {\n\t\t\t\t\"servlet\": [ \n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxCDS\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.CDSServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t \"configGlossary:installationAt\": \"Philadelphia, PA\",\n\t\t\t\t\t \"configGlossary:adminEmail\": \"ksm@pobox.com\",\n\t\t\t\t\t \"configGlossary:poweredBy\": \"Cofax\",\n\t\t\t\t\t \"configGlossary:poweredByIcon\": \"/images/cofax.gif\",\n\t\t\t\t\t \"configGlossary:staticPath\": \"/content/static\",\n\t\t\t\t\t \"templateProcessorClass\": \"org.cofax.WysiwygTemplate\",\n\t\t\t\t\t \"templateLoaderClass\": \"org.cofax.FilesTemplateLoader\",\n\t\t\t\t\t \"templatePath\": \"templates\",\n\t\t\t\t\t \"templateOverridePath\": \"\",\n\t\t\t\t\t \"defaultListTemplate\": \"listTemplate.htm\",\n\t\t\t\t\t \"defaultFileTemplate\": \"articleTemplate.htm\",\n\t\t\t\t\t \"useJSP\": false,\n\t\t\t\t\t \"jspListTemplate\": \"listTemplate.jsp\",\n\t\t\t\t\t \"jspFileTemplate\": \"articleTemplate.jsp\",\n\t\t\t\t\t \"cachePackageTagsTrack\": 200,\n\t\t\t\t\t \"cachePackageTagsStore\": 200,\n\t\t\t\t\t \"cachePackageTagsRefresh\": 60,\n\t\t\t\t\t \"cacheTemplatesTrack\": 100,\n\t\t\t\t\t \"cacheTemplatesStore\": 50,\n\t\t\t\t\t \"cacheTemplatesRefresh\": 15,\n\t\t\t\t\t \"cachePagesTrack\": 200,\n\t\t\t\t\t \"cachePagesStore\": 100,\n\t\t\t\t\t \"cachePagesRefresh\": 10,\n\t\t\t\t\t \"cachePagesDirtyRead\": 10,\n\t\t\t\t\t \"searchEngineListTemplate\": \"forSearchEnginesList.htm\",\n\t\t\t\t\t \"searchEngineFileTemplate\": \"forSearchEngines.htm\",\n\t\t\t\t\t \"searchEngineRobotsDb\": \"WEB-INF/robots.db\",\n\t\t\t\t\t \"useDataStore\": true,\n\t\t\t\t\t \"dataStoreClass\": \"org.cofax.SqlDataStore\",\n\t\t\t\t\t \"redirectionClass\": \"org.cofax.SqlRedirection\",\n\t\t\t\t\t \"dataStoreName\": \"cofax\",\n\t\t\t\t\t \"dataStoreDriver\": \"com.microsoft.jdbc.sqlserver.SQLServerDriver\",\n\t\t\t\t\t \"dataStoreUrl\": \"jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon\",\n\t\t\t\t\t \"dataStoreUser\": \"sa\",\n\t\t\t\t\t \"dataStorePassword\": \"dataStoreTestQuery\",\n\t\t\t\t\t \"dataStoreTestQuery\": \"SET NOCOUNT ON;select test='test';\",\n\t\t\t\t\t \"dataStoreLogFile\": \"/usr/local/tomcat/logs/datastore.log\",\n\t\t\t\t\t \"dataStoreInitConns\": 10,\n\t\t\t\t\t \"dataStoreMaxConns\": 100,\n\t\t\t\t\t \"dataStoreConnUsageLimit\": 100,\n\t\t\t\t\t \"dataStoreLogLevel\": \"debug\",\n\t\t\t\t\t \"maxUrlLength\": 500}},\n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxEmail\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.EmailServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t\"mailHost\": \"mail1\",\n\t\t\t\t\t\"mailHostOverride\": \"mail2\"}},\n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxAdmin\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.AdminServlet\"},\n\t\t\t \n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"fileServlet\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cds.FileServlet\"},\n\t\t\t\t {\n\t\t\t\t\t\"servlet-name\": \"cofaxTools\",\n\t\t\t\t\t\"servlet-class\": \"org.cofax.cms.CofaxToolsServlet\",\n\t\t\t\t\t\"init-param\": {\n\t\t\t\t\t \"templatePath\": \"toolstemplates/\",\n\t\t\t\t\t \"log\": 1,\n\t\t\t\t\t \"logLocation\": \"/usr/local/tomcat/logs/CofaxTools.log\",\n\t\t\t\t\t \"logMaxSize\": \"\",\n\t\t\t\t\t \"dataLog\": 1,\n\t\t\t\t\t \"dataLogLocation\": \"/usr/local/tomcat/logs/dataLog.log\",\n\t\t\t\t\t \"dataLogMaxSize\": \"\",\n\t\t\t\t\t \"removePageCache\": \"/content/admin/remove?cache=pages\u0026id=\",\n\t\t\t\t\t \"removeTemplateCache\": \"/content/admin/remove?cache=templates\u0026id=\",\n\t\t\t\t\t \"fileTransferFolder\": \"/usr/local/tomcat/webapps/content/fileTransferFolder\",\n\t\t\t\t\t \"lookInContext\": 1,\n\t\t\t\t\t \"adminGroupID\": 4,\n\t\t\t\t\t \"betaServer\": true}}],\n\t\t\t\t\"servlet-mapping\": {\n\t\t\t\t \"cofaxCDS\": \"/\",\n\t\t\t\t \"cofaxEmail\": \"/cofaxutil/aemail/*\",\n\t\t\t\t \"cofaxAdmin\": \"/admin/*\",\n\t\t\t\t \"fileServlet\": \"/static/*\",\n\t\t\t\t \"cofaxTools\": \"/tools/*\"},\n\t\t\t \n\t\t\t\t\"taglib\": {\n\t\t\t\t \"taglib-uri\": \"cofax.tld\",\n\t\t\t\t \"taglib-location\": \"/WEB-INF/tlds/cofax.tld\"}}}`,\n\t\t},\n\t\t{\n\t\t\tname: \"SVG Viewer\",\n\t\t\tvalue: `{\"menu\": {\n\t\t\t\t\"header\": \"SVG Viewer\",\n\t\t\t\t\"items\": [\n\t\t\t\t\t{\"id\": \"Open\"},\n\t\t\t\t\t{\"id\": \"OpenNew\", \"label\": \"Open New\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"ZoomIn\", \"label\": \"Zoom In\"},\n\t\t\t\t\t{\"id\": \"ZoomOut\", \"label\": \"Zoom Out\"},\n\t\t\t\t\t{\"id\": \"OriginalView\", \"label\": \"Original View\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Quality\"},\n\t\t\t\t\t{\"id\": \"Pause\"},\n\t\t\t\t\t{\"id\": \"Mute\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Find\", \"label\": \"Find...\"},\n\t\t\t\t\t{\"id\": \"FindAgain\", \"label\": \"Find Again\"},\n\t\t\t\t\t{\"id\": \"Copy\"},\n\t\t\t\t\t{\"id\": \"CopyAgain\", \"label\": \"Copy Again\"},\n\t\t\t\t\t{\"id\": \"CopySVG\", \"label\": \"Copy SVG\"},\n\t\t\t\t\t{\"id\": \"ViewSVG\", \"label\": \"View SVG\"},\n\t\t\t\t\t{\"id\": \"ViewSource\", \"label\": \"View Source\"},\n\t\t\t\t\t{\"id\": \"SaveAs\", \"label\": \"Save As\"},\n\t\t\t\t\tnull,\n\t\t\t\t\t{\"id\": \"Help\"},\n\t\t\t\t\t{\"id\": \"About\", \"label\": \"About Adobe CVG Viewer...\"}\n\t\t\t\t]\n\t\t\t}}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_, err := Unmarshal([]byte(test.value))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshalSafe(t *testing.T) {\n\tjson := []byte(`{ \"store\": {\n\t\t\"book\": [ \n\t\t { \"category\": \"reference\",\n\t\t\t\"author\": \"Nigel Rees\",\n\t\t\t\"title\": \"Sayings of the Century\",\n\t\t\t\"price\": 8.95\n\t\t },\n\t\t { \"category\": \"fiction\",\n\t\t\t\"author\": \"Evelyn Waugh\",\n\t\t\t\"title\": \"Sword of Honour\",\n\t\t\t\"price\": 12.99\n\t\t },\n\t\t { \"category\": \"fiction\",\n\t\t\t\"author\": \"Herman Melville\",\n\t\t\t\"title\": \"Moby Dick\",\n\t\t\t\"isbn\": \"0-553-21311-3\",\n\t\t\t\"price\": 8.99\n\t\t },\n\t\t { \"category\": \"fiction\",\n\t\t\t\"author\": \"J. R. R. Tolkien\",\n\t\t\t\"title\": \"The Lord of the Rings\",\n\t\t\t\"isbn\": \"0-395-19395-8\",\n\t\t\t\"price\": 22.99\n\t\t }\n\t\t],\n\t\t\"bicycle\": {\n\t\t \"color\": \"red\",\n\t\t \"price\": 19.95\n\t\t}\n\t }\n\t}`)\n\tsafe, err := UnmarshalSafe(json)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t} else if safe == nil {\n\t\tt.Errorf(\"Error on Unmarshal: safe is nil\")\n\t} else {\n\t\troot, err := Unmarshal(json)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t\t} else if root == nil {\n\t\t\tt.Errorf(\"Error on Unmarshal: root is nil\")\n\t\t} else if !bytes.Equal(root.source(), safe.source()) {\n\t\t\tt.Errorf(\"Error on UnmarshalSafe: values not same\")\n\t\t}\n\t}\n}\n\n// BenchmarkGoStdUnmarshal-8 \t 61698\t 19350 ns/op\t 288 B/op\t 6 allocs/op\n// BenchmarkUnmarshal-8 \t 45620\t 26165 ns/op\t 21889 B/op\t 367 allocs/op\n//\n// type bench struct {\n// \tName string `json:\"name\"`\n// \tValue int `json:\"value\"`\n// }\n\n// func BenchmarkGoStdUnmarshal(b *testing.B) {\n// \tdata := []byte(webApp)\n// \tfor i := 0; i \u003c b.N; i++ {\n// \t\terr := json.Unmarshal(data, \u0026bench{})\n// \t\tif err != nil {\n// \t\t\tb.Fatal(err)\n// \t\t}\n// \t}\n// }\n\n// func BenchmarkUnmarshal(b *testing.B) {\n// \tdata := []byte(webApp)\n// \tfor i := 0; i \u003c b.N; i++ {\n// \t\t_, err := Unmarshal(data)\n// \t\tif err != nil {\n// \t\t\tb.Fatal(err)\n// \t\t}\n// \t}\n// }\n" + }, + { + "name": "encode.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Marshal returns the JSON encoding of a Node.\nfunc Marshal(node *Node) ([]byte, error) {\n\tvar (\n\t\tbuf bytes.Buffer\n\t\tsVal string\n\t\tbVal bool\n\t\tnVal float64\n\t\toVal []byte\n\t\terr error\n\t)\n\n\tif node == nil {\n\t\treturn nil, errors.New(\"node is nil\")\n\t}\n\n\tif !node.modified \u0026\u0026 !node.ready() {\n\t\treturn nil, errors.New(\"node is not ready\")\n\t}\n\n\tif !node.modified \u0026\u0026 node.ready() {\n\t\tbuf.Write(node.source())\n\t}\n\n\tif node.modified {\n\t\tswitch node.nodeType {\n\t\tcase Null:\n\t\t\tbuf.Write(nullLiteral)\n\n\t\tcase Number:\n\t\t\tnVal, err = node.GetNumeric()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tnum := strconv.FormatFloat(nVal, 'f', -1, 64)\n\t\t\tbuf.WriteString(num)\n\n\t\tcase String:\n\t\t\tsVal, err = node.GetString()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tquoted := ufmt.Sprintf(\"%s\", strconv.Quote(sVal))\n\t\t\tbuf.WriteString(quoted)\n\n\t\tcase Boolean:\n\t\t\tbVal, err = node.GetBool()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbStr := ufmt.Sprintf(\"%t\", bVal)\n\t\t\tbuf.WriteString(bStr)\n\n\t\tcase Array:\n\t\t\tbuf.WriteByte(bracketOpen)\n\n\t\t\tfor i := 0; i \u003c len(node.next); i++ {\n\t\t\t\tif i != 0 {\n\t\t\t\t\tbuf.WriteByte(comma)\n\t\t\t\t}\n\n\t\t\t\telem, ok := node.next[strconv.Itoa(i)]\n\t\t\t\tif !ok {\n\t\t\t\t\treturn nil, ufmt.Errorf(\"array element %d is not found\", i)\n\t\t\t\t}\n\n\t\t\t\toVal, err = Marshal(elem)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.Write(oVal)\n\t\t\t}\n\n\t\t\tbuf.WriteByte(bracketClose)\n\n\t\tcase Object:\n\t\t\tbuf.WriteByte(curlyOpen)\n\n\t\t\tbVal = false\n\t\t\tfor k, v := range node.next {\n\t\t\t\tif bVal {\n\t\t\t\t\tbuf.WriteByte(comma)\n\t\t\t\t} else {\n\t\t\t\t\tbVal = true\n\t\t\t\t}\n\n\t\t\t\tkey := ufmt.Sprintf(\"%s\", strconv.Quote(k))\n\t\t\t\tbuf.WriteString(key)\n\t\t\t\tbuf.WriteByte(colon)\n\n\t\t\t\toVal, err = Marshal(v)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tbuf.Write(oVal)\n\t\t\t}\n\n\t\t\tbuf.WriteByte(curlyClose)\n\t\t}\n\t}\n\n\treturn buf.Bytes(), nil\n}\n" + }, + { + "name": "encode_test.gno", + "body": "package json\n\nimport \"testing\"\n\nfunc TestMarshal_Primitive(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t}{\n\t\t{\n\t\t\tname: \"null\",\n\t\t\tnode: NullNode(\"\"),\n\t\t},\n\t\t{\n\t\t\tname: \"true\",\n\t\t\tnode: BoolNode(\"\", true),\n\t\t},\n\t\t{\n\t\t\tname: \"false\",\n\t\t\tnode: BoolNode(\"\", false),\n\t\t},\n\t\t{\n\t\t\tname: `\"string\"`,\n\t\t\tnode: StringNode(\"\", \"string\"),\n\t\t},\n\t\t{\n\t\t\tname: `\"one \\\"encoded\\\" string\"`,\n\t\t\tnode: StringNode(\"\", `one \"encoded\" string`),\n\t\t},\n\t\t{\n\t\t\tname: `{\"foo\":\"bar\"}`,\n\t\t\tnode: ObjectNode(\"\", map[string]*Node{\n\t\t\t\t\"foo\": StringNode(\"foo\", \"bar\"),\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"42\",\n\t\t\tnode: NumberNode(\"\", 42),\n\t\t},\n\t\t{\n\t\t\tname: \"3.14\",\n\t\t\tnode: NumberNode(\"\", 3.14),\n\t\t},\n\t\t{\n\t\t\tname: `[1,2,3]`,\n\t\t\tnode: ArrayNode(\"\", []*Node{\n\t\t\t\tNumberNode(\"0\", 1),\n\t\t\t\tNumberNode(\"2\", 2),\n\t\t\t\tNumberNode(\"3\", 3),\n\t\t\t}),\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvalue, err := Marshal(test.node)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t\t} else if string(value) != test.name {\n\t\t\t\tt.Errorf(\"wrong result: '%s', expected '%s'\", value, test.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMarshal_Object(t *testing.T) {\n\tnode := ObjectNode(\"\", map[string]*Node{\n\t\t\"foo\": StringNode(\"foo\", \"bar\"),\n\t\t\"baz\": NumberNode(\"baz\", 100500),\n\t\t\"qux\": NullNode(\"qux\"),\n\t})\n\n\tmustKey := []string{\"foo\", \"baz\", \"qux\"}\n\n\tvalue, err := Marshal(node)\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\", err)\n\t}\n\n\t// the order of keys in the map is not guaranteed\n\t// so we need to unmarshal the result and check the keys\n\tdecoded, err := Unmarshal(value)\n\tif err != nil {\n\t\tt.Errorf(\"unexpected error: %s\", err)\n\t}\n\n\tfor _, key := range mustKey {\n\t\tif node, err := decoded.GetKey(key); err != nil {\n\t\t\tt.Errorf(\"unexpected error: %s\", err)\n\t\t} else {\n\t\t\tif node == nil {\n\t\t\t\tt.Errorf(\"node is nil\")\n\t\t\t} else if node.key == nil {\n\t\t\t\tt.Errorf(\"key is nil\")\n\t\t\t} else if *node.key != key {\n\t\t\t\tt.Errorf(\"wrong key: '%s', expected '%s'\", *node.key, key)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc valueNode(prev *Node, key string, typ ValueType, val any) *Node {\n\tcurr := \u0026Node{\n\t\tprev: prev,\n\t\tdata: nil,\n\t\tkey: \u0026key,\n\t\tborders: [2]int{0, 0},\n\t\tvalue: val,\n\t\tmodified: true,\n\t}\n\n\tif val != nil {\n\t\tcurr.nodeType = typ\n\t}\n\n\treturn curr\n}\n\nfunc TestMarshal_Errors(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode func() (node *Node)\n\t}{\n\t\t{\n\t\t\tname: \"nil\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"broken\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\tnode = Must(Unmarshal([]byte(`{}`)))\n\t\t\t\tnode.borders[1] = 0\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Numeric\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", Number, false)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"String\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", String, false)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Bool\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn valueNode(nil, \"\", Boolean, 1)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Array_1\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\tnode = ArrayNode(\"\", nil)\n\t\t\t\tnode.next[\"1\"] = NullNode(\"1\")\n\t\t\t\treturn\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Array_2\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn ArrayNode(\"\", []*Node{valueNode(nil, \"\", Boolean, 1)})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Object\",\n\t\t\tnode: func() (node *Node) {\n\t\t\t\treturn ObjectNode(\"\", map[string]*Node{\"key\": valueNode(nil, \"key\", Boolean, 1)})\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvalue, err := Marshal(test.node())\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected error\")\n\t\t\t} else if len(value) != 0 {\n\t\t\t\tt.Errorf(\"wrong result\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMarshal_Nil(t *testing.T) {\n\t_, err := Marshal(nil)\n\tif err == nil {\n\t\tt.Error(\"Expected error for nil node, but got nil\")\n\t}\n}\n\nfunc TestMarshal_NotModified(t *testing.T) {\n\tnode := \u0026Node{}\n\t_, err := Marshal(node)\n\tif err == nil {\n\t\tt.Error(\"Expected error for not modified node, but got nil\")\n\t}\n}\n\nfunc TestMarshalCycleReference(t *testing.T) {\n\tnode1 := \u0026Node{\n\t\tkey: stringPtr(\"node1\"),\n\t\tnodeType: String,\n\t\tnext: map[string]*Node{\n\t\t\t\"next\": nil,\n\t\t},\n\t}\n\n\tnode2 := \u0026Node{\n\t\tkey: stringPtr(\"node2\"),\n\t\tnodeType: String,\n\t\tprev: node1,\n\t}\n\n\tnode1.next[\"next\"] = node2\n\n\t_, err := Marshal(node1)\n\tif err == nil {\n\t\tt.Error(\"Expected error for cycle reference, but got nil\")\n\t}\n}\n\nfunc TestMarshalNoCycleReference(t *testing.T) {\n\tnode1 := \u0026Node{\n\t\tkey: stringPtr(\"node1\"),\n\t\tnodeType: String,\n\t\tvalue: \"value1\",\n\t\tmodified: true,\n\t}\n\n\tnode2 := \u0026Node{\n\t\tkey: stringPtr(\"node2\"),\n\t\tnodeType: String,\n\t\tvalue: \"value2\",\n\t\tmodified: true,\n\t}\n\n\t_, err := Marshal(node1)\n\tif err != nil {\n\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t}\n\n\t_, err = Marshal(node2)\n\tif err != nil {\n\t\tt.Errorf(\"Unexpected error: %v\", err)\n\t}\n}\n\nfunc stringPtr(s string) *string {\n\treturn \u0026s\n}\n" + }, + { + "name": "errors.gno", + "body": "package json\n\nimport \"errors\"\n\nvar (\n\terrNilNode = errors.New(\"node is nil\")\n\terrNotArrayNode = errors.New(\"node is not array\")\n\terrNotBoolNode = errors.New(\"node is not boolean\")\n\terrNotNullNode = errors.New(\"node is not null\")\n\terrNotNumberNode = errors.New(\"node is not number\")\n\terrNotObjectNode = errors.New(\"node is not object\")\n\terrNotStringNode = errors.New(\"node is not string\")\n\terrInvalidToken = errors.New(\"invalid token\")\n\terrIndexNotFound = errors.New(\"index not found\")\n\terrInvalidAppend = errors.New(\"can't append value to non-appendable node\")\n\terrInvalidAppendCycle = errors.New(\"appending value to itself or its children or parents will cause a cycle\")\n\terrInvalidEscapeSequence = errors.New(\"invalid escape sequence\")\n\terrInvalidStringValue = errors.New(\"invalid string value\")\n\terrEmptyBooleanNode = errors.New(\"boolean node is empty\")\n\terrEmptyStringNode = errors.New(\"string node is empty\")\n\terrKeyRequired = errors.New(\"key is required for object\")\n\terrUnmatchedParenthesis = errors.New(\"mismatched bracket or parenthesis\")\n\terrUnmatchedQuotePath = errors.New(\"unmatched quote in path\")\n)\n\nvar (\n\terrInvalidStringInput = errors.New(\"invalid string input\")\n\terrMalformedBooleanValue = errors.New(\"malformed boolean value\")\n\terrEmptyByteSlice = errors.New(\"empty byte slice\")\n\terrInvalidExponentValue = errors.New(\"invalid exponent value\")\n\terrNonDigitCharacters = errors.New(\"non-digit characters found\")\n\terrNumericRangeExceeded = errors.New(\"numeric value exceeds the range limit\")\n\terrMultipleDecimalPoints = errors.New(\"multiple decimal points found\")\n)\n" + }, + { + "name": "escape.gno", + "body": "package json\n\nimport (\n\t\"unicode/utf8\"\n)\n\nconst (\n\tsupplementalPlanesOffset = 0x10000\n\thighSurrogateOffset = 0xD800\n\tlowSurrogateOffset = 0xDC00\n\tsurrogateEnd = 0xDFFF\n\tbasicMultilingualPlaneOffset = 0xFFFF\n\tbadHex = -1\n\n\tsingleUnicodeEscapeLen = 6\n\tsurrogatePairLen = 12\n)\n\nvar hexLookupTable = [256]int{\n\t'0': 0x0, '1': 0x1, '2': 0x2, '3': 0x3, '4': 0x4,\n\t'5': 0x5, '6': 0x6, '7': 0x7, '8': 0x8, '9': 0x9,\n\t'A': 0xA, 'B': 0xB, 'C': 0xC, 'D': 0xD, 'E': 0xE, 'F': 0xF,\n\t'a': 0xA, 'b': 0xB, 'c': 0xC, 'd': 0xD, 'e': 0xE, 'f': 0xF,\n\t// Fill unspecified index-value pairs with key and value of -1\n\t'G': -1, 'H': -1, 'I': -1, 'J': -1,\n\t'K': -1, 'L': -1, 'M': -1, 'N': -1,\n\t'O': -1, 'P': -1, 'Q': -1, 'R': -1,\n\t'S': -1, 'T': -1, 'U': -1, 'V': -1,\n\t'W': -1, 'X': -1, 'Y': -1, 'Z': -1,\n\t'g': -1, 'h': -1, 'i': -1, 'j': -1,\n\t'k': -1, 'l': -1, 'm': -1, 'n': -1,\n\t'o': -1, 'p': -1, 'q': -1, 'r': -1,\n\t's': -1, 't': -1, 'u': -1, 'v': -1,\n\t'w': -1, 'x': -1, 'y': -1, 'z': -1,\n}\n\nfunc h2i(c byte) int {\n\treturn hexLookupTable[c]\n}\n\n// Unescape takes an input byte slice, processes it to Unescape certain characters,\n// and writes the result into an output byte slice.\n//\n// it returns the processed slice and any error encountered during the Unescape operation.\nfunc Unescape(input, output []byte) ([]byte, error) {\n\t// ensure the output slice has enough capacity to hold the input slice.\n\tinputLen := len(input)\n\tif cap(output) \u003c inputLen {\n\t\toutput = make([]byte, inputLen)\n\t}\n\n\tinPos, outPos := 0, 0\n\n\tfor inPos \u003c len(input) {\n\t\tc := input[inPos]\n\t\tif c != backSlash {\n\t\t\toutput[outPos] = c\n\t\t\tinPos++\n\t\t\toutPos++\n\t\t} else {\n\t\t\t// process escape sequence\n\t\t\tinLen, outLen, err := processEscapedUTF8(input[inPos:], output[outPos:])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tinPos += inLen\n\t\t\toutPos += outLen\n\t\t}\n\t}\n\n\treturn output[:outPos], nil\n}\n\n// isSurrogatePair returns true if the rune is a surrogate pair.\n//\n// A surrogate pairs are used in UTF-16 encoding to encode characters\n// outside the Basic Multilingual Plane (BMP).\nfunc isSurrogatePair(r rune) bool {\n\treturn highSurrogateOffset \u003c= r \u0026\u0026 r \u003c= surrogateEnd\n}\n\n// isHighSurrogate checks if the rune is a high surrogate (U+D800 to U+DBFF).\nfunc isHighSurrogate(r rune) bool {\n\treturn r \u003e= highSurrogateOffset \u0026\u0026 r \u003c= 0xDBFF\n}\n\n// isLowSurrogate checks if the rune is a low surrogate (U+DC00 to U+DFFF).\nfunc isLowSurrogate(r rune) bool {\n\treturn r \u003e= lowSurrogateOffset \u0026\u0026 r \u003c= surrogateEnd\n}\n\n// combineSurrogates reconstruct the original unicode code points in the\n// supplemental plane by combinin the high and low surrogate.\n//\n// The hight surrogate in the range from U+D800 to U+DBFF,\n// and the low surrogate in the range from U+DC00 to U+DFFF.\n//\n// The formula to combine the surrogates is:\n// (high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000\nfunc combineSurrogates(high, low rune) rune {\n\treturn ((high - highSurrogateOffset) \u003c\u003c 10) + (low - lowSurrogateOffset) + supplementalPlanesOffset\n}\n\n// deocdeSingleUnicodeEscape decodes a unicode escape sequence (e.g., \\uXXXX) into a rune.\nfunc decodeSingleUnicodeEscape(b []byte) (rune, bool) {\n\tif len(b) \u003c 6 {\n\t\treturn utf8.RuneError, false\n\t}\n\n\t// convert hex to decimal\n\th1, h2, h3, h4 := h2i(b[2]), h2i(b[3]), h2i(b[4]), h2i(b[5])\n\tif h1 == badHex || h2 == badHex || h3 == badHex || h4 == badHex {\n\t\treturn utf8.RuneError, false\n\t}\n\n\treturn rune(h1\u003c\u003c12 + h2\u003c\u003c8 + h3\u003c\u003c4 + h4), true\n}\n\n// decodeUnicodeEscape decodes a Unicode escape sequence from a byte slice.\n// It handles both single Unicode escape sequences and surrogate pairs.\nfunc decodeUnicodeEscape(b []byte) (rune, int) {\n\t// decode the first Unicode escape sequence.\n\tr, ok := decodeSingleUnicodeEscape(b)\n\tif !ok {\n\t\treturn utf8.RuneError, -1\n\t}\n\n\t// if the rune is within the BMP and not a surrogate, return it\n\tif r \u003c= basicMultilingualPlaneOffset \u0026\u0026 !isSurrogatePair(r) {\n\t\treturn r, 6\n\t}\n\n\tif !isHighSurrogate(r) {\n\t\t// invalid surrogate pair.\n\t\treturn utf8.RuneError, -1\n\t}\n\n\t// if the rune is a high surrogate, need to decode the next escape sequence.\n\n\t// ensure there are enough bytes for the next escape sequence.\n\tif len(b) \u003c surrogatePairLen {\n\t\treturn utf8.RuneError, -1\n\t}\n\t// decode the second Unicode escape sequence.\n\tr2, ok := decodeSingleUnicodeEscape(b[singleUnicodeEscapeLen:])\n\tif !ok {\n\t\treturn utf8.RuneError, -1\n\t}\n\t// check if the second rune is a low surrogate.\n\tif isLowSurrogate(r2) {\n\t\tcombined := combineSurrogates(r, r2)\n\t\treturn combined, surrogatePairLen\n\t}\n\treturn utf8.RuneError, -1\n}\n\nvar escapeByteSet = [256]byte{\n\t'\"': doubleQuote,\n\t'\\\\': backSlash,\n\t'/': slash,\n\t'b': backSpace,\n\t'f': formFeed,\n\t'n': newLine,\n\t'r': carriageReturn,\n\t't': tab,\n}\n\n// Unquote takes a byte slice and unquotes it by removing\n// the surrounding quotes and unescaping the contents.\nfunc Unquote(s []byte, border byte) (string, bool) {\n\ts, ok := unquoteBytes(s, border)\n\treturn string(s), ok\n}\n\n// unquoteBytes takes a byte slice and unquotes it by removing\nfunc unquoteBytes(s []byte, border byte) ([]byte, bool) {\n\tif len(s) \u003c 2 || s[0] != border || s[len(s)-1] != border {\n\t\treturn nil, false\n\t}\n\n\ts = s[1 : len(s)-1]\n\n\tr := 0\n\tfor r \u003c len(s) {\n\t\tc := s[r]\n\n\t\tif c == backSlash || c == border || c \u003c 0x20 {\n\t\t\tbreak\n\t\t}\n\n\t\tif c \u003c utf8.RuneSelf {\n\t\t\tr++\n\t\t\tcontinue\n\t\t}\n\n\t\trr, size := utf8.DecodeRune(s[r:])\n\t\tif rr == utf8.RuneError \u0026\u0026 size == 1 {\n\t\t\tbreak\n\t\t}\n\n\t\tr += size\n\t}\n\n\tif r == len(s) {\n\t\treturn s, true\n\t}\n\n\tutfDoubleMax := utf8.UTFMax * 2\n\tb := make([]byte, len(s)+utfDoubleMax)\n\tw := copy(b, s[0:r])\n\n\tfor r \u003c len(s) {\n\t\tif w \u003e= len(b)-utf8.UTFMax {\n\t\t\tnb := make([]byte, utfDoubleMax+(2*len(b)))\n\t\t\tcopy(nb, b)\n\t\t\tb = nb\n\t\t}\n\n\t\tc := s[r]\n\t\tif c == backSlash {\n\t\t\tr++\n\t\t\tif r \u003e= len(s) {\n\t\t\t\treturn nil, false\n\t\t\t}\n\n\t\t\tif s[r] == 'u' {\n\t\t\t\trr, res := decodeUnicodeEscape(s[r-1:])\n\t\t\t\tif res \u003c 0 {\n\t\t\t\t\treturn nil, false\n\t\t\t\t}\n\n\t\t\t\tw += utf8.EncodeRune(b[w:], rr)\n\t\t\t\tr += 5\n\t\t\t} else {\n\t\t\t\tdecode := escapeByteSet[s[r]]\n\t\t\t\tif decode == 0 {\n\t\t\t\t\treturn nil, false\n\t\t\t\t}\n\n\t\t\t\tif decode == doubleQuote || decode == backSlash || decode == slash {\n\t\t\t\t\tdecode = s[r]\n\t\t\t\t}\n\n\t\t\t\tb[w] = decode\n\t\t\t\tr++\n\t\t\t\tw++\n\t\t\t}\n\t\t} else if c == border || c \u003c 0x20 {\n\t\t\treturn nil, false\n\t\t} else if c \u003c utf8.RuneSelf {\n\t\t\tb[w] = c\n\t\t\tr++\n\t\t\tw++\n\t\t} else {\n\t\t\trr, size := utf8.DecodeRune(s[r:])\n\n\t\t\tif rr == utf8.RuneError \u0026\u0026 size == 1 {\n\t\t\t\treturn nil, false\n\t\t\t}\n\n\t\t\tr += size\n\t\t\tw += utf8.EncodeRune(b[w:], rr)\n\t\t}\n\t}\n\n\treturn b[:w], true\n}\n\n// processEscapedUTF8 converts escape sequences to UTF-8 characters.\n// It decodes Unicode escape sequences (\\uXXXX) to UTF-8 and\n// converts standard escape sequences (e.g., \\n) to their corresponding special characters.\nfunc processEscapedUTF8(in, out []byte) (int, int, error) {\n\tif len(in) \u003c 2 || in[0] != backSlash {\n\t\treturn -1, -1, errInvalidEscapeSequence\n\t}\n\n\tescapeSeqLen := 2\n\tescapeChar := in[1]\n\n\tif escapeChar != 'u' {\n\t\tval := escapeByteSet[escapeChar]\n\t\tif val == 0 {\n\t\t\treturn -1, -1, errInvalidEscapeSequence\n\t\t}\n\n\t\tout[0] = val\n\t\treturn escapeSeqLen, 1, nil\n\t}\n\n\tr, size := decodeUnicodeEscape(in)\n\tif size == -1 {\n\t\treturn -1, -1, errInvalidEscapeSequence\n\t}\n\n\toutLen := utf8.EncodeRune(out, r)\n\n\treturn size, outLen, nil\n}\n" + }, + { + "name": "escape_test.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"unicode/utf8\"\n)\n\nfunc TestHexToInt(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tc byte\n\t\twant int\n\t}{\n\t\t{\"Digit 0\", '0', 0},\n\t\t{\"Digit 9\", '9', 9},\n\t\t{\"Uppercase A\", 'A', 10},\n\t\t{\"Uppercase F\", 'F', 15},\n\t\t{\"Lowercase a\", 'a', 10},\n\t\t{\"Lowercase f\", 'f', 15},\n\t\t{\"Invalid character1\", 'g', badHex},\n\t\t{\"Invalid character2\", 'G', badHex},\n\t\t{\"Invalid character3\", 'z', badHex},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := h2i(tt.c); got != tt.want {\n\t\t\t\tt.Errorf(\"h2i() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIsSurrogatePair(t *testing.T) {\n\ttestCases := []struct {\n\t\tname string\n\t\tr rune\n\t\texpected bool\n\t}{\n\t\t{\"high surrogate start\", 0xD800, true},\n\t\t{\"high surrogate end\", 0xDBFF, true},\n\t\t{\"low surrogate start\", 0xDC00, true},\n\t\t{\"low surrogate end\", 0xDFFF, true},\n\t\t{\"Non-surrogate\", 0x0000, false},\n\t\t{\"Non-surrogate 2\", 0xE000, false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif got := isSurrogatePair(tc.r); got != tc.expected {\n\t\t\t\tt.Errorf(\"isSurrogate() = %v, want %v\", got, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCombineSurrogates(t *testing.T) {\n\ttestCases := []struct {\n\t\thigh, low rune\n\t\texpected rune\n\t}{\n\t\t{0xD83D, 0xDC36, 0x1F436}, // 🐶 U+1F436 DOG FACE\n\t\t{0xD83D, 0xDE00, 0x1F600}, // 😀 U+1F600 GRINNING FACE\n\t\t{0xD83C, 0xDF03, 0x1F303}, // 🌃 U+1F303 NIGHT WITH STARS\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult := combineSurrogates(tc.high, tc.low)\n\t\tif result != tc.expected {\n\t\t\tt.Errorf(\"combineSurrogates(%U, %U) = %U; want %U\", tc.high, tc.low, result, tc.expected)\n\t\t}\n\t}\n}\n\nfunc TestDecodeSingleUnicodeEscape(t *testing.T) {\n\ttestCases := []struct {\n\t\tinput []byte\n\t\texpected rune\n\t\tisValid bool\n\t}{\n\t\t// valid unicode escape sequences\n\t\t{[]byte(`\\u0041`), 'A', true},\n\t\t{[]byte(`\\u03B1`), 'α', true},\n\t\t{[]byte(`\\u00E9`), 'é', true}, // valid non-English character\n\t\t{[]byte(`\\u0021`), '!', true}, // valid special character\n\t\t{[]byte(`\\uFF11`), '1', true},\n\t\t{[]byte(`\\uD83D`), 0xD83D, true},\n\t\t{[]byte(`\\uDE03`), 0xDE03, true},\n\n\t\t// invalid unicode escape sequences\n\t\t{[]byte(`\\u004`), utf8.RuneError, false}, // too short\n\t\t{[]byte(`\\uXYZW`), utf8.RuneError, false}, // invalid hex\n\t\t{[]byte(`\\u00G1`), utf8.RuneError, false}, // non-hex character\n\t}\n\n\tfor _, tc := range testCases {\n\t\tresult, isValid := decodeSingleUnicodeEscape(tc.input)\n\t\tif result != tc.expected || isValid != tc.isValid {\n\t\t\tt.Errorf(\"decodeSingleUnicodeEscape(%s) = (%U, %v); want (%U, %v)\", tc.input, result, isValid, tc.expected, tc.isValid)\n\t\t}\n\t}\n}\n\nfunc TestDecodeUnicodeEscape(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\texpected rune\n\t\tsize int\n\t}{\n\t\t{[]byte(`\\u0041`), 'A', 6},\n\t\t{[]byte(`\\uD83D\\uDE00`), 0x1F600, 12}, // 😀\n\t\t{[]byte(`\\uD834\\uDD1E`), 0x1D11E, 12}, // 𝄞\n\t\t{[]byte(`\\uFFFF`), '\\uFFFF', 6},\n\t\t{[]byte(`\\uXYZW`), utf8.RuneError, -1},\n\t\t{[]byte(`\\uD800`), utf8.RuneError, -1}, // single high surrogate\n\t\t{[]byte(`\\uDC00`), utf8.RuneError, -1}, // single low surrogate\n\t\t{[]byte(`\\uD800\\uDC00`), 0x10000, 12}, // First code point above U+FFFF\n\t\t{[]byte(`\\uDBFF\\uDFFF`), 0x10FFFF, 12}, // Maximum code point\n\t\t{[]byte(`\\uD83D\\u0041`), utf8.RuneError, -1}, // invalid surrogate pair\n\t}\n\n\tfor _, tc := range tests {\n\t\tr, size := decodeUnicodeEscape(tc.input)\n\t\tif r != tc.expected || size != tc.size {\n\t\t\tt.Errorf(\"decodeUnicodeEscape(%q) = (%U, %d); want (%U, %d)\", tc.input, r, size, tc.expected, tc.size)\n\t\t}\n\t}\n}\n\nfunc TestUnescapeToUTF8(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\texpectedIn int\n\t\texpectedOut int\n\t\tisError bool\n\t}{\n\t\t// valid escape sequences\n\t\t{[]byte(`\\n`), 2, 1, false},\n\t\t{[]byte(`\\t`), 2, 1, false},\n\t\t{[]byte(`\\u0041`), 6, 1, false},\n\t\t{[]byte(`\\u03B1`), 6, 2, false},\n\t\t{[]byte(`\\uD830\\uDE03`), 12, 4, false},\n\n\t\t// invalid escape sequences\n\t\t{[]byte(`\\`), -1, -1, true}, // incomplete escape sequence\n\t\t{[]byte(`\\x`), -1, -1, true}, // invalid escape character\n\t\t{[]byte(`\\u`), -1, -1, true}, // incomplete unicode escape sequence\n\t\t{[]byte(`\\u004`), -1, -1, true}, // invalid unicode escape sequence\n\t\t{[]byte(`\\uXYZW`), -1, -1, true}, // invalid unicode escape sequence\n\t\t{[]byte(`\\uD83D\\u0041`), -1, -1, true}, // invalid unicode escape sequence\n\t}\n\n\tfor _, tc := range tests {\n\t\tinput := make([]byte, len(tc.input))\n\t\tcopy(input, tc.input)\n\t\toutput := make([]byte, utf8.UTFMax)\n\t\tinLen, outLen, err := processEscapedUTF8(input, output)\n\t\tif (err != nil) != tc.isError {\n\t\t\tt.Errorf(\"processEscapedUTF8(%q) = %v; want %v\", tc.input, err, tc.isError)\n\t\t}\n\n\t\tif inLen != tc.expectedIn || outLen != tc.expectedOut {\n\t\t\tt.Errorf(\"processEscapedUTF8(%q) = (%d, %d); want (%d, %d)\", tc.input, inLen, outLen, tc.expectedIn, tc.expectedOut)\n\t\t}\n\t}\n}\n\nfunc TestUnescape(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []byte\n\t\texpected []byte\n\t\tisError bool\n\t}{\n\t\t{\"NoEscape\", []byte(\"hello world\"), []byte(\"hello world\"), false},\n\t\t{\"SingleEscape\", []byte(\"hello\\\\nworld\"), []byte(\"hello\\nworld\"), false},\n\t\t{\"MultipleEscapes\", []byte(\"line1\\\\nline2\\\\r\\\\nline3\"), []byte(\"line1\\nline2\\r\\nline3\"), false},\n\t\t{\"UnicodeEscape\", []byte(\"snowman:\\\\u2603\"), []byte(\"snowman:\\u2603\"), false},\n\t\t{\"SurrogatePair\", []byte(\"emoji:\\\\uD83D\\\\uDE00\"), []byte(\"emoji:😀\"), false},\n\t\t{\"InvalidEscape\", []byte(\"hello\\\\xworld\"), nil, true},\n\t\t{\"IncompleteUnicode\", []byte(\"incomplete:\\\\u123\"), nil, true},\n\t\t{\"InvalidSurrogatePair\", []byte(\"invalid:\\\\uD83D\\\\u0041\"), nil, true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\toutput := make([]byte, len(tc.input)*2) // Allocate extra space for possible expansion\n\t\t\tresult, err := Unescape(tc.input, output)\n\t\t\tif (err != nil) != tc.isError {\n\t\t\t\tt.Errorf(\"Unescape(%q) error = %v; want error = %v\", tc.input, err, tc.isError)\n\t\t\t}\n\n\t\t\tif !tc.isError \u0026\u0026 !bytes.Equal(result, tc.expected) {\n\t\t\t\tt.Errorf(\"Unescape(%q) = %q; want %q\", tc.input, result, tc.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnquoteBytes(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\tborder byte\n\t\texpected []byte\n\t\tok bool\n\t}{\n\t\t{[]byte(\"\\\"hello\\\"\"), '\"', []byte(\"hello\"), true},\n\t\t{[]byte(\"'hello'\"), '\\'', []byte(\"hello\"), true},\n\t\t{[]byte(\"\\\"hello\"), '\"', nil, false},\n\t\t{[]byte(\"hello\\\"\"), '\"', nil, false},\n\t\t{[]byte(\"\\\"he\\\\\\\"llo\\\"\"), '\"', []byte(\"he\\\"llo\"), true},\n\t\t{[]byte(\"\\\"he\\\\nllo\\\"\"), '\"', []byte(\"he\\nllo\"), true},\n\t\t{[]byte(\"\\\"\\\"\"), '\"', []byte(\"\"), true},\n\t\t{[]byte(\"''\"), '\\'', []byte(\"\"), true},\n\t\t{[]byte(\"\\\"\\\\u0041\\\"\"), '\"', []byte(\"A\"), true},\n\t\t{[]byte(`\"Hello, 世界\"`), '\"', []byte(\"Hello, 世界\"), true},\n\t\t{[]byte(`\"Hello, \\x80\"`), '\"', nil, false},\n\t\t{[]byte(`\"invalid surrogate: \\uD83D\\u0041\"`), '\"', nil, false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tresult, pass := unquoteBytes(tc.input, tc.border)\n\n\t\tif pass != tc.ok {\n\t\t\tt.Errorf(\"unquoteBytes(%q) = %v; want %v\", tc.input, pass, tc.ok)\n\t\t}\n\n\t\tif !bytes.Equal(result, tc.expected) {\n\t\t\tt.Errorf(\"unquoteBytes(%q) = %q; want %q\", tc.input, result, tc.expected)\n\t\t}\n\t}\n}\n" + }, + { + "name": "indent.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n)\n\n// indentGrowthFactor specifies the growth factor of indenting JSON input.\n// A factor no higher than 2 ensures that wasted space never exceeds 50%.\nconst indentGrowthFactor = 2\n\n// IndentJSON formats the JSON data with the specified indentation.\nfunc Indent(data []byte, indent string) ([]byte, error) {\n\tvar (\n\t\tout bytes.Buffer\n\t\tlevel int\n\t\tinArray bool\n\t\tarrayDepth int\n\t)\n\n\tfor i := 0; i \u003c len(data); i++ {\n\t\tc := data[i] // current character\n\n\t\tswitch c {\n\t\tcase bracketOpen:\n\t\t\tarrayDepth++\n\t\t\tif arrayDepth \u003e 1 {\n\t\t\t\tlevel++ // increase the level if it's nested array\n\t\t\t\tinArray = true\n\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// case of the top-level array\n\t\t\t\tinArray = true\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase bracketClose:\n\t\t\tif inArray \u0026\u0026 arrayDepth \u003e 1 { // nested array\n\t\t\t\tlevel--\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tarrayDepth--\n\t\t\tif arrayDepth == 0 {\n\t\t\t\tinArray = false\n\t\t\t}\n\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\tcase curlyOpen:\n\t\t\t// check if the empty object or array\n\t\t\t// we don't need to apply the indent when it's empty containers.\n\t\t\tif i+1 \u003c len(data) \u0026\u0026 data[i+1] == curlyClose {\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\ti++ // skip next character\n\t\t\t\tif err := out.WriteByte(data[i]); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tlevel++\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase curlyClose:\n\t\t\tlevel--\n\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\tcase comma, colon:\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif inArray \u0026\u0026 arrayDepth \u003e 1 { // nested array\n\t\t\t\tif err := writeNewlineAndIndent(\u0026out, level, indent); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t} else if c == colon {\n\t\t\t\tif err := out.WriteByte(' '); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\n\t\tdefault:\n\t\t\tif err := out.WriteByte(c); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn out.Bytes(), nil\n}\n\nfunc writeNewlineAndIndent(out *bytes.Buffer, level int, indent string) error {\n\tif err := out.WriteByte('\\n'); err != nil {\n\t\treturn err\n\t}\n\n\tidt := strings.Repeat(indent, level*indentGrowthFactor)\n\tif _, err := out.WriteString(idt); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n" + }, + { + "name": "indent_test.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n)\n\nfunc TestIndentJSON(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []byte\n\t\tindent string\n\t\texpected []byte\n\t}{\n\t\t{\n\t\t\tname: \"empty object\",\n\t\t\tinput: []byte(`{}`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(`{}`),\n\t\t},\n\t\t{\n\t\t\tname: \"empty array\",\n\t\t\tinput: []byte(`[]`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(`[]`),\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tinput: []byte(`{{}}`),\n\t\t\tindent: \"\\t\",\n\t\t\texpected: []byte(\"{\\n\\t\\t{}\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname: \"nested array\",\n\t\t\tinput: []byte(`[[[]]]`),\n\t\t\tindent: \"\\t\",\n\t\t\texpected: []byte(\"[[\\n\\t\\t[\\n\\t\\t\\t\\t\\n\\t\\t]\\n]]\"),\n\t\t},\n\t\t{\n\t\t\tname: \"top-level array\",\n\t\t\tinput: []byte(`[\"apple\",\"banana\",\"cherry\"]`),\n\t\t\tindent: \"\\t\",\n\t\t\texpected: []byte(`[\"apple\",\"banana\",\"cherry\"]`),\n\t\t},\n\t\t{\n\t\t\tname: \"array of arrays\",\n\t\t\tinput: []byte(`[\"apple\",[\"banana\",\"cherry\"],\"date\"]`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(\"[\\\"apple\\\",[\\n \\\"banana\\\",\\n \\\"cherry\\\"\\n],\\\"date\\\"]\"),\n\t\t},\n\n\t\t{\n\t\t\tname: \"nested array in object\",\n\t\t\tinput: []byte(`{\"fruits\":[\"apple\",[\"banana\",\"cherry\"],\"date\"]}`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(\"{\\n \\\"fruits\\\": [\\\"apple\\\",[\\n \\\"banana\\\",\\n \\\"cherry\\\"\\n ],\\\"date\\\"]\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname: \"complex nested structure\",\n\t\t\tinput: []byte(`{\"data\":{\"array\":[1,2,3],\"bool\":true,\"nestedArray\":[[\"a\",\"b\"],\"c\"]}}`),\n\t\t\tindent: \" \",\n\t\t\texpected: []byte(\"{\\n \\\"data\\\": {\\n \\\"array\\\": [1,2,3],\\\"bool\\\": true,\\\"nestedArray\\\": [[\\n \\\"a\\\",\\n \\\"b\\\"\\n ],\\\"c\\\"]\\n }\\n}\"),\n\t\t},\n\t\t{\n\t\t\tname: \"custom ident character\",\n\t\t\tinput: []byte(`{\"fruits\":[\"apple\",[\"banana\",\"cherry\"],\"date\"]}`),\n\t\t\tindent: \"*\",\n\t\t\texpected: []byte(\"{\\n**\\\"fruits\\\": [\\\"apple\\\",[\\n****\\\"banana\\\",\\n****\\\"cherry\\\"\\n**],\\\"date\\\"]\\n}\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tactual, err := Indent(tt.input, tt.indent)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"IndentJSON() error = %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !bytes.Equal(actual, tt.expected) {\n\t\t\t\tt.Errorf(\"IndentJSON() = %q, want %q\", actual, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "internal.gno", + "body": "package json\n\n// Reference: https://github.com/freddierice/php_source/blob/467ed5d6edff72219afd3e644516f131118ef48e/ext/json/JSON_parser.c\n// Copyright (c) 2005 JSON.org\n\n// Go implementation is taken from: https://github.com/spyzhov/ajson/blob/master/internal/state.go\n\ntype (\n\tStates int8 // possible states of the parser\n\tClasses int8 // JSON string character types\n)\n\nconst __ = -1\n\n// enum classes\nconst (\n\tC_SPACE Classes = iota /* space */\n\tC_WHITE /* other whitespace */\n\tC_LCURB /* { */\n\tC_RCURB /* } */\n\tC_LSQRB /* [ */\n\tC_RSQRB /* ] */\n\tC_COLON /* : */\n\tC_COMMA /* , */\n\tC_QUOTE /* \" */\n\tC_BACKS /* \\ */\n\tC_SLASH /* / */\n\tC_PLUS /* + */\n\tC_MINUS /* - */\n\tC_POINT /* . */\n\tC_ZERO /* 0 */\n\tC_DIGIT /* 123456789 */\n\tC_LOW_A /* a */\n\tC_LOW_B /* b */\n\tC_LOW_C /* c */\n\tC_LOW_D /* d */\n\tC_LOW_E /* e */\n\tC_LOW_F /* f */\n\tC_LOW_L /* l */\n\tC_LOW_N /* n */\n\tC_LOW_R /* r */\n\tC_LOW_S /* s */\n\tC_LOW_T /* t */\n\tC_LOW_U /* u */\n\tC_ABCDF /* ABCDF */\n\tC_E /* E */\n\tC_ETC /* everything else */\n)\n\n// AsciiClasses array maps the 128 ASCII characters into character classes.\nvar AsciiClasses = [128]Classes{\n\t/*\n\t This array maps the 128 ASCII characters into character classes.\n\t The remaining Unicode characters should be mapped to C_ETC.\n\t Non-whitespace control characters are errors.\n\t*/\n\t__, __, __, __, __, __, __, __,\n\t__, C_WHITE, C_WHITE, __, __, C_WHITE, __, __,\n\t__, __, __, __, __, __, __, __,\n\t__, __, __, __, __, __, __, __,\n\n\tC_SPACE, C_ETC, C_QUOTE, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_PLUS, C_COMMA, C_MINUS, C_POINT, C_SLASH,\n\tC_ZERO, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT,\n\tC_DIGIT, C_DIGIT, C_COLON, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\n\tC_ETC, C_ABCDF, C_ABCDF, C_ABCDF, C_ABCDF, C_E, C_ABCDF, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LSQRB, C_BACKS, C_RSQRB, C_ETC, C_ETC,\n\n\tC_ETC, C_LOW_A, C_LOW_B, C_LOW_C, C_LOW_D, C_LOW_E, C_LOW_F, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_LOW_L, C_ETC, C_LOW_N, C_ETC,\n\tC_ETC, C_ETC, C_LOW_R, C_LOW_S, C_LOW_T, C_LOW_U, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LCURB, C_ETC, C_RCURB, C_ETC, C_ETC,\n}\n\n// QuoteAsciiClasses is a HACK for single quote from AsciiClasses\nvar QuoteAsciiClasses = [128]Classes{\n\t/*\n\t This array maps the 128 ASCII characters into character classes.\n\t The remaining Unicode characters should be mapped to C_ETC.\n\t Non-whitespace control characters are errors.\n\t*/\n\t__, __, __, __, __, __, __, __,\n\t__, C_WHITE, C_WHITE, __, __, C_WHITE, __, __,\n\t__, __, __, __, __, __, __, __,\n\t__, __, __, __, __, __, __, __,\n\n\tC_SPACE, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_QUOTE,\n\tC_ETC, C_ETC, C_ETC, C_PLUS, C_COMMA, C_MINUS, C_POINT, C_SLASH,\n\tC_ZERO, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT, C_DIGIT,\n\tC_DIGIT, C_DIGIT, C_COLON, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\n\tC_ETC, C_ABCDF, C_ABCDF, C_ABCDF, C_ABCDF, C_E, C_ABCDF, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LSQRB, C_BACKS, C_RSQRB, C_ETC, C_ETC,\n\n\tC_ETC, C_LOW_A, C_LOW_B, C_LOW_C, C_LOW_D, C_LOW_E, C_LOW_F, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_ETC, C_LOW_L, C_ETC, C_LOW_N, C_ETC,\n\tC_ETC, C_ETC, C_LOW_R, C_LOW_S, C_LOW_T, C_LOW_U, C_ETC, C_ETC,\n\tC_ETC, C_ETC, C_ETC, C_LCURB, C_ETC, C_RCURB, C_ETC, C_ETC,\n}\n\n/*\nThe state codes.\n*/\nconst (\n\tGO States = iota /* start */\n\tOK /* ok */\n\tOB /* object */\n\tKE /* key */\n\tCO /* colon */\n\tVA /* value */\n\tAR /* array */\n\tST /* string */\n\tES /* escape */\n\tU1 /* u1 */\n\tU2 /* u2 */\n\tU3 /* u3 */\n\tU4 /* u4 */\n\tMI /* minus */\n\tZE /* zero */\n\tIN /* integer */\n\tDT /* dot */\n\tFR /* fraction */\n\tE1 /* e */\n\tE2 /* ex */\n\tE3 /* exp */\n\tT1 /* tr */\n\tT2 /* tru */\n\tT3 /* true */\n\tF1 /* fa */\n\tF2 /* fal */\n\tF3 /* fals */\n\tF4 /* false */\n\tN1 /* nu */\n\tN2 /* nul */\n\tN3 /* null */\n)\n\n// List of action codes.\n// these constants are defining an action that should be performed under certain conditions.\nconst (\n\tcl States = -2 /* colon */\n\tcm States = -3 /* comma */\n\tqt States = -4 /* quote */\n\tbo States = -5 /* bracket open */\n\tco States = -6 /* curly bracket open */\n\tbc States = -7 /* bracket close */\n\tcc States = -8 /* curly bracket close */\n\tec States = -9 /* curly bracket empty */\n)\n\n// StateTransitionTable is the state transition table takes the current state and the current symbol, and returns either\n// a new state or an action. An action is represented as a negative number. A JSON text is accepted if at the end of the\n// text the state is OK and if the mode is DONE.\nvar StateTransitionTable = [31][31]States{\n\t/*\n\t The state transition table takes the current state and the current symbol,\n\t and returns either a new state or an action. An action is represented as a\n\t negative number. A JSON text is accepted if at the end of the text the\n\t state is OK and if the mode is DONE.\n\t white 1-9 ABCDF etc\n\t space | { } [ ] : , \" \\ / + - . 0 | a b c d e f l n r s t u | E |*/\n\t/*start GO*/ {GO, GO, co, __, bo, __, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*ok OK*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*object OB*/ {OB, OB, __, ec, __, __, __, __, ST, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*key KE*/ {KE, KE, __, __, __, __, __, __, ST, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*colon CO*/ {CO, CO, __, __, __, __, cl, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*value VA*/ {VA, VA, co, __, bo, __, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*array AR*/ {AR, AR, co, __, bo, bc, __, __, ST, __, __, __, MI, __, ZE, IN, __, __, __, __, __, F1, __, N1, __, __, T1, __, __, __, __},\n\t/*string ST*/ {ST, __, ST, ST, ST, ST, ST, ST, qt, ES, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST, ST},\n\t/*escape ES*/ {__, __, __, __, __, __, __, __, ST, ST, ST, __, __, __, __, __, __, ST, __, __, __, ST, __, ST, ST, __, ST, U1, __, __, __},\n\t/*u1 U1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U2, U2, U2, U2, U2, U2, U2, U2, __, __, __, __, __, __, U2, U2, __},\n\t/*u2 U2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U3, U3, U3, U3, U3, U3, U3, U3, __, __, __, __, __, __, U3, U3, __},\n\t/*u3 U3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, U4, U4, U4, U4, U4, U4, U4, U4, __, __, __, __, __, __, U4, U4, __},\n\t/*u4 U4*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, ST, ST, ST, ST, ST, ST, ST, ST, __, __, __, __, __, __, ST, ST, __},\n\t/*minus MI*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, ZE, IN, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*zero ZE*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, DT, __, __, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*int IN*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, DT, IN, IN, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*dot DT*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, FR, FR, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*frac FR*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, FR, FR, __, __, __, __, E1, __, __, __, __, __, __, __, __, E1, __},\n\t/*e E1*/ {__, __, __, __, __, __, __, __, __, __, __, E2, E2, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*ex E2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*exp E3*/ {OK, OK, __, cc, __, bc, __, cm, __, __, __, __, __, __, E3, E3, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*tr T1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, T2, __, __, __, __, __, __},\n\t/*tru T2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, T3, __, __, __},\n\t/*true T3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __, __, __},\n\t/*fa F1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F2, __, __, __, __, __, __, __, __, __, __, __, __, __, __},\n\t/*fal F2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F3, __, __, __, __, __, __, __, __},\n\t/*fals F3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, F4, __, __, __, __, __},\n\t/*false F4*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __, __, __},\n\t/*nu N1*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, N2, __, __, __},\n\t/*nul N2*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, N3, __, __, __, __, __, __, __, __},\n\t/*null N3*/ {__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, OK, __, __, __, __, __, __, __, __},\n}\n" + }, + { + "name": "node.gno", + "body": "package json\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Node represents a JSON node.\ntype Node struct {\n\tprev *Node // prev is the parent node of the current node.\n\tnext map[string]*Node // next is the child nodes of the current node.\n\tkey *string // key holds the key of the current node in the parent node.\n\tdata []byte // byte slice of JSON data\n\tvalue any // value holds the value of the current node.\n\tnodeType ValueType // NodeType holds the type of the current node. (Object, Array, String, Number, Boolean, Null)\n\tindex *int // index holds the index of the current node in the parent array node.\n\tborders [2]int // borders stores the start and end index of the current node in the data.\n\tmodified bool // modified indicates the current node is changed or not.\n}\n\n// NewNode creates a new node instance with the given parent node, buffer, type, and key.\nfunc NewNode(prev *Node, b *buffer, typ ValueType, key **string) (*Node, error) {\n\tcurr := \u0026Node{\n\t\tprev: prev,\n\t\tdata: b.data,\n\t\tborders: [2]int{b.index, 0},\n\t\tkey: *key,\n\t\tnodeType: typ,\n\t\tmodified: false,\n\t}\n\n\tif typ == Object || typ == Array {\n\t\tcurr.next = make(map[string]*Node)\n\t}\n\n\tif prev != nil {\n\t\tif prev.IsArray() {\n\t\t\tsize := len(prev.next)\n\t\t\tcurr.index = \u0026size\n\n\t\t\tprev.next[strconv.Itoa(size)] = curr\n\t\t} else if prev.IsObject() {\n\t\t\tif key == nil {\n\t\t\t\treturn nil, errKeyRequired\n\t\t\t}\n\n\t\t\tprev.next[**key] = curr\n\t\t} else {\n\t\t\treturn nil, errors.New(\"invalid parent type\")\n\t\t}\n\t}\n\n\treturn curr, nil\n}\n\n// load retrieves the value of the current node.\nfunc (n *Node) load() any {\n\treturn n.value\n}\n\n// Changed checks the current node is changed or not.\nfunc (n *Node) Changed() bool {\n\treturn n.modified\n}\n\n// Key returns the key of the current node.\nfunc (n *Node) Key() string {\n\tif n == nil || n.key == nil {\n\t\treturn \"\"\n\t}\n\n\treturn *n.key\n}\n\n// HasKey checks the current node has the given key or not.\nfunc (n *Node) HasKey(key string) bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\t_, ok := n.next[key]\n\treturn ok\n}\n\n// GetKey returns the value of the given key from the current object node.\nfunc (n *Node) GetKey(key string) (*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif n.Type() != Object {\n\t\treturn nil, ufmt.Errorf(\"target node is not object type. got: %s\", n.Type().String())\n\t}\n\n\tvalue, ok := n.next[key]\n\tif !ok {\n\t\treturn nil, ufmt.Errorf(\"key not found: %s\", key)\n\t}\n\n\treturn value, nil\n}\n\n// MustKey returns the value of the given key from the current object node.\nfunc (n *Node) MustKey(key string) *Node {\n\tval, err := n.GetKey(key)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn val\n}\n\n// UniqueKeyLists traverses the current JSON nodes and collects all the unique keys.\nfunc (n *Node) UniqueKeyLists() []string {\n\tvar collectKeys func(*Node) []string\n\tcollectKeys = func(node *Node) []string {\n\t\tif node == nil || !node.IsObject() {\n\t\t\treturn nil\n\t\t}\n\n\t\tresult := make(map[string]bool)\n\t\tfor key, childNode := range node.next {\n\t\t\tresult[key] = true\n\t\t\tchildKeys := collectKeys(childNode)\n\t\t\tfor _, childKey := range childKeys {\n\t\t\t\tresult[childKey] = true\n\t\t\t}\n\t\t}\n\n\t\tkeys := make([]string, 0, len(result))\n\t\tfor key := range result {\n\t\t\tkeys = append(keys, key)\n\t\t}\n\t\treturn keys\n\t}\n\n\treturn collectKeys(n)\n}\n\n// Empty returns true if the current node is empty.\nfunc (n *Node) Empty() bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\treturn len(n.next) == 0\n}\n\n// Type returns the type (ValueType) of the current node.\nfunc (n *Node) Type() ValueType {\n\treturn n.nodeType\n}\n\n// Value returns the value of the current node.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tval, err := root.MustKey(\"key\").Value()\n//\tif err != nil {\n//\t\tt.Errorf(\"Value returns error: %v\", err)\n//\t}\n//\n//\tresult: \"value\"\nfunc (n *Node) Value() (value any, err error) {\n\tvalue = n.load()\n\n\tif value == nil {\n\t\tswitch n.nodeType {\n\t\tcase Null:\n\t\t\treturn nil, nil\n\n\t\tcase Number:\n\t\t\tvalue, err = strconv.ParseFloat(string(n.source()), 64)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tn.value = value\n\n\t\tcase String:\n\t\t\tvar ok bool\n\t\t\tvalue, ok = Unquote(n.source(), doubleQuote)\n\t\t\tif !ok {\n\t\t\t\treturn \"\", errInvalidStringValue\n\t\t\t}\n\n\t\t\tn.value = value\n\n\t\tcase Boolean:\n\t\t\tif len(n.source()) == 0 {\n\t\t\t\treturn nil, errEmptyBooleanNode\n\t\t\t}\n\n\t\t\tb := n.source()[0]\n\t\t\tvalue = b == 't' || b == 'T'\n\t\t\tn.value = value\n\n\t\tcase Array:\n\t\t\telems := make([]*Node, len(n.next))\n\n\t\t\tfor _, e := range n.next {\n\t\t\t\telems[*e.index] = e\n\t\t\t}\n\n\t\t\tvalue = elems\n\t\t\tn.value = value\n\n\t\tcase Object:\n\t\t\tobj := make(map[string]*Node, len(n.next))\n\n\t\t\tfor k, v := range n.next {\n\t\t\t\tobj[k] = v\n\t\t\t}\n\n\t\t\tvalue = obj\n\t\t\tn.value = value\n\t\t}\n\t}\n\n\treturn value, nil\n}\n\n// Delete removes the current node from the parent node.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(`{\"key\": \"value\"}`))\n//\tif err := root.MustKey(\"key\").Delete(); err != nil {\n//\t\tt.Errorf(\"Delete returns error: %v\", err)\n//\t}\n//\n//\tresult: {} (empty object)\nfunc (n *Node) Delete() error {\n\tif n == nil {\n\t\treturn errors.New(\"can't delete nil node\")\n\t}\n\n\tif n.prev == nil {\n\t\treturn nil\n\t}\n\n\treturn n.prev.remove(n)\n}\n\n// Size returns the size (length) of the current array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\n//\n//\tif root.Size() != 2 {\n//\t\tt.Errorf(\"ArrayNode returns wrong size: %d\", root.Size())\n//\t}\nfunc (n *Node) Size() int {\n\tif n == nil {\n\t\treturn 0\n\t}\n\n\treturn len(n.next)\n}\n\n// Index returns the index of the current node in the parent array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\n//\n//\tif root.MustIndex(1).Index() != 1 {\n//\t\tt.Errorf(\"Index returns wrong index: %d\", root.MustIndex(1).Index())\n//\t}\n//\n// We can also use the index to the byte slice of the JSON data directly.\n//\n// Example:\n//\n//\troot := Unmarshal([]byte(`[\"foo\", 1]`))\n//\tif root == nil {\n//\t\tt.Errorf(\"Unmarshal returns nil\")\n//\t}\n//\n//\tif string(root.MustIndex(1).source()) != \"1\" {\n//\t\tt.Errorf(\"source returns wrong result: %s\", root.MustIndex(1).source())\n//\t}\nfunc (n *Node) Index() int {\n\tif n == nil || n.index == nil {\n\t\treturn -1\n\t}\n\n\treturn *n.index\n}\n\n// MustIndex returns the array element at the given index.\n//\n// If the index is negative, it returns the index is from the end of the array.\n// Also, it panics if the index is not found.\n//\n// check the Index method for detailed usage.\nfunc (n *Node) MustIndex(expectIdx int) *Node {\n\tval, err := n.GetIndex(expectIdx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn val\n}\n\n// GetIndex returns the array element at the given index.\n//\n// if the index is negative, it returns the index is from the end of the array.\nfunc (n *Node) GetIndex(idx int) (*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsArray() {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\tif idx \u003e n.Size() {\n\t\treturn nil, errors.New(\"input index exceeds the array size\")\n\t}\n\n\tif idx \u003c 0 {\n\t\tidx += len(n.next)\n\t}\n\n\tchild, ok := n.next[strconv.Itoa(idx)]\n\tif !ok {\n\t\treturn nil, errIndexNotFound\n\t}\n\n\treturn child, nil\n}\n\n// DeleteIndex removes the array element at the given index.\nfunc (n *Node) DeleteIndex(idx int) error {\n\tnode, err := n.GetIndex(idx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn n.remove(node)\n}\n\n// NullNode creates a new null type node.\n//\n// Usage:\n//\n//\t_ := NullNode(\"\")\nfunc NullNode(key string) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: nil,\n\t\tnodeType: Null,\n\t\tmodified: true,\n\t}\n}\n\n// NumberNode creates a new number type node.\n//\n// Usage:\n//\n//\troot := NumberNode(\"\", 1)\n//\tif root == nil {\n//\t\tt.Errorf(\"NumberNode returns nil\")\n//\t}\nfunc NumberNode(key string, value float64) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: value,\n\t\tnodeType: Number,\n\t\tmodified: true,\n\t}\n}\n\n// StringNode creates a new string type node.\n//\n// Usage:\n//\n//\troot := StringNode(\"\", \"foo\")\n//\tif root == nil {\n//\t\tt.Errorf(\"StringNode returns nil\")\n//\t}\nfunc StringNode(key string, value string) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: value,\n\t\tnodeType: String,\n\t\tmodified: true,\n\t}\n}\n\n// BoolNode creates a new given boolean value node.\n//\n// Usage:\n//\n//\troot := BoolNode(\"\", true)\n//\tif root == nil {\n//\t\tt.Errorf(\"BoolNode returns nil\")\n//\t}\nfunc BoolNode(key string, value bool) *Node {\n\treturn \u0026Node{\n\t\tkey: \u0026key,\n\t\tvalue: value,\n\t\tnodeType: Boolean,\n\t\tmodified: true,\n\t}\n}\n\n// ArrayNode creates a new array type node.\n//\n// If the given value is nil, it creates an empty array node.\n//\n// Usage:\n//\n//\troot := ArrayNode(\"\", []*Node{StringNode(\"\", \"foo\"), NumberNode(\"\", 1)})\n//\tif root == nil {\n//\t\tt.Errorf(\"ArrayNode returns nil\")\n//\t}\nfunc ArrayNode(key string, value []*Node) *Node {\n\tcurr := \u0026Node{\n\t\tkey: \u0026key,\n\t\tnodeType: Array,\n\t\tmodified: true,\n\t}\n\n\tcurr.next = make(map[string]*Node, len(value))\n\tif value != nil {\n\t\tcurr.value = value\n\n\t\tfor i, v := range value {\n\t\t\tidx := i\n\t\t\tcurr.next[strconv.Itoa(i)] = v\n\n\t\t\tv.prev = curr\n\t\t\tv.index = \u0026idx\n\t\t}\n\t}\n\n\treturn curr\n}\n\n// ObjectNode creates a new object type node.\n//\n// If the given value is nil, it creates an empty object node.\n//\n// next is a map of key and value pairs of the object.\nfunc ObjectNode(key string, value map[string]*Node) *Node {\n\tcurr := \u0026Node{\n\t\tnodeType: Object,\n\t\tkey: \u0026key,\n\t\tnext: value,\n\t\tmodified: true,\n\t}\n\n\tif value != nil {\n\t\tcurr.value = value\n\n\t\tfor key, val := range value {\n\t\t\tvkey := key\n\t\t\tval.prev = curr\n\t\t\tval.key = \u0026vkey\n\t\t}\n\t} else {\n\t\tcurr.next = make(map[string]*Node)\n\t}\n\n\treturn curr\n}\n\n// IsArray returns true if the current node is array type.\nfunc (n *Node) IsArray() bool {\n\treturn n.nodeType == Array\n}\n\n// IsObject returns true if the current node is object type.\nfunc (n *Node) IsObject() bool {\n\treturn n.nodeType == Object\n}\n\n// IsNull returns true if the current node is null type.\nfunc (n *Node) IsNull() bool {\n\treturn n.nodeType == Null\n}\n\n// IsBool returns true if the current node is boolean type.\nfunc (n *Node) IsBool() bool {\n\treturn n.nodeType == Boolean\n}\n\n// IsString returns true if the current node is string type.\nfunc (n *Node) IsString() bool {\n\treturn n.nodeType == String\n}\n\n// IsNumber returns true if the current node is number type.\nfunc (n *Node) IsNumber() bool {\n\treturn n.nodeType == Number\n}\n\n// ready checks the current node is ready or not.\n//\n// the meaning of ready is the current node is parsed and has a valid value.\nfunc (n *Node) ready() bool {\n\treturn n.borders[1] != 0\n}\n\n// source returns the source of the current node.\nfunc (n *Node) source() []byte {\n\tif n == nil {\n\t\treturn nil\n\t}\n\n\tif n.ready() \u0026\u0026 !n.modified \u0026\u0026 n.data != nil {\n\t\treturn (n.data)[n.borders[0]:n.borders[1]]\n\t}\n\n\treturn nil\n}\n\n// root returns the root node of the current node.\nfunc (n *Node) root() *Node {\n\tif n == nil {\n\t\treturn nil\n\t}\n\n\tcurr := n\n\tfor curr.prev != nil {\n\t\tcurr = curr.prev\n\t}\n\n\treturn curr\n}\n\n// GetNull returns the null value if current node is null type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"null\"))\n//\tval, err := root.GetNull()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetNull returns error: %v\", err)\n//\t}\n//\tif val != nil {\n//\t\tt.Errorf(\"GetNull returns wrong result: %v\", val)\n//\t}\nfunc (n *Node) GetNull() (any, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsNull() {\n\t\treturn nil, errNotNullNode\n\t}\n\n\treturn nil, nil\n}\n\n// MustNull returns the null value if current node is null type.\n//\n// It panics if the current node is not null type.\nfunc (n *Node) MustNull() any {\n\tv, err := n.GetNull()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetNumeric returns the numeric (int/float) value if current node is number type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"10.5\"))\n//\tval, err := root.GetNumeric()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetNumeric returns error: %v\", err)\n//\t}\n//\tprintln(val) // 10.5\nfunc (n *Node) GetNumeric() (float64, error) {\n\tif n == nil {\n\t\treturn 0, errNilNode\n\t}\n\n\tif n.nodeType != Number {\n\t\treturn 0, errNotNumberNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tv, ok := val.(float64)\n\tif !ok {\n\t\treturn 0, errNotNumberNode\n\t}\n\n\treturn v, nil\n}\n\n// MustNumeric returns the numeric (int/float) value if current node is number type.\n//\n// It panics if the current node is not number type.\nfunc (n *Node) MustNumeric() float64 {\n\tv, err := n.GetNumeric()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetString returns the string value if current node is string type.\n//\n// Usage:\n//\n//\troot, err := Unmarshal([]byte(\"foo\"))\n//\tif err != nil {\n//\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n//\t}\n//\n//\tstr, err := root.GetString()\n//\tif err != nil {\n//\t\tt.Errorf(\"should retrieve string value: %s\", err)\n//\t}\n//\n//\tprintln(str) // \"foo\"\nfunc (n *Node) GetString() (string, error) {\n\tif n == nil {\n\t\treturn \"\", errEmptyStringNode\n\t}\n\n\tif !n.IsString() {\n\t\treturn \"\", errNotStringNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tv, ok := val.(string)\n\tif !ok {\n\t\treturn \"\", errNotStringNode\n\t}\n\n\treturn v, nil\n}\n\n// MustString returns the string value if current node is string type.\n//\n// It panics if the current node is not string type.\nfunc (n *Node) MustString() string {\n\tv, err := n.GetString()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetBool returns the boolean value if current node is boolean type.\n//\n// Usage:\n//\n//\troot := Unmarshal([]byte(\"true\"))\n//\tval, err := root.GetBool()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetBool returns error: %v\", err)\n//\t}\n//\tprintln(val) // true\nfunc (n *Node) GetBool() (bool, error) {\n\tif n == nil {\n\t\treturn false, errNilNode\n\t}\n\n\tif n.nodeType != Boolean {\n\t\treturn false, errNotBoolNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tv, ok := val.(bool)\n\tif !ok {\n\t\treturn false, errNotBoolNode\n\t}\n\n\treturn v, nil\n}\n\n// MustBool returns the boolean value if current node is boolean type.\n//\n// It panics if the current node is not boolean type.\nfunc (n *Node) MustBool() bool {\n\tv, err := n.GetBool()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// GetArray returns the array value if current node is array type.\n//\n// Usage:\n//\n//\t\troot := Must(Unmarshal([]byte(`[\"foo\", 1]`)))\n//\t\tarr, err := root.GetArray()\n//\t\tif err != nil {\n//\t\t\tt.Errorf(\"GetArray returns error: %v\", err)\n//\t\t}\n//\n//\t\tfor _, val := range arr {\n//\t\t\tprintln(val)\n//\t\t}\n//\n//\t result: \"foo\", 1\nfunc (n *Node) GetArray() ([]*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif n.nodeType != Array {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv, ok := val.([]*Node)\n\tif !ok {\n\t\treturn nil, errNotArrayNode\n\t}\n\n\treturn v, nil\n}\n\n// MustArray returns the array value if current node is array type.\n//\n// It panics if the current node is not array type.\nfunc (n *Node) MustArray() []*Node {\n\tv, err := n.GetArray()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// AppendArray appends the given values to the current array node.\n//\n// If the current node is not array type, it returns an error.\n//\n// Example 1:\n//\n//\troot := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`)))\n//\tif err := root.AppendArray(NullNode(\"\")); err != nil {\n//\t\tt.Errorf(\"should not return error: %s\", err)\n//\t}\n//\n//\tresult: [{\"foo\":\"bar\"}, null]\n//\n// Example 2:\n//\n//\troot := Must(Unmarshal([]byte(`[\"bar\", \"baz\"]`)))\n//\terr := root.AppendArray(NumberNode(\"\", 1), StringNode(\"\", \"foo\"))\n//\tif err != nil {\n//\t\tt.Errorf(\"AppendArray returns error: %v\", err)\n//\t }\n//\n//\tresult: [\"bar\", \"baz\", 1, \"foo\"]\nfunc (n *Node) AppendArray(value ...*Node) error {\n\tif !n.IsArray() {\n\t\treturn errInvalidAppend\n\t}\n\n\tfor _, val := range value {\n\t\tif err := n.append(nil, val); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tn.mark()\n\treturn nil\n}\n\n// ArrayEach executes the callback for each element in the JSON array.\n//\n// Usage:\n//\n//\tjsonArrayNode.ArrayEach(func(i int, valueNode *Node) {\n//\t ufmt.Println(i, valueNode)\n//\t})\nfunc (n *Node) ArrayEach(callback func(i int, target *Node)) {\n\tif n == nil || !n.IsArray() {\n\t\treturn\n\t}\n\n\tfor idx := 0; idx \u003c len(n.next); idx++ {\n\t\telement, err := n.GetIndex(idx)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tcallback(idx, element)\n\t}\n}\n\n// GetObject returns the object value if current node is object type.\n//\n// Usage:\n//\n//\troot := Must(Unmarshal([]byte(`{\"key\": \"value\"}`)))\n//\tobj, err := root.GetObject()\n//\tif err != nil {\n//\t\tt.Errorf(\"GetObject returns error: %v\", err)\n//\t}\n//\n//\tresult: map[string]*Node{\"key\": StringNode(\"key\", \"value\")}\nfunc (n *Node) GetObject() (map[string]*Node, error) {\n\tif n == nil {\n\t\treturn nil, errNilNode\n\t}\n\n\tif !n.IsObject() {\n\t\treturn nil, errNotObjectNode\n\t}\n\n\tval, err := n.Value()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tv, ok := val.(map[string]*Node)\n\tif !ok {\n\t\treturn nil, errNotObjectNode\n\t}\n\n\treturn v, nil\n}\n\n// MustObject returns the object value if current node is object type.\n//\n// It panics if the current node is not object type.\nfunc (n *Node) MustObject() map[string]*Node {\n\tv, err := n.GetObject()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn v\n}\n\n// AppendObject appends the given key and value to the current object node.\n//\n// If the current node is not object type, it returns an error.\nfunc (n *Node) AppendObject(key string, value *Node) error {\n\tif !n.IsObject() {\n\t\treturn errInvalidAppend\n\t}\n\n\tif err := n.append(\u0026key, value); err != nil {\n\t\treturn err\n\t}\n\n\tn.mark()\n\treturn nil\n}\n\n// ObjectEach executes the callback for each key-value pair in the JSON object.\n//\n// Usage:\n//\n//\tjsonObjectNode.ObjectEach(func(key string, valueNode *Node) {\n//\t ufmt.Println(key, valueNode)\n//\t})\nfunc (n *Node) ObjectEach(callback func(key string, value *Node)) {\n\tif n == nil || !n.IsObject() {\n\t\treturn\n\t}\n\n\tfor key, child := range n.next {\n\t\tcallback(key, child)\n\t}\n}\n\n// String converts the node to a string representation.\nfunc (n *Node) String() string {\n\tif n == nil {\n\t\treturn \"\"\n\t}\n\n\tif n.ready() \u0026\u0026 !n.modified {\n\t\treturn string(n.source())\n\t}\n\n\tval, err := Marshal(n)\n\tif err != nil {\n\t\treturn \"error: \" + err.Error()\n\t}\n\n\treturn string(val)\n}\n\n// Path builds the path of the current node.\n//\n// For example:\n//\n//\t{ \"key\": { \"sub\": [ \"val1\", \"val2\" ] }}\n//\n// The path of \"val2\" is: $.key.sub[1]\nfunc (n *Node) Path() string {\n\tif n == nil {\n\t\treturn \"\"\n\t}\n\n\tvar sb strings.Builder\n\n\tif n.prev == nil {\n\t\tsb.WriteString(\"$\")\n\t} else {\n\t\tsb.WriteString(n.prev.Path())\n\n\t\tif n.key != nil {\n\t\t\tsb.WriteString(\"['\" + n.Key() + \"']\")\n\t\t} else {\n\t\t\tsb.WriteString(\"[\" + strconv.Itoa(n.Index()) + \"]\")\n\t\t}\n\t}\n\n\treturn sb.String()\n}\n\n// mark marks the current node as modified.\nfunc (n *Node) mark() {\n\tnode := n\n\tfor node != nil \u0026\u0026 !node.modified {\n\t\tnode.modified = true\n\t\tnode = node.prev\n\t}\n}\n\n// isContainer checks the current node type is array or object.\nfunc (n *Node) isContainer() bool {\n\treturn n.IsArray() || n.IsObject()\n}\n\n// remove removes the value from the current container type node.\nfunc (n *Node) remove(v *Node) error {\n\tif !n.isContainer() {\n\t\treturn ufmt.Errorf(\n\t\t\t\"can't remove value from non-array or non-object node. got=%s\",\n\t\t\tn.Type().String(),\n\t\t)\n\t}\n\n\tif v.prev != n {\n\t\treturn errors.New(\"invalid parent node\")\n\t}\n\n\tn.mark()\n\tif n.IsArray() {\n\t\tdelete(n.next, strconv.Itoa(*v.index))\n\t\tn.dropIndex(*v.index)\n\t} else {\n\t\tdelete(n.next, *v.key)\n\t}\n\n\tv.prev = nil\n\treturn nil\n}\n\n// dropIndex rebase the index of current array node values.\nfunc (n *Node) dropIndex(idx int) {\n\tfor i := idx + 1; i \u003c= len(n.next); i++ {\n\t\tprv := i - 1\n\t\tif curr, ok := n.next[strconv.Itoa(i)]; ok {\n\t\t\tcurr.index = \u0026prv\n\t\t\tn.next[strconv.Itoa(prv)] = curr\n\t\t}\n\n\t\tdelete(n.next, strconv.Itoa(i))\n\t}\n}\n\n// append is a helper function to append the given value to the current container type node.\nfunc (n *Node) append(key *string, val *Node) error {\n\tif n.isSameOrParentNode(val) {\n\t\treturn errInvalidAppendCycle\n\t}\n\n\tif val.prev != nil {\n\t\tif err := val.prev.remove(val); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tval.prev = n\n\tval.key = key\n\n\tif key == nil {\n\t\tsize := len(n.next)\n\t\tval.index = \u0026size\n\t\tn.next[strconv.Itoa(size)] = val\n\t} else {\n\t\tif old, ok := n.next[*key]; ok {\n\t\t\tif err := n.remove(old); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tn.next[*key] = val\n\t}\n\n\treturn nil\n}\n\nfunc (n *Node) isSameOrParentNode(nd *Node) bool {\n\treturn n == nd || n.isParentNode(nd)\n}\n\nfunc (n *Node) isParentNode(nd *Node) bool {\n\tif n == nil {\n\t\treturn false\n\t}\n\n\tfor curr := nd.prev; curr != nil; curr = curr.prev {\n\t\tif curr == n {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// cptrs returns the pointer of the given string value.\nfunc cptrs(cpy *string) *string {\n\tif cpy == nil {\n\t\treturn nil\n\t}\n\n\tval := *cpy\n\n\treturn \u0026val\n}\n\n// cptri returns the pointer of the given integer value.\nfunc cptri(i *int) *int {\n\tif i == nil {\n\t\treturn nil\n\t}\n\n\tval := *i\n\treturn \u0026val\n}\n\n// Must panics if the given node is not fulfilled the expectation.\n// Usage:\n//\n//\tnode := Must(Unmarshal([]byte(`{\"key\": \"value\"}`))\nfunc Must(root *Node, expect error) *Node {\n\tif expect != nil {\n\t\tpanic(expect)\n\t}\n\n\treturn root\n}\n" + }, + { + "name": "node_test.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tnilKey *string\n\tdummyKey = \"key\"\n)\n\ntype _args struct {\n\tprev *Node\n\tbuf *buffer\n\ttyp ValueType\n\tkey **string\n}\n\ntype simpleNode struct {\n\tname string\n\tnode *Node\n}\n\nfunc TestNode_CreateNewNode(t *testing.T) {\n\trel := \u0026dummyKey\n\n\ttests := []struct {\n\t\tname string\n\t\targs _args\n\t\texpectCurr *Node\n\t\texpectErr bool\n\t\texpectPanic bool\n\t}{\n\t\t{\n\t\t\tname: \"child for non container type\",\n\t\t\targs: _args{\n\t\t\t\tprev: BoolNode(\"\", true),\n\t\t\t\tbuf: newBuffer(make([]byte, 10)),\n\t\t\t\ttyp: Boolean,\n\t\t\t\tkey: \u0026rel,\n\t\t\t},\n\t\t\texpectCurr: nil,\n\t\t\texpectErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tif tt.expectPanic {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tt.Errorf(\"%s panic occurred when not expected: %v\", tt.name, r)\n\t\t\t\t} else if tt.expectPanic {\n\t\t\t\t\tt.Errorf(\"%s expected panic but didn't occur\", tt.name)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tgot, err := NewNode(tt.args.prev, tt.args.buf, tt.args.typ, tt.args.key)\n\t\t\tif (err != nil) != tt.expectErr {\n\t\t\t\tt.Errorf(\"%s error = %v, expect error %v\", tt.name, err, tt.expectErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.expectErr {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !compareNodes(got, tt.expectCurr) {\n\t\t\t\tt.Errorf(\"%s got = %v, want %v\", tt.name, got, tt.expectCurr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Value(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t\t_type ValueType\n\t\texpected any\n\t\terrExpected bool\n\t}{\n\t\t{name: \"null\", data: []byte(\"null\"), _type: Null, expected: nil},\n\t\t{name: \"1\", data: []byte(\"1\"), _type: Number, expected: float64(1)},\n\t\t{name: \".1\", data: []byte(\".1\"), _type: Number, expected: float64(.1)},\n\t\t{name: \"-.1e1\", data: []byte(\"-.1e1\"), _type: Number, expected: float64(-1)},\n\t\t{name: \"string\", data: []byte(\"\\\"foo\\\"\"), _type: String, expected: \"foo\"},\n\t\t{name: \"space\", data: []byte(\"\\\"foo bar\\\"\"), _type: String, expected: \"foo bar\"},\n\t\t{name: \"true\", data: []byte(\"true\"), _type: Boolean, expected: true},\n\t\t{name: \"invalid true\", data: []byte(\"tru\"), _type: Unknown, errExpected: true},\n\t\t{name: \"invalid false\", data: []byte(\"fals\"), _type: Unknown, errExpected: true},\n\t\t{name: \"false\", data: []byte(\"false\"), _type: Boolean, expected: false},\n\t\t{name: \"e1\", data: []byte(\"e1\"), _type: Unknown, errExpected: true},\n\t\t{name: \"1a\", data: []byte(\"1a\"), _type: Unknown, errExpected: true},\n\t\t{name: \"string error\", data: []byte(\"\\\"foo\\nbar\\\"\"), _type: String, errExpected: true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tcurr := \u0026Node{\n\t\t\t\tdata: tt.data,\n\t\t\t\tnodeType: tt._type,\n\t\t\t\tborders: [2]int{0, len(tt.data)},\n\t\t\t}\n\n\t\t\tgot, err := curr.Value()\n\t\t\tif err != nil {\n\t\t\t\tif !tt.errExpected {\n\t\t\t\t\tt.Errorf(\"%s error = %v, expect error %v\", tt.name, err, tt.errExpected)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"%s got = %v, want %v\", tt.name, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Delete(t *testing.T) {\n\troot := Must(Unmarshal([]byte(`{\"foo\":\"bar\"}`)))\n\tif err := root.Delete(); err != nil {\n\t\tt.Errorf(\"Delete returns error: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `{\"foo\":\"bar\"}` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tfoo := root.MustKey(\"foo\")\n\tif err := foo.Delete(); err != nil {\n\t\tt.Errorf(\"Delete returns error while handling foo: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `{}` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif value, err := Marshal(foo); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `\"bar\"` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif foo.prev != nil {\n\t\tt.Errorf(\"foo.prev should be nil\")\n\t}\n}\n\nfunc TestNode_ObjectNode(t *testing.T) {\n\tobjs := map[string]*Node{\n\t\t\"key1\": NullNode(\"null\"),\n\t\t\"key2\": NumberNode(\"answer\", 42),\n\t\t\"key3\": StringNode(\"string\", \"foobar\"),\n\t\t\"key4\": BoolNode(\"bool\", true),\n\t}\n\n\tnode := ObjectNode(\"test\", objs)\n\n\tif len(node.next) != len(objs) {\n\t\tt.Errorf(\"ObjectNode: want %v got %v\", len(objs), len(node.next))\n\t}\n\n\tfor k, v := range objs {\n\t\tif node.next[k] == nil {\n\t\t\tt.Errorf(\"ObjectNode: want %v got %v\", v, node.next[k])\n\t\t}\n\t}\n}\n\nfunc TestNode_AppendObject(t *testing.T) {\n\tif err := Must(Unmarshal([]byte(`{\"foo\":\"bar\",\"baz\":null}`))).AppendObject(\"biz\", NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"AppendArray should return error\")\n\t}\n\n\troot := Must(Unmarshal([]byte(`{\"foo\":\"bar\"}`)))\n\tif err := root.AppendObject(\"baz\", NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"AppendObject should not return error: %s\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if isSameObject(string(value), `\"{\"foo\":\"bar\",\"baz\":null}\"`) {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\t// FIXME: this may fail if execute test in more than 3 times in a row.\n\tif err := root.AppendObject(\"biz\", NumberNode(\"\", 42)); err != nil {\n\t\tt.Errorf(\"AppendObject returns error: %v\", err)\n\t}\n\n\tval, err := Marshal(root)\n\tif err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t}\n\n\t// FIXME: this may fail if execute test in more than 3 times in a row.\n\tif isSameObject(string(val), `\"{\"foo\":\"bar\",\"baz\":null,\"biz\":42}\"`) {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(val))\n\t}\n}\n\nfunc TestNode_ArrayNode(t *testing.T) {\n\tarr := []*Node{\n\t\tNullNode(\"nil\"),\n\t\tNumberNode(\"num\", 42),\n\t\tStringNode(\"str\", \"foobar\"),\n\t\tBoolNode(\"bool\", true),\n\t}\n\n\tnode := ArrayNode(\"test\", arr)\n\n\tif len(node.next) != len(arr) {\n\t\tt.Errorf(\"ArrayNode: want %v got %v\", len(arr), len(node.next))\n\t}\n\n\tfor i, v := range arr {\n\t\tif node.next[strconv.Itoa(i)] == nil {\n\t\t\tt.Errorf(\"ArrayNode: want %v got %v\", v, node.next[strconv.Itoa(i)])\n\t\t}\n\t}\n}\n\nfunc TestNode_AppendArray(t *testing.T) {\n\tif err := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`))).AppendArray(NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"should return error\")\n\t}\n\n\troot := Must(Unmarshal([]byte(`[{\"foo\":\"bar\"}]`)))\n\tif err := root.AppendArray(NullNode(\"\")); err != nil {\n\t\tt.Errorf(\"should not return error: %s\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `[{\"foo\":\"bar\"},null]` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n\n\tif err := root.AppendArray(\n\t\tNumberNode(\"\", 1),\n\t\tStringNode(\"\", \"foo\"),\n\t\tMust(Unmarshal([]byte(`[0,1,null,true,\"example\"]`))),\n\t\tMust(Unmarshal([]byte(`{\"foo\": true, \"bar\": null, \"baz\": 123}`))),\n\t); err != nil {\n\t\tt.Errorf(\"AppendArray returns error: %v\", err)\n\t}\n\n\tif value, err := Marshal(root); err != nil {\n\t\tt.Errorf(\"Marshal returns error: %v\", err)\n\t} else if string(value) != `[{\"foo\":\"bar\"},null,1,\"foo\",[0,1,null,true,\"example\"],{\"foo\": true, \"bar\": null, \"baz\": 123}]` {\n\t\tt.Errorf(\"Marshal returns wrong value: %s\", string(value))\n\t}\n}\n\n/******** value getter ********/\n\nfunc TestNode_GetBool(t *testing.T) {\n\troot, err := Unmarshal([]byte(`true`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tvalue, err := root.GetBool()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetBool(): %s\", err.Error())\n\t}\n\n\tif !value {\n\t\tt.Errorf(\"root.GetBool() is corrupted\")\n\t}\n}\n\nfunc TestNode_GetBool_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"literally null node\", NullNode(\"\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetBool(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsBool(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"true\", BoolNode(\"\", true)},\n\t\t{\"false\", BoolNode(\"\", false)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif !tt.node.IsBool() {\n\t\t\t\tt.Errorf(\"%s should be a bool\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsBool_With_Unmarshal(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson []byte\n\t\twant bool\n\t}{\n\t\t{\"true\", []byte(\"true\"), true},\n\t\t{\"false\", []byte(\"false\"), true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(tt.json)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\t\t}\n\n\t\t\tif root.IsBool() != tt.want {\n\t\t\t\tt.Errorf(\"%s should be a bool\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar nullJson = []byte(`null`)\n\nfunc TestNode_GetNull(t *testing.T) {\n\troot, err := Unmarshal(nullJson)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tvalue, err := root.GetNull()\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting null, %s\", err)\n\t}\n\n\tif value != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value)\n\t}\n}\n\nfunc TestNode_GetNull_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"number node is null\", NumberNode(\"\", 42)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetNull(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_MustNull(t *testing.T) {\n\troot, err := Unmarshal(nullJson)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tvalue := root.MustNull()\n\tif value != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value)\n\t}\n}\n\nfunc TestNode_GetNumeric_Float(t *testing.T) {\n\troot, err := Unmarshal([]byte(`123.456`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(123.456) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123.456, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_Scientific_Notation(t *testing.T) {\n\troot, err := Unmarshal([]byte(`1e3`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(1000) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 1000, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_With_Unmarshal(t *testing.T) {\n\troot, err := Unmarshal([]byte(`123`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tvalue, err := root.GetNumeric()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetNumeric(): %s\", err)\n\t}\n\n\tif value != float64(123) {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123, got: %v\", value))\n\t}\n}\n\nfunc TestNode_GetNumeric_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"string node\", StringNode(\"\", \"123\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetNumeric(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetString(t *testing.T) {\n\troot, err := Unmarshal([]byte(`\"123foobar 3456\"`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t}\n\n\tvalue, err := root.GetString()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetString(): %s\", err)\n\t}\n\n\tif value != \"123foobar 3456\" {\n\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: 123, got: %s\", value))\n\t}\n}\n\nfunc TestNode_GetString_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"number node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetString(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_MustString(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tdata []byte\n\t}{\n\t\t{\"foo\", []byte(`\"foo\"`)},\n\t\t{\"foo bar\", []byte(`\"foo bar\"`)},\n\t\t{\"\", []byte(`\"\"`)},\n\t\t{\"안녕하세요\", []byte(`\"안녕하세요\"`)},\n\t\t{\"こんにちは\", []byte(`\"こんにちは\"`)},\n\t\t{\"你好\", []byte(`\"你好\"`)},\n\t\t{\"one \\\"encoded\\\" string\", []byte(`\"one \\\"encoded\\\" string\"`)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal(tt.data)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\t\t}\n\n\t\t\tvalue := root.MustString()\n\t\t\tif value != tt.name {\n\t\t\t\tt.Errorf(\"value is not matched. expected: %s, got: %s\", tt.name, value)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnmarshal_Array(t *testing.T) {\n\troot, err := Unmarshal([]byte(\" [1,[\\\"1\\\",[1,[1,2,3]]]]\\r\\n\"))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal: %s\", err.Error())\n\t}\n\n\tif root == nil {\n\t\tt.Errorf(\"Error on Unmarshal: root is nil\")\n\t}\n\n\tif root.Type() != Array {\n\t\tt.Errorf(\"Error on Unmarshal: wrong type\")\n\t}\n\n\tarray, err := root.GetArray()\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting array, %s\", err)\n\t} else if len(array) != 2 {\n\t\tt.Errorf(\"expected 2 elements, got %d\", len(array))\n\t} else if val, err := array[0].GetNumeric(); err != nil {\n\t\tt.Errorf(\"value of array[0] is not numeric. got: %v\", array[0].value)\n\t} else if val != 1 {\n\t\tt.Errorf(\"Error on array[0].GetNumeric(): expected to be '1', got: %v\", val)\n\t} else if val, err := array[1].GetArray(); err != nil {\n\t\tt.Errorf(\"error occurred while getting array, %s\", err.Error())\n\t} else if len(val) != 2 {\n\t\tt.Errorf(\"Error on array[1].GetArray(): expected 2 elements, got %d\", len(val))\n\t} else if el, err := val[0].GetString(); err != nil {\n\t\tt.Errorf(\"error occurred while getting string, %s\", err.Error())\n\t} else if el != \"1\" {\n\t\tt.Errorf(\"Error on val[0].GetString(): expected to be '1', got: %s\", el)\n\t}\n}\n\nvar sampleArr = []byte(`[-1, 2, 3, 4, 5, 6]`)\n\nfunc TestNode_GetArray(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tarray, err := root.GetArray()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetArray(): %s\", err)\n\t}\n\n\tif len(array) != 6 {\n\t\tt.Errorf(ufmt.Sprintf(\"length is not matched. expected: 3, got: %d\", len(array)))\n\t}\n\n\tfor i, node := range array {\n\t\tfor j, val := range []int{-1, 2, 3, 4, 5, 6} {\n\t\t\tif i == j {\n\t\t\t\tif v, err := node.GetNumeric(); err != nil {\n\t\t\t\t\tt.Errorf(ufmt.Sprintf(\"Error on node.GetNumeric(): %s\", err))\n\t\t\t\t} else if v != float64(val) {\n\t\t\t\t\tt.Errorf(ufmt.Sprintf(\"value is not matched. expected: %d, got: %v\", val, v))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestNode_GetArray_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t\t{\"number node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetArray(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_IsArray(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err)\n\t\treturn\n\t}\n\n\tif root.Type() != Array {\n\t\tt.Errorf(ufmt.Sprintf(\"Must be an array. got: %s\", root.Type().String()))\n\t}\n}\n\nfunc TestNode_ArrayEach(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson string\n\t\texpected []int\n\t}{\n\t\t{\n\t\t\tname: \"empty array\",\n\t\t\tjson: `[]`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname: \"single element\",\n\t\t\tjson: `[42]`,\n\t\t\texpected: []int{42},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple elements\",\n\t\t\tjson: `[1, 2, 3, 4, 5]`,\n\t\t\texpected: []int{1, 2, 3, 4, 5},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple elements but all values are same\",\n\t\t\tjson: `[1, 1, 1, 1, 1]`,\n\t\t\texpected: []int{1, 1, 1, 1, 1},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple elements with non-numeric values\",\n\t\t\tjson: `[\"a\", \"b\", \"c\", \"d\", \"e\"]`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname: \"non-array node\",\n\t\t\tjson: `{\"not\": \"an array\"}`,\n\t\t\texpected: []int{},\n\t\t},\n\t\t{\n\t\t\tname: \"array containing numeric and non-numeric elements\",\n\t\t\tjson: `[\"1\", 2, 3, \"4\", 5, \"6\"]`,\n\t\t\texpected: []int{2, 3, 5},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tc.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Unmarshal failed: %v\", err)\n\t\t\t}\n\n\t\t\tvar result []int // callback result\n\t\t\troot.ArrayEach(func(index int, element *Node) {\n\t\t\t\tif val, err := strconv.Atoi(element.String()); err == nil {\n\t\t\t\t\tresult = append(result, val)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif len(result) != len(tc.expected) {\n\t\t\t\tt.Errorf(\"%s: expected %d elements, got %d\", tc.name, len(tc.expected), len(result))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor i, val := range result {\n\t\t\t\tif val != tc.expected[i] {\n\t\t\t\t\tt.Errorf(\"%s: expected value at index %d to be %d, got %d\", tc.name, i, tc.expected[i], val)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Key(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null, \"baz\": 123, \"biz\": [1,2,3]}`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t}\n\n\tobj := root.MustObject()\n\tfor key, node := range obj {\n\t\tif key != node.Key() {\n\t\t\tt.Errorf(\"Key() = %v, want %v\", node.Key(), key)\n\t\t}\n\t}\n\n\tkeys := []string{\"foo\", \"bar\", \"baz\", \"biz\"}\n\tfor _, key := range keys {\n\t\tif obj[key].Key() != key {\n\t\t\tt.Errorf(\"Key() = %v, want %v\", obj[key].Key(), key)\n\t\t}\n\t}\n\n\t// TODO: resolve stack overflow\n\t// if root.MustKey(\"foo\").Clone().Key() != \"\" {\n\t// \tt.Errorf(\"wrong key found for cloned key\")\n\t// }\n\n\tif (*Node)(nil).Key() != \"\" {\n\t\tt.Errorf(\"wrong key found for nil node\")\n\t}\n}\n\nfunc TestNode_Size(t *testing.T) {\n\troot, err := Unmarshal(sampleArr)\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\tsize := root.Size()\n\tif size != 6 {\n\t\tt.Errorf(ufmt.Sprintf(\"Size() must be 6. got: %v\", size))\n\t}\n\n\tif (*Node)(nil).Size() != 0 {\n\t\tt.Errorf(ufmt.Sprintf(\"Size() must be 0. got: %v\", (*Node)(nil).Size()))\n\t}\n}\n\nfunc TestNode_Index(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`))\n\tif err != nil {\n\t\tt.Error(\"error occurred while unmarshal\")\n\t}\n\n\tarr := root.MustArray()\n\tfor i, node := range arr {\n\t\tif i != node.Index() {\n\t\t\tt.Errorf(ufmt.Sprintf(\"Index() must be nil. got: %v\", i))\n\t\t}\n\t}\n}\n\nfunc TestNode_Index_NotSucceed(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant int\n\t}{\n\t\t{\"nil node\", (*Node)(nil), -1},\n\t\t{\"null node\", NullNode(\"\"), -1},\n\t\t{\"object node\", ObjectNode(\"\", nil), -1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Index(); got != tt.want {\n\t\t\t\tt.Errorf(\"Index() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetIndex(t *testing.T) {\n\troot := Must(Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`)))\n\texpected := []int{1, 2, 3, 4, 5, 6}\n\n\tif len(expected) != root.Size() {\n\t\tt.Errorf(\"length is not matched. expected: %d, got: %d\", len(expected), root.Size())\n\t}\n\n\t// TODO: if length exceeds, stack overflow occurs. need to fix\n\tfor i, v := range expected {\n\t\tval, err := root.GetIndex(i)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"error occurred while getting index %d, %s\", i, err)\n\t\t}\n\n\t\tif val.MustNumeric() != float64(v) {\n\t\t\tt.Errorf(\"value is not matched. expected: %d, got: %v\", v, val.MustNumeric())\n\t\t}\n\t}\n}\n\nfunc TestNode_GetIndex_InputIndex_Exceed_Original_Node_Index(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[1, 2, 3, 4, 5, 6]`))\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\t_, err = root.GetIndex(10)\n\tif err == nil {\n\t\tt.Errorf(\"GetIndex should return error\")\n\t}\n}\n\nfunc TestNode_DeleteIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\texpected string\n\t\tindex int\n\t\tok bool\n\t}{\n\t\t{`null`, ``, 0, false},\n\t\t{`1`, ``, 0, false},\n\t\t{`{}`, ``, 0, false},\n\t\t{`{\"foo\":\"bar\"}`, ``, 0, false},\n\t\t{`true`, ``, 0, false},\n\t\t{`[]`, ``, 0, false},\n\t\t{`[]`, ``, -1, false},\n\t\t{`[1]`, `[]`, 0, true},\n\t\t{`[{}]`, `[]`, 0, true},\n\t\t{`[{}, [], 42]`, `[{}, []]`, -1, true},\n\t\t{`[{}, [], 42]`, `[[], 42]`, 0, true},\n\t\t{`[{}, [], 42]`, `[{}, 42]`, 1, true},\n\t\t{`[{}, [], 42]`, `[{}, []]`, 2, true},\n\t\t{`[{}, [], 42]`, ``, 10, false},\n\t\t{`[{}, [], 42]`, ``, -10, false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot := Must(Unmarshal([]byte(tt.name)))\n\t\t\terr := root.DeleteIndex(tt.index)\n\t\t\tif err != nil \u0026\u0026 tt.ok {\n\t\t\t\tt.Errorf(\"DeleteIndex returns error: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetKey(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true, \"bar\": null}`))\n\tif err != nil {\n\t\tt.Error(\"error occurred while unmarshal\")\n\t}\n\n\tvalue, err := root.GetKey(\"foo\")\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting key, %s\", err)\n\t}\n\n\tif value.MustBool() != true {\n\t\tt.Errorf(\"value is not matched. expected: true, got: %v\", value.MustBool())\n\t}\n\n\tvalue, err = root.GetKey(\"bar\")\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while getting key, %s\", err)\n\t}\n\n\t_, err = root.GetKey(\"baz\")\n\tif err == nil {\n\t\tt.Errorf(\"key baz is not exist. must be failed\")\n\t}\n\n\tif value.MustNull() != nil {\n\t\tt.Errorf(\"value is not matched. expected: nil, got: %v\", value.MustNull())\n\t}\n}\n\nfunc TestNode_GetKey_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"null node\", NullNode(\"\")},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetKey(\"\"); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_GetUniqueKeyList(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname: \"simple foo/bar\",\n\t\t\tjson: `{\"foo\": true, \"bar\": null}`,\n\t\t\texpected: []string{\"foo\", \"bar\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty object\",\n\t\t\tjson: `{}`,\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"nested object\",\n\t\t\tjson: `{\n\t\t\t\t\"outer\": {\n\t\t\t\t\t\"inner\": {\n\t\t\t\t\t\t\"key\": \"value\"\n\t\t\t\t\t},\n\t\t\t\t\t\"array\": [1, 2, 3]\n\t\t\t\t},\n\t\t\t\t\"another\": \"item\"\n\t\t\t}`,\n\t\t\texpected: []string{\"outer\", \"inner\", \"key\", \"array\", \"another\"},\n\t\t},\n\t\t{\n\t\t\tname: \"complex object\",\n\t\t\tjson: `{\n\t\t\t\t\"Image\": {\n\t\t\t\t\t\"Width\": 800,\n\t\t\t\t\t\"Height\": 600,\n\t\t\t\t\t\"Title\": \"View from 15th Floor\",\n\t\t\t\t\t\"Thumbnail\": {\n\t\t\t\t\t\t\"Url\": \"http://www.example.com/image/481989943\",\n\t\t\t\t\t\t\"Height\": 125,\n\t\t\t\t\t\t\"Width\": 100\n\t\t\t\t\t},\n\t\t\t\t\t\"Animated\": false,\n\t\t\t\t\t\"IDs\": [116, 943, 234, 38793]\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpected: []string{\"Image\", \"Width\", \"Height\", \"Title\", \"Thumbnail\", \"Url\", \"Animated\", \"IDs\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tt.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"error occurred while unmarshal\")\n\t\t\t}\n\n\t\t\tvalue := root.UniqueKeyLists()\n\t\t\tif len(value) != len(tt.expected) {\n\t\t\t\tt.Errorf(\"%s length must be %v. got: %v. retrieved keys: %s\", tt.name, len(tt.expected), len(value), value)\n\t\t\t}\n\n\t\t\tfor _, key := range value {\n\t\t\t\tif !contains(tt.expected, key) {\n\t\t\t\t\tt.Errorf(\"EachKey() must be in %v. got: %v\", tt.expected, key)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TODO: resolve stack overflow\nfunc TestNode_IsEmpty(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\texpected bool\n\t}{\n\t\t{\"nil node\", (*Node)(nil), false}, // nil node is not empty.\n\t\t// {\"null node\", NullNode(\"\"), true},\n\t\t{\"empty object\", ObjectNode(\"\", nil), true},\n\t\t{\"empty array\", ArrayNode(\"\", nil), true},\n\t\t{\"non-empty object\", ObjectNode(\"\", map[string]*Node{\"foo\": BoolNode(\"foo\", true)}), false},\n\t\t{\"non-empty array\", ArrayNode(\"\", []*Node{BoolNode(\"0\", true)}), false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Empty(); got != tt.expected {\n\t\t\t\tt.Errorf(\"%s = %v, want %v\", tt.name, got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Index_EmptyList(t *testing.T) {\n\troot, err := Unmarshal([]byte(`[]`))\n\tif err != nil {\n\t\tt.Errorf(\"error occurred while unmarshal\")\n\t}\n\n\tarray := root.MustArray()\n\tfor i, node := range array {\n\t\tif i != node.Index() {\n\t\t\tt.Errorf(ufmt.Sprintf(\"Index() must be nil. got: %v\", i))\n\t\t}\n\t}\n}\n\nfunc TestNode_GetObject(t *testing.T) {\n\troot, err := Unmarshal([]byte(`{\"foo\": true,\"bar\": null}`))\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tvalue, err := root.GetObject()\n\tif err != nil {\n\t\tt.Errorf(\"Error on root.GetObject(): %s\", err.Error())\n\t}\n\n\tif _, ok := value[\"foo\"]; !ok {\n\t\tt.Errorf(\"root.GetObject() is corrupted: foo\")\n\t}\n\n\tif _, ok := value[\"bar\"]; !ok {\n\t\tt.Errorf(\"root.GetObject() is corrupted: bar\")\n\t}\n}\n\nfunc TestNode_GetObject_NotSucceed(t *testing.T) {\n\ttests := []simpleNode{\n\t\t{\"nil node\", (*Node)(nil)},\n\t\t{\"get object from null node\", NullNode(\"\")},\n\t\t{\"not object node\", NumberNode(\"\", 123)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif _, err := tt.node.GetObject(); err == nil {\n\t\t\t\tt.Errorf(\"%s should be an error\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_ObjectEach(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tjson string\n\t\texpected map[string]int\n\t}{\n\t\t{\n\t\t\tname: \"empty object\",\n\t\t\tjson: `{}`,\n\t\t\texpected: make(map[string]int),\n\t\t},\n\t\t{\n\t\t\tname: \"single key-value pair\",\n\t\t\tjson: `{\"key\": 42}`,\n\t\t\texpected: map[string]int{\"key\": 42},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple key-value pairs\",\n\t\t\tjson: `{\"one\": 1, \"two\": 2, \"three\": 3}`,\n\t\t\texpected: map[string]int{\"one\": 1, \"two\": 2, \"three\": 3},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple key-value pairs with some non-numeric values\",\n\t\t\tjson: `{\"one\": 1, \"two\": \"2\", \"three\": 3, \"four\": \"4\"}`,\n\t\t\texpected: map[string]int{\"one\": 1, \"three\": 3},\n\t\t},\n\t\t{\n\t\t\tname: \"non-object node\",\n\t\t\tjson: `[\"not\", \"an\", \"object\"]`,\n\t\t\texpected: make(map[string]int),\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\troot, err := Unmarshal([]byte(tc.json))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Unmarshal failed: %v\", err)\n\t\t\t}\n\n\t\t\tresult := make(map[string]int)\n\t\t\troot.ObjectEach(func(key string, value *Node) {\n\t\t\t\t// extract integer values from the object\n\t\t\t\tif val, err := strconv.Atoi(value.String()); err == nil {\n\t\t\t\t\tresult[key] = val\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tif len(result) != len(tc.expected) {\n\t\t\t\tt.Errorf(\"%s: expected %d key-value pairs, got %d\", tc.name, len(tc.expected), len(result))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor key, val := range tc.expected {\n\t\t\t\tif result[key] != val {\n\t\t\t\t\tt.Errorf(\"%s: expected value for key %s to be %d, got %d\", tc.name, key, val, result[key])\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_ExampleMust(t *testing.T) {\n\tdata := []byte(`{\n \"Image\": {\n \"Width\": 800,\n \"Height\": 600,\n \"Title\": \"View from 15th Floor\",\n \"Thumbnail\": {\n \"Url\": \"http://www.example.com/image/481989943\",\n \"Height\": 125,\n \"Width\": 100\n },\n \"Animated\" : false,\n \"IDs\": [116, 943, 234, 38793]\n }\n }`)\n\n\troot := Must(Unmarshal(data))\n\tif root.Size() != 1 {\n\t\tt.Errorf(\"root.Size() must be 1. got: %v\", root.Size())\n\t}\n\n\tufmt.Sprintf(\"Object has %d inheritors inside\", root.Size())\n\t// Output:\n\t// Object has 1 inheritors inside\n}\n\n// Calculate AVG price from different types of objects, JSON from: https://goessner.net/articles/JsonPath/index.html#e3\nfunc TestExampleUnmarshal(t *testing.T) {\n\tdata := []byte(`{ \"store\": {\n \"book\": [ \n { \"category\": \"reference\",\n \"author\": \"Nigel Rees\",\n \"title\": \"Sayings of the Century\",\n \"price\": 8.95\n },\n { \"category\": \"fiction\",\n \"author\": \"Evelyn Waugh\",\n \"title\": \"Sword of Honour\",\n \"price\": 12.99\n },\n { \"category\": \"fiction\",\n \"author\": \"Herman Melville\",\n \"title\": \"Moby Dick\",\n \"isbn\": \"0-553-21311-3\",\n \"price\": 8.99\n },\n { \"category\": \"fiction\",\n \"author\": \"J. R. R. Tolkien\",\n \"title\": \"The Lord of the Rings\",\n \"isbn\": \"0-395-19395-8\",\n \"price\": 22.99\n }\n ],\n \"bicycle\": { \"color\": \"red\",\n \"price\": 19.95\n },\n \"tools\": null\n }\n}`)\n\n\troot, err := Unmarshal(data)\n\tif err != nil {\n\t\tt.Errorf(\"error occurred when unmarshal\")\n\t}\n\n\tstore := root.MustKey(\"store\").MustObject()\n\n\tvar prices float64\n\tsize := 0\n\tfor _, objects := range store {\n\t\tif objects.IsArray() \u0026\u0026 objects.Size() \u003e 0 {\n\t\t\tsize += objects.Size()\n\t\t\tfor _, object := range objects.MustArray() {\n\t\t\t\tprices += object.MustKey(\"price\").MustNumeric()\n\t\t\t}\n\t\t} else if objects.IsObject() \u0026\u0026 objects.HasKey(\"price\") {\n\t\t\tsize++\n\t\t\tprices += objects.MustKey(\"price\").MustNumeric()\n\t\t}\n\t}\n\n\tresult := int(prices / float64(size))\n\tufmt.Sprintf(\"AVG price: %d\", result)\n}\n\nfunc TestNode_ExampleMust_panic(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"The code did not panic\")\n\t\t}\n\t}()\n\tdata := []byte(`{]`)\n\troot := Must(Unmarshal(data))\n\tufmt.Sprintf(\"Object has %d inheritors inside\", root.Size())\n}\n\nfunc TestNode_Path(t *testing.T) {\n\tdata := []byte(`{\n \"Image\": {\n \"Width\": 800,\n \"Height\": 600,\n \"Title\": \"View from 15th Floor\",\n \"Thumbnail\": {\n \"Url\": \"http://www.example.com/image/481989943\",\n \"Height\": 125,\n \"Width\": 100\n },\n \"Animated\" : false,\n \"IDs\": [116, 943, 234, 38793]\n }\n }`)\n\n\troot, err := Unmarshal(data)\n\tif err != nil {\n\t\tt.Errorf(\"Error on Unmarshal(): %s\", err.Error())\n\t\treturn\n\t}\n\n\tif root.Path() != \"$\" {\n\t\tt.Errorf(\"Wrong root.Path()\")\n\t}\n\n\telement := root.MustKey(\"Image\").MustKey(\"Thumbnail\").MustKey(\"Url\")\n\tif element.Path() != \"$['Image']['Thumbnail']['Url']\" {\n\t\tt.Errorf(\"Wrong path found: %s\", element.Path())\n\t}\n\n\tif (*Node)(nil).Path() != \"\" {\n\t\tt.Errorf(\"Wrong (nil).Path()\")\n\t}\n}\n\nfunc TestNode_Path2(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"Node with key\",\n\t\t\tnode: \u0026Node{\n\t\t\t\tprev: \u0026Node{},\n\t\t\t\tkey: func() *string { s := \"key\"; return \u0026s }(),\n\t\t\t},\n\t\t\twant: \"$['key']\",\n\t\t},\n\t\t{\n\t\t\tname: \"Node with index\",\n\t\t\tnode: \u0026Node{\n\t\t\t\tprev: \u0026Node{},\n\t\t\t\tindex: func() *int { i := 1; return \u0026i }(),\n\t\t\t},\n\t\t\twant: \"$[1]\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.Path(); got != tt.want {\n\t\t\t\tt.Errorf(\"Path() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNode_Root(t *testing.T) {\n\troot := \u0026Node{}\n\tchild := \u0026Node{prev: root}\n\tgrandChild := \u0026Node{prev: child}\n\n\ttests := []struct {\n\t\tname string\n\t\tnode *Node\n\t\twant *Node\n\t}{\n\t\t{\n\t\t\tname: \"Root node\",\n\t\t\tnode: root,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Child node\",\n\t\t\tnode: child,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Grandchild node\",\n\t\t\tnode: grandChild,\n\t\t\twant: root,\n\t\t},\n\t\t{\n\t\t\tname: \"Node is nil\",\n\t\t\tnode: nil,\n\t\t\twant: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif got := tt.node.root(); got != tt.want {\n\t\t\t\tt.Errorf(\"root() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc contains(slice []string, item string) bool {\n\tfor _, a := range slice {\n\t\tif a == item {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ignore the sequence of keys by ordering them.\n// need to avoid import encoding/json and reflect package.\n// because gno does not support them for now.\n// TODO: use encoding/json to compare the result after if possible in gno.\nfunc isSameObject(a, b string) bool {\n\taPairs := strings.Split(strings.Trim(a, \"{}\"), \",\")\n\tbPairs := strings.Split(strings.Trim(b, \"{}\"), \",\")\n\n\taMap := make(map[string]string)\n\tbMap := make(map[string]string)\n\tfor _, pair := range aPairs {\n\t\tkv := strings.Split(pair, \":\")\n\t\tkey := strings.Trim(kv[0], `\"`)\n\t\tvalue := strings.Trim(kv[1], `\"`)\n\t\taMap[key] = value\n\t}\n\tfor _, pair := range bPairs {\n\t\tkv := strings.Split(pair, \":\")\n\t\tkey := strings.Trim(kv[0], `\"`)\n\t\tvalue := strings.Trim(kv[1], `\"`)\n\t\tbMap[key] = value\n\t}\n\n\taKeys := make([]string, 0, len(aMap))\n\tbKeys := make([]string, 0, len(bMap))\n\tfor k := range aMap {\n\t\taKeys = append(aKeys, k)\n\t}\n\n\tfor k := range bMap {\n\t\tbKeys = append(bKeys, k)\n\t}\n\n\tsort.Strings(aKeys)\n\tsort.Strings(bKeys)\n\n\tif len(aKeys) != len(bKeys) {\n\t\treturn false\n\t}\n\n\tfor i := range aKeys {\n\t\tif aKeys[i] != bKeys[i] {\n\t\t\treturn false\n\t\t}\n\n\t\tif aMap[aKeys[i]] != bMap[bKeys[i]] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc compareNodes(n1, n2 *Node) bool {\n\tif n1 == nil || n2 == nil {\n\t\treturn n1 == n2\n\t}\n\n\tif n1.key != n2.key {\n\t\treturn false\n\t}\n\n\tif !bytes.Equal(n1.data, n2.data) {\n\t\treturn false\n\t}\n\n\tif n1.index != n2.index {\n\t\treturn false\n\t}\n\n\tif n1.borders != n2.borders {\n\t\treturn false\n\t}\n\n\tif n1.modified != n2.modified {\n\t\treturn false\n\t}\n\n\tif n1.nodeType != n2.nodeType {\n\t\treturn false\n\t}\n\n\tif !compareNodes(n1.prev, n2.prev) {\n\t\treturn false\n\t}\n\n\tif len(n1.next) != len(n2.next) {\n\t\treturn false\n\t}\n\n\tfor k, v := range n1.next {\n\t\tif !compareNodes(v, n2.next[k]) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n" + }, + { + "name": "parser.gno", + "body": "package json\n\nimport (\n\t\"bytes\"\n)\n\nconst (\n\tunescapeStackBufSize = 64\n\tabsMinInt64 = 1 \u003c\u003c 63\n\tmaxInt64 = absMinInt64 - 1\n\tmaxUint64 = 1\u003c\u003c64 - 1\n)\n\n// PaseStringLiteral parses a string from the given byte slice.\nfunc ParseStringLiteral(data []byte) (string, error) {\n\tvar buf [unescapeStackBufSize]byte\n\n\tbf, err := Unescape(data, buf[:])\n\tif err != nil {\n\t\treturn \"\", errInvalidStringInput\n\t}\n\n\treturn string(bf), nil\n}\n\n// ParseBoolLiteral parses a boolean value from the given byte slice.\nfunc ParseBoolLiteral(data []byte) (bool, error) {\n\tswitch {\n\tcase bytes.Equal(data, trueLiteral):\n\t\treturn true, nil\n\tcase bytes.Equal(data, falseLiteral):\n\t\treturn false, nil\n\tdefault:\n\t\treturn false, errMalformedBooleanValue\n\t}\n}\n" + }, + { + "name": "parser_test.gno", + "body": "package json\n\nimport \"testing\"\n\nfunc TestParseStringLiteral(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected string\n\t\tisError bool\n\t}{\n\t\t{`\"Hello, World!\"`, \"\\\"Hello, World!\\\"\", false},\n\t\t{`\\uFF11`, \"\\uFF11\", false},\n\t\t{`\\uFFFF`, \"\\uFFFF\", false},\n\t\t{`true`, \"true\", false},\n\t\t{`false`, \"false\", false},\n\t\t{`\\uDF00`, \"\", true},\n\t}\n\n\tfor i, tt := range tests {\n\t\ts, err := ParseStringLiteral([]byte(tt.input))\n\n\t\tif !tt.isError \u0026\u0026 err != nil {\n\t\t\tt.Errorf(\"%d. unexpected error: %s\", i, err)\n\t\t}\n\n\t\tif tt.isError \u0026\u0026 err == nil {\n\t\t\tt.Errorf(\"%d. expected error, but not error\", i)\n\t\t}\n\n\t\tif s != tt.expected {\n\t\t\tt.Errorf(\"%d. expected=%s, but actual=%s\", i, tt.expected, s)\n\t\t}\n\t}\n}\n\nfunc TestParseBoolLiteral(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected bool\n\t\tisError bool\n\t}{\n\t\t{`true`, true, false},\n\t\t{`false`, false, false},\n\t\t{`TRUE`, false, true},\n\t\t{`FALSE`, false, true},\n\t\t{`foo`, false, true},\n\t\t{`\"true\"`, false, true},\n\t\t{`\"false\"`, false, true},\n\t}\n\n\tfor i, tt := range tests {\n\t\tb, err := ParseBoolLiteral([]byte(tt.input))\n\n\t\tif !tt.isError \u0026\u0026 err != nil {\n\t\t\tt.Errorf(\"%d. unexpected error: %s\", i, err)\n\t\t}\n\n\t\tif tt.isError \u0026\u0026 err == nil {\n\t\t\tt.Errorf(\"%d. expected error, but not error\", i)\n\t\t}\n\n\t\tif b != tt.expected {\n\t\t\tt.Errorf(\"%d. expected=%t, but actual=%t\", i, tt.expected, b)\n\t\t}\n\t}\n}\n" + }, + { + "name": "path.gno", + "body": "package json\n\nimport (\n\t\"errors\"\n)\n\n// ParsePath takes a JSONPath string and returns a slice of strings representing the path segments.\nfunc ParsePath(path string) ([]string, error) {\n\tbuf := newBuffer([]byte(path))\n\tresult := make([]string, 0)\n\n\tfor {\n\t\tb, err := buf.current()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tswitch {\n\t\tcase b == dollarSign || b == atSign:\n\t\t\tresult = append(result, string(b))\n\t\t\tbuf.step()\n\n\t\tcase b == dot:\n\t\t\tbuf.step()\n\n\t\t\tif next, _ := buf.current(); next == dot {\n\t\t\t\tbuf.step()\n\t\t\t\tresult = append(result, \"..\")\n\n\t\t\t\textractNextSegment(buf, \u0026result)\n\t\t\t} else {\n\t\t\t\textractNextSegment(buf, \u0026result)\n\t\t\t}\n\n\t\tcase b == bracketOpen:\n\t\t\tstart := buf.index\n\t\t\tbuf.step()\n\n\t\t\tfor {\n\t\t\t\tif buf.index \u003e= buf.length || buf.data[buf.index] == bracketClose {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tbuf.step()\n\t\t\t}\n\n\t\t\tif buf.index \u003e= buf.length {\n\t\t\t\treturn nil, errors.New(\"unexpected end of path\")\n\t\t\t}\n\n\t\t\tsegment := string(buf.sliceFromIndices(start+1, buf.index))\n\t\t\tresult = append(result, segment)\n\n\t\t\tbuf.step()\n\n\t\tdefault:\n\t\t\tbuf.step()\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// extractNextSegment extracts the segment from the current index\n// to the next significant character and adds it to the resulting slice.\nfunc extractNextSegment(buf *buffer, result *[]string) {\n\tstart := buf.index\n\tbuf.skipToNextSignificantToken()\n\n\tif buf.index \u003c= start {\n\t\treturn\n\t}\n\n\tsegment := string(buf.sliceFromIndices(start, buf.index))\n\tif segment != \"\" {\n\t\t*result = append(*result, segment)\n\t}\n}\n" + }, + { + "name": "path_test.gno", + "body": "package json\n\nimport \"testing\"\n\nfunc TestParseJSONPath(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tpath string\n\t\texpected []string\n\t}{\n\t\t{name: \"Empty string path\", path: \"\", expected: []string{}},\n\t\t{name: \"Root only path\", path: \"$\", expected: []string{\"$\"}},\n\t\t{name: \"Root with dot path\", path: \"$.\", expected: []string{\"$\"}},\n\t\t{name: \"All objects in path\", path: \"$..\", expected: []string{\"$\", \"..\"}},\n\t\t{name: \"Only children in path\", path: \"$.*\", expected: []string{\"$\", \"*\"}},\n\t\t{name: \"All objects' children in path\", path: \"$..*\", expected: []string{\"$\", \"..\", \"*\"}},\n\t\t{name: \"Simple dot notation path\", path: \"$.root.element\", expected: []string{\"$\", \"root\", \"element\"}},\n\t\t{name: \"Complex dot notation path with wildcard\", path: \"$.root.*.element\", expected: []string{\"$\", \"root\", \"*\", \"element\"}},\n\t\t{name: \"Path with array wildcard\", path: \"$.phoneNumbers[*].type\", expected: []string{\"$\", \"phoneNumbers\", \"*\", \"type\"}},\n\t\t{name: \"Path with filter expression\", path: \"$.store.book[?(@.price \u003c 10)].title\", expected: []string{\"$\", \"store\", \"book\", \"?(@.price \u003c 10)\", \"title\"}},\n\t\t{name: \"Path with formula\", path: \"$..phoneNumbers..('ty' + 'pe')\", expected: []string{\"$\", \"..\", \"phoneNumbers\", \"..\", \"('ty' + 'pe')\"}},\n\t\t{name: \"Simple bracket notation path\", path: \"$['root']['element']\", expected: []string{\"$\", \"'root'\", \"'element'\"}},\n\t\t{name: \"Complex bracket notation path with wildcard\", path: \"$['root'][*]['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Bracket notation path with integer index\", path: \"$['store']['book'][0]['title']\", expected: []string{\"$\", \"'store'\", \"'book'\", \"0\", \"'title'\"}},\n\t\t{name: \"Complex path with wildcard in bracket notation\", path: \"$['root'].*['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Mixed notation path with dot after bracket\", path: \"$.['root'].*.['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Mixed notation path with dot before bracket\", path: \"$['root'].*.['element']\", expected: []string{\"$\", \"'root'\", \"*\", \"'element'\"}},\n\t\t{name: \"Single character path with root\", path: \"$.a\", expected: []string{\"$\", \"a\"}},\n\t\t{name: \"Multiple characters path with root\", path: \"$.abc\", expected: []string{\"$\", \"abc\"}},\n\t\t{name: \"Multiple segments path with root\", path: \"$.a.b.c\", expected: []string{\"$\", \"a\", \"b\", \"c\"}},\n\t\t{name: \"Multiple segments path with wildcard and root\", path: \"$.a.*.c\", expected: []string{\"$\", \"a\", \"*\", \"c\"}},\n\t\t{name: \"Multiple segments path with filter and root\", path: \"$.a[?(@.b == 'c')].d\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"d\"}},\n\t\t{name: \"Complex path with multiple filters\", path: \"$.a[?(@.b == 'c')].d[?(@.e == 'f')].g\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"d\", \"?(@.e == 'f')\", \"g\"}},\n\t\t{name: \"Complex path with multiple filters and wildcards\", path: \"$.a[?(@.b == 'c')].*.d[?(@.e == 'f')].g\", expected: []string{\"$\", \"a\", \"?(@.b == 'c')\", \"*\", \"d\", \"?(@.e == 'f')\", \"g\"}},\n\t\t{name: \"Path with array index and root\", path: \"$.a[0].b\", expected: []string{\"$\", \"a\", \"0\", \"b\"}},\n\t\t{name: \"Path with multiple array indices and root\", path: \"$.a[0].b[1].c\", expected: []string{\"$\", \"a\", \"0\", \"b\", \"1\", \"c\"}},\n\t\t{name: \"Path with array index, wildcard and root\", path: \"$.a[0].*.c\", expected: []string{\"$\", \"a\", \"0\", \"*\", \"c\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\treult, _ := ParsePath(tt.path)\n\t\t\tif !isEqualSlice(reult, tt.expected) {\n\t\t\t\tt.Errorf(\"ParsePath(%s) expected: %v, got: %v\", tt.path, tt.expected, reult)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc isEqualSlice(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n" + }, + { + "name": "token.gno", + "body": "package json\n\nconst (\n\tbracketOpen = '['\n\tbracketClose = ']'\n\tparenOpen = '('\n\tparenClose = ')'\n\tcurlyOpen = '{'\n\tcurlyClose = '}'\n\tcomma = ','\n\tdot = '.'\n\tcolon = ':'\n\tbackTick = '`'\n\tsingleQuote = '\\''\n\tdoubleQuote = '\"'\n\temptyString = \"\"\n\twhiteSpace = ' '\n\tplus = '+'\n\tminus = '-'\n\taesterisk = '*'\n\tbang = '!'\n\tquestion = '?'\n\tnewLine = '\\n'\n\ttab = '\\t'\n\tcarriageReturn = '\\r'\n\tformFeed = '\\f'\n\tbackSpace = '\\b'\n\tslash = '/'\n\tbackSlash = '\\\\'\n\tunderScore = '_'\n\tdollarSign = '$'\n\tatSign = '@'\n\tandSign = '\u0026'\n\torSign = '|'\n)\n\nvar (\n\ttrueLiteral = []byte(\"true\")\n\tfalseLiteral = []byte(\"false\")\n\tnullLiteral = []byte(\"null\")\n)\n\ntype ValueType int\n\nconst (\n\tNotExist ValueType = iota\n\tString\n\tNumber\n\tFloat\n\tObject\n\tArray\n\tBoolean\n\tNull\n\tUnknown\n)\n\nfunc (v ValueType) String() string {\n\tswitch v {\n\tcase NotExist:\n\t\treturn \"not-exist\"\n\tcase String:\n\t\treturn \"string\"\n\tcase Number:\n\t\treturn \"number\"\n\tcase Object:\n\t\treturn \"object\"\n\tcase Array:\n\t\treturn \"array\"\n\tcase Boolean:\n\t\treturn \"boolean\"\n\tcase Null:\n\t\treturn \"null\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "ostBw9WjFzdckpZvhIeYdGf6KAvfizwzf71MLOByfR6GuJbwPDqi0vEV9dqAs2CCn9sBusU+4IV2HGpK3D48Cg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "cford32", + "path": "gno.land/p/demo/cford32", + "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": "# cford32\n\n```\npackage cford32 // import \"gno.land/p/demo/cford32\"\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/demo/seqid.\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/demo/seqid].\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\tswitch {\n\tdefault:\n\t\treturn 0, CorruptInputError(0)\n\tcase len(b) == 7 \u0026\u0026 b[0] \u003e= '0' \u0026\u0026 b[0] \u003c= 'f':\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 b[0] \u003e= 'g' \u0026\u0026 b[0] \u003c= 'z':\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\"strings\"\n\t\"testing\"\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 12); 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\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "f4rS2MLVGOcQoY/RlCShbwbbbVbJgVobAmaPMRgIUT3Owe47AkBTVsdnGSqXgRQVvISrsK6afN9pt7Hgjc50Dw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "seqid", + "path": "gno.land/p/demo/seqid", + "files": [ + { + "name": "README.md", + "body": "# seqid\n\n```\npackage seqid // import \"gno.land/p/demo/seqid\"\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": "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/demo/cford32\"\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/demo/cford32].\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "X9esf4bytgbYosCVOUZ8byBUg6NWdAmUbMmQbC4qpYKwc6qG47Ov7UCGVUAjWbIBvSOh2F8TXlSPKYCN0Vf8AA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "diff", + "path": "gno.land/p/demo/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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "fe0kSLiXjOAvv55ScpRAjeuyEGU3tuyS6ATTkSVT8Lgt/ZqNZmB/xDmaKX+3Mec7cK9RfxDawtrPfACs45juDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "uassert", + "path": "gno.land/p/demo/uassert", + "files": [ + { + "name": "doc.gno", + "body": "package uassert // import \"gno.land/p/demo/uassert\"\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 autofail(t TestingT, success bool, customMsgs []string, failureMessage string, args ...any) bool {\n\tif success {\n\t\treturn true\n\t}\n\treturn fail(t, customMsgs, failureMessage, args...)\n}\n\nfunc checkDidPanic(f func()) (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\tf()\n\tdidPanic = false\n\treturn\n}\n" + }, + { + "name": "mock_test.gno", + "body": "package uassert\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype mockTestingT struct {\n\tfmt string\n\targs []any\n}\n\n// --- interface mock\n\nvar _ 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\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/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// PanicsWithMessage asserts that the code inside the specified func panics,\n// and that the recovered panic value satisfies the given message\nfunc PanicsWithMessage(t TestingT, msg string, f func(), 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\tif panicValue != msg {\n\t\treturn fail(t, msgs, \"func should panic with message:\\t%s\\n\\tPanic value:\\t%s\", msg, panicValue)\n\t}\n\treturn true\n}\n\n// NotPanics asserts that the code inside the specified func does NOT panic.\nfunc NotPanics(t TestingT, f func(), msgs ...string) bool {\n\tt.Helper()\n\n\tdidPanic, panicValue := checkDidPanic(f)\n\n\tif didPanic {\n\t\treturn fail(t, msgs, \"func should not panic\\n\\tPanic value:\\t%s\", panicValue)\n\t}\n\treturn true\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 std.Address:\n\t\tif av, ok := actual.(std.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 std.Address:\n\t\tif av, ok := actual.(std.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}\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 std.Address:\n\t\t\tvar zeroAddr std.Address\n\t\t\tif val != zeroAddr {\n\t\t\t\treturn fail(t, msgs, \"uassert.Empty: not empty std.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 std.Address:\n\t\t\tvar zeroAddr std.Address\n\t\t\tif val == zeroAddr {\n\t\t\t\treturn fail(t, msgs, \"uassert.NotEmpty: empty std.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" + }, + { + "name": "uassert_test.gno", + "body": "package uassert\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"std\"\n\t\"testing\"\n)\n\nvar _ TestingT = (*testing.T)(nil)\n\nfunc TestMock(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tmockT.empty(t)\n\tNoError(mockT, errors.New(\"foo\"))\n\tmockT.equals(t, \"error: unexpected error: foo\")\n\tNoError(mockT, errors.New(\"foo\"), \"custom message\")\n\tmockT.equals(t, \"error: unexpected error: foo - custom message\")\n\tNoError(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\tTrue(t, NoError(mockT, nil))\n\tmockT.empty(t)\n\tFalse(t, 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\tTrue(t, Error(mockT, errors.New(\"foo bar\")))\n\tmockT.empty(t)\n\tFalse(t, 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\tFalse(t, ErrorContains(mockT, err, \"\"), \"ErrorContains should return false for nil arg\")\n}\n\nfunc TestTrue(t *testing.T) {\n\tmockT := new(mockTestingT)\n\tif !True(mockT, true) {\n\t\tt.Error(\"True should return true\")\n\t}\n\tmockT.empty(t)\n\tif 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 !False(mockT, false) {\n\t\tt.Error(\"False should return true\")\n\t}\n\tmockT.empty(t)\n\tif 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 !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 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 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:\\tat the disco\\n\\tPanic value:\\tpanic\")\n\n\tif 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:\\tPanic!\\n\\tPanic value:\\tpanic\")\n}\n\nfunc TestNotPanics(t *testing.T) {\n\tmockT := new(mockTestingT)\n\n\tif !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 NotPanics(mockT, func() {\n\t\tpanic(\"Panic!\")\n\t}) {\n\t\tt.Error(\"NotPanics should return false\")\n\t}\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{std.Address(\"g12345\"), std.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 := 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{std.Address(\"g12345\"), std.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{std.Address(\"g12345\"), std.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 := 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{std.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 := 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 := 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{std.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{std.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 := 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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "yLtADrISqLjz0xboxg+1hrC7GbOrPdrkVCm0MN73+dW1aGLPNdOdNrg511dcJxBo/rYkK3JSzEe8k4TslDrHDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "testutils", + "path": "gno.land/p/demo/testutils", + "files": [ + { + "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 \"std\"\n\nfunc TestAddress(name string) std.Address {\n\tif len(name) \u003e std.RawAddressSize {\n\t\tpanic(\"address name cannot be greater than std.AddressSize bytes\")\n\t}\n\taddr := std.RawAddress{}\n\t// TODO: use strings.RepeatString or similar.\n\t// NOTE: I miss python's \"\".Join().\n\tblanks := \"____________________\"\n\tcopy(addr[:], []byte(blanks))\n\tcopy(addr[:], []byte(name))\n\treturn std.Address(std.EncodeBech32(\"g\", addr))\n}\n" + }, + { + "name": "crypto_test.gno", + "body": "package testutils\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestTestAddress(t *testing.T) {\n\ttestAddr := TestAddress(\"author1\")\n\tuassert.Equal(t, \"g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6\", string(testAddr))\n}\n" + }, + { + "name": "misc.gno", + "body": "package testutils\n\n// For testing std.CallerAt().\nfunc WrapCall(fn func()) {\n\tfn()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "nZL4CTgd2W1lytAUzVIuLPlP/33IqRtDqPIbz1Ov+wRWAkRxhjHWX4tOcEFzNvyMttw07qHu+Jg0EIQitK8ZBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "urequire", + "path": "gno.land/p/demo/urequire", + "files": [ + { + "name": "urequire.gno", + "body": "// urequire is a sister package for uassert.\n// XXX: codegen the package.\npackage urequire\n\nimport \"gno.land/p/demo/uassert\"\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\nfunc PanicsWithMessage(t uassert.TestingT, msg string, f func(), msgs ...string) {\n\tt.Helper()\n\tif uassert.PanicsWithMessage(t, msg, f, msgs...) {\n\t\treturn\n\t}\n\tt.FailNow()\n}\n\nfunc NotPanics(t uassert.TestingT, f func(), 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\t// XXX: find a way to unit test this package\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "YLXVa13Jp5OOUN403bXuGdkc2G1odKlWpV5R34NwWbDPz9jTFRM72SFJzDjk1GVx4fAgn+OSS1Nfx1YIbN24DA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "forms", + "path": "gno.land/p/agherasie/forms", + "files": [ + { + "name": "create.gno", + "body": "package forms\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/json\"\n)\n\nconst dateFormat = \"2006-01-02T15:04:05Z\"\n\nfunc CreateField(label string, fieldType string, required bool) Field {\n\treturn Field{\n\t\tLabel: label,\n\t\tFieldType: fieldType,\n\t\tRequired: required,\n\t}\n}\n\n// CreateForm creates a new form with the given parameters\nfunc (db *FormDB) CreateForm(title string, description string, openAt string, closeAt string, data string) (string, error) {\n\t// Parsing the dates\n\tvar parsedOpenTime, parsedCloseTime time.Time\n\n\tif openAt != \"\" {\n\t\tvar err error\n\t\tparsedOpenTime, err = time.Parse(dateFormat, openAt)\n\t\tif err != nil {\n\t\t\treturn \"\", errInvalidDate\n\t\t}\n\t}\n\n\tif closeAt != \"\" {\n\t\tvar err error\n\t\tparsedCloseTime, err = time.Parse(dateFormat, closeAt)\n\t\tif err != nil {\n\t\t\treturn \"\", errInvalidDate\n\t\t}\n\t}\n\n\t// Parsing the json submission\n\tnode, err := json.Unmarshal([]byte(data))\n\tif err != nil {\n\t\treturn \"\", errInvalidJson\n\t}\n\n\tfieldsCount := node.Size()\n\tfields := make([]Field, fieldsCount)\n\n\t// Parsing the json submission to create the gno data structures\n\tfor i := 0; i \u003c fieldsCount; i++ {\n\t\tfield := node.MustIndex(i)\n\n\t\tfields[i] = CreateField(\n\t\t\tfield.MustKey(\"label\").MustString(),\n\t\t\tfield.MustKey(\"fieldType\").MustString(),\n\t\t\tfield.MustKey(\"required\").MustBool(),\n\t\t)\n\t}\n\n\t// Generating the form ID\n\tid := db.IDCounter.Next().String()\n\n\t// Creating the form\n\tform := Form{\n\t\tID: id,\n\t\tOwner: std.PreviousRealm().Address(),\n\t\tTitle: title,\n\t\tDescription: description,\n\t\tCreatedAt: time.Now(),\n\t\topenAt: parsedOpenTime,\n\t\tcloseAt: parsedCloseTime,\n\t\tFields: fields,\n\t}\n\n\t// Adding the form to the database\n\tdb.Forms = append(db.Forms, \u0026form)\n\n\treturn id, nil\n}\n" + }, + { + "name": "create_test.gno", + "body": "package forms\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestCreateForm(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\tdb := NewDB()\n\ttitle := \"Simple Form\"\n\tdescription := \"This is a form\"\n\topenAt := \"2021-01-01T00:00:00Z\"\n\tcloseAt := \"2021-01-02T00:00:00Z\"\n\tdata := `[\n\t\t{\n\t\t\t\"label\": \"Name\",\n\t\t\t\"fieldType\": \"string\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Age\",\n\t\t\t\"fieldType\": \"number\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Is this a test?\",\n\t\t\t\"fieldType\": \"boolean\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Food\",\n\t\t\t\"fieldType\": \"['Pizza', 'Schnitzel', 'Burger']\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Foods\",\n\t\t\t\"fieldType\": \"{'Pizza', 'Schnitzel', 'Burger'}\",\n\t\t\t\"required\": true\n\t\t}\n\t]`\n\n\turequire.NotPanics(t, func() {\n\t\tid, err := db.CreateForm(title, description, openAt, closeAt, data)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\turequire.True(t, id != \"\", \"Form ID is empty\")\n\n\t\tform, err := db.GetForm(id)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\turequire.True(t, form.ID == id, \"Form ID is not correct\")\n\t\turequire.True(t, form.Owner == alice, \"Owner is not correct\")\n\t\turequire.True(t, form.Title == title, \"Title is not correct\")\n\t\turequire.True(t, form.Description == description, \"Description is not correct\")\n\t\turequire.True(t, len(form.Fields) == 5, \"Not enough fields were provided\")\n\t\turequire.True(t, form.Fields[0].Label == \"Name\", \"Field 0 label is not correct\")\n\t\turequire.True(t, form.Fields[0].FieldType == \"string\", \"Field 0 type is not correct\")\n\t\turequire.True(t, form.Fields[0].Required == true, \"Field 0 required is not correct\")\n\t\turequire.True(t, form.Fields[1].Label == \"Age\", \"Field 1 label is not correct\")\n\t\turequire.True(t, form.Fields[1].FieldType == \"number\", \"Field 1 type is not correct\")\n\t})\n}\n" + }, + { + "name": "doc.gno", + "body": "// # Gno forms\n\n// gno-forms is a package which demonstrates a form editing and sharing application in gno\n\n// ## Features\n// - **Form Creation**: Create new forms with specified titles, descriptions, and fields.\n// - **Form Submission**: Submit answers to forms.\n// - **Form Retrieval**: Retrieve existing forms and their submissions.\n// - **Form Deadline**: Set a precise time range during which a form can be interacted with.\n\n// ## Field Types\n// The system supports the following field types:\n\n// | type | example |\n// |--------------|-------------------------------------------------------------------------------------------------|\n// | string | `{\"label\": \"Name\", \"fieldType\": \"string\", \"required\": true}` |\n// | number | `{\"label\": \"Age\", \"fieldType\": \"number\", \"required\": true}` |\n// | boolean | `{\"label\": \"Is Student?\", \"fieldType\": \"boolean\", \"required\": false}` |\n// | choice | `{\"label\": \"Favorite Food\", \"fieldType\": \"['Pizza', 'Schnitzel', 'Burger']\", \"required\": true}` |\n// | multi-choice | `{\"label\": \"Hobbies\", \"fieldType\": \"{'Reading', 'Swimming', 'Gaming'}\", \"required\": false}` |\n\n// ## Web-app\n\n// The external repo where the initial development took place and where you can find the frontend is [here](https://github.com/agherasie/gno-forms).\npackage forms\n" + }, + { + "name": "errors.gno", + "body": "package forms\n\nimport \"errors\"\n\nvar (\n\terrNoOpenDate = errors.New(\"Form has no open date\")\n\terrNoCloseDate = errors.New(\"Form has no close date\")\n\terrInvalidJson = errors.New(\"Invalid JSON\")\n\terrInvalidDate = errors.New(\"Invalid date\")\n\terrFormNotFound = errors.New(\"Form not found\")\n\terrAnswerNotFound = errors.New(\"Answer not found\")\n\terrAlreadySubmitted = errors.New(\"You already submitted this form\")\n\terrFormClosed = errors.New(\"Form is closed\")\n\terrInvalidAnswers = errors.New(\"Invalid answers\")\n)\n" + }, + { + "name": "forms.gno", + "body": "package forms\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/seqid\"\n)\n\n// FieldType examples :\n// - string: \"string\";\n// - number: \"number\";\n// - boolean: \"boolean\";\n// - choice: \"['Pizza', 'Schnitzel', 'Burger']\";\n// - multi-choice: \"{'Pizza', 'Schnitzel', 'Burger'}\";\ntype Field struct {\n\tLabel string\n\tFieldType string\n\tRequired bool\n}\n\ntype Form struct {\n\tID string\n\tOwner std.Address\n\tTitle string\n\tDescription string\n\tFields []Field\n\tCreatedAt time.Time\n\topenAt time.Time\n\tcloseAt time.Time\n}\n\n// Answers example :\n// - [\"Alex\", 21, true, 0, [0, 1]]\ntype Submission struct {\n\tFormID string\n\tAuthor std.Address\n\tAnswers string // json\n\tSubmittedAt time.Time\n}\n\ntype FormDB struct {\n\tForms []*Form\n\tAnswers []*Submission\n\tIDCounter seqid.ID\n}\n\nfunc NewDB() *FormDB {\n\treturn \u0026FormDB{\n\t\tForms: make([]*Form, 0),\n\t\tAnswers: make([]*Submission, 0),\n\t}\n}\n\n// This function checks if the form is open by verifying the given dates\n// - If a form doesn't have any dates, it's considered open\n// - If a form has only an open date, it's considered open if the open date is in the past\n// - If a form has only a close date, it's considered open if the close date is in the future\n// - If a form has both open and close dates, it's considered open if the current date is between the open and close dates\nfunc (form *Form) IsOpen() bool {\n\topenAt, errOpen := form.OpenAt()\n\tclosedAt, errClose := form.CloseAt()\n\n\tnoOpenDate := errOpen != nil\n\tnoCloseDate := errClose != nil\n\n\tif noOpenDate \u0026\u0026 noCloseDate {\n\t\treturn true\n\t}\n\n\tif noOpenDate \u0026\u0026 !noCloseDate {\n\t\treturn time.Now().Before(closedAt)\n\t}\n\n\tif !noOpenDate \u0026\u0026 noCloseDate {\n\t\treturn time.Now().After(openAt)\n\t}\n\n\tnow := time.Now()\n\treturn now.After(openAt) \u0026\u0026 now.Before(closedAt)\n}\n\n// OpenAt returns the open date of the form if it exists\nfunc (form *Form) OpenAt() (time.Time, error) {\n\tif form.openAt.IsZero() {\n\t\treturn time.Time{}, errNoOpenDate\n\t}\n\n\treturn form.openAt, nil\n}\n\n// CloseAt returns the close date of the form if it exists\nfunc (form *Form) CloseAt() (time.Time, error) {\n\tif form.closeAt.IsZero() {\n\t\treturn time.Time{}, errNoCloseDate\n\t}\n\n\treturn form.closeAt, nil\n}\n\n// GetForm returns a form by its ID if it exists\nfunc (db *FormDB) GetForm(id string) (*Form, error) {\n\tfor _, form := range db.Forms {\n\t\tif form.ID == id {\n\t\t\treturn form, nil\n\t\t}\n\t}\n\treturn nil, errFormNotFound\n}\n\n// GetAnswer returns an answer by its form - and author ids if it exists\nfunc (db *FormDB) GetAnswer(formID string, author std.Address) (*Submission, error) {\n\tfor _, answer := range db.Answers {\n\t\tif answer.FormID == formID \u0026\u0026 answer.Author.String() == author.String() {\n\t\t\treturn answer, nil\n\t\t}\n\t}\n\treturn nil, errAnswerNotFound\n}\n\n// GetSubmissionsByFormID returns a list containing the existing form submissions by the form ID\nfunc (db *FormDB) GetSubmissionsByFormID(formID string) []*Submission {\n\tsubmissions := make([]*Submission, 0)\n\n\tfor _, answer := range db.Answers {\n\t\tif answer.FormID == formID {\n\t\t\tsubmissions = append(submissions, answer)\n\t\t}\n\t}\n\n\treturn submissions\n}\n" + }, + { + "name": "forms_json.gno", + "body": "package forms\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/json\"\n)\n\ntype FormNodeBuilder struct {\n\t*json.NodeBuilder\n}\n\ntype FormArrayBuilder struct {\n\t*json.ArrayBuilder\n}\n\nfunc (b *FormNodeBuilder) WriteArray(key string, fn func(*FormArrayBuilder)) *FormNodeBuilder {\n\tb.NodeBuilder.WriteArray(key, func(ab *json.ArrayBuilder) {\n\t\tfn(\u0026FormArrayBuilder{ab})\n\t})\n\treturn b\n}\n\nfunc (b *FormNodeBuilder) WriteObject(key string, fn func(*FormNodeBuilder)) *FormNodeBuilder {\n\tb.NodeBuilder.WriteObject(key, func(nb *json.NodeBuilder) {\n\t\tfn(\u0026FormNodeBuilder{nb})\n\t})\n\treturn b\n}\n\nfunc (b *FormArrayBuilder) WriteObject(fn func(*FormNodeBuilder)) *FormArrayBuilder {\n\tb.ArrayBuilder.WriteObject(func(nb *json.NodeBuilder) {\n\t\tfn(\u0026FormNodeBuilder{nb})\n\t})\n\treturn b\n}\n\nfunc (b *FormNodeBuilder) WriteFormFields(key string, fields []Field) *FormNodeBuilder {\n\tb.WriteArray(key, func(builder *FormArrayBuilder) {\n\t\tfor _, field := range fields {\n\t\t\tbuilder.WriteObject(func(builder *FormNodeBuilder) {\n\t\t\t\tbuilder.WriteString(\"label\", field.Label).\n\t\t\t\t\tWriteString(\"fieldType\", field.FieldType).\n\t\t\t\t\tWriteBool(\"required\", field.Required)\n\t\t\t})\n\t\t}\n\t})\n\treturn b\n}\n\nfunc (b *FormNodeBuilder) WriteFormSubmission(key string, submission *Submission) *FormNodeBuilder {\n\tb.WriteObject(key, func(builder *FormNodeBuilder) {\n\t\tbuilder.WriteString(\"submittedAt\", submission.SubmittedAt.Format(\"2006-01-02 15:04:05\")).\n\t\t\tWriteString(\"answers\", strings.ReplaceAll(submission.Answers, \"\\\"\", \"'\"))\n\t})\n\treturn b\n}\n\nfunc (b *FormNodeBuilder) WriteForm(key string, value *Form) *FormNodeBuilder {\n\tb.WriteString(\"id\", value.ID).\n\t\tWriteString(\"owner\", value.Owner.String()).\n\t\tWriteString(\"title\", value.Title).\n\t\tWriteString(\"description\", value.Description).\n\t\tWriteString(\"createdAt\", value.CreatedAt.Format(\"2006-01-02 15:04:05\"))\n\tb.WriteFormFields(\"fields\", value.Fields)\n\treturn b\n}\n\nfunc (b *FormArrayBuilder) WriteForm(key string, value *Form) *FormArrayBuilder {\n\tb.WriteObject(func(builder *FormNodeBuilder) {\n\t\tbuilder.WriteString(\"id\", value.ID).\n\t\t\tWriteString(\"owner\", value.Owner.String()).\n\t\t\tWriteString(\"title\", value.Title).\n\t\t\tWriteString(\"description\", value.Description).\n\t\t\tWriteString(\"createdAt\", value.CreatedAt.Format(\"2006-01-02 15:04:05\"))\n\t\tbuilder.WriteFormFields(\"fields\", value.Fields)\n\t})\n\treturn b\n}\n" + }, + { + "name": "submit.gno", + "body": "package forms\n\nimport (\n\t\"std\"\n\t\"time\"\n)\n\n// This function allows to submit a form\nfunc (db *FormDB) SubmitForm(formID string, answers string) {\n\t// Check if form exists\n\tform, err := db.GetForm(formID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Check if form was already submitted by this user\n\tpreviousAnswer, err := db.GetAnswer(formID, std.PreviousRealm().Address())\n\tif previousAnswer != nil {\n\t\tpanic(errAlreadySubmitted)\n\t}\n\n\t// Check time restrictions\n\tif !form.IsOpen() {\n\t\tpanic(errFormClosed)\n\t}\n\n\t// Check if answers are formatted correctly\n\tif ValidateAnswers(answers, form.Fields) == false {\n\t\tpanic(errInvalidAnswers)\n\t}\n\n\t// Save answers\n\tanswer := Submission{\n\t\tFormID: formID,\n\t\tAnswers: answers,\n\t\tAuthor: std.PreviousRealm().Address(),\n\t\tSubmittedAt: time.Now(),\n\t}\n\tdb.Answers = append(db.Answers, \u0026answer)\n}\n" + }, + { + "name": "submit_test.gno", + "body": "package forms\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestAnswerForm(t *testing.T) {\n\tdb := NewDB()\n\n\tdata := `[\n\t\t{\n\t\t\t\"label\": \"Name\",\n\t\t\t\"fieldType\": \"string\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Age\",\n\t\t\t\"fieldType\": \"number\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Is this a test?\",\n\t\t\t\"fieldType\": \"boolean\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Food\",\n\t\t\t\"fieldType\": \"[Pizza|Schnitzel|Burger]\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Foods\",\n\t\t\t\"fieldType\": \"{Pizza|Schnitzel|Burger}\",\n\t\t\t\"required\": true\n\t\t}\n\t]`\n\n\tformID, err := db.CreateForm(\"Test Form\", \"Test Description\", \"\", \"\", data)\n\tif err != nil {\n\t\tt.Error(err)\n\t}\n\tanswers := `[\"Alex\", 21, true, 0, [0, 1]]`\n\tdb.SubmitForm(formID, answers)\n\n\turequire.True(t, len(db.Answers) == 1, \"Expected 1 answer, got\", string(len(db.Answers)))\n\turequire.True(t, db.Answers[0].FormID == formID, \"Expected form ID\", formID, \"got\", db.Answers[0].FormID)\n\turequire.True(t, db.Answers[0].Answers == answers, \"Expected answers\", answers, \"got\", db.Answers[0].Answers)\n}\n\nfunc TestAnswerFormDates(t *testing.T) {\n\tdb := NewDB()\n\n\tnow := time.Now()\n\ttomorrow := now.AddDate(0, 0, 1).Format(\"2006-01-02T15:04:05Z\")\n\tyesterday := now.AddDate(0, 0, -1).Format(\"2006-01-02T15:04:05Z\")\n\n\tdata := `[\n\t\t{\n\t\t\t\"label\": \"Name\",\n\t\t\t\"fieldType\": \"string\",\n\t\t\t\"required\": true\n\t\t}\n\t]`\n\tanswers := `[\"Test\"]`\n\n\turequire.PanicsWithMessage(t, \"Form is closed\", func() {\n\t\tformID, err := db.CreateForm(\"Test Form\", \"Test Description\", tomorrow, \"\", data)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tdb.SubmitForm(formID, answers)\n\t})\n\n\turequire.PanicsWithMessage(t, \"Form is closed\", func() {\n\t\tformID, err := db.CreateForm(\"Test Form\", \"Test Description\", \"\", yesterday, data)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\tdb.SubmitForm(formID, answers)\n\t})\n\n\turequire.NotPanics(t, func() {\n\t\tformID, err := db.CreateForm(\"Test Form\", \"Test Description\", yesterday, tomorrow, data)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tdb.SubmitForm(formID, answers)\n\t})\n}\n" + }, + { + "name": "validate.gno", + "body": "package forms\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/json\"\n)\n\nfunc validateBooleanField(node *json.Node, field Field) bool {\n\tif node.IsBool() == false {\n\t\treturn false\n\t}\n\n\tanswer, err := node.GetBool()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// If the field is required, checkbox must be checked\n\tif field.Required == true \u0026\u0026 answer == false {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc validateStringField(node *json.Node, field Field) bool {\n\tif node.IsString() == false {\n\t\treturn false\n\t}\n\n\tanswer, err := node.GetString()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// If the field is required, the answer must not be empty\n\tif field.Required == true \u0026\u0026 answer == \"\" {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc validateNumberField(node *json.Node, field Field) bool {\n\tif node.IsNumber() == false {\n\t\treturn false\n\t}\n\n\t_, err := node.GetNumeric()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc validateMultiChoiceField(node *json.Node, field Field) bool {\n\tchoices := strings.Split(field.FieldType[1:len(field.FieldType)-1], \"|\")\n\n\tif node.IsArray() == false {\n\t\treturn false\n\t}\n\n\tif field.Required == true \u0026\u0026 node.Size() == 0 {\n\t\treturn false\n\t}\n\n\tif node.Size() \u003e len(choices) {\n\t\treturn false\n\t}\n\n\tfor i := 0; i \u003c node.Size(); i++ {\n\t\tchoiceNode, err := node.GetIndex(i)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tchoiceIdx := choiceNode.MustNumeric()\n\t\tif choiceIdx \u003c 0 || int(choiceIdx) \u003e= len(choices) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc validateChoiceField(node *json.Node, field Field) bool {\n\tchoices := strings.Split(field.FieldType[1:len(field.FieldType)-1], \"|\")\n\n\tif node.IsNumber() == false {\n\t\treturn false\n\t}\n\n\tchoiceIdx := node.MustNumeric()\n\tif choiceIdx \u003c 0 || int(choiceIdx) \u003e= len(choices) {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc ValidateAnswer(answer *json.Node, field Field) bool {\n\tif field.FieldType == \"boolean\" {\n\t\treturn validateBooleanField(answer, field)\n\t} else if field.FieldType == \"string\" {\n\t\treturn validateStringField(answer, field)\n\t} else if field.FieldType == \"number\" {\n\t\treturn validateNumberField(answer, field)\n\t} else if strings.HasPrefix(field.FieldType, \"{\") \u0026\u0026 strings.HasSuffix(field.FieldType, \"}\") {\n\t\treturn validateMultiChoiceField(answer, field)\n\t} else if strings.HasPrefix(field.FieldType, \"[\") \u0026\u0026 strings.HasSuffix(field.FieldType, \"]\") {\n\t\treturn validateChoiceField(answer, field)\n\t}\n\n\treturn false\n}\n\n// ValidateAnswers checks if the given answers are valid for the given fields\nfunc ValidateAnswers(answers string, fields []Field) bool {\n\tunmarshalled, err := json.Unmarshal([]byte(answers))\n\tif err != nil {\n\t\treturn false\n\t}\n\n\t// If the number of answers is different from the number of fields, it's invalid\n\tif len(fields) != unmarshalled.Size() {\n\t\treturn false\n\t}\n\n\tfor i, field := range fields {\n\t\tanswer, err := unmarshalled.GetIndex(i)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\n\t\t// If the answer is empty and the field is not required, it's valid\n\t\tif answer.IsNull() \u0026\u0026 !field.Required {\n\t\t\treturn true\n\t\t}\n\n\t\tif !ValidateAnswer(answer, field) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n" + }, + { + "name": "validate_test.gno", + "body": "package forms\n\nimport (\n\t\"testing\"\n)\n\nfunc TestAnswerFormInvalidForm(t *testing.T) {\n\tdb := NewDB()\n\n\tdataAllTypes := `[\n\t\t{\n\t\t\t\"label\": \"Name\",\n\t\t\t\"fieldType\": \"string\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Age\",\n\t\t\t\"fieldType\": \"number\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Is this a test?\",\n\t\t\t\"fieldType\": \"boolean\",\n\t\t\t\"required\": false\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Food\",\n\t\t\t\"fieldType\": \"[Pizza|Schnitzel|Burger]\",\n\t\t\t\"required\": true\n\t\t},\n\t\t{\n\t\t\t\"label\": \"Favorite Foods\",\n\t\t\t\"fieldType\": \"{Pizza|Schnitzel|Burger}\",\n\t\t\t\"required\": true\n\t\t}\n\t]`\n\tdataOneRequiredText := `[\n\t\t{\n\t\t\t\"label\": \"Name\",\n\t\t\t\"fieldType\": \"string\",\n\t\t\t\"required\": true\n\t\t}\n\t]`\n\n\ttests := []struct {\n\t\tname string\n\t\tanswer string\n\t\texpectPanic bool\n\t\tdata string\n\t}{\n\t\t{\n\t\t\tname: \"correct\",\n\t\t\tanswer: `[\"Alex\", 21, true, 0, [0, 1]]`,\n\t\t\texpectPanic: false,\n\t\t\tdata: dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid string\",\n\t\t\tanswer: `[0, 21, true, 0, [0, 1]`,\n\t\t\texpectPanic: true,\n\t\t\tdata: dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid number\",\n\t\t\tanswer: `[\"Alex\", \"21\", true, 0, [0, 1]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata: dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid boolean\",\n\t\t\tanswer: `[\"Alex\", 21, 1, 0, [0, 1]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata: dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid choice\",\n\t\t\tanswer: `[\"Alex\", 21, true, 10, [0, 1]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata: dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid multi-choice 1\",\n\t\t\tanswer: `[\"Alex\", 21, true, 0, [0, 1, 2, 3, 4, 5]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata: dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid multi-choice 2\",\n\t\t\tanswer: `[\"Alex\", 21, true, 0, [5]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata: dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid multi-choice 3\",\n\t\t\tanswer: `[\"Alex\", 21, true, 0, 0]`,\n\t\t\texpectPanic: true,\n\t\t\tdata: dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname: \"required string\",\n\t\t\tanswer: `[\"\", 21, true, 0, [0, 1]]`,\n\t\t\texpectPanic: true,\n\t\t\tdata: dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname: \"unrequired number\",\n\t\t\tanswer: `[\"Alex\", null, true, 0, [0, 1]]`,\n\t\t\texpectPanic: false,\n\t\t\tdata: dataAllTypes,\n\t\t},\n\t\t{\n\t\t\tname: \"correct one field\",\n\t\t\tanswer: `[\"Alex\"]`,\n\t\t\texpectPanic: false,\n\t\t\tdata: dataOneRequiredText,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tformID, err := db.CreateForm(\"Test Form\", \"Test Description\", \"\", \"\", tt.data)\n\t\t\tif err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\tif tt.expectPanic {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tt.Errorf(\"%s panic occurred when not expected: %v\", tt.name, r)\n\t\t\t\t} else if tt.expectPanic {\n\t\t\t\t\tt.Errorf(\"%s expected panic but didn't occur\", tt.name)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tdb.SubmitForm(formID, tt.answer)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Iyiup92RB9GiophG6XAHq5CTEEbfUTCwRLqKhYo23d/zwUQ4Meh+v4WDXn4ILJ7+yob2suouCbPVq8M4NMKCAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "bank", + "path": "gno.land/p/demo/bank", + "files": [ + { + "name": "types.gno", + "body": "// TODO: this is an example, and needs to be fixed up and tested.\n\npackage bank\n\n// NOTE: unexposed struct for security.\ntype order struct {\n\tfrom Address\n\tto Address\n\tamount Coins\n\tprocessed bool\n}\n\n// NOTE: unexposed methods for security.\nfunc (ch *order) string() string {\n\treturn \"TODO\"\n}\n\n// Wraps the internal *order for external use.\ntype Order struct {\n\t*order\n}\n\n// XXX only exposed for demonstration. TODO unexpose, make full demo.\nfunc NewOrder(from Address, to Address, amount Coins) Order {\n\treturn Order{\n\t\torder: \u0026order{\n\t\t\tfrom: from,\n\t\t\tto: to,\n\t\t\tamount: amount,\n\t\t},\n\t}\n}\n\n// Panics if error, or already processed.\nfunc (o Order) Execute() {\n\tif o.order.processed {\n\t\tpanic(\"order already processed\")\n\t}\n\to.order.processed = true\n\t// TODO implemement.\n}\n\nfunc (o Order) IsZero() bool {\n\treturn o.order == nil\n}\n\nfunc (o Order) From() Address {\n\treturn o.order.from\n}\n\nfunc (o Order) To() Address {\n\treturn o.order.to\n}\n\nfunc (o Order) Amount() Coins {\n\treturn o.order.amount\n}\n\nfunc (o Order) Processed() bool {\n\treturn o.order.processed\n}\n\n//----------------------------------------\n// Escrow\n\ntype EscrowTerms struct {\n\tPartyA Address\n\tPartyB Address\n\tAmountA Coins\n\tAmountB Coins\n}\n\ntype EscrowContract struct {\n\tEscrowTerms\n\tOrderA Order\n\tOrderB Order\n}\n\nfunc CreateEscrow(terms EscrowTerms) *EscrowContract {\n\treturn \u0026EscrowContract{\n\t\tEscrowTerms: terms,\n\t}\n}\n\nfunc (esc *EscrowContract) SetOrderA(order Order) {\n\tif !esc.OrderA.IsZero() {\n\t\tpanic(\"order-a already set\")\n\t}\n\tif esc.EscrowTerms.PartyA != order.From() {\n\t\tpanic(\"invalid order-a:from mismatch\")\n\t}\n\tif esc.EscrowTerms.PartyB != order.To() {\n\t\tpanic(\"invalid order-a:to mismatch\")\n\t}\n\tif !esc.EscrowTerms.AmountA.Equal(order.Amount()) {\n\t\tpanic(\"invalid order-a amount\")\n\t}\n\tesc.OrderA = order\n}\n\nfunc (esc *EscrowContract) SetOrderB(order Order) {\n\tif !esc.OrderB.IsZero() {\n\t\tpanic(\"order-b already set\")\n\t}\n\tif esc.EscrowTerms.PartyB != order.From() {\n\t\tpanic(\"invalid order-b:from mismatch\")\n\t}\n\tif esc.EscrowTerms.PartyA != order.To() {\n\t\tpanic(\"invalid order-b:to mismatch\")\n\t}\n\tif !esc.EscrowTerms.AmountB.Equal(order.Amount()) {\n\t\tpanic(\"invalid order-b amount\")\n\t}\n\tesc.OrderA = order\n}\n\nfunc (esc *EscrowContract) Execute() {\n\tif esc.OrderA.IsZero() {\n\t\tpanic(\"order-a not yet set\")\n\t}\n\tif esc.OrderB.IsZero() {\n\t\tpanic(\"order-b not yet set\")\n\t}\n\t// NOTE: succeeds atomically.\n\tesc.OrderA.Execute()\n\tesc.OrderB.Execute()\n}\n\n//----------------------------------------\n// TODO: actually implement these in std package.\n\ntype (\n\tAddress string\n\tCoins []Coin\n\tCoin struct {\n\t\tDenom bool\n\t\tAmount int64\n\t}\n)\n\nfunc (a Coins) Equal(b Coins) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i, v := range a {\n\t\tif v != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "PoAqz1hiZEmuymCQTx9POqeGrVutjw2ZqAxgb+LfIcpL8Ou3K+loEun6mYl9HV+sHapfjFm3xnQNIjpzo6upDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "avl", + "path": "gno.land/p/demo/avl", + "files": [ + { + "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/demo/ufmt\"\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/demo/avl\"\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() {\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\n// Realm:\n// switchrealm[\"gno.land/r/test\"]\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:4]=\n// @@ -1,8 +1,8 @@\n// {\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:4\",\n// - \"ModTime\": \"0\",\n// - \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// + \"ModTime\": \"7\",\n// + \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:9]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"value1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:9\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:8]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b28057ab7be6383785c0a5503e8a531bdbc21851\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:9\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:7]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"6da365f0d6cacbcdf53cd5a4b125803cddce08c2\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:4\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"f216afe7b5a17f4ebdbb98dceccedbc22e237596\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:6]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"ff1a50d8489090af37a2c7766d659f0d717939b5\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\"\n// }\n// }\n// }\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:2]=\n// @@ -3,7 +3,7 @@\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"IsEscaped\": true,\n// - \"ModTime\": \"3\",\n// + \"ModTime\": \"5\",\n// \"RefCount\": \"2\"\n// },\n// \"Parent\": null,\n// @@ -30,8 +30,8 @@\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// - \"Hash\": \"424b49c215f471979ccd718172a016e6ec9dd934\",\n// - \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:4\"\n// + \"Hash\": \"ae86874f9b47fa5e64c30b3e92e9d07f2ec967a4\",\n// + \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/demo/avl\"\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() {\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\n// Realm:\n// switchrealm[\"gno.land/r/test\"]\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:6]=\n// @@ -1,7 +1,7 @@\n// {\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\",\n// - \"ModTime\": \"0\",\n// + \"ModTime\": \"11\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:5\",\n// \"RefCount\": \"1\"\n// },\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:8]=\n// @@ -1,7 +1,7 @@\n// {\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\",\n// - \"ModTime\": \"0\",\n// + \"ModTime\": \"13\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:5\",\n// \"RefCount\": \"1\"\n// },\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:15]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"value2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:14]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"143aebc820da33550f7338723fb1e2eec575b196\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:13]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"cafae89e4d4aaaefe7fdf0691084508d4274a981\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:8\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"2e733a8e9e74fe14f0a5d10fb0f6728fa53d052d\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:12]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b2e446f490656c19a83c43055de29c96e92a1549\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:11]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AwAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"4e56eeb96eb1d9b27cf603140cd03a1622b6358b\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"7b61530859954d1d14b2f696c91c5f37d39c21e7\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:10\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:10]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:10\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"fedc6d430b38c985dc6a985b2fcaee97e88ba6da\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\"\n// }\n// }\n// }\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:2]=\n// @@ -3,7 +3,7 @@\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"IsEscaped\": true,\n// - \"ModTime\": \"3\",\n// + \"ModTime\": \"9\",\n// \"RefCount\": \"2\"\n// },\n// \"Parent\": null,\n// @@ -30,8 +30,8 @@\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// - \"Hash\": \"213a2c56908ed00c6d21377f37be61a76102cd5f\",\n// - \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:4\"\n// + \"Hash\": \"515b45e4a6f5fa153a0251d7108781d86c52ce1c\",\n// + \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:10\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:4]\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:5]\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\nvar tree avl.Tree\n\nfunc init() {\n\ttree.Set(\"key0\", \"value0\")\n\ttree.Set(\"key1\", \"value1\")\n}\n\nfunc main() {\n\tvar updated bool\n\tupdated = tree.Set(\"key2\", \"value2\")\n\tprintln(updated, tree.Size())\n}\n\n// Output:\n// false 3\n\n// Realm:\n// switchrealm[\"gno.land/r/test\"]\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:7]=\n// @@ -1,7 +1,7 @@\n// {\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\",\n// - \"ModTime\": \"0\",\n// + \"ModTime\": \"12\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\",\n// \"RefCount\": \"1\"\n// },\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:9]=\n// @@ -1,7 +1,7 @@\n// {\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:9\",\n// - \"ModTime\": \"0\",\n// + \"ModTime\": \"14\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:6\",\n// \"RefCount\": \"1\"\n// },\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:16]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"value2\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:16\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:15]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"db333c89cd6773709e031f1f4e4ed4d3fed66c11\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:16\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:14]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key2\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"db39c9c0a60e0d5b30dbaf9be6150d3fec16aa4b\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:9\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b4fc2fdd2d0fe936c87ed2ace97136cffeed207f\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:15\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:13]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"2e9127534f91b385426d76e8e164f50f635cc1de\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:14\"\n// }\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:12]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"key1\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AwAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"43e03b0c877b40c34e12bc2b15560e8ecd42ae9d\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:7\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"4b123e2424d900a427f9dee88a70ce61f3cdcf5b\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:13\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[a8ada09dee16d791fd406d629fe29bb0ed084a30:11]={\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"76d9227e755efd6674d8fa34e12decb7a9855488\",\n// \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:12\"\n// }\n// }\n// }\n// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:3]=\n// @@ -12,8 +12,8 @@\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// - \"Hash\": \"fbf007d972314fd7a2005d628c444b0831c16402\",\n// - \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:5\"\n// + \"Hash\": \"ff46b4dd63457c3fd59801e725f65af524ec829d\",\n// + \"ObjectID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:11\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// @@ -22,7 +22,7 @@\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:3\",\n// - \"ModTime\": \"4\",\n// + \"ModTime\": \"10\",\n// \"OwnerID\": \"a8ada09dee16d791fd406d629fe29bb0ed084a30:2\",\n// \"RefCount\": \"1\"\n// }\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:5]\n// d[a8ada09dee16d791fd406d629fe29bb0ed084a30:6]\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "H0j53hDX2eNdVYas2w8b4xenUX8TVAFCjxVoeB2O5ZeXLkozrj39BvkMvDT9LPSKQDis+yR7m/N7sGqdLTUdBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "acl", + "path": "gno.land/p/demo/acl", + "files": [ + { + "name": "acl.gno", + "body": "package acl\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nfunc New() *Directory {\n\treturn \u0026Directory{\n\t\tuserGroups: avl.Tree{},\n\t\tpermBuckets: avl.Tree{},\n\t}\n}\n\ntype Directory struct {\n\tpermBuckets avl.Tree // identifier -\u003e perms\n\tuserGroups avl.Tree // std.Address -\u003e []string\n}\n\nfunc (d *Directory) HasPerm(addr std.Address, verb, resource string) bool {\n\t// FIXME: consider memoize.\n\n\t// user perms\n\tif d.getBucketPerms(\"u:\"+addr.String()).hasPerm(verb, resource) {\n\t\treturn true\n\t}\n\n\t// everyone's perms.\n\tif d.getBucketPerms(\"g:\"+Everyone).hasPerm(verb, resource) {\n\t\treturn true\n\t}\n\n\t// user groups' perms.\n\tgroups, ok := d.userGroups.Get(addr.String())\n\tif ok {\n\t\tfor _, group := range groups.([]string) {\n\t\t\tif d.getBucketPerms(\"g:\"+group).hasPerm(verb, resource) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (d *Directory) getBucketPerms(bucket string) perms {\n\tres, ok := d.permBuckets.Get(bucket)\n\tif ok {\n\t\treturn res.(perms)\n\t}\n\treturn perms{}\n}\n\nfunc (d *Directory) HasRole(addr std.Address, role string) bool {\n\treturn d.HasPerm(addr, \"role\", role)\n}\n\nfunc (d *Directory) AddUserPerm(addr std.Address, verb, resource string) {\n\tbucket := \"u:\" + addr.String()\n\tp := perm{\n\t\tverbs: []string{verb},\n\t\tresources: []string{resource},\n\t}\n\td.addPermToBucket(bucket, p)\n}\n\nfunc (d *Directory) AddGroupPerm(name string, verb, resource string) {\n\tbucket := \"g:\" + name\n\tp := perm{\n\t\tverbs: []string{verb},\n\t\tresources: []string{resource},\n\t}\n\td.addPermToBucket(bucket, p)\n}\n\nfunc (d *Directory) addPermToBucket(bucket string, p perm) {\n\tvar ps perms\n\n\texisting, ok := d.permBuckets.Get(bucket)\n\tif ok {\n\t\tps = existing.(perms)\n\t}\n\tps = append(ps, p)\n\n\td.permBuckets.Set(bucket, ps)\n}\n\nfunc (d *Directory) AddUserToGroup(user std.Address, group string) {\n\texisting, ok := d.userGroups.Get(user.String())\n\tvar groups []string\n\tif ok {\n\t\tgroups = existing.([]string)\n\t}\n\tgroups = append(groups, group)\n\td.userGroups.Set(user.String(), groups)\n}\n\n// TODO: helpers to remove permissions.\n// TODO: helpers to adds multiple permissions at once -\u003e {verbs: []string{\"read\",\"write\"}}.\n// TODO: helpers to delete users from gorups.\n// TODO: helpers to quickly reset states.\n" + }, + { + "name": "acl_test.gno", + "body": "package acl\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc Test(t *testing.T) {\n\tadm := testutils.TestAddress(\"admin\")\n\tmod := testutils.TestAddress(\"mod\")\n\tusr := testutils.TestAddress(\"user\")\n\tcst := testutils.TestAddress(\"custom\")\n\n\tdir := New()\n\n\t// by default, no one has perm.\n\tshouldNotHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldNotHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding all the rights to admin.\n\tdir.AddUserPerm(adm, \".*\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding custom regexp rule for user \"cst\".\n\tdir.AddUserPerm(cst, \"write\", \"r/demo/boards:gnolang/.*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding a group perm for a new group.\n\t// no changes expected.\n\tdir.AddGroupPerm(\"mods\", \"role\", \"moderator\")\n\tdir.AddGroupPerm(\"mods\", \"write\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// assigning the user \"mod\" to the \"mods\" group.\n\tdir.AddUserToGroup(mod, \"mods\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\") // new\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\")\n\n\t// adding \"read\" permission for everyone.\n\tdir.AddGroupPerm(Everyone, \"read\", \".*\")\n\tshouldHasRole(t, dir, adm, \"foo\")\n\tshouldNotHasRole(t, dir, mod, \"foo\")\n\tshouldNotHasRole(t, dir, usr, \"foo\")\n\tshouldNotHasRole(t, dir, cst, \"foo\")\n\tshouldHasPerm(t, dir, adm, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldNotHasPerm(t, dir, usr, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, cst, \"write\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, adm, \"read\", \"r/demo/boards:gnolang/1\")\n\tshouldHasPerm(t, dir, mod, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, usr, \"read\", \"r/demo/boards:gnolang/1\") // new\n\tshouldHasPerm(t, dir, cst, \"read\", \"r/demo/boards:gnolang/1\") // new\n}\n\nfunc shouldHasRole(t *testing.T, dir *Directory, addr std.Address, role string) {\n\tt.Helper()\n\tcheck := dir.HasRole(addr, role)\n\tuassert.Equal(t, true, check, ufmt.Sprintf(\"%s should has role %s\", addr.String(), role))\n}\n\nfunc shouldNotHasRole(t *testing.T, dir *Directory, addr std.Address, role string) {\n\tt.Helper()\n\tcheck := dir.HasRole(addr, role)\n\tuassert.Equal(t, false, check, ufmt.Sprintf(\"%s should not has role %s\", addr.String(), role))\n}\n\nfunc shouldHasPerm(t *testing.T, dir *Directory, addr std.Address, verb string, resource string) {\n\tt.Helper()\n\tcheck := dir.HasPerm(addr, verb, resource)\n\tuassert.Equal(t, true, check, ufmt.Sprintf(\"%s should has perm for %s - %s\", addr.String(), verb, resource))\n}\n\nfunc shouldNotHasPerm(t *testing.T, dir *Directory, addr std.Address, verb string, resource string) {\n\tt.Helper()\n\tcheck := dir.HasPerm(addr, verb, resource)\n\tuassert.Equal(t, false, check, ufmt.Sprintf(\"%s should not has perm for %s - %s\", addr.String(), verb, resource))\n}\n" + }, + { + "name": "const.gno", + "body": "package acl\n\nconst Everyone string = \"everyone\"\n" + }, + { + "name": "perm.gno", + "body": "package acl\n\nimport \"regexp\"\n\ntype perm struct {\n\tverbs []string\n\tresources []string\n}\n\nfunc (perm perm) hasPerm(verb, resource string) bool {\n\t// check verb\n\tverbOK := false\n\tfor _, pattern := range perm.verbs {\n\t\tif match(pattern, verb) {\n\t\t\tverbOK = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !verbOK {\n\t\treturn false\n\t}\n\n\t// check resource\n\tfor _, pattern := range perm.resources {\n\t\tif match(pattern, resource) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc match(pattern, target string) bool {\n\tif pattern == \".*\" {\n\t\treturn true\n\t}\n\n\tif pattern == target {\n\t\treturn true\n\t}\n\n\t// regexp handling\n\tmatch, _ := regexp.MatchString(pattern, target)\n\treturn match\n}\n" + }, + { + "name": "perms.gno", + "body": "package acl\n\ntype perms []perm\n\nfunc (perms perms) hasPerm(verb, resource string) bool {\n\tfor _, perm := range perms {\n\t\tif perm.hasPerm(verb, resource) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "yPigKRQKkZIcrN7XYRkUxkTD7osUzEE4kraaWZ3eRdFTgzCK7SJZs0k1Z+Ts9sGdoVIxc2AxzO3wGSbcQN/oCA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "list", + "path": "gno.land/p/demo/avl/list", + "files": [ + { + "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/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\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//\tl := list.New()\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//\tl := list.New()\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//\tl := list.New()\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//\tl := list.New()\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//\tl := list.New()\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//\tl := list.New()\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//\tl := list.New()\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//\tl := list.New()\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" + }, + { + "name": "list_test.gno", + "body": "package list\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/ufmt\"\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_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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "VTyfsPI72ulMWj8Eb7iZxQtMPGIaJyBZv3cQMY8AsQGB+/I3oTvlutqJ2A3Mce57OtbIPUgJ0RP/E7cRsBz5DQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "rotree", + "path": "gno.land/p/demo/avl/rotree", + "files": [ + { + "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/demo/avl\"\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/demo/avl\"\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "wncsNFmLsbrCFDoL9C+FntlUsnkRbbKs11o7wszt0aGJsn9RoaGG6OCof+bXocMngaD8O6PrDNCEyj6Ji2YdBQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "pager", + "path": "gno.land/p/demo/avl/pager", + "files": [ + { + "name": "pager.gno", + "body": "package pager\n\nimport (\n\t\"math\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl/rotree\"\n\t\"gno.land/p/demo/ufmt\"\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/demo/avl\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\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/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "DP+cmL4btNewi4TiWLsXSF0+A927J3u15UEbspAKBxR9sZCw7B8uHxWYPXb7SQQzXK2ns1CHrBIcI9EuZc8MBA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "rolist", + "path": "gno.land/p/demo/avl/rolist", + "files": [ + { + "name": "rolist.gno", + "body": "// Package rolist provides a read-only wrapper for list.List with safe value transformation.\n//\n// It is useful when you want to expose a read-only view of a list while ensuring that\n// the sensitive data cannot be modified.\n//\n// Example:\n//\n//\t// Define a user structure with sensitive data\n//\ttype User struct {\n//\t Name string\n//\t Balance int\n//\t Internal string // sensitive field\n//\t}\n//\n//\t// Create and populate the original list\n//\tprivateList := list.New()\n//\tprivateList.Append(\u0026User{\n//\t Name: \"Alice\",\n//\t Balance: 100,\n//\t Internal: \"sensitive\",\n//\t})\n//\n//\t// Create a safe transformation function that copies the struct\n//\t// while excluding sensitive data\n//\tmakeEntrySafeFn := func(v any) any {\n//\t u := v.(*User)\n//\t return \u0026User{\n//\t Name: u.Name,\n//\t Balance: u.Balance,\n//\t Internal: \"\", // omit sensitive data\n//\t }\n//\t}\n//\n//\t// Create a read-only view of the list\n//\tpublicList := rolist.Wrap(list, makeEntrySafeFn)\n//\n//\t// Safely access the data\n//\tvalue := publicList.Get(0)\n//\tuser := value.(*User)\n//\t// user.Name == \"Alice\"\n//\t// user.Balance == 100\n//\t// user.Internal == \"\" (sensitive data is filtered)\npackage rolist\n\nimport (\n\t\"gno.land/p/demo/avl/list\"\n)\n\n// IReadOnlyList defines the read-only operations available on a list.\ntype IReadOnlyList interface {\n\tLen() int\n\tGet(index int) any\n\tSlice(startIndex, endIndex int) []any\n\tForEach(fn func(index int, value any) bool)\n}\n\n// ReadOnlyList wraps a list.List and provides read-only access.\ntype ReadOnlyList struct {\n\tlist *list.List\n\tmakeEntrySafeFn func(any) any\n}\n\n// Verify interface implementations\nvar _ IReadOnlyList = (*ReadOnlyList)(nil)\nvar _ IReadOnlyList = (interface{ list.IList })(nil) // is subset of list.IList\n\n// Wrap creates a new ReadOnlyList from an existing list.List and a safety transformation function.\n// If makeEntrySafeFn is nil, values will be returned as-is without transformation.\nfunc Wrap(list *list.List, makeEntrySafeFn func(any) any) *ReadOnlyList {\n\treturn \u0026ReadOnlyList{\n\t\tlist: list,\n\t\tmakeEntrySafeFn: makeEntrySafeFn,\n\t}\n}\n\n// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value\nfunc (rol *ReadOnlyList) getSafeValue(value any) any {\n\tif rol.makeEntrySafeFn == nil {\n\t\treturn value\n\t}\n\treturn rol.makeEntrySafeFn(value)\n}\n\n// Len returns the number of elements in the list.\nfunc (rol *ReadOnlyList) Len() int {\n\treturn rol.list.Len()\n}\n\n// Get returns the value at the specified index, converted to a safe format.\n// Returns nil if index is out of bounds.\nfunc (rol *ReadOnlyList) Get(index int) any {\n\tvalue := rol.list.Get(index)\n\tif value == nil {\n\t\treturn nil\n\t}\n\treturn rol.getSafeValue(value)\n}\n\n// Slice returns a slice of values from startIndex (inclusive) to endIndex (exclusive),\n// with all values converted to a safe format.\nfunc (rol *ReadOnlyList) Slice(startIndex, endIndex int) []any {\n\tvalues := rol.list.Slice(startIndex, endIndex)\n\tif values == nil {\n\t\treturn nil\n\t}\n\n\tresult := make([]any, len(values))\n\tfor i, v := range values {\n\t\tresult[i] = rol.getSafeValue(v)\n\t}\n\treturn result\n}\n\n// ForEach iterates through all elements in the list, providing safe versions of the values.\nfunc (rol *ReadOnlyList) ForEach(fn func(index int, value any) bool) {\n\trol.list.ForEach(func(index int, value any) bool {\n\t\treturn fn(index, rol.getSafeValue(value))\n\t})\n}\n" + }, + { + "name": "rolist_test.gno", + "body": "package rolist\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl/list\"\n)\n\nfunc TestExample(t *testing.T) {\n\t// User represents our internal data structure\n\ttype User struct {\n\t\tID string\n\t\tName string\n\t\tBalance int\n\t\tInternal string // sensitive internal data\n\t}\n\n\t// Create and populate the original list\n\tl := \u0026list.List{}\n\tl.Append(\n\t\t\u0026User{\n\t\t\tID: \"1\",\n\t\t\tName: \"Alice\",\n\t\t\tBalance: 100,\n\t\t\tInternal: \"sensitive_data_1\",\n\t\t},\n\t\t\u0026User{\n\t\t\tID: \"2\",\n\t\t\tName: \"Bob\",\n\t\t\tBalance: 200,\n\t\t\tInternal: \"sensitive_data_2\",\n\t\t},\n\t)\n\n\t// Define a makeEntrySafeFn that:\n\t// 1. Creates a defensive copy of the User struct\n\t// 2. Omits sensitive internal data\n\tmakeEntrySafeFn := func(v any) any {\n\t\toriginalUser := v.(*User)\n\t\treturn \u0026User{\n\t\t\tID: originalUser.ID,\n\t\t\tName: originalUser.Name,\n\t\t\tBalance: originalUser.Balance,\n\t\t\tInternal: \"\", // Omit sensitive data\n\t\t}\n\t}\n\n\t// Create a read-only view of the list\n\troList := Wrap(l, makeEntrySafeFn)\n\n\t// Test retrieving and verifying a user\n\tt.Run(\"Get User\", func(t *testing.T) {\n\t\t// Get user from read-only list\n\t\tvalue := roList.Get(0)\n\t\tif value == nil {\n\t\t\tt.Fatal(\"User at index 0 not found\")\n\t\t}\n\n\t\tuser := value.(*User)\n\n\t\t// Verify user data is correct\n\t\tif user.Name != \"Alice\" || user.Balance != 100 {\n\t\t\tt.Errorf(\"Unexpected user data: got name=%s balance=%d\", user.Name, user.Balance)\n\t\t}\n\n\t\t// Verify sensitive data is not exposed\n\t\tif user.Internal != \"\" {\n\t\t\tt.Error(\"Sensitive data should not be exposed\")\n\t\t}\n\n\t\t// Verify it's a different instance than the original\n\t\toriginalUser := l.Get(0).(*User)\n\t\tif user == originalUser {\n\t\t\tt.Error(\"Read-only list should return a copy, not the original pointer\")\n\t\t}\n\t})\n\n\t// Test slice functionality\n\tt.Run(\"Slice Users\", func(t *testing.T) {\n\t\tusers := roList.Slice(0, 2)\n\t\tif len(users) != 2 {\n\t\t\tt.Fatalf(\"Expected 2 users, got %d\", len(users))\n\t\t}\n\n\t\tfor _, v := range users {\n\t\t\tuser := v.(*User)\n\t\t\tif user.Internal != \"\" {\n\t\t\t\tt.Error(\"Sensitive data exposed in slice\")\n\t\t\t}\n\t\t}\n\t})\n\n\t// Test ForEach functionality\n\tt.Run(\"ForEach Users\", func(t *testing.T) {\n\t\tcount := 0\n\t\troList.ForEach(func(index int, value any) bool {\n\t\t\tuser := value.(*User)\n\t\t\tif user.Internal != \"\" {\n\t\t\t\tt.Error(\"Sensitive data exposed during iteration\")\n\t\t\t}\n\t\t\tcount++\n\t\t\treturn false\n\t\t})\n\n\t\tif count != 2 {\n\t\t\tt.Errorf(\"Expected 2 users, got %d\", count)\n\t\t}\n\t})\n}\n\nfunc TestNilMakeEntrySafeFn(t *testing.T) {\n\t// Create a list with some test data\n\tl := \u0026list.List{}\n\toriginalValue := []int{1, 2, 3}\n\tl.Append(originalValue)\n\n\t// Create a ReadOnlyList with nil makeEntrySafeFn\n\troList := Wrap(l, nil)\n\n\t// Test that we get back the original value\n\tvalue := roList.Get(0)\n\tif value == nil {\n\t\tt.Fatal(\"Value not found\")\n\t}\n\n\t// Verify it's the exact same slice (not a copy)\n\tretrievedSlice := value.([]int)\n\tif \u0026retrievedSlice[0] != \u0026originalValue[0] {\n\t\tt.Error(\"Expected to get back the original slice reference\")\n\t}\n}\n\nfunc TestReadOnlyList(t *testing.T) {\n\t// Example of a makeEntrySafeFn that appends \"_readonly\" to demonstrate transformation\n\tmakeEntrySafeFn := func(value any) any {\n\t\treturn value.(string) + \"_readonly\"\n\t}\n\n\tl := \u0026list.List{}\n\tl.Append(\"value1\", \"value2\", \"value3\")\n\n\troList := Wrap(l, makeEntrySafeFn)\n\n\ttests := []struct {\n\t\tname string\n\t\tindex int\n\t\texpected any\n\t}{\n\t\t{\"ExistingIndex0\", 0, \"value1_readonly\"},\n\t\t{\"ExistingIndex1\", 1, \"value2_readonly\"},\n\t\t{\"NonExistingIndex\", 3, nil},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvalue := roList.Get(tt.index)\n\t\t\tif value != tt.expected {\n\t\t\t\tt.Errorf(\"For index %d, expected %v, got %v\", tt.index, tt.expected, value)\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "CXf7fDGBVC/kI7Hk0+ysMheI3mu5P5FlY6SDKYsuOy5NtlatNJicE5QCRnXV9mOOSBT+IqP6FczzHcYWH1cgDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "avlhelpers", + "path": "gno.land/p/demo/avlhelpers", + "files": [ + { + "name": "avlhelpers.gno", + "body": "package avlhelpers\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n// Iterate the keys in-order starting from the given prefix.\n// It calls the provided callback function for each key-value pair encountered.\n// If the callback returns true, the iteration is stopped.\n// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.\nfunc IterateByteStringKeysByPrefix(tree avl.ITree, prefix string, cb avl.IterCbFn) {\n\tend := \"\"\n\tn := len(prefix)\n\t// To make the end of the search, increment the final character ASCII by one.\n\tfor n \u003e 0 {\n\t\tif ascii := int(prefix[n-1]); ascii \u003c 0xff {\n\t\t\tend = prefix[0:n-1] + string(ascii+1)\n\t\t\tbreak\n\t\t}\n\n\t\t// The last character is 0xff. Try the previous character.\n\t\tn--\n\t}\n\n\ttree.Iterate(prefix, end, cb)\n}\n\n// Get a list of keys starting from the given prefix. Limit the\n// number of results to maxResults.\n// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.\nfunc ListByteStringKeysByPrefix(tree avl.ITree, prefix string, maxResults int) []string {\n\tresult := []string{}\n\tIterateByteStringKeysByPrefix(tree, prefix, func(key string, value any) bool {\n\t\tresult = append(result, key)\n\t\tif len(result) \u003e= maxResults {\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\treturn result\n}\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// PKGPATH: gno.land/r/test\npackage test\n\nimport (\n\t\"encoding/hex\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avlhelpers\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc main() {\n\ttree := avl.NewTree()\n\n\t{\n\t\t// Empty tree.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t}\n\n\ttree.Set(\"alice\", \"\")\n\ttree.Set(\"andy\", \"\")\n\ttree.Set(\"bob\", \"\")\n\n\t{\n\t\t// Match only alice.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"al\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t}\n\n\t{\n\t\t// Match alice and andy.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t\tprintln(\"match: \" + matches[1])\n\t}\n\n\t{\n\t\t// Match alice and andy limited to 1.\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\", 1)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(\"match: \" + matches[0])\n\t}\n\n\ttree = avl.NewTree()\n\ttree.Set(\"a\\xff\", \"\")\n\ttree.Set(\"a\\xff\\xff\", \"\")\n\ttree.Set(\"b\", \"\")\n\ttree.Set(\"\\xff\\xff\\x00\", \"\")\n\n\t{\n\t\t// Match only \"a\\xff\\xff\".\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\\xff\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t}\n\n\t{\n\t\t// Match \"a\\xff\" and \"a\\xff\\xff\".\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"a\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[1]))))\n\t}\n\n\t{\n\t\t// Edge case: Match only \"\\xff\\xff\\x00\".\n\t\tmatches := avlhelpers.ListByteStringKeysByPrefix(tree, \"\\xff\\xff\", 10)\n\t\tprintln(ufmt.Sprintf(\"# matches: %d\", len(matches)))\n\t\tprintln(ufmt.Sprintf(\"match: %s\", hex.EncodeToString([]byte(matches[0]))))\n\t}\n}\n\n// Output:\n// # matches: 0\n// # matches: 1\n// match: alice\n// # matches: 2\n// match: alice\n// match: andy\n// # matches: 1\n// match: alice\n// # matches: 1\n// match: 61ffff\n// # matches: 2\n// match: 61ff\n// match: 61ffff\n// # matches: 1\n// match: ffff00\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "n3SyOL3ZfYo9lwz7xQBvnyRJikHk69oRTncAihh1KNT7efqWvQM3RtMx2FweljliAsJC42iZ6JcQMRqv5KJWCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "bf", + "path": "gno.land/p/demo/bf", + "files": [ + { + "name": "bf.gno", + "body": "package bf\n\nimport (\n\t\"strings\"\n)\n\nconst maxlen = 30000\n\nfunc Execute(code string) string {\n\tvar (\n\t\tmemory = make([]byte, maxlen) // memory tape\n\t\tpointer = 0 // initial memory pointer\n\t\tbuf strings.Builder\n\t)\n\n\t// Loop through each character in the code\n\tfor i := 0; i \u003c len(code); i++ {\n\t\tswitch code[i] {\n\t\tcase '\u003e':\n\t\t\t// Increment memory pointer\n\t\t\tpointer++\n\t\t\tif pointer \u003e= maxlen {\n\t\t\t\tpointer = 0\n\t\t\t}\n\t\tcase '\u003c':\n\t\t\t// Decrement memory pointer\n\t\t\tpointer--\n\t\t\tif pointer \u003c 0 {\n\t\t\t\tpointer = maxlen - 1\n\t\t\t}\n\t\tcase '+':\n\t\t\t// Increment the byte at the memory pointer\n\t\t\tmemory[pointer]++\n\t\tcase '-':\n\t\t\t// Decrement the byte at the memory pointer\n\t\t\tmemory[pointer]--\n\t\tcase '.':\n\t\t\t// Output the byte at the memory pointer\n\t\t\tbuf.WriteByte(memory[pointer])\n\t\tcase ',':\n\t\t\t// Input a byte and store it in the memory\n\t\t\tpanic(\"unsupported\")\n\t\t\t// fmt.Scan(\u0026memory[pointer])\n\t\tcase '[':\n\t\t\t// Jump forward past the matching ']' if the byte at the memory pointer is zero\n\t\t\tif memory[pointer] == 0 {\n\t\t\t\tbraceCount := 1\n\t\t\t\tfor braceCount \u003e 0 {\n\t\t\t\t\ti++\n\t\t\t\t\tif code[i] == '[' {\n\t\t\t\t\t\tbraceCount++\n\t\t\t\t\t} else if code[i] == ']' {\n\t\t\t\t\t\tbraceCount--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase ']':\n\t\t\t// Jump backward to the matching '[' if the byte at the memory pointer is nonzero\n\t\t\tif memory[pointer] != 0 {\n\t\t\t\tbraceCount := 1\n\t\t\t\tfor braceCount \u003e 0 {\n\t\t\t\t\ti--\n\t\t\t\t\tif code[i] == ']' {\n\t\t\t\t\t\tbraceCount++\n\t\t\t\t\t} else if code[i] == '[' {\n\t\t\t\t\t\tbraceCount--\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ti-- // Move back one more to compensate for the upcoming increment in the loop\n\t\t\t}\n\t\t}\n\t}\n\treturn buf.String()\n}\n" + }, + { + "name": "bf_test.gno", + "body": "package bf\n\nimport \"testing\"\n\nfunc TestExecuteBrainfuck(t *testing.T) {\n\ttestCases := []struct {\n\t\tname string\n\t\tcode string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"hello\",\n\t\t\tcode: \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\",\n\t\t\texpected: \"Hello World\",\n\t\t},\n\t\t{\n\t\t\tname: \"increment\",\n\t\t\tcode: \"+++++ +++++ [ \u003e +++++ ++ \u003c - ] \u003e +++++ .\",\n\t\t\texpected: \"K\",\n\t\t},\n\t\t// Add more test cases as needed\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tresult := Execute(tc.code)\n\t\t\tif result != tc.expected {\n\t\t\t\tt.Errorf(\"Expected output: %s, but got: %s\", tc.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "doc.gno", + "body": "// Package bf implements a minimalist Brainfuck virtual machine in Gno.\n//\n// Brainfuck is an esoteric programming language known for its simplicity and minimalistic design.\n// It operates on an array of memory cells, with a memory pointer that can move left or right.\n// The language consists of eight commands: \u003e \u003c + - . , [ ].\n//\n// Usage:\n// To execute Brainfuck code, use the Execute function and provide the code as a string.\n//\n//\tcode := \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\"\n//\toutput := bf.Execute(code)\n//\n// Note:\n// This implementation is a minimalist version and may not handle all edge cases or advanced features of the Brainfuck language.\n//\n// Reference:\n// For more information on Brainfuck, refer to the Wikipedia page: https://en.wikipedia.org/wiki/Brainfuck\npackage bf // import \"gno.land/p/demo/bf\"\n" + }, + { + "name": "run.gno", + "body": "package bf\n\n// for `gno run`\nfunc main() {\n\tcode := \"++++++++++[\u003e+++++++\u003e++++++++++\u003e+++\u003e+\u003c\u003c\u003c\u003c-]\u003e++.\u003e+.+++++++..+++.\u003e++.\u003c\u003c+++++++++++++++.\u003e.+++.------.--------.\"\n\t// TODO: code = os.Args...\n\tExecute(code)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "brtVaBzxy+XIFlpAknALxFDpVkYObRCsPxTPUYWaQ5o4t4CKJ0345WfWqyQgS+NDKp5Q1dMdlPaXfGDeIB4hDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "mux", + "path": "gno.land/p/demo/mux", + "files": [ + { + "name": "doc.gno", + "body": "// 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": "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/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\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/demo/uassert\"\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Xpeq9miYbIIE4i17e4aqdooR/7E6jcrUacKOokQF7t6KvXYL2KhCJVaJ+obKlYZHbNdxtlMP1acmY9lnqftdCQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "blog", + "path": "gno.land/p/demo/blog", + "files": [ + { + "name": "blog.gno", + "body": "package blog\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\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, req *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\tres.Write(\"\u003cdiv class='columns-3'\u003e\")\n\tb.PostsPublished.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tpost := value.(*Post)\n\t\tres.Write(post.RenderListItem())\n\t\treturn false\n\t})\n\tres.Write(\"\u003c/div\u003e\")\n\n\t// FIXME: tag list/cloud.\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 std.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 std.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 std.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 := \"\u003cdiv\u003e\\n\\n\"\n\toutput += ufmt.Sprintf(\"### [%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\toutput += \"\u003c/div\u003e\"\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 std.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": "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.Replace(title, \" \", \"\", -1)\n}\n\nfunc getPublishedKey(t time.Time) string {\n\treturn t.Format(time.RFC3339)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "7mAm8nEW1FqGCbyQKvplJXDdjYBQbkxjKYULZo2x9gLA3a9Oe3PEuRJiJFjxkwLq80CUGZIWqmAujpOHO26LBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "btree", + "path": "gno.land/p/demo/btree", + "files": [ + { + "name": "btree.gno", + "body": "//////////\n//\n// Copyright 2014 Google Inc.\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use this file except in compliance with the License.\n//\n// Copyright 2024 New Tendermint\n//\n// This Gno port of the original Go BTree is substantially rewritten/reimplemented\n// from the original, primarily for clarity of code, clarity of documentation,\n// and for compatibility with Gno.\n//\n// Authors:\n// Original version authors -- https://github.com/google/btree/graphs/contributors\n// Kirk Haines \u003cwyhaines@gmail.com\u003e\n//\n//////////\n\n// Package btree implements in-memory B-Trees of arbitrary degree.\n//\n// It has a flatter structure than an equivalent red-black or other binary tree,\n// which may yield better memory usage and/or performance.\npackage btree\n\nimport \"sort\"\n\n//////////\n//\n// Types\n//\n//////////\n\n// BTreeOption is a function interface for setting options on a btree with `New()`.\ntype BTreeOption func(*BTree)\n\n// BTree is an implementation of a B-Tree.\n//\n// BTree stores Record instances in an ordered structure, allowing easy insertion,\n// removal, and iteration.\ntype BTree struct {\n\tdegree int\n\tlength int\n\troot *node\n\tcowCtx *copyOnWriteContext\n}\n\n//\tAny type that implements this interface can be stored in the BTree. This allows considerable\n//\n// flexiblity in storage within the BTree.\ntype Record interface {\n\t// Less compares self to `than`, returning true if self is less than `than`\n\tLess(than Record) bool\n}\n\n// records is the storage within a node. It is expressed as a slice of Record, where a Record\n// is any struct that implements the Record interface.\ntype records []Record\n\n// node is an internal node in a tree.\n//\n// It must at all times maintain on of the two conditions:\n// - len(children) == 0, len(records) unconstrained\n// - len(children) == len(records) + 1\ntype node struct {\n\trecords records\n\tchildren children\n\tcowCtx *copyOnWriteContext\n}\n\n// children is the list of child nodes below the current node. It is a slice of nodes.\ntype children []*node\n\n// FreeNodeList represents a slice of nodes which are available for reuse. The default\n// behavior of New() is for each BTree instance to have its own FreeNodeList. However,\n// it is possible for multiple instances of BTree to share the same tree. If one uses\n// New(WithFreeNodeList()) to create a tree, one may pass an existing FreeNodeList, allowing\n// multiple trees to use a single list. In an application with multiple trees, it might\n// be more efficient to allocate a single FreeNodeList with a significant initial capacity,\n// and then have all of the trees use that same large FreeNodeList.\ntype FreeNodeList struct {\n\tnodes []*node\n}\n\n// copyOnWriteContext manages node ownership and ensures that cloned trees\n// maintain isolation from each other when a node is changed.\n//\n// Ownership Rules:\n// - Each node is associated with a specific copyOnWriteContext.\n// - A tree can modify a node directly only if the tree's context matches the node's context.\n// - If a tree attempts to modify a node with a different context, it must create a\n// new, writable copy of that node (i.e., perform a clone) before making changes.\n//\n// Write Operation Invariant:\n// - During any write operation, the current node being modified must have the same\n// context as the tree requesting the write.\n// - To maintain this invariant, before descending into a child node, the system checks\n// if the child’s context matches the tree's context.\n// - If the contexts match, the node can be modified in place.\n// - If the contexts do not match, a mutable copy of the child node is created with the\n// correct context before proceeding.\n//\n// Practical Implications:\n// - The node currently being modified inherits the requesting tree's context, allowing\n// in-place modifications.\n// - Child nodes may initially have different contexts. Before any modification, these\n// children are copied to ensure they share the correct context, enabling safe and\n// isolated updates without affecting other trees that might be referencing the original nodes.\n//\n// Example Usage:\n// When a tree performs a write operation (e.g., inserting or deleting a node), it uses\n// its copyOnWriteContext to determine whether it can modify nodes directly or needs to\n// create copies. This mechanism ensures that trees can share nodes efficiently while\n// maintaining data integrity.\ntype copyOnWriteContext struct {\n\tnodes *FreeNodeList\n}\n\n// Record implements an interface with a single function, Less. Any type that implements\n// RecordIterator allows callers of all of the iteration functions for the BTree\n// to evaluate an element of the tree as it is traversed. The function will receive\n// a stored element from the tree. The function must return either a true or a false value.\n// True indicates that iteration should continue, while false indicates that it should halt.\ntype RecordIterator func(i Record) bool\n\n//////////\n//\n// Functions\n//\n//////////\n\n// NewFreeNodeList creates a new free list.\n// size is the maximum size of the returned free list.\nfunc NewFreeNodeList(size int) *FreeNodeList {\n\treturn \u0026FreeNodeList{nodes: make([]*node, 0, size)}\n}\n\nfunc (freeList *FreeNodeList) newNode() (nodeInstance *node) {\n\tindex := len(freeList.nodes) - 1\n\tif index \u003c 0 {\n\t\treturn new(node)\n\t}\n\tnodeInstance = freeList.nodes[index]\n\tfreeList.nodes[index] = nil\n\tfreeList.nodes = freeList.nodes[:index]\n\n\treturn nodeInstance\n}\n\n// freeNode adds the given node to the list, returning true if it was added\n// and false if it was discarded.\n\nfunc (freeList *FreeNodeList) freeNode(nodeInstance *node) (nodeWasAdded bool) {\n\tif len(freeList.nodes) \u003c cap(freeList.nodes) {\n\t\tfreeList.nodes = append(freeList.nodes, nodeInstance)\n\t\tnodeWasAdded = true\n\t}\n\treturn\n}\n\n// A default size for the free node list. We might want to run some benchmarks to see if\n// there are any pros or cons to this size versus other sizes. This seems to be a reasonable\n// compromise to reduce GC pressure by reusing nodes where possible, without stacking up too\n// much baggage in a given tree.\nconst DefaultFreeNodeListSize = 32\n\n// WithDegree sets the degree of the B-Tree.\nfunc WithDegree(degree int) BTreeOption {\n\treturn func(bt *BTree) {\n\t\tif degree \u003c= 1 {\n\t\t\tpanic(\"Degrees less than 1 do not make any sense for a BTree. Please provide a degree of 1 or greater.\")\n\t\t}\n\t\tbt.degree = degree\n\t}\n}\n\n// WithFreeNodeList sets a custom free node list for the B-Tree.\nfunc WithFreeNodeList(freeList *FreeNodeList) BTreeOption {\n\treturn func(bt *BTree) {\n\t\tbt.cowCtx = \u0026copyOnWriteContext{nodes: freeList}\n\t}\n}\n\n// New creates a new B-Tree with optional configurations. If configuration is not provided,\n// it will default to 16 element nodes. Degree may not be less than 1 (which effectively\n// makes the tree into a binary tree).\n//\n// `New(WithDegree(2))`, for example, will create a 2-3-4 tree (each node contains 1-3 records\n// and 2-4 children).\n//\n// `New(WithFreeNodeList(NewFreeNodeList(64)))` will create a tree with a degree of 16, and\n// with a free node list with a size of 64.\nfunc New(options ...BTreeOption) *BTree {\n\tbtree := \u0026BTree{\n\t\tdegree: 16, // default degree\n\t\tcowCtx: \u0026copyOnWriteContext{nodes: NewFreeNodeList(DefaultFreeNodeListSize)},\n\t}\n\tfor _, opt := range options {\n\t\topt(btree)\n\t}\n\treturn btree\n}\n\n// insertAt inserts a value into the given index, pushing all subsequent values\n// forward.\nfunc (recordsSlice *records) insertAt(index int, newRecord Record) {\n\toriginalLength := len(*recordsSlice)\n\n\t// Extend the slice by one element\n\t*recordsSlice = append(*recordsSlice, nil)\n\n\t// Move elements from the end to avoid overwriting during the copy\n\t// TODO: Make this work with slice appends, instead. It should be faster?\n\tif index \u003c originalLength {\n\t\tfor position := originalLength; position \u003e index; position-- {\n\t\t\t(*recordsSlice)[position] = (*recordsSlice)[position-1]\n\t\t}\n\t}\n\n\t// Insert the new record\n\t(*recordsSlice)[index] = newRecord\n}\n\n// removeAt removes a Record from the records slice at the specified index.\n// It shifts subsequent records to fill the gap and returns the removed Record.\nfunc (recordSlicePointer *records) removeAt(index int) Record {\n\trecordSlice := *recordSlicePointer\n\tremovedRecord := recordSlice[index]\n\tcopy(recordSlice[index:], recordSlice[index+1:])\n\trecordSlice[len(recordSlice)-1] = nil\n\t*recordSlicePointer = recordSlice[:len(recordSlice)-1]\n\n\treturn removedRecord\n}\n\n// Pop removes and returns the last Record from the records slice.\n// It also clears the reference to the removed Record to aid garbage collection.\nfunc (r *records) pop() Record {\n\trecordSlice := *r\n\tlastIndex := len(recordSlice) - 1\n\tremovedRecord := recordSlice[lastIndex]\n\trecordSlice[lastIndex] = nil\n\t*r = recordSlice[:lastIndex]\n\treturn removedRecord\n}\n\n// This slice is intended only as a supply of records for the truncate function\n// that follows, and it should not be changed or altered.\nvar emptyRecords = make(records, 32)\n\n// truncate reduces the length of the slice to the specified index,\n// and clears the elements beyond that index to prevent memory leaks.\n// The index must be less than or equal to the current length of the slice.\nfunc (originalSlice *records) truncate(index int) {\n\t// Split the slice into the part to keep and the part to clear.\n\trecordsToKeep := (*originalSlice)[:index]\n\trecordsToClear := (*originalSlice)[index:]\n\n\t// Update the original slice to only contain the records to keep.\n\t*originalSlice = recordsToKeep\n\n\t// Clear the memory of the part that was truncated.\n\tfor len(recordsToClear) \u003e 0 {\n\t\t// Copy empty values from `emptyRecords` to the recordsToClear slice.\n\t\t// This effectively \"clears\" the memory by overwriting elements.\n\t\tnumCleared := copy(recordsToClear, emptyRecords)\n\t\trecordsToClear = recordsToClear[numCleared:]\n\t}\n}\n\n// Find determines the appropriate index at which a given Record should be inserted\n// into the sorted records slice. If the Record already exists in the slice,\n// the method returns its index and sets found to true.\n//\n// Parameters:\n// - record: The Record to search for within the records slice.\n//\n// Returns:\n// - insertIndex: The index at which the Record should be inserted.\n// - found: A boolean indicating whether the Record already exists in the slice.\nfunc (recordsSlice records) find(record Record) (insertIndex int, found bool) {\n\ttotalRecords := len(recordsSlice)\n\n\t// Perform a binary search to find the insertion point for the record\n\tinsertionPoint := sort.Search(totalRecords, func(currentIndex int) bool {\n\t\treturn record.Less(recordsSlice[currentIndex])\n\t})\n\n\tif insertionPoint \u003e 0 {\n\t\tpreviousRecord := recordsSlice[insertionPoint-1]\n\n\t\tif !previousRecord.Less(record) {\n\t\t\treturn insertionPoint - 1, true\n\t\t}\n\t}\n\n\treturn insertionPoint, false\n}\n\n// insertAt inserts a value into the given index, pushing all subsequent values\n// forward.\nfunc (childSlice *children) insertAt(index int, n *node) {\n\toriginalLength := len(*childSlice)\n\n\t// Extend the slice by one element\n\t*childSlice = append(*childSlice, nil)\n\n\t// Move elements from the end to avoid overwriting during the copy\n\tif index \u003c originalLength {\n\t\tfor i := originalLength; i \u003e index; i-- {\n\t\t\t(*childSlice)[i] = (*childSlice)[i-1]\n\t\t}\n\t}\n\n\t// Insert the new record\n\t(*childSlice)[index] = n\n}\n\n// removeAt removes a Record from the records slice at the specified index.\n// It shifts subsequent records to fill the gap and returns the removed Record.\nfunc (childSlicePointer *children) removeAt(index int) *node {\n\tchildSlice := *childSlicePointer\n\tremovedChild := childSlice[index]\n\tcopy(childSlice[index:], childSlice[index+1:])\n\tchildSlice[len(childSlice)-1] = nil\n\t*childSlicePointer = childSlice[:len(childSlice)-1]\n\n\treturn removedChild\n}\n\n// Pop removes and returns the last Record from the records slice.\n// It also clears the reference to the removed Record to aid garbage collection.\nfunc (childSlicePointer *children) pop() *node {\n\tchildSlice := *childSlicePointer\n\tlastIndex := len(childSlice) - 1\n\tremovedChild := childSlice[lastIndex]\n\tchildSlice[lastIndex] = nil\n\t*childSlicePointer = childSlice[:lastIndex]\n\treturn removedChild\n}\n\n// This slice is intended only as a supply of records for the truncate function\n// that follows, and it should not be changed or altered.\nvar emptyChildren = make(children, 32)\n\n// truncate reduces the length of the slice to the specified index,\n// and clears the elements beyond that index to prevent memory leaks.\n// The index must be less than or equal to the current length of the slice.\nfunc (originalSlice *children) truncate(index int) {\n\t// Split the slice into the part to keep and the part to clear.\n\tchildrenToKeep := (*originalSlice)[:index]\n\tchildrenToClear := (*originalSlice)[index:]\n\n\t// Update the original slice to only contain the records to keep.\n\t*originalSlice = childrenToKeep\n\n\t// Clear the memory of the part that was truncated.\n\tfor len(childrenToClear) \u003e 0 {\n\t\t// Copy empty values from `emptyChildren` to the recordsToClear slice.\n\t\t// This effectively \"clears\" the memory by overwriting elements.\n\t\tnumCleared := copy(childrenToClear, emptyChildren)\n\n\t\t// Slice recordsToClear to exclude the elements that were just cleared.\n\t\tchildrenToClear = childrenToClear[numCleared:]\n\t}\n}\n\n// mutableFor creates a mutable copy of the node if the current node does not\n// already belong to the provided copy-on-write context (COW). If the node is\n// already associated with the given COW context, it returns the current node.\n//\n// Parameters:\n// - cowCtx: The copy-on-write context that should own the returned node.\n//\n// Returns:\n// - A pointer to the mutable node associated with the given COW context.\n//\n// If the current node belongs to a different COW context, this function:\n// - Allocates a new node using the provided context.\n// - Copies the node’s records and children slices into the newly allocated node.\n// - Returns the new node which is now owned by the given COW context.\nfunc (n *node) mutableFor(cowCtx *copyOnWriteContext) *node {\n\t// If the current node is already owned by the provided context, return it as-is.\n\tif n.cowCtx == cowCtx {\n\t\treturn n\n\t}\n\n\t// Create a new node in the provided context.\n\tnewNode := cowCtx.newNode()\n\n\t// Copy the records from the current node into the new node.\n\tnewNode.records = append(newNode.records[:0], n.records...)\n\n\t// Copy the children from the current node into the new node.\n\tnewNode.children = append(newNode.children[:0], n.children...)\n\n\treturn newNode\n}\n\n// mutableChild ensures that the child node at the given index is mutable and\n// associated with the same COW context as the parent node. If the child node\n// belongs to a different context, a copy of the child is created and stored in the\n// parent node.\n//\n// Parameters:\n// - i: The index of the child node to be made mutable.\n//\n// Returns:\n// - A pointer to the mutable child node.\nfunc (n *node) mutableChild(i int) *node {\n\t// Ensure that the child at index `i` is mutable and belongs to the same context as the parent.\n\tmutableChildNode := n.children[i].mutableFor(n.cowCtx)\n\t// Update the child node reference in the current node to the mutable version.\n\tn.children[i] = mutableChildNode\n\treturn mutableChildNode\n}\n\n// split splits the given node at the given index. The current node shrinks,\n// and this function returns the record that existed at that index and a new node\n// containing all records/children after it.\nfunc (n *node) split(i int) (Record, *node) {\n\trecord := n.records[i]\n\tnext := n.cowCtx.newNode()\n\tnext.records = append(next.records, n.records[i+1:]...)\n\tn.records.truncate(i)\n\tif len(n.children) \u003e 0 {\n\t\tnext.children = append(next.children, n.children[i+1:]...)\n\t\tn.children.truncate(i + 1)\n\t}\n\treturn record, next\n}\n\n// maybeSplitChild checks if a child should be split, and if so splits it.\n// Returns whether or not a split occurred.\nfunc (n *node) maybeSplitChild(i, maxRecords int) bool {\n\tif len(n.children[i].records) \u003c maxRecords {\n\t\treturn false\n\t}\n\tfirst := n.mutableChild(i)\n\trecord, second := first.split(maxRecords / 2)\n\tn.records.insertAt(i, record)\n\tn.children.insertAt(i+1, second)\n\treturn true\n}\n\n// insert adds a record to the subtree rooted at the current node, ensuring that no node in the subtree\n// exceeds the maximum number of allowed records (`maxRecords`). If an equivalent record is already present,\n// it replaces the existing one and returns it; otherwise, it returns nil.\n//\n// Parameters:\n// - record: The record to be inserted.\n// - maxRecords: The maximum number of records allowed per node.\n//\n// Returns:\n// - The record that was replaced if an equivalent record already existed, otherwise nil.\nfunc (n *node) insert(record Record, maxRecords int) Record {\n\t// Find the position where the new record should be inserted and check if an equivalent record already exists.\n\tinsertionIndex, recordExists := n.records.find(record)\n\n\tif recordExists {\n\t\t// If an equivalent record is found, replace it and return the old record.\n\t\texistingRecord := n.records[insertionIndex]\n\t\tn.records[insertionIndex] = record\n\t\treturn existingRecord\n\t}\n\n\t// If the current node is a leaf (has no children), insert the new record at the calculated index.\n\tif len(n.children) == 0 {\n\t\tn.records.insertAt(insertionIndex, record)\n\t\treturn nil\n\t}\n\n\t// Check if the child node at the insertion index needs to be split due to exceeding maxRecords.\n\tif n.maybeSplitChild(insertionIndex, maxRecords) {\n\t\t// If a split occurred, compare the new record with the record moved up to the current node.\n\t\tsplitRecord := n.records[insertionIndex]\n\t\tswitch {\n\t\tcase record.Less(splitRecord):\n\t\t\t// The new record belongs to the first (left) split node; no change to insertion index.\n\t\tcase splitRecord.Less(record):\n\t\t\t// The new record belongs to the second (right) split node; move the insertion index to the next position.\n\t\t\tinsertionIndex++\n\t\tdefault:\n\t\t\t// If the record is equivalent to the split record, replace it and return the old record.\n\t\t\texistingRecord := n.records[insertionIndex]\n\t\t\tn.records[insertionIndex] = record\n\t\t\treturn existingRecord\n\t\t}\n\t}\n\n\t// Recursively insert the record into the appropriate child node, now guaranteed to have space.\n\treturn n.mutableChild(insertionIndex).insert(record, maxRecords)\n}\n\n// get finds the given key in the subtree and returns it.\nfunc (n *node) get(key Record) Record {\n\ti, found := n.records.find(key)\n\tif found {\n\t\treturn n.records[i]\n\t} else if len(n.children) \u003e 0 {\n\t\treturn n.children[i].get(key)\n\t}\n\treturn nil\n}\n\n// min returns the first record in the subtree.\nfunc min(n *node) Record {\n\tif n == nil {\n\t\treturn nil\n\t}\n\tfor len(n.children) \u003e 0 {\n\t\tn = n.children[0]\n\t}\n\tif len(n.records) == 0 {\n\t\treturn nil\n\t}\n\treturn n.records[0]\n}\n\n// max returns the last record in the subtree.\nfunc max(n *node) Record {\n\tif n == nil {\n\t\treturn nil\n\t}\n\tfor len(n.children) \u003e 0 {\n\t\tn = n.children[len(n.children)-1]\n\t}\n\tif len(n.records) == 0 {\n\t\treturn nil\n\t}\n\treturn n.records[len(n.records)-1]\n}\n\n// toRemove details what record to remove in a node.remove call.\ntype toRemove int\n\nconst (\n\tremoveRecord toRemove = iota // removes the given record\n\tremoveMin // removes smallest record in the subtree\n\tremoveMax // removes largest record in the subtree\n)\n\n// remove removes a record from the subtree rooted at the current node.\n//\n// Parameters:\n// - record: The record to be removed (can be nil when the removal type indicates min or max).\n// - minRecords: The minimum number of records a node should have after removal.\n// - typ: The type of removal operation to perform (removeMin, removeMax, or removeRecord).\n//\n// Returns:\n// - The record that was removed, or nil if no such record was found.\nfunc (n *node) remove(record Record, minRecords int, removalType toRemove) Record {\n\tvar targetIndex int\n\tvar recordFound bool\n\n\t// Determine the index of the record to remove based on the removal type.\n\tswitch removalType {\n\tcase removeMax:\n\t\t// If this node is a leaf, remove and return the last record.\n\t\tif len(n.children) == 0 {\n\t\t\treturn n.records.pop()\n\t\t}\n\t\ttargetIndex = len(n.records) // The last record index for removing max.\n\n\tcase removeMin:\n\t\t// If this node is a leaf, remove and return the first record.\n\t\tif len(n.children) == 0 {\n\t\t\treturn n.records.removeAt(0)\n\t\t}\n\t\ttargetIndex = 0 // The first record index for removing min.\n\n\tcase removeRecord:\n\t\t// Locate the index of the record to be removed.\n\t\ttargetIndex, recordFound = n.records.find(record)\n\t\tif len(n.children) == 0 {\n\t\t\tif recordFound {\n\t\t\t\treturn n.records.removeAt(targetIndex)\n\t\t\t}\n\t\t\treturn nil // The record was not found in the leaf node.\n\t\t}\n\n\tdefault:\n\t\tpanic(\"invalid removal type\")\n\t}\n\n\t// If the current node has children, handle the removal recursively.\n\tif len(n.children[targetIndex].records) \u003c= minRecords {\n\t\t// If the target child node has too few records, grow it before proceeding with removal.\n\t\treturn n.growChildAndRemove(targetIndex, record, minRecords, removalType)\n\t}\n\n\t// Get a mutable reference to the child node at the target index.\n\ttargetChild := n.mutableChild(targetIndex)\n\n\t// If the record to be removed was found in the current node:\n\tif recordFound {\n\t\t// Replace the current record with its predecessor from the child node, and return the removed record.\n\t\treplacedRecord := n.records[targetIndex]\n\t\tn.records[targetIndex] = targetChild.remove(nil, minRecords, removeMax)\n\t\treturn replacedRecord\n\t}\n\n\t// Recursively remove the record from the child node.\n\treturn targetChild.remove(record, minRecords, removalType)\n}\n\n// growChildAndRemove grows child 'i' to make sure it's possible to remove an\n// record from it while keeping it at minRecords, then calls remove to actually\n// remove it.\n//\n// Most documentation says we have to do two sets of special casing:\n// 1. record is in this node\n// 2. record is in child\n//\n// In both cases, we need to handle the two subcases:\n//\n//\tA) node has enough values that it can spare one\n//\tB) node doesn't have enough values\n//\n// For the latter, we have to check:\n//\n//\ta) left sibling has node to spare\n//\tb) right sibling has node to spare\n//\tc) we must merge\n//\n// To simplify our code here, we handle cases #1 and #2 the same:\n// If a node doesn't have enough records, we make sure it does (using a,b,c).\n// We then simply redo our remove call, and the second time (regardless of\n// whether we're in case 1 or 2), we'll have enough records and can guarantee\n// that we hit case A.\nfunc (n *node) growChildAndRemove(i int, record Record, minRecords int, typ toRemove) Record {\n\tif i \u003e 0 \u0026\u0026 len(n.children[i-1].records) \u003e minRecords {\n\t\t// Steal from left child\n\t\tchild := n.mutableChild(i)\n\t\tstealFrom := n.mutableChild(i - 1)\n\t\tstolenRecord := stealFrom.records.pop()\n\t\tchild.records.insertAt(0, n.records[i-1])\n\t\tn.records[i-1] = stolenRecord\n\t\tif len(stealFrom.children) \u003e 0 {\n\t\t\tchild.children.insertAt(0, stealFrom.children.pop())\n\t\t}\n\t} else if i \u003c len(n.records) \u0026\u0026 len(n.children[i+1].records) \u003e minRecords {\n\t\t// steal from right child\n\t\tchild := n.mutableChild(i)\n\t\tstealFrom := n.mutableChild(i + 1)\n\t\tstolenRecord := stealFrom.records.removeAt(0)\n\t\tchild.records = append(child.records, n.records[i])\n\t\tn.records[i] = stolenRecord\n\t\tif len(stealFrom.children) \u003e 0 {\n\t\t\tchild.children = append(child.children, stealFrom.children.removeAt(0))\n\t\t}\n\t} else {\n\t\tif i \u003e= len(n.records) {\n\t\t\ti--\n\t\t}\n\t\tchild := n.mutableChild(i)\n\t\t// merge with right child\n\t\tmergeRecord := n.records.removeAt(i)\n\t\tmergeChild := n.children.removeAt(i + 1).mutableFor(n.cowCtx)\n\t\tchild.records = append(child.records, mergeRecord)\n\t\tchild.records = append(child.records, mergeChild.records...)\n\t\tchild.children = append(child.children, mergeChild.children...)\n\t\tn.cowCtx.freeNode(mergeChild)\n\t}\n\treturn n.remove(record, minRecords, typ)\n}\n\ntype direction int\n\nconst (\n\tdescend = direction(-1)\n\tascend = direction(+1)\n)\n\n// iterate provides a simple method for iterating over elements in the tree.\n//\n// When ascending, the 'start' should be less than 'stop' and when descending,\n// the 'start' should be greater than 'stop'. Setting 'includeStart' to true\n// will force the iterator to include the first record when it equals 'start',\n// thus creating a \"greaterOrEqual\" or \"lessThanEqual\" rather than just a\n// \"greaterThan\" or \"lessThan\" queries.\nfunc (n *node) iterate(dir direction, start, stop Record, includeStart bool, hit bool, iter RecordIterator) (bool, bool) {\n\tvar ok, found bool\n\tvar index int\n\tswitch dir {\n\tcase ascend:\n\t\tif start != nil {\n\t\t\tindex, _ = n.records.find(start)\n\t\t}\n\t\tfor i := index; i \u003c len(n.records); i++ {\n\t\t\tif len(n.children) \u003e 0 {\n\t\t\t\tif hit, ok = n.children[i].iterate(dir, start, stop, includeStart, hit, iter); !ok {\n\t\t\t\t\treturn hit, false\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !includeStart \u0026\u0026 !hit \u0026\u0026 start != nil \u0026\u0026 !start.Less(n.records[i]) {\n\t\t\t\thit = true\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\thit = true\n\t\t\tif stop != nil \u0026\u0026 !n.records[i].Less(stop) {\n\t\t\t\treturn hit, false\n\t\t\t}\n\t\t\tif !iter(n.records[i]) {\n\t\t\t\treturn hit, false\n\t\t\t}\n\t\t}\n\t\tif len(n.children) \u003e 0 {\n\t\t\tif hit, ok = n.children[len(n.children)-1].iterate(dir, start, stop, includeStart, hit, iter); !ok {\n\t\t\t\treturn hit, false\n\t\t\t}\n\t\t}\n\tcase descend:\n\t\tif start != nil {\n\t\t\tindex, found = n.records.find(start)\n\t\t\tif !found {\n\t\t\t\tindex = index - 1\n\t\t\t}\n\t\t} else {\n\t\t\tindex = len(n.records) - 1\n\t\t}\n\t\tfor i := index; i \u003e= 0; i-- {\n\t\t\tif start != nil \u0026\u0026 !n.records[i].Less(start) {\n\t\t\t\tif !includeStart || hit || start.Less(n.records[i]) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\tif len(n.children) \u003e 0 {\n\t\t\t\tif hit, ok = n.children[i+1].iterate(dir, start, stop, includeStart, hit, iter); !ok {\n\t\t\t\t\treturn hit, false\n\t\t\t\t}\n\t\t\t}\n\t\t\tif stop != nil \u0026\u0026 !stop.Less(n.records[i]) {\n\t\t\t\treturn hit, false //\tcontinue\n\t\t\t}\n\t\t\thit = true\n\t\t\tif !iter(n.records[i]) {\n\t\t\t\treturn hit, false\n\t\t\t}\n\t\t}\n\t\tif len(n.children) \u003e 0 {\n\t\t\tif hit, ok = n.children[0].iterate(dir, start, stop, includeStart, hit, iter); !ok {\n\t\t\t\treturn hit, false\n\t\t\t}\n\t\t}\n\t}\n\treturn hit, true\n}\n\nfunc (tree *BTree) Iterate(dir direction, start, stop Record, includeStart bool, hit bool, iter RecordIterator) (bool, bool) {\n\treturn tree.root.iterate(dir, start, stop, includeStart, hit, iter)\n}\n\n// Clone creates a new BTree instance that shares the current tree's structure using a copy-on-write (COW) approach.\n//\n// How Cloning Works:\n// - The cloned tree (`clonedTree`) shares the current tree’s nodes in a read-only state. This means that no additional memory\n// is allocated for shared nodes, and read operations on the cloned tree are as fast as on the original tree.\n// - When either the original tree (`t`) or the cloned tree (`clonedTree`) needs to perform a write operation (such as an insert, delete, etc.),\n// a new copy of the affected nodes is created on-demand. This ensures that modifications to one tree do not affect the other.\n//\n// Performance Implications:\n// - **Clone Creation:** The creation of a clone is inexpensive since it only involves copying references to the original tree's nodes\n// and creating new copy-on-write contexts.\n// - **Read Operations:** Reading from either the original tree or the cloned tree has no additional performance overhead compared to the original tree.\n// - **Write Operations:** The first write operation on either tree may experience a slight slow-down due to the allocation of new nodes,\n// but subsequent write operations will perform at the same speed as if the tree were not cloned.\n//\n// Returns:\n// - A new BTree instance (`clonedTree`) that shares the original tree's structure.\nfunc (t *BTree) Clone() *BTree {\n\t// Create two independent copy-on-write contexts, one for the original tree (`t`) and one for the cloned tree.\n\toriginalContext := *t.cowCtx\n\tclonedContext := *t.cowCtx\n\n\t// Create a shallow copy of the current tree, which will be the new cloned tree.\n\tclonedTree := *t\n\n\t// Assign the new contexts to their respective trees.\n\tt.cowCtx = \u0026originalContext\n\tclonedTree.cowCtx = \u0026clonedContext\n\n\treturn \u0026clonedTree\n}\n\n// maxRecords returns the max number of records to allow per node.\nfunc (t *BTree) maxRecords() int {\n\treturn t.degree*2 - 1\n}\n\n// minRecords returns the min number of records to allow per node (ignored for the\n// root node).\nfunc (t *BTree) minRecords() int {\n\treturn t.degree - 1\n}\n\nfunc (c *copyOnWriteContext) newNode() (n *node) {\n\tn = c.nodes.newNode()\n\tn.cowCtx = c\n\treturn\n}\n\ntype freeType int\n\nconst (\n\tftFreelistFull freeType = iota // node was freed (available for GC, not stored in nodes)\n\tftStored // node was stored in the nodes for later use\n\tftNotOwned // node was ignored by COW, since it's owned by another one\n)\n\n// freeNode frees a node within a given COW context, if it's owned by that\n// context. It returns what happened to the node (see freeType const\n// documentation).\nfunc (c *copyOnWriteContext) freeNode(n *node) freeType {\n\tif n.cowCtx == c {\n\t\t// clear to allow GC\n\t\tn.records.truncate(0)\n\t\tn.children.truncate(0)\n\t\tn.cowCtx = nil\n\t\tif c.nodes.freeNode(n) {\n\t\t\treturn ftStored\n\t\t} else {\n\t\t\treturn ftFreelistFull\n\t\t}\n\t} else {\n\t\treturn ftNotOwned\n\t}\n}\n\n// Insert adds the given record to the B-tree. If a record already exists in the tree with the same value,\n// it is replaced, and the old record is returned. Otherwise, it returns nil.\n//\n// Notes:\n// - The function panics if a nil record is provided as input.\n// - If the root node is empty, a new root node is created and the record is inserted.\n//\n// Parameters:\n// - record: The record to be inserted into the B-tree.\n//\n// Returns:\n// - The replaced record if an equivalent record already exists, or nil if no replacement occurred.\nfunc (t *BTree) Insert(record Record) Record {\n\tif record == nil {\n\t\tpanic(\"nil record cannot be added to BTree\")\n\t}\n\n\t// If the tree is empty (no root), create a new root node and insert the record.\n\tif t.root == nil {\n\t\tt.root = t.cowCtx.newNode()\n\t\tt.root.records = append(t.root.records, record)\n\t\tt.length++\n\t\treturn nil\n\t}\n\n\t// Ensure that the root node is mutable (associated with the current tree's copy-on-write context).\n\tt.root = t.root.mutableFor(t.cowCtx)\n\n\t// If the root node is full (contains the maximum number of records), split the root.\n\tif len(t.root.records) \u003e= t.maxRecords() {\n\t\t// Split the root node, promoting the middle record and creating a new child node.\n\t\tmiddleRecord, newChildNode := t.root.split(t.maxRecords() / 2)\n\n\t\t// Create a new root node to hold the promoted middle record.\n\t\toldRoot := t.root\n\t\tt.root = t.cowCtx.newNode()\n\t\tt.root.records = append(t.root.records, middleRecord)\n\t\tt.root.children = append(t.root.children, oldRoot, newChildNode)\n\t}\n\n\t// Insert the new record into the subtree rooted at the current root node.\n\treplacedRecord := t.root.insert(record, t.maxRecords())\n\n\t// If no record was replaced, increase the tree's length.\n\tif replacedRecord == nil {\n\t\tt.length++\n\t}\n\n\treturn replacedRecord\n}\n\n// Delete removes an record equal to the passed in record from the tree, returning\n// it. If no such record exists, returns nil.\nfunc (t *BTree) Delete(record Record) Record {\n\treturn t.deleteRecord(record, removeRecord)\n}\n\n// DeleteMin removes the smallest record in the tree and returns it.\n// If no such record exists, returns nil.\nfunc (t *BTree) DeleteMin() Record {\n\treturn t.deleteRecord(nil, removeMin)\n}\n\n// Shift is identical to DeleteMin. If the tree is thought of as an ordered list, then Shift()\n// removes the element at the start of the list, the smallest element, and returns it.\nfunc (t *BTree) Shift() Record {\n\treturn t.deleteRecord(nil, removeMin)\n}\n\n// DeleteMax removes the largest record in the tree and returns it.\n// If no such record exists, returns nil.\nfunc (t *BTree) DeleteMax() Record {\n\treturn t.deleteRecord(nil, removeMax)\n}\n\n// Pop is identical to DeleteMax. If the tree is thought of as an ordered list, then Shift()\n// removes the element at the end of the list, the largest element, and returns it.\nfunc (t *BTree) Pop() Record {\n\treturn t.deleteRecord(nil, removeMax)\n}\n\n// deleteRecord removes a record from the B-tree based on the specified removal type (removeMin, removeMax, or removeRecord).\n// It returns the removed record if it was found, or nil if no matching record was found.\n//\n// Parameters:\n// - record: The record to be removed (can be nil if the removal type indicates min or max).\n// - removalType: The type of removal operation to perform (removeMin, removeMax, or removeRecord).\n//\n// Returns:\n// - The removed record if it existed in the tree, or nil if it was not found.\nfunc (t *BTree) deleteRecord(record Record, removalType toRemove) Record {\n\t// If the tree is empty or the root has no records, return nil.\n\tif t.root == nil || len(t.root.records) == 0 {\n\t\treturn nil\n\t}\n\n\t// Ensure the root node is mutable (associated with the tree's copy-on-write context).\n\tt.root = t.root.mutableFor(t.cowCtx)\n\n\t// Attempt to remove the specified record from the root node.\n\tremovedRecord := t.root.remove(record, t.minRecords(), removalType)\n\n\t// Check if the root node has become empty but still has children.\n\t// In this case, the tree height should be reduced, making the first child the new root.\n\tif len(t.root.records) == 0 \u0026\u0026 len(t.root.children) \u003e 0 {\n\t\toldRoot := t.root\n\t\tt.root = t.root.children[0]\n\t\t// Free the old root node, as it is no longer needed.\n\t\tt.cowCtx.freeNode(oldRoot)\n\t}\n\n\t// If a record was successfully removed, decrease the tree's length.\n\tif removedRecord != nil {\n\t\tt.length--\n\t}\n\n\treturn removedRecord\n}\n\n// AscendRange calls the iterator for every value in the tree within the range\n// [greaterOrEqual, lessThan), until iterator returns false.\nfunc (t *BTree) AscendRange(greaterOrEqual, lessThan Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(ascend, greaterOrEqual, lessThan, true, false, iterator)\n}\n\n// AscendLessThan calls the iterator for every value in the tree within the range\n// [first, pivot), until iterator returns false.\nfunc (t *BTree) AscendLessThan(pivot Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(ascend, nil, pivot, false, false, iterator)\n}\n\n// AscendGreaterOrEqual calls the iterator for every value in the tree within\n// the range [pivot, last], until iterator returns false.\nfunc (t *BTree) AscendGreaterOrEqual(pivot Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(ascend, pivot, nil, true, false, iterator)\n}\n\n// Ascend calls the iterator for every value in the tree within the range\n// [first, last], until iterator returns false.\nfunc (t *BTree) Ascend(iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(ascend, nil, nil, false, false, iterator)\n}\n\n// DescendRange calls the iterator for every value in the tree within the range\n// [lessOrEqual, greaterThan), until iterator returns false.\nfunc (t *BTree) DescendRange(lessOrEqual, greaterThan Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(descend, lessOrEqual, greaterThan, true, false, iterator)\n}\n\n// DescendLessOrEqual calls the iterator for every value in the tree within the range\n// [pivot, first], until iterator returns false.\nfunc (t *BTree) DescendLessOrEqual(pivot Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(descend, pivot, nil, true, false, iterator)\n}\n\n// DescendGreaterThan calls the iterator for every value in the tree within\n// the range [last, pivot), until iterator returns false.\nfunc (t *BTree) DescendGreaterThan(pivot Record, iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(descend, nil, pivot, false, false, iterator)\n}\n\n// Descend calls the iterator for every value in the tree within the range\n// [last, first], until iterator returns false.\nfunc (t *BTree) Descend(iterator RecordIterator) {\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.iterate(descend, nil, nil, false, false, iterator)\n}\n\n// Get looks for the key record in the tree, returning it. It returns nil if\n// unable to find that record.\nfunc (t *BTree) Get(key Record) Record {\n\tif t.root == nil {\n\t\treturn nil\n\t}\n\treturn t.root.get(key)\n}\n\n// Min returns the smallest record in the tree, or nil if the tree is empty.\nfunc (t *BTree) Min() Record {\n\treturn min(t.root)\n}\n\n// Max returns the largest record in the tree, or nil if the tree is empty.\nfunc (t *BTree) Max() Record {\n\treturn max(t.root)\n}\n\n// Has returns true if the given key is in the tree.\nfunc (t *BTree) Has(key Record) bool {\n\treturn t.Get(key) != nil\n}\n\n// Len returns the number of records currently in the tree.\nfunc (t *BTree) Len() int {\n\treturn t.length\n}\n\n// Clear removes all elements from the B-tree.\n//\n// Parameters:\n// - addNodesToFreelist:\n// - If true, the tree's nodes are added to the freelist during the clearing process,\n// up to the freelist's capacity.\n// - If false, the root node is simply dereferenced, allowing Go's garbage collector\n// to reclaim the memory.\n//\n// Benefits:\n// - **Performance:**\n// - Significantly faster than deleting each element individually, as it avoids the overhead\n// of searching and updating the tree structure for each deletion.\n// - More efficient than creating a new tree, since it reuses existing nodes by adding them\n// to the freelist instead of discarding them to the garbage collector.\n//\n// Time Complexity:\n// - **O(1):**\n// - When `addNodesToFreelist` is false.\n// - When `addNodesToFreelist` is true but the freelist is already full.\n// - **O(freelist size):**\n// - When adding nodes to the freelist up to its capacity.\n// - **O(tree size):**\n// - When iterating through all nodes to add to the freelist, but none can be added due to\n// ownership by another tree.\n\nfunc (tree *BTree) Clear(addNodesToFreelist bool) {\n\tif tree.root != nil \u0026\u0026 addNodesToFreelist {\n\t\ttree.root.reset(tree.cowCtx)\n\t}\n\ttree.root = nil\n\ttree.length = 0\n}\n\n// reset adds all nodes in the current subtree to the freelist.\n//\n// The function operates recursively:\n// - It first attempts to reset all child nodes.\n// - If the freelist becomes full at any point, the process stops immediately.\n//\n// Parameters:\n// - copyOnWriteCtx: The copy-on-write context managing the freelist.\n//\n// Returns:\n// - true: Indicates that the parent node should continue attempting to reset its nodes.\n// - false: Indicates that the freelist is full and no further nodes should be added.\n//\n// Usage:\n// This method is called during the `Clear` operation of the B-tree to efficiently reuse\n// nodes by adding them to the freelist, thereby avoiding unnecessary allocations and reducing\n// garbage collection overhead.\nfunc (currentNode *node) reset(copyOnWriteCtx *copyOnWriteContext) bool {\n\t// Iterate through each child node and attempt to reset it.\n\tfor _, childNode := range currentNode.children {\n\t\t// If any child reset operation signals that the freelist is full, stop the process.\n\t\tif !childNode.reset(copyOnWriteCtx) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Attempt to add the current node to the freelist.\n\t// If the freelist is full after this operation, indicate to the parent to stop.\n\tfreelistStatus := copyOnWriteCtx.freeNode(currentNode)\n\treturn freelistStatus != ftFreelistFull\n}\n" + }, + { + "name": "btree_test.gno", + "body": "package btree\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"testing\"\n)\n\n// Content represents a key-value pair where the Key can be either an int or string\n// and the Value can be any type.\ntype Content struct {\n\tKey any\n\tValue any\n}\n\n// Less compares two Content records by their Keys.\n// The Key must be either an int or a string.\nfunc (c Content) Less(than Record) bool {\n\tother, ok := than.(Content)\n\tif !ok {\n\t\tpanic(\"cannot compare: incompatible types\")\n\t}\n\n\tswitch key := c.Key.(type) {\n\tcase int:\n\t\tswitch otherKey := other.Key.(type) {\n\t\tcase int:\n\t\t\treturn key \u003c otherKey\n\t\tcase string:\n\t\t\treturn true // ints are always less than strings\n\t\tdefault:\n\t\t\tpanic(\"unsupported key type: must be int or string\")\n\t\t}\n\tcase string:\n\t\tswitch otherKey := other.Key.(type) {\n\t\tcase int:\n\t\t\treturn false // strings are always greater than ints\n\t\tcase string:\n\t\t\treturn key \u003c otherKey\n\t\tdefault:\n\t\t\tpanic(\"unsupported key type: must be int or string\")\n\t\t}\n\tdefault:\n\t\tpanic(\"unsupported key type: must be int or string\")\n\t}\n}\n\ntype ContentSlice []Content\n\nfunc (s ContentSlice) Len() int {\n\treturn len(s)\n}\n\nfunc (s ContentSlice) Less(i, j int) bool {\n\treturn s[i].Less(s[j])\n}\n\nfunc (s ContentSlice) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\nfunc (s ContentSlice) Copy() ContentSlice {\n\tnewSlice := make(ContentSlice, len(s))\n\tcopy(newSlice, s)\n\treturn newSlice\n}\n\n// Ensure Content implements the Record interface.\nvar _ Record = Content{}\n\n// ****************************************************************************\n// Test helpers\n// ****************************************************************************\n\nfunc genericSeeding(tree *BTree, size int) *BTree {\n\tfor i := 0; i \u003c size; i++ {\n\t\ttree.Insert(Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)})\n\t}\n\treturn tree\n}\n\nfunc intSlicesCompare(left, right []int) int {\n\tif len(left) != len(right) {\n\t\tif len(left) \u003e len(right) {\n\t\t\treturn 1\n\t\t} else {\n\t\t\treturn -1\n\t\t}\n\t}\n\n\tfor position, leftInt := range left {\n\t\tif leftInt != right[position] {\n\t\t\tif leftInt \u003e right[position] {\n\t\t\t\treturn 1\n\t\t\t} else {\n\t\t\t\treturn -1\n\t\t\t}\n\t\t}\n\t}\n\n\treturn 0\n}\n\n// ****************************************************************************\n// Tests\n// ****************************************************************************\n\nfunc TestLen(t *testing.T) {\n\tlength := genericSeeding(New(WithDegree(10)), 7).Len()\n\tif length != 7 {\n\t\tt.Errorf(\"Length is incorrect. Expected 7, but got %d.\", length)\n\t}\n\n\tlength = genericSeeding(New(WithDegree(5)), 111).Len()\n\tif length != 111 {\n\t\tt.Errorf(\"Length is incorrect. Expected 111, but got %d.\", length)\n\t}\n\n\tlength = genericSeeding(New(WithDegree(30)), 123).Len()\n\tif length != 123 {\n\t\tt.Errorf(\"Length is incorrect. Expected 123, but got %d.\", length)\n\t}\n}\n\nfunc TestHas(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 40)\n\n\tif tree.Has(Content{Key: 7}) != true {\n\t\tt.Errorf(\"Has(7) reported false, but it should be true.\")\n\t}\n\tif tree.Has(Content{Key: 39}) != true {\n\t\tt.Errorf(\"Has(40) reported false, but it should be true.\")\n\t}\n\tif tree.Has(Content{Key: 1111}) == true {\n\t\tt.Errorf(\"Has(1111) reported true, but it should be false.\")\n\t}\n}\n\nfunc TestMin(t *testing.T) {\n\tmin := genericSeeding(New(WithDegree(10)), 53).Min().(Content)\n\n\tif min.Key != 0 {\n\t\tt.Errorf(\"Minimum should have been 0, but it was reported as %d.\", min)\n\t}\n}\n\nfunc TestMax(t *testing.T) {\n\tmax := genericSeeding(New(WithDegree(10)), 53).Max().(Content)\n\n\tif max.Key != 52 {\n\t\tt.Errorf(\"Maximum should have been 52, but it was reported as %d.\", max)\n\t}\n}\n\nfunc TestGet(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 40)\n\n\tif val := tree.Get(Content{Key: 7}); val != nil \u0026\u0026 val.(Content).Value != \"Value_7\" {\n\t\tt.Errorf(\"Get(7) should have returned 'Value_7', but it returned %v.\", val)\n\t}\n\tif val := tree.Get(Content{Key: 39}); val != nil \u0026\u0026 val.(Content).Value != \"Value_39\" {\n\t\tt.Errorf(\"Get(39) should have returned 'Value_39', but it returned %v.\", val)\n\t}\n\tif val := tree.Get(Content{Key: 1111}); val != nil {\n\t\tt.Errorf(\"Get(1111) returned %v, but it should be nil.\", val)\n\t}\n}\n\nfunc TestDescend(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 5)\n\n\texpected := []int{4, 3, 2, 1, 0}\n\tfound := []int{}\n\n\ttree.Descend(func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"Descend returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestDescendGreaterThan(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{9, 8, 7, 6, 5}\n\tfound := []int{}\n\n\ttree.DescendGreaterThan(Content{Key: 4}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"DescendGreaterThan returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestDescendLessOrEqual(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{4, 3, 2, 1, 0}\n\tfound := []int{}\n\n\ttree.DescendLessOrEqual(Content{Key: 4}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"DescendLessOrEqual returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestDescendRange(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{6, 5, 4, 3, 2}\n\tfound := []int{}\n\n\ttree.DescendRange(Content{Key: 6}, Content{Key: 1}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"DescendRange returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestAscend(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 5)\n\n\texpected := []int{0, 1, 2, 3, 4}\n\tfound := []int{}\n\n\ttree.Ascend(func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"Ascend returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestAscendGreaterOrEqual(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{5, 6, 7, 8, 9}\n\tfound := []int{}\n\n\ttree.AscendGreaterOrEqual(Content{Key: 5}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"AscendGreaterOrEqual returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestAscendLessThan(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{0, 1, 2, 3, 4}\n\tfound := []int{}\n\n\ttree.AscendLessThan(Content{Key: 5}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"AscendLessThan returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestAscendRange(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(10)), 10)\n\n\texpected := []int{2, 3, 4, 5, 6}\n\tfound := []int{}\n\n\ttree.AscendRange(Content{Key: 2}, Content{Key: 7}, func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tfound = append(found, record.Key.(int))\n\t\treturn true\n\t})\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"AscendRange returned the wrong sequence. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestDeleteMin(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(3)), 100)\n\n\texpected := []int{0, 1, 2, 3, 4}\n\tfound := []int{}\n\n\tfound = append(found, tree.DeleteMin().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMin().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMin().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMin().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMin().(Content).Key.(int))\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestShift(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(3)), 100)\n\n\texpected := []int{0, 1, 2, 3, 4}\n\tfound := []int{}\n\n\tfound = append(found, tree.Shift().(Content).Key.(int))\n\tfound = append(found, tree.Shift().(Content).Key.(int))\n\tfound = append(found, tree.Shift().(Content).Key.(int))\n\tfound = append(found, tree.Shift().(Content).Key.(int))\n\tfound = append(found, tree.Shift().(Content).Key.(int))\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"5 rounds of Shift returned the wrong elements. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestDeleteMax(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(3)), 100)\n\n\texpected := []int{99, 98, 97, 96, 95}\n\tfound := []int{}\n\n\tfound = append(found, tree.DeleteMax().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMax().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMax().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMax().(Content).Key.(int))\n\tfound = append(found, tree.DeleteMax().(Content).Key.(int))\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"5 rounds of DeleteMax returned the wrong elements. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestPop(t *testing.T) {\n\ttree := genericSeeding(New(WithDegree(3)), 100)\n\n\texpected := []int{99, 98, 97, 96, 95}\n\tfound := []int{}\n\n\tfound = append(found, tree.Pop().(Content).Key.(int))\n\tfound = append(found, tree.Pop().(Content).Key.(int))\n\tfound = append(found, tree.Pop().(Content).Key.(int))\n\tfound = append(found, tree.Pop().(Content).Key.(int))\n\tfound = append(found, tree.Pop().(Content).Key.(int))\n\n\tif intSlicesCompare(expected, found) != 0 {\n\t\tt.Errorf(\"5 rounds of Pop returned the wrong elements. Expected %v, but got %v.\", expected, found)\n\t}\n}\n\nfunc TestInsertGet(t *testing.T) {\n\ttree := New(WithDegree(4))\n\n\texpected := []Content{}\n\n\tfor count := 0; count \u003c 20; count++ {\n\t\tvalue := fmt.Sprintf(\"Value_%d\", count)\n\t\ttree.Insert(Content{Key: count, Value: value})\n\t\texpected = append(expected, Content{Key: count, Value: value})\n\t}\n\n\tfor count := 0; count \u003c 20; count++ {\n\t\tval := tree.Get(Content{Key: count})\n\t\tif val == nil || val.(Content) != expected[count] {\n\t\t\tt.Errorf(\"Insert/Get doesn't appear to be working. Expected to retrieve %v with key %d, but got %v.\", expected[count], count, val)\n\t\t}\n\t}\n}\n\nfunc TestClone(t *testing.T) {\n\t// Implement the clone test\n}\n\n// ***** The following tests are functional or stress testing type tests.\n\nfunc TestBTree(t *testing.T) {\n\t// Create a B-Tree of degree 3\n\ttree := New(WithDegree(3))\n\n\t// insertData := []Content{}\n\tvar insertData ContentSlice\n\n\t// Insert integer keys\n\tintKeys := []int{10, 20, 5, 6, 12, 30, 7, 17}\n\tfor _, key := range intKeys {\n\t\tcontent := Content{Key: key, Value: fmt.Sprintf(\"Value_%d\", key)}\n\t\tinsertData = append(insertData, content)\n\t\tresult := tree.Insert(content)\n\t\tif result != nil {\n\t\t\tt.Errorf(\"**** Already in the tree? %v\", result)\n\t\t}\n\t}\n\n\t// Insert string keys\n\tstringKeys := []string{\"apple\", \"banana\", \"cherry\", \"date\", \"fig\", \"grape\"}\n\tfor _, key := range stringKeys {\n\t\tcontent := Content{Key: key, Value: fmt.Sprintf(\"Fruit_%s\", key)}\n\t\tinsertData = append(insertData, content)\n\t\ttree.Insert(content)\n\t}\n\n\tif tree.Len() != 14 {\n\t\tt.Errorf(\"Tree length wrong. Expected 14 but got %d\", tree.Len())\n\t}\n\n\t// Search for existing and non-existing keys\n\tsearchTests := []struct {\n\t\ttest Content\n\t\texpected bool\n\t}{\n\t\t{Content{Key: 10, Value: \"Value_10\"}, true},\n\t\t{Content{Key: 15, Value: \"\"}, false},\n\t\t{Content{Key: \"banana\", Value: \"Fruit_banana\"}, true},\n\t\t{Content{Key: \"kiwi\", Value: \"\"}, false},\n\t}\n\n\tt.Logf(\"Search Tests:\\n\")\n\tfor _, test := range searchTests {\n\t\tval := tree.Get(test.test)\n\n\t\tif test.expected {\n\t\t\tif val != nil \u0026\u0026 val.(Content).Value == test.test.Value {\n\t\t\t\tt.Logf(\"Found expected key:value %v:%v\", test.test.Key, test.test.Value)\n\t\t\t} else {\n\t\t\t\tif val == nil {\n\t\t\t\t\tt.Logf(\"Didn't find %v, but expected\", test.test.Key)\n\t\t\t\t} else {\n\t\t\t\t\tt.Errorf(\"Expected key %v:%v, but found %v:%v.\", test.test.Key, test.test.Value, val.(Content).Key, val.(Content).Value)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif val != nil {\n\t\t\t\tt.Errorf(\"Did not expect key %v, but found key:value %v:%v\", test.test.Key, val.(Content).Key, val.(Content).Value)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"Didn't find %v, but wasn't expected\", test.test.Key)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Iterate in order\n\tt.Logf(\"\\nIn-order Iteration:\\n\")\n\tpos := 0\n\n\tif tree.Len() != 14 {\n\t\tt.Errorf(\"Tree length wrong. Expected 14 but got %d\", tree.Len())\n\t}\n\n\tsortedInsertData := insertData.Copy()\n\tsort.Sort(sortedInsertData)\n\n\tt.Logf(\"Insert Data Length: %d\", len(insertData))\n\tt.Logf(\"Sorted Data Length: %d\", len(sortedInsertData))\n\tt.Logf(\"Tree Length: %d\", tree.Len())\n\n\ttree.Ascend(func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tt.Logf(\"Key:Value == %v:%v\", record.Key, record.Value)\n\t\tif record.Key != sortedInsertData[pos].Key {\n\t\t\tt.Errorf(\"Out of order! Expected %v, but got %v\", sortedInsertData[pos].Key, record.Key)\n\t\t}\n\t\tpos++\n\t\treturn true\n\t})\n\t// // Reverse Iterate\n\tt.Logf(\"\\nReverse-order Iteration:\\n\")\n\tpos = len(sortedInsertData) - 1\n\n\ttree.Descend(func(_record Record) bool {\n\t\trecord := _record.(Content)\n\t\tt.Logf(\"Key:Value == %v:%v\", record.Key, record.Value)\n\t\tif record.Key != sortedInsertData[pos].Key {\n\t\t\tt.Errorf(\"Out of order! Expected %v, but got %v\", sortedInsertData[pos].Key, record.Key)\n\t\t}\n\t\tpos--\n\t\treturn true\n\t})\n\n\tdeleteTests := []Content{\n\t\t{Key: 10, Value: \"Value_10\"},\n\t\t{Key: 15, Value: \"\"},\n\t\t{Key: \"banana\", Value: \"Fruit_banana\"},\n\t\t{Key: \"kiwi\", Value: \"\"},\n\t}\n\tfor _, test := range deleteTests {\n\t\tfmt.Printf(\"\\nDeleting %+v\\n\", test)\n\t\ttree.Delete(test)\n\t}\n\n\tif tree.Len() != 12 {\n\t\tt.Errorf(\"Tree length wrong. Expected 12 but got %d\", tree.Len())\n\t}\n\n\tfor _, test := range deleteTests {\n\t\tval := tree.Get(test)\n\t\tif val != nil {\n\t\t\tt.Errorf(\"Did not expect key %v, but found key:value %v:%v\", test.Key, val.(Content).Key, val.(Content).Value)\n\t\t} else {\n\t\t\tt.Logf(\"Didn't find %v, but wasn't expected\", test.Key)\n\t\t}\n\t}\n}\n\n// Write a test that populates a large B-Tree with 1000 records.\n// It should then `Clone` the tree, make some changes to both the original and the clone,\n// And then clone the clone, and make some changes to all three trees, and then check that the changes are isolated\n// to the tree they were made in.\nfunc TestBTreeCloneIsolation(t *testing.T) {\n\tt.Logf(\"Creating B-Tree of degree 10 with 1000 records\\n\")\n\tsize := 1000\n\ttree := genericSeeding(New(WithDegree(10)), size)\n\n\t// Clone the tree\n\tt.Logf(\"Cloning the tree\\n\")\n\tclone := tree.Clone()\n\n\t// Make some changes to the original and the clone\n\tt.Logf(\"Making changes to the original and the clone\\n\")\n\tfor i := 0; i \u003c size; i += 2 {\n\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n\t\ttree.Delete(content)\n\t\tcontent = Content{Key: i + 1, Value: fmt.Sprintf(\"Value_%d\", i+1)}\n\t\tclone.Delete(content)\n\t}\n\n\t// Clone the clone\n\tt.Logf(\"Cloning the clone\\n\")\n\tclone2 := clone.Clone()\n\n\t// Make some changes to all three trees\n\tt.Logf(\"Making changes to all three trees\\n\")\n\tfor i := 0; i \u003c size; i += 3 {\n\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n\t\ttree.Delete(content)\n\t\tcontent = Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i+1)}\n\t\tclone.Delete(content)\n\t\tcontent = Content{Key: i + 2, Value: fmt.Sprintf(\"Value_%d\", i+2)}\n\t\tclone2.Delete(content)\n\t}\n\n\t// Check that the changes are isolated to the tree they were made in\n\tt.Logf(\"Checking that the changes are isolated to the tree they were made in\\n\")\n\tfor i := 0; i \u003c size; i++ {\n\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n\t\tval := tree.Get(content)\n\n\t\tif i%3 == 0 || i%2 == 0 {\n\t\t\tif val != nil {\n\t\t\t\tt.Errorf(\"Didn't expect key %v, but found key:value %v:%v\", content.Key, val.(Content).Key, val.(Content).Value)\n\t\t\t}\n\t\t} else {\n\t\t\tif val == nil {\n\t\t\t\tt.Errorf(\"Expected key %v, but didn't find it\", content.Key)\n\t\t\t}\n\t\t}\n\n\t\tval = clone.Get(content)\n\t\tif i%2 != 0 || i%3 == 0 {\n\t\t\tif val != nil {\n\t\t\t\tt.Errorf(\"Didn't expect key %v, but found key:value %v:%v\", content.Key, val.(Content).Key, val.(Content).Value)\n\t\t\t}\n\t\t} else {\n\t\t\tif val == nil {\n\t\t\t\tt.Errorf(\"Expected key %v, but didn't find it\", content.Key)\n\t\t\t}\n\t\t}\n\n\t\tval = clone2.Get(content)\n\t\tif i%2 != 0 || (i-2)%3 == 0 {\n\t\t\tif val != nil {\n\t\t\t\tt.Errorf(\"Didn't expect key %v, but found key:value %v:%v\", content.Key, val.(Content).Key, val.(Content).Value)\n\t\t\t}\n\t\t} else {\n\t\t\tif val == nil {\n\t\t\t\tt.Errorf(\"Expected key %v, but didn't find it\", content.Key)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// --------------------\n// Stress tests. Disabled for testing performance\n\n//func TestStress(t *testing.T) {\n//\t// Loop through creating B-Trees with a range of degrees from 3 to 12, stepping by 3.\n//\t// Insert 1000 records into each tree, then search for each record.\n//\t// Delete half of the records, skipping every other one, then search for each record.\n//\n//\tfor degree := 3; degree \u003c= 12; degree += 3 {\n//\t\tt.Logf(\"Testing B-Tree of degree %d\\n\", degree)\n//\t\ttree := New(WithDegree(degree))\n//\n//\t\t// Insert 1000 records\n//\t\tt.Logf(\"Inserting 1000 records\\n\")\n//\t\tfor i := 0; i \u003c 1000; i++ {\n//\t\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\t\ttree.Insert(content)\n//\t\t}\n//\n//\t\t// Search for all records\n//\t\tfor i := 0; i \u003c 1000; i++ {\n//\t\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\t\tval := tree.Get(content)\n//\t\t\tif val == nil {\n//\t\t\t\tt.Errorf(\"Expected key %v, but didn't find it\", content.Key)\n//\t\t\t}\n//\t\t}\n//\n//\t\t// Delete half of the records\n//\t\tfor i := 0; i \u003c 1000; i += 2 {\n//\t\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\t\ttree.Delete(content)\n//\t\t}\n//\n//\t\t// Search for all records\n//\t\tfor i := 0; i \u003c 1000; i++ {\n//\t\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\t\tval := tree.Get(content)\n//\t\t\tif i%2 == 0 {\n//\t\t\t\tif val != nil {\n//\t\t\t\t\tt.Errorf(\"Didn't expect key %v, but found key:value %v:%v\", content.Key, val.(Content).Key, val.(Content).Value)\n//\t\t\t\t}\n//\t\t\t} else {\n//\t\t\t\tif val == nil {\n//\t\t\t\t\tt.Errorf(\"Expected key %v, but didn't find it\", content.Key)\n//\t\t\t\t}\n//\t\t\t}\n//\t\t}\n//\t}\n//\n//\t// Now create a very large tree, with 100000 records\n//\t// Then delete roughly one third of them, using a very basic random number generation scheme\n//\t// (implement it right here) to determine which records to delete.\n//\t// Print a few lines using Logf to let the user know what's happening.\n//\n//\tt.Logf(\"Testing B-Tree of degree 10 with 100000 records\\n\")\n//\ttree := New(WithDegree(10))\n//\n//\t// Insert 100000 records\n//\tt.Logf(\"Inserting 100000 records\\n\")\n//\tfor i := 0; i \u003c 100000; i++ {\n//\t\tcontent := Content{Key: i, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\ttree.Insert(content)\n//\t}\n//\n//\t// Implement a very basic random number generator\n//\tseed := 0\n//\trandom := func() int {\n//\t\tseed = (seed*1103515245 + 12345) \u0026 0x7fffffff\n//\t\treturn seed\n//\t}\n//\n//\t// Delete one third of the records\n//\tt.Logf(\"Deleting one third of the records\\n\")\n//\tfor i := 0; i \u003c 35000; i++ {\n//\t\tcontent := Content{Key: random() % 100000, Value: fmt.Sprintf(\"Value_%d\", i)}\n//\t\ttree.Delete(content)\n//\t}\n//}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "RYCQPruGoRxZoAFFOub8DFq51ikvFfXinzN7R5eT6oJ1P86fYxRnRLmmBQUj29F6fokavNNMByerca1WjYq+Cw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "combinederr", + "path": "gno.land/p/demo/combinederr", + "files": [ + { + "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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "xUfkIg9FnQZrt1EAD81kDGNdT+H+kIFBW5Gj3IhWEnut1WckeXiMAj5DNDbEjE7aVbhpL/2X1O3+K1NA7iXKAQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "context", + "path": "gno.land/p/demo/context", + "files": [ + { + "name": "context.gno", + "body": "// Package context provides a minimal implementation of Go context with support\n// for Value and WithValue.\n//\n// Adapted from https://github.com/golang/go/tree/master/src/context/.\n// Copyright 2016 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\npackage context\n\ntype Context interface {\n\t// Value returns the value associated with this context for key, or nil\n\t// if no value is associated with key.\n\tValue(key any) any\n}\n\n// Empty returns a non-nil, empty context, similar with context.Background and\n// context.TODO in Go.\nfunc Empty() Context {\n\treturn \u0026emptyCtx{}\n}\n\ntype emptyCtx struct{}\n\nfunc (ctx emptyCtx) Value(key any) any {\n\treturn nil\n}\n\nfunc (ctx emptyCtx) String() string {\n\treturn \"context.Empty\"\n}\n\ntype valueCtx struct {\n\tparent Context\n\tkey, val any\n}\n\nfunc (ctx *valueCtx) Value(key any) any {\n\tif ctx.key == key {\n\t\treturn ctx.val\n\t}\n\treturn ctx.parent.Value(key)\n}\n\nfunc stringify(v any) string {\n\tswitch s := v.(type) {\n\tcase stringer:\n\t\treturn s.String()\n\tcase string:\n\t\treturn s\n\t}\n\treturn \"non-stringer\"\n}\n\ntype stringer interface {\n\tString() string\n}\n\nfunc (c *valueCtx) String() string {\n\treturn stringify(c.parent) + \".WithValue(\" +\n\t\tstringify(c.key) + \", \" +\n\t\tstringify(c.val) + \")\"\n}\n\n// WithValue returns a copy of parent in which the value associated with key is\n// val.\nfunc WithValue(parent Context, key, val any) Context {\n\tif key == nil {\n\t\tpanic(\"nil key\")\n\t}\n\t// XXX: if !reflect.TypeOf(key).Comparable() { panic(\"key is not comparable\") }\n\treturn \u0026valueCtx{parent, key, val}\n}\n" + }, + { + "name": "context_test.gno", + "body": "package context\n\nimport \"testing\"\n\nfunc TestContextExample(t *testing.T) {\n\ttype favContextKey string\n\n\tk := favContextKey(\"language\")\n\tctx := WithValue(Empty(), k, \"Gno\")\n\n\tif v := ctx.Value(k); v != nil {\n\t\tif v.(string) != \"Gno\" {\n\t\t\tt.Errorf(\"language value should be Gno, but is %s\", v)\n\t\t}\n\t} else {\n\t\tt.Errorf(\"language key value was not found\")\n\t}\n\n\tif v := ctx.Value(favContextKey(\"color\")); v != nil {\n\t\tt.Errorf(\"color key was found\")\n\t}\n}\n\n// otherContext is a Context that's not one of the types defined in context.go.\n// This lets us test code paths that differ based on the underlying type of the\n// Context.\ntype otherContext struct {\n\tContext\n}\n\ntype (\n\tkey1 int\n\tkey2 int\n)\n\n// func (k key2) String() string { return fmt.Sprintf(\"%[1]T(%[1]d)\", k) }\n\nvar (\n\tk1 = key1(1)\n\tk2 = key2(1) // same int as k1, different type\n\tk3 = key2(3) // same type as k2, different int\n)\n\nfunc TestValues(t *testing.T) {\n\tcheck := func(c Context, nm, v1, v2, v3 string) {\n\t\tif v, ok := c.Value(k1).(string); ok == (len(v1) == 0) || v != v1 {\n\t\t\tt.Errorf(`%s.Value(k1).(string) = %q, %t want %q, %t`, nm, v, ok, v1, len(v1) != 0)\n\t\t}\n\t\tif v, ok := c.Value(k2).(string); ok == (len(v2) == 0) || v != v2 {\n\t\t\tt.Errorf(`%s.Value(k2).(string) = %q, %t want %q, %t`, nm, v, ok, v2, len(v2) != 0)\n\t\t}\n\t\tif v, ok := c.Value(k3).(string); ok == (len(v3) == 0) || v != v3 {\n\t\t\tt.Errorf(`%s.Value(k3).(string) = %q, %t want %q, %t`, nm, v, ok, v3, len(v3) != 0)\n\t\t}\n\t}\n\n\tc0 := Empty()\n\tcheck(c0, \"c0\", \"\", \"\", \"\")\n\n\tt.Skip() // XXX: depends on https://github.com/gnolang/gno/issues/2386\n\n\tc1 := WithValue(Empty(), k1, \"c1k1\")\n\tcheck(c1, \"c1\", \"c1k1\", \"\", \"\")\n\n\t/*if got, want := c1.String(), `context.Empty.WithValue(context_test.key1, c1k1)`; got != want {\n\t\tt.Errorf(\"c.String() = %q want %q\", got, want)\n\t}*/\n\n\tc2 := WithValue(c1, k2, \"c2k2\")\n\tcheck(c2, \"c2\", \"c1k1\", \"c2k2\", \"\")\n\n\t/*if got, want := fmt.Sprint(c2), `context.Empty.WithValue(context_test.key1, c1k1).WithValue(context_test.key2(1), c2k2)`; got != want {\n\t\tt.Errorf(\"c.String() = %q want %q\", got, want)\n\t}*/\n\n\tc3 := WithValue(c2, k3, \"c3k3\")\n\tcheck(c3, \"c2\", \"c1k1\", \"c2k2\", \"c3k3\")\n\n\tc4 := WithValue(c3, k1, nil)\n\tcheck(c4, \"c4\", \"\", \"c2k2\", \"c3k3\")\n\n\to0 := otherContext{Empty()}\n\tcheck(o0, \"o0\", \"\", \"\", \"\")\n\n\to1 := otherContext{WithValue(Empty(), k1, \"c1k1\")}\n\tcheck(o1, \"o1\", \"c1k1\", \"\", \"\")\n\n\to2 := WithValue(o1, k2, \"o2k2\")\n\tcheck(o2, \"o2\", \"c1k1\", \"o2k2\", \"\")\n\n\to3 := otherContext{c4}\n\tcheck(o3, \"o3\", \"\", \"c2k2\", \"c3k3\")\n\n\to4 := WithValue(o3, k3, nil)\n\tcheck(o4, \"o4\", \"\", \"c2k2\", \"\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "ExHg4WgJo/KYP6b/4ohiTOPEB6UoQ7jMbHI3CenR532lk+5/G8rKu/YksiZ9KOXFeKeB5vshGADwC4im4mKJCA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "dao", + "path": "gno.land/p/demo/dao", + "files": [ + { + "name": "dao.gno", + "body": "package dao\n\nconst (\n\tProposalAddedEvent = \"ProposalAdded\" // emitted when a new proposal has been added\n\tProposalAcceptedEvent = \"ProposalAccepted\" // emitted when a proposal has been accepted\n\tProposalNotAcceptedEvent = \"ProposalNotAccepted\" // emitted when a proposal has not been accepted\n\tProposalExecutedEvent = \"ProposalExecuted\" // emitted when a proposal has been executed\n\n\tProposalEventIDKey = \"proposal-id\"\n\tProposalEventAuthorKey = \"proposal-author\"\n\tProposalEventExecutionKey = \"exec-status\"\n)\n\n// ProposalRequest is a single govdao proposal request\n// that contains the necessary information to\n// log and generate a valid proposal\ntype ProposalRequest struct {\n\tTitle string // the title associated with the proposal\n\tDescription string // the description associated with the proposal\n\tExecutor Executor // the proposal executor\n}\n\n// DAO defines the DAO abstraction\ntype DAO interface {\n\t// PropStore is the DAO proposal storage\n\tPropStore\n\n\t// Propose adds a new proposal to the executor-based GOVDAO.\n\t// Returns the generated proposal ID\n\tPropose(request ProposalRequest) (uint64, error)\n\n\t// ExecuteProposal executes the proposal with the given ID\n\tExecuteProposal(id uint64) error\n}\n" + }, + { + "name": "doc.gno", + "body": "// Package dao houses common DAO building blocks (framework), which can be used or adopted by any\n// specific DAO implementation. By design, the DAO should house the proposals it receives, but not the actual\n// DAO members or proposal votes. These abstractions should be implemented by a separate entity, to keep the DAO\n// agnostic of implementation details such as these (member / vote management).\npackage dao\n" + }, + { + "name": "events.gno", + "body": "package dao\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// EmitProposalAdded emits an event signaling that\n// a given proposal was added\nfunc EmitProposalAdded(id uint64, proposer std.Address) {\n\tstd.Emit(\n\t\tProposalAddedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t\tProposalEventAuthorKey, proposer.String(),\n\t)\n}\n\n// EmitProposalAccepted emits an event signaling that\n// a given proposal was accepted\nfunc EmitProposalAccepted(id uint64) {\n\tstd.Emit(\n\t\tProposalAcceptedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t)\n}\n\n// EmitProposalNotAccepted emits an event signaling that\n// a given proposal was not accepted\nfunc EmitProposalNotAccepted(id uint64) {\n\tstd.Emit(\n\t\tProposalNotAcceptedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t)\n}\n\n// EmitProposalExecuted emits an event signaling that\n// a given proposal was executed, with the given status\nfunc EmitProposalExecuted(id uint64, status ProposalStatus) {\n\tstd.Emit(\n\t\tProposalExecutedEvent,\n\t\tProposalEventIDKey, ufmt.Sprintf(\"%d\", id),\n\t\tProposalEventExecutionKey, status.String(),\n\t)\n}\n\n// EmitVoteAdded emits an event signaling that\n// a vote was cast for a given proposal\nfunc EmitVoteAdded(id uint64, voter std.Address, option VoteOption) {\n\tstd.Emit(\n\t\tVoteAddedEvent,\n\t\tVoteAddedIDKey, ufmt.Sprintf(\"%d\", id),\n\t\tVoteAddedAuthorKey, voter.String(),\n\t\tVoteAddedOptionKey, option.String(),\n\t)\n}\n" + }, + { + "name": "executor.gno", + "body": "package dao\n\n// Executor represents a minimal closure-oriented proposal design.\n// It is intended to be used by a govdao governance proposal (v1, v2, etc)\ntype Executor interface {\n\t// Execute executes the given proposal, and returns any error encountered\n\t// during the execution\n\tExecute() error\n}\n" + }, + { + "name": "proposals.gno", + "body": "package dao\n\nimport \"std\"\n\n// ProposalStatus is the currently active proposal status,\n// changed based on DAO functionality.\n// Status transitions:\n//\n// ACTIVE -\u003e ACCEPTED -\u003e EXECUTION(SUCCEEDED/FAILED)\n//\n// ACTIVE -\u003e NOT ACCEPTED\ntype ProposalStatus string\n\nvar (\n\tActive ProposalStatus = \"active\" // proposal is still active\n\tAccepted ProposalStatus = \"accepted\" // proposal gathered quorum\n\tNotAccepted ProposalStatus = \"not accepted\" // proposal failed to gather quorum\n\tExecutionSuccessful ProposalStatus = \"execution successful\" // proposal is executed successfully\n\tExecutionFailed ProposalStatus = \"execution failed\" // proposal has failed during execution\n)\n\nfunc (s ProposalStatus) String() string {\n\treturn string(s)\n}\n\n// PropStore defines the proposal storage abstraction\ntype PropStore interface {\n\t// Proposals returns the given paginated proposals\n\tProposals(offset, count uint64) []Proposal\n\n\t// ProposalByID returns the proposal associated with\n\t// the given ID, if any\n\tProposalByID(id uint64) (Proposal, error)\n\n\t// Size returns the number of proposals in\n\t// the proposal store\n\tSize() int\n}\n\n// Proposal is the single proposal abstraction\ntype Proposal interface {\n\t// Author returns the author of the proposal\n\tAuthor() std.Address\n\n\t// Title returns the title of the proposal\n\tTitle() string\n\n\t// Description returns the description of the proposal\n\tDescription() string\n\n\t// Status returns the status of the proposal\n\tStatus() ProposalStatus\n\n\t// Executor returns the proposal executor\n\tExecutor() Executor\n\n\t// Stats returns the voting stats of the proposal\n\tStats() Stats\n\n\t// IsExpired returns a flag indicating if the proposal expired\n\tIsExpired() bool\n\n\t// Render renders the proposal in a readable format\n\tRender() string\n}\n" + }, + { + "name": "vote.gno", + "body": "package dao\n\n// NOTE:\n// This voting pods will be removed in a future version of the\n// p/demo/dao package. A DAO shouldn't have to comply with or define how the voting mechanism works internally;\n// it should be viewed as an entity that makes decisions\n//\n// The extent of \"votes being enforced\" in this implementation is just in the context\n// of types a DAO can use (import), and in the context of \"Stats\", where\n// there is a notion of \"Yay\", \"Nay\" and \"Abstain\" votes.\nconst (\n\tVoteAddedEvent = \"VoteAdded\" // emitted when a vote was cast for a proposal\n\n\tVoteAddedIDKey = \"proposal-id\"\n\tVoteAddedAuthorKey = \"author\"\n\tVoteAddedOptionKey = \"option\"\n)\n\n// VoteOption is the limited voting option for a DAO proposal\ntype VoteOption string\n\nconst (\n\tYesVote VoteOption = \"YES\" // Proposal should be accepted\n\tNoVote VoteOption = \"NO\" // Proposal should be rejected\n\tAbstainVote VoteOption = \"ABSTAIN\" // Side is not chosen\n)\n\nfunc (v VoteOption) String() string {\n\treturn string(v)\n}\n\n// Stats encompasses the proposal voting stats\ntype Stats struct {\n\tYayVotes uint64\n\tNayVotes uint64\n\tAbstainVotes uint64\n\n\tTotalVotingPower uint64\n}\n\n// YayPercent returns the percentage (0-100) of the yay votes\n// in relation to the total voting power\nfunc (v Stats) YayPercent() uint64 {\n\treturn v.YayVotes * 100 / v.TotalVotingPower\n}\n\n// NayPercent returns the percentage (0-100) of the nay votes\n// in relation to the total voting power\nfunc (v Stats) NayPercent() uint64 {\n\treturn v.NayVotes * 100 / v.TotalVotingPower\n}\n\n// AbstainPercent returns the percentage (0-100) of the abstain votes\n// in relation to the total voting power\nfunc (v Stats) AbstainPercent() uint64 {\n\treturn v.AbstainVotes * 100 / v.TotalVotingPower\n}\n\n// MissingVotes returns the summed voting power that has not\n// participated in proposal voting yet\nfunc (v Stats) MissingVotes() uint64 {\n\treturn v.TotalVotingPower - (v.YayVotes + v.NayVotes + v.AbstainVotes)\n}\n\n// MissingVotesPercent returns the percentage (0-100) of the missing votes\n// in relation to the total voting power\nfunc (v Stats) MissingVotesPercent() uint64 {\n\treturn v.MissingVotes() * 100 / v.TotalVotingPower\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "GtvoMOHQbCIiKt7N68Ko+lzz4evzkMym8Ltac0CT0QIwyMCJIsfK6TKQr5vaMfV7VVmcYqdxprUgEoqu6EpoBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "dom", + "path": "gno.land/p/demo/dom", + "files": [ + { + "name": "dom.gno", + "body": "// XXX This is only used for testing in ./tests.\n// Otherwise this package is deprecated.\n// TODO: replace with a package that is supported, and delete this.\n\npackage dom\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Plot struct {\n\tName string\n\tPosts avl.Tree // postsCtr -\u003e *Post\n\tPostsCtr int\n}\n\nfunc (plot *Plot) AddPost(title string, body string) {\n\tctr := plot.PostsCtr\n\tplot.PostsCtr++\n\tkey := strconv.Itoa(ctr)\n\tpost := \u0026Post{\n\t\tTitle: title,\n\t\tBody: body,\n\t}\n\tplot.Posts.Set(key, post)\n}\n\nfunc (plot *Plot) String() string {\n\tstr := \"# [plot] \" + plot.Name + \"\\n\"\n\tif plot.Posts.Size() \u003e 0 {\n\t\tplot.Posts.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tstr += \"\\n\"\n\t\t\tstr += value.(*Post).String()\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\ntype Post struct {\n\tTitle string\n\tBody string\n\tComments avl.Tree\n}\n\nfunc (post *Post) String() string {\n\tstr := \"## \" + post.Title + \"\\n\"\n\tstr += \"\"\n\tstr += post.Body\n\tif post.Comments.Size() \u003e 0 {\n\t\tpost.Comments.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tstr += \"\\n\"\n\t\t\tstr += value.(*Comment).String()\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\ntype Comment struct {\n\tCreator string\n\tBody string\n}\n\nfunc (cmm Comment) String() string {\n\treturn cmm.Body + \" - @\" + cmm.Creator + \"\\n\"\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "fd1k1mmYzjXhbExd24hqcL5M5xZ+OrB875AtAT1t7nFNoL7mwk+TtJ5STWPknXZ/F0NSe9TGa+Dc1AXf4R8QBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "entropy", + "path": "gno.land/p/demo/entropy", + "files": [ + { + "name": "entropy.gno", + "body": "// Entropy generates fully deterministic, cost-effective, and hard to guess\n// numbers.\n//\n// It is designed both for single-usage, like seeding math/rand or for being\n// reused which increases the entropy and its cost effectiveness.\n//\n// Disclaimer: this package is unsafe and won't prevent others to guess values\n// in advance.\n//\n// It uses the Bernstein's hash djb2 to be CPU-cycle efficient.\npackage entropy\n\nimport (\n\t\"math\"\n\t\"std\"\n\t\"time\"\n)\n\ntype Instance struct {\n\tvalue uint32\n}\n\nfunc New() *Instance {\n\tr := Instance{value: 5381}\n\tr.addEntropy()\n\treturn \u0026r\n}\n\nfunc FromSeed(seed uint32) *Instance {\n\tr := Instance{value: seed}\n\tr.addEntropy()\n\treturn \u0026r\n}\n\nfunc (i *Instance) Seed() uint32 {\n\treturn i.value\n}\n\nfunc (i *Instance) djb2String(input string) {\n\tfor _, c := range input {\n\t\ti.djb2Uint32(uint32(c))\n\t}\n}\n\n// super fast random algorithm.\n// http://www.cse.yorku.ca/~oz/hash.html\nfunc (i *Instance) djb2Uint32(input uint32) {\n\ti.value = (i.value \u003c\u003c 5) + i.value + input\n}\n\n// AddEntropy uses various runtime variables to add entropy to the existing seed.\nfunc (i *Instance) addEntropy() {\n\t// FIXME: reapply the 5381 initial value?\n\n\t// inherit previous entropy\n\t// nothing to do\n\n\t// handle callers\n\t{\n\t\tcaller1 := std.CallerAt(1).String()\n\t\ti.djb2String(caller1)\n\t\tcaller2 := std.CallerAt(2).String()\n\t\ti.djb2String(caller2)\n\t}\n\n\t// height\n\t{\n\t\theight := std.ChainHeight()\n\t\tif height \u003e= math.MaxUint32 {\n\t\t\theight -= math.MaxUint32\n\t\t}\n\t\ti.djb2Uint32(uint32(height))\n\t}\n\n\t// time\n\t{\n\t\tsecs := time.Now().Second()\n\t\ti.djb2Uint32(uint32(secs))\n\t\tnsecs := time.Now().Nanosecond()\n\t\ti.djb2Uint32(uint32(nsecs))\n\t}\n\n\t// FIXME: compute other hard-to-guess but deterministic variables, like real gas?\n}\n\nfunc (i *Instance) Value() uint32 {\n\ti.addEntropy()\n\treturn i.value\n}\n\nfunc (i *Instance) Value64() uint64 {\n\ti.addEntropy()\n\thigh := i.value\n\ti.addEntropy()\n\n\treturn (uint64(high) \u003c\u003c 32) | uint64(i.value)\n}\n" + }, + { + "name": "entropy_test.gno", + "body": "package entropy\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n)\n\nfunc TestInstance(t *testing.T) {\n\tinstance := New()\n\tif instance == nil {\n\t\tt.Errorf(\"instance should not be nil\")\n\t}\n}\n\nfunc TestInstanceValue(t *testing.T) {\n\tbaseEntropy := New()\n\tbaseResult := computeValue(t, baseEntropy)\n\n\tsameHeightEntropy := New()\n\tsameHeightResult := computeValue(t, sameHeightEntropy)\n\n\tif baseResult != sameHeightResult {\n\t\tt.Errorf(\"should have the same result: new=%s, base=%s\", sameHeightResult, baseResult)\n\t}\n\n\ttesting.SkipHeights(1)\n\tdifferentHeightEntropy := New()\n\tdifferentHeightResult := computeValue(t, differentHeightEntropy)\n\n\tif baseResult == differentHeightResult {\n\t\tt.Errorf(\"should have different result: new=%s, base=%s\", differentHeightResult, baseResult)\n\t}\n}\n\nfunc TestInstanceValue64(t *testing.T) {\n\tbaseEntropy := New()\n\tbaseResult := computeValue64(t, baseEntropy)\n\n\tsameHeightEntropy := New()\n\tsameHeightResult := computeValue64(t, sameHeightEntropy)\n\n\tif baseResult != sameHeightResult {\n\t\tt.Errorf(\"should have the same result: new=%s, base=%s\", sameHeightResult, baseResult)\n\t}\n\n\ttesting.SkipHeights(1)\n\tdifferentHeightEntropy := New()\n\tdifferentHeightResult := computeValue64(t, differentHeightEntropy)\n\n\tif baseResult == differentHeightResult {\n\t\tt.Errorf(\"should have different result: new=%s, base=%s\", differentHeightResult, baseResult)\n\t}\n}\n\nfunc computeValue(t *testing.T, r *Instance) string {\n\tt.Helper()\n\n\tout := \"\"\n\tfor i := 0; i \u003c 10; i++ {\n\t\tval := int(r.Value())\n\t\tout += strconv.Itoa(val) + \" \"\n\t}\n\n\treturn out\n}\n\nfunc computeValue64(t *testing.T, r *Instance) string {\n\tt.Helper()\n\n\tout := \"\"\n\tfor i := 0; i \u003c 10; i++ {\n\t\tval := int(r.Value64())\n\t\tout += strconv.Itoa(val) + \" \"\n\t}\n\n\treturn out\n}\n" + }, + { + "name": "z_filetest.gno", + "body": "package main\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/entropy\"\n)\n\nfunc main() {\n\t// initial\n\tprintln(\"---\")\n\tr := entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value64())\n\n\t// should be the same\n\tprintln(\"---\")\n\tr = entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value64())\n\n\ttesting.SkipHeights(1)\n\tprintln(\"---\")\n\tr = entropy.New()\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value())\n\tprintln(r.Value64())\n}\n\n// Output:\n// ---\n// 4129293727\n// 2141104956\n// 1950222777\n// 3348280598\n// 438354259\n// 6353385488959065197\n// ---\n// 4129293727\n// 2141104956\n// 1950222777\n// 3348280598\n// 438354259\n// 6353385488959065197\n// ---\n// 49506731\n// 1539580078\n// 2695928529\n// 1895482388\n// 3462727799\n// 16745038698684748445\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "WCATi7rUBe0FL0LphObmPBeUaQF2eqeSY2lTivSl7fXba72MSfnHbhuuPhwqPEptaRjp4IoFEXA3xm+x5YMHCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "flow", + "path": "gno.land/p/demo/flow", + "files": [ + { + "name": "LICENSE", + "body": "https://github.com/mxk/go-flowrate/blob/master/LICENSE\nBSD 3-Clause \"New\" or \"Revised\" License\n\nCopyright (c) 2014 The Go-FlowRate Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n\n * Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the\n distribution.\n\n * Neither the name of the go-flowrate project nor the names of its\n contributors may be used to endorse or promote products derived\n from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "README.md", + "body": "Data Flow Rate Control\n======================\n\nTo download and install this package run:\n\ngo get github.com/mxk/go-flowrate/flowrate\n\nThe documentation is available at:\n\nhttp://godoc.org/github.com/mxk/go-flowrate/flowrate\n" + }, + { + "name": "flow.gno", + "body": "//\n// Written by Maxim Khitrov (November 2012)\n//\n// XXX modified to disable blocking, time.Sleep().\n\n// Package flow provides the tools for monitoring and limiting the flow rate\n// of an arbitrary data stream.\npackage flow\n\nimport (\n\t\"math\"\n\t// \"sync\"\n\t\"time\"\n)\n\n// Monitor monitors and limits the transfer rate of a data stream.\ntype Monitor struct {\n\t// mu sync.Mutex // Mutex guarding access to all internal fields\n\tactive bool // Flag indicating an active transfer\n\tstart time.Duration // Transfer start time (clock() value)\n\tbytes int64 // Total number of bytes transferred\n\tsamples int64 // Total number of samples taken\n\n\trSample float64 // Most recent transfer rate sample (bytes per second)\n\trEMA float64 // Exponential moving average of rSample\n\trPeak float64 // Peak transfer rate (max of all rSamples)\n\trWindow float64 // rEMA window (seconds)\n\n\tsBytes int64 // Number of bytes transferred since sLast\n\tsLast time.Duration // Most recent sample time (stop time when inactive)\n\tsRate time.Duration // Sampling rate\n\n\ttBytes int64 // Number of bytes expected in the current transfer\n\ttLast time.Duration // Time of the most recent transfer of at least 1 byte\n}\n\n// New creates a new flow control monitor. Instantaneous transfer rate is\n// measured and updated for each sampleRate interval. windowSize determines the\n// weight of each sample in the exponential moving average (EMA) calculation.\n// The exact formulas are:\n//\n//\tsampleTime = currentTime - prevSampleTime\n//\tsampleRate = byteCount / sampleTime\n//\tweight = 1 - exp(-sampleTime/windowSize)\n//\tnewRate = weight*sampleRate + (1-weight)*oldRate\n//\n// The default values for sampleRate and windowSize (if \u003c= 0) are 100ms and 1s,\n// respectively.\nfunc New(sampleRate, windowSize time.Duration) *Monitor {\n\tif sampleRate = clockRound(sampleRate); sampleRate \u003c= 0 {\n\t\tsampleRate = 5 * clockRate\n\t}\n\tif windowSize \u003c= 0 {\n\t\twindowSize = 1 * time.Second\n\t}\n\tnow := clock()\n\treturn \u0026Monitor{\n\t\tactive: true,\n\t\tstart: now,\n\t\trWindow: windowSize.Seconds(),\n\t\tsLast: now,\n\t\tsRate: sampleRate,\n\t\ttLast: now,\n\t}\n}\n\n// Update records the transfer of n bytes and returns n. It should be called\n// after each Read/Write operation, even if n is 0.\nfunc (m *Monitor) Update(n int) int {\n\t// m.mu.Lock()\n\tm.update(n)\n\t// m.mu.Unlock()\n\treturn n\n}\n\n// Hack to set the current rEMA.\nfunc (m *Monitor) SetREMA(rEMA float64) {\n\t// m.mu.Lock()\n\tm.rEMA = rEMA\n\tm.samples++\n\t// m.mu.Unlock()\n}\n\n// IO is a convenience method intended to wrap io.Reader and io.Writer method\n// execution. It calls m.Update(n) and then returns (n, err) unmodified.\nfunc (m *Monitor) IO(n int, err error) (int, error) {\n\treturn m.Update(n), err\n}\n\n// Done marks the transfer as finished and prevents any further updates or\n// limiting. Instantaneous and current transfer rates drop to 0. Update, IO, and\n// Limit methods become NOOPs. It returns the total number of bytes transferred.\nfunc (m *Monitor) Done() int64 {\n\t// m.mu.Lock()\n\tif now := m.update(0); m.sBytes \u003e 0 {\n\t\tm.reset(now)\n\t}\n\tm.active = false\n\tm.tLast = 0\n\tn := m.bytes\n\t// m.mu.Unlock()\n\treturn n\n}\n\n// timeRemLimit is the maximum Status.TimeRem value.\nconst timeRemLimit = 999*time.Hour + 59*time.Minute + 59*time.Second\n\n// Status represents the current Monitor status. All transfer rates are in bytes\n// per second rounded to the nearest byte.\ntype Status struct {\n\tActive bool // Flag indicating an active transfer\n\tStart time.Time // Transfer start time\n\tDuration time.Duration // Time period covered by the statistics\n\tIdle time.Duration // Time since the last transfer of at least 1 byte\n\tBytes int64 // Total number of bytes transferred\n\tSamples int64 // Total number of samples taken\n\tInstRate int64 // Instantaneous transfer rate\n\tCurRate int64 // Current transfer rate (EMA of InstRate)\n\tAvgRate int64 // Average transfer rate (Bytes / Duration)\n\tPeakRate int64 // Maximum instantaneous transfer rate\n\tBytesRem int64 // Number of bytes remaining in the transfer\n\tTimeRem time.Duration // Estimated time to completion\n\tProgress Percent // Overall transfer progress\n}\n\nfunc (s Status) String() string {\n\treturn \"STATUS{}\"\n}\n\n// Status returns current transfer status information. The returned value\n// becomes static after a call to Done.\nfunc (m *Monitor) Status() Status {\n\t// m.mu.Lock()\n\tnow := m.update(0)\n\ts := Status{\n\t\tActive: m.active,\n\t\tStart: clockToTime(m.start),\n\t\tDuration: m.sLast - m.start,\n\t\tIdle: now - m.tLast,\n\t\tBytes: m.bytes,\n\t\tSamples: m.samples,\n\t\tPeakRate: round(m.rPeak),\n\t\tBytesRem: m.tBytes - m.bytes,\n\t\tProgress: percentOf(float64(m.bytes), float64(m.tBytes)),\n\t}\n\tif s.BytesRem \u003c 0 {\n\t\ts.BytesRem = 0\n\t}\n\tif s.Duration \u003e 0 {\n\t\trAvg := float64(s.Bytes) / s.Duration.Seconds()\n\t\ts.AvgRate = round(rAvg)\n\t\tif s.Active {\n\t\t\ts.InstRate = round(m.rSample)\n\t\t\ts.CurRate = round(m.rEMA)\n\t\t\tif s.BytesRem \u003e 0 {\n\t\t\t\tif tRate := 0.8*m.rEMA + 0.2*rAvg; tRate \u003e 0 {\n\t\t\t\t\tns := float64(s.BytesRem) / tRate * 1e9\n\t\t\t\t\tif ns \u003e float64(timeRemLimit) {\n\t\t\t\t\t\tns = float64(timeRemLimit)\n\t\t\t\t\t}\n\t\t\t\t\ts.TimeRem = clockRound(time.Duration(ns))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\t// m.mu.Unlock()\n\treturn s\n}\n\n// Limit restricts the instantaneous (per-sample) data flow to rate bytes per\n// second. It returns the maximum number of bytes (0 \u003c= n \u003c= want) that may be\n// transferred immediately without exceeding the limit. If block == true, the\n// call blocks until n \u003e 0. want is returned unmodified if want \u003c 1, rate \u003c 1,\n// or the transfer is inactive (after a call to Done).\n//\n// At least one byte is always allowed to be transferred in any given sampling\n// period. Thus, if the sampling rate is 100ms, the lowest achievable flow rate\n// is 10 bytes per second.\n//\n// For usage examples, see the implementation of Reader and Writer in io.go.\nfunc (m *Monitor) Limit(want int, rate int64, block bool) (n int) {\n\tif block {\n\t\tpanic(\"blocking not yet supported\")\n\t}\n\tif want \u003c 1 || rate \u003c 1 {\n\t\treturn want\n\t}\n\t// m.mu.Lock()\n\n\t// Determine the maximum number of bytes that can be sent in one sample\n\tlimit := round(float64(rate) * m.sRate.Seconds())\n\tif limit \u003c= 0 {\n\t\tlimit = 1\n\t}\n\n\t_ = m.update(0)\n\t/* XXX\n\t// If block == true, wait until m.sBytes \u003c limit\n\tif now := m.update(0); block {\n\t\tfor m.sBytes \u003e= limit \u0026\u0026 m.active {\n\t\t\tnow = m.waitNextSample(now)\n\t\t}\n\t}\n\t*/\n\n\t// Make limit \u003c= want (unlimited if the transfer is no longer active)\n\tif limit -= m.sBytes; limit \u003e int64(want) || !m.active {\n\t\tlimit = int64(want)\n\t}\n\t// m.mu.Unlock()\n\n\tif limit \u003c 0 {\n\t\tlimit = 0\n\t}\n\treturn int(limit)\n}\n\n// SetTransferSize specifies the total size of the data transfer, which allows\n// the Monitor to calculate the overall progress and time to completion.\nfunc (m *Monitor) SetTransferSize(bytes int64) {\n\tif bytes \u003c 0 {\n\t\tbytes = 0\n\t}\n\t// m.mu.Lock()\n\tm.tBytes = bytes\n\t// m.mu.Unlock()\n}\n\n// update accumulates the transferred byte count for the current sample until\n// clock() - m.sLast \u003e= m.sRate. The monitor status is updated once the current\n// sample is done.\nfunc (m *Monitor) update(n int) (now time.Duration) {\n\tif !m.active {\n\t\treturn\n\t}\n\tif now = clock(); n \u003e 0 {\n\t\tm.tLast = now\n\t}\n\tm.sBytes += int64(n)\n\tif sTime := now - m.sLast; sTime \u003e= m.sRate {\n\t\tt := sTime.Seconds()\n\t\tif m.rSample = float64(m.sBytes) / t; m.rSample \u003e m.rPeak {\n\t\t\tm.rPeak = m.rSample\n\t\t}\n\n\t\t// Exponential moving average using a method similar to *nix load\n\t\t// average calculation. Longer sampling periods carry greater weight.\n\t\tif m.samples \u003e 0 {\n\t\t\tw := math.Exp(-t / m.rWindow)\n\t\t\tm.rEMA = m.rSample + w*(m.rEMA-m.rSample)\n\t\t} else {\n\t\t\tm.rEMA = m.rSample\n\t\t}\n\t\tm.reset(now)\n\t}\n\treturn\n}\n\n// reset clears the current sample state in preparation for the next sample.\nfunc (m *Monitor) reset(sampleTime time.Duration) {\n\tm.bytes += m.sBytes\n\tm.samples++\n\tm.sBytes = 0\n\tm.sLast = sampleTime\n}\n\n/*\n// waitNextSample sleeps for the remainder of the current sample. The lock is\n// released and reacquired during the actual sleep period, so it's possible for\n// the transfer to be inactive when this method returns.\nfunc (m *Monitor) waitNextSample(now time.Duration) time.Duration {\n\tconst minWait = 5 * time.Millisecond\n\tcurrent := m.sLast\n\n\t// sleep until the last sample time changes (ideally, just one iteration)\n\tfor m.sLast == current \u0026\u0026 m.active {\n\t\td := current + m.sRate - now\n\t\t// m.mu.Unlock()\n\t\tif d \u003c minWait {\n\t\t\td = minWait\n\t\t}\n\t\ttime.Sleep(d)\n\t\t// m.mu.Lock()\n\t\tnow = m.update(0)\n\t}\n\treturn now\n}\n*/\n" + }, + { + "name": "io.gno", + "body": "//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"errors\"\n\t\"io\"\n)\n\n// ErrLimit is returned by the Writer when a non-blocking write is short due to\n// the transfer rate limit.\nvar ErrLimit = errors.New(\"flowrate: flow rate limit exceeded\")\n\n// Limiter is implemented by the Reader and Writer to provide a consistent\n// interface for monitoring and controlling data transfer.\ntype Limiter interface {\n\tDone() int64\n\tStatus() Status\n\tSetTransferSize(bytes int64)\n\tSetLimit(new int64) (old int64)\n\tSetBlocking(new bool) (old bool)\n}\n\n// Reader implements io.ReadCloser with a restriction on the rate of data\n// transfer.\ntype Reader struct {\n\tio.Reader // Data source\n\t*Monitor // Flow control monitor\n\n\tlimit int64 // Rate limit in bytes per second (unlimited when \u003c= 0)\n\tblock bool // What to do when no new bytes can be read due to the limit\n}\n\n// NewReader restricts all Read operations on r to limit bytes per second.\nfunc NewReader(r io.Reader, limit int64) *Reader {\n\treturn \u0026Reader{r, New(0, 0), limit, false} // XXX default false\n}\n\n// Read reads up to len(p) bytes into p without exceeding the current transfer\n// rate limit. It returns (0, nil) immediately if r is non-blocking and no new\n// bytes can be read at this time.\nfunc (r *Reader) Read(p []byte) (n int, err error) {\n\tp = p[:r.Limit(len(p), r.limit, r.block)]\n\tif len(p) \u003e 0 {\n\t\tn, err = r.IO(r.Reader.Read(p))\n\t}\n\treturn\n}\n\n// SetLimit changes the transfer rate limit to new bytes per second and returns\n// the previous setting.\nfunc (r *Reader) SetLimit(new int64) (old int64) {\n\told, r.limit = r.limit, new\n\treturn\n}\n\n// SetBlocking changes the blocking behavior and returns the previous setting. A\n// Read call on a non-blocking reader returns immediately if no additional bytes\n// may be read at this time due to the rate limit.\nfunc (r *Reader) SetBlocking(new bool) (old bool) {\n\tif new == true {\n\t\tpanic(\"blocking not yet supported\")\n\t}\n\told, r.block = r.block, new\n\treturn\n}\n\n// Close closes the underlying reader if it implements the io.Closer interface.\nfunc (r *Reader) Close() error {\n\tdefer r.Done()\n\tif c, ok := r.Reader.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\n// Writer implements io.WriteCloser with a restriction on the rate of data\n// transfer.\ntype Writer struct {\n\tio.Writer // Data destination\n\t*Monitor // Flow control monitor\n\n\tlimit int64 // Rate limit in bytes per second (unlimited when \u003c= 0)\n\tblock bool // What to do when no new bytes can be written due to the limit\n}\n\n// NewWriter restricts all Write operations on w to limit bytes per second. The\n// transfer rate and the default blocking behavior (true) can be changed\n// directly on the returned *Writer.\nfunc NewWriter(w io.Writer, limit int64) *Writer {\n\treturn \u0026Writer{w, New(0, 0), limit, false} // XXX default false\n}\n\n// Write writes len(p) bytes from p to the underlying data stream without\n// exceeding the current transfer rate limit. It returns (n, ErrLimit) if w is\n// non-blocking and no additional bytes can be written at this time.\nfunc (w *Writer) Write(p []byte) (n int, err error) {\n\tvar c int\n\tfor len(p) \u003e 0 \u0026\u0026 err == nil {\n\t\ts := p[:w.Limit(len(p), w.limit, w.block)]\n\t\tif len(s) \u003e 0 {\n\t\t\tc, err = w.IO(w.Writer.Write(s))\n\t\t} else {\n\t\t\treturn n, ErrLimit\n\t\t}\n\t\tp = p[c:]\n\t\tn += c\n\t}\n\treturn\n}\n\n// SetLimit changes the transfer rate limit to new bytes per second and returns\n// the previous setting.\nfunc (w *Writer) SetLimit(new int64) (old int64) {\n\told, w.limit = w.limit, new\n\treturn\n}\n\n// SetBlocking changes the blocking behavior and returns the previous setting. A\n// Write call on a non-blocking writer returns as soon as no additional bytes\n// may be written at this time due to the rate limit.\nfunc (w *Writer) SetBlocking(new bool) (old bool) {\n\told, w.block = w.block, new\n\treturn\n}\n\n// Close closes the underlying writer if it implements the io.Closer interface.\nfunc (w *Writer) Close() error {\n\tdefer w.Done()\n\tif c, ok := w.Writer.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n" + }, + { + "name": "io_test.gno", + "body": "//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n\n\tios_test \"os\"\n)\n\n// XXX ugh, I can't even sleep milliseconds.\n// XXX\n\nconst (\n\t_50ms = 50 * time.Millisecond\n\t_100ms = 100 * time.Millisecond\n\t_200ms = 200 * time.Millisecond\n\t_300ms = 300 * time.Millisecond\n\t_400ms = 400 * time.Millisecond\n\t_500ms = 500 * time.Millisecond\n)\n\nfunc nextStatus(m *Monitor) Status {\n\tsamples := m.samples\n\tfor i := 0; i \u003c 30; i++ {\n\t\tif s := m.Status(); s.Samples != samples {\n\t\t\treturn s\n\t\t}\n\t\tios_test.Sleep(5 * time.Millisecond)\n\t}\n\treturn m.Status()\n}\n\nfunc TestReader(t *testing.T) {\n\tin := make([]byte, 100)\n\tfor i := range in {\n\t\tin[i] = byte(i)\n\t}\n\tb := make([]byte, 100)\n\tr := NewReader(bytes.NewReader(in), 100)\n\tstart := time.Now()\n\n\t// Make sure r implements Limiter\n\t_ = Limiter(r)\n\n\t// 1st read of 10 bytes is performed immediately\n\tif n, err := r.Read(b); n != 10 {\n\t\tt.Fatalf(\"r.Read(b) expected 10 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b) expected 10 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"r.Read(b) took too long (%v)\", rt.String())\n\t}\n\n\t// No new Reads allowed in the current sample\n\tr.SetBlocking(false)\n\tif n, err := r.Read(b); n != 0 {\n\t\tt.Fatalf(\"r.Read(b) expected 0 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b) expected 0 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"r.Read(b) took too long (%v)\", rt.String())\n\t}\n\n\tstatus := [6]Status{0: r.Status()} // No samples in the first status\n\n\t// 2nd read of 10 bytes blocks until the next sample\n\t// r.SetBlocking(true)\n\tios_test.Sleep(100 * time.Millisecond)\n\tif n, err := r.Read(b[10:]); n != 10 {\n\t\tt.Fatalf(\"r.Read(b[10:]) expected 10 (\u003cnil\u003e); got %v\", n)\n\t} else if err != nil {\n\t\tt.Fatalf(\"r.Read(b[10:]) expected 10 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003c _100ms {\n\t\tt.Fatalf(\"r.Read(b[10:]) returned ahead of time (%v)\", rt.String())\n\t}\n\n\tstatus[1] = r.Status() // 1st sample\n\tstatus[2] = nextStatus(r.Monitor) // 2nd sample\n\tstatus[3] = nextStatus(r.Monitor) // No activity for the 3rd sample\n\n\tif n := r.Done(); n != 20 {\n\t\tt.Fatalf(\"r.Done() expected 20; got %v\", n)\n\t}\n\n\tstatus[4] = r.Status()\n\tstatus[5] = nextStatus(r.Monitor) // Timeout\n\tstart = status[0].Start\n\n\t// Active, Start, Duration, Idle, Bytes, Samples, InstRate, CurRate, AvgRate, PeakRate, BytesRem, TimeRem, Progress\n\twant := []Status{\n\t\t{true, start, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},\n\t\t{true, start, _100ms, 0, 10, 1, 100, 100, 100, 100, 0, 0, 0},\n\t\t{true, start, _200ms, _100ms, 20, 2, 100, 100, 100, 100, 0, 0, 0},\n\t\t{true, start, _300ms, _200ms, 20, 3, 0, 90, 67, 100, 0, 0, 0},\n\t\t{false, start, _300ms, 0, 20, 3, 0, 0, 67, 100, 0, 0, 0},\n\t\t{false, start, _300ms, 0, 20, 3, 0, 0, 67, 100, 0, 0, 0},\n\t}\n\tfor i, s := range status {\n\t\t// XXX s := s\n\t\tif !statusesAreEqual(\u0026s, \u0026want[i]) {\n\t\t\tt.Errorf(\"r.Status(%v)\\nexpected: %v\\ngot : %v\", i, want[i].String(), s.String())\n\t\t}\n\t}\n\tif !bytes.Equal(b[:20], in[:20]) {\n\t\tt.Errorf(\"r.Read() input doesn't match output\")\n\t}\n}\n\n// XXX blocking writer test doesn't work.\nfunc _TestWriter(t *testing.T) {\n\tb := make([]byte, 100)\n\tfor i := range b {\n\t\tb[i] = byte(i)\n\t}\n\tw := NewWriter(\u0026bytes.Buffer{}, 200)\n\tstart := time.Now()\n\n\t// Make sure w implements Limiter\n\t_ = Limiter(w)\n\n\t// Non-blocking 20-byte write for the first sample returns ErrLimit\n\tw.SetBlocking(false)\n\tif n, err := w.Write(b); n != 20 || err != ErrLimit {\n\t\tt.Fatalf(\"w.Write(b) expected 20 (ErrLimit); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003e _50ms {\n\t\tt.Fatalf(\"w.Write(b) took too long (%v)\", rt)\n\t}\n\n\t// Blocking 80-byte write\n\t// w.SetBlocking(true)\n\t// XXX This test doesn't work, because w.Write calls w.Limit(block=false),\n\t// XXX and it returns ErrLimit after 20. What we want is to keep waiting until 80 is returned,\n\t// XXX but blocking isn't supported. Sleeping 800 shouldn't be sufficient either (its a burst).\n\t// XXX This limits the usage of Limiter and m.Limit().\n\tios_test.Sleep(800 * time.Millisecond)\n\tif n, err := w.Write(b[20:]); n \u003c 80 {\n\t} else if n != 80 || err != nil {\n\t\tt.Fatalf(\"w.Write(b[20:]) expected 80 (\u003cnil\u003e); got %v (%v)\", n, err.Error())\n\t} else if rt := time.Since(start); rt \u003c _300ms {\n\t\t// Explanation for `rt \u003c _300ms` (as opposed to `\u003c _400ms`)\n\t\t//\n\t\t// |\u003c-- start | |\n\t\t// epochs: -----0ms|---100ms|---200ms|---300ms|---400ms\n\t\t// sends: 20|20 |20 |20 |20#\n\t\t//\n\t\t// NOTE: The '#' symbol can thus happen before 400ms is up.\n\t\t// Thus, we can only panic if rt \u003c _300ms.\n\t\tt.Fatalf(\"w.Write(b[20:]) returned ahead of time (%v)\", rt.String())\n\t}\n\n\tw.SetTransferSize(100)\n\tstatus := []Status{w.Status(), nextStatus(w.Monitor)}\n\tstart = status[0].Start\n\n\t// Active, Start, Duration, Idle, Bytes, Samples, InstRate, CurRate, AvgRate, PeakRate, BytesRem, TimeRem, Progress\n\twant := []Status{\n\t\t{true, start, _400ms, 0, 80, 4, 200, 200, 200, 200, 20, _100ms, 80000},\n\t\t{true, start, _500ms, _100ms, 100, 5, 200, 200, 200, 200, 0, 0, 100000},\n\t}\n\tfor i, s := range status {\n\t\t// XXX s := s\n\t\tif !statusesAreEqual(\u0026s, \u0026want[i]) {\n\t\t\tt.Errorf(\"w.Status(%v)\\nexpected: %v\\ngot : %v\\n\", i, want[i].String(), s.String())\n\t\t}\n\t}\n\tif !bytes.Equal(b, w.Writer.(*bytes.Buffer).Bytes()) {\n\t\tt.Errorf(\"w.Write() input doesn't match output\")\n\t}\n}\n\nconst (\n\tmaxDeviationForDuration = 50 * time.Millisecond\n\tmaxDeviationForRate int64 = 50\n)\n\n// statusesAreEqual returns true if s1 is equal to s2. Equality here means\n// general equality of fields except for the duration and rates, which can\n// drift due to unpredictable delays (e.g. thread wakes up 25ms after\n// `time.Sleep` has ended).\nfunc statusesAreEqual(s1 *Status, s2 *Status) bool {\n\tif s1.Active == s2.Active \u0026\u0026\n\t\ts1.Start == s2.Start \u0026\u0026\n\t\tdurationsAreEqual(s1.Duration, s2.Duration, maxDeviationForDuration) \u0026\u0026\n\t\ts1.Idle == s2.Idle \u0026\u0026\n\t\ts1.Bytes == s2.Bytes \u0026\u0026\n\t\ts1.Samples == s2.Samples \u0026\u0026\n\t\tratesAreEqual(s1.InstRate, s2.InstRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.CurRate, s2.CurRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.AvgRate, s2.AvgRate, maxDeviationForRate) \u0026\u0026\n\t\tratesAreEqual(s1.PeakRate, s2.PeakRate, maxDeviationForRate) \u0026\u0026\n\t\ts1.BytesRem == s2.BytesRem \u0026\u0026\n\t\tdurationsAreEqual(s1.TimeRem, s2.TimeRem, maxDeviationForDuration) \u0026\u0026\n\t\ts1.Progress == s2.Progress {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc durationsAreEqual(d1 time.Duration, d2 time.Duration, maxDeviation time.Duration) bool {\n\treturn d2-d1 \u003c= maxDeviation\n}\n\nfunc ratesAreEqual(r1 int64, r2 int64, maxDeviation int64) bool {\n\tsub := r1 - r2\n\tif sub \u003c 0 {\n\t\tsub = -sub\n\t}\n\tif sub \u003c= maxDeviation {\n\t\treturn true\n\t}\n\treturn false\n}\n" + }, + { + "name": "util.gno", + "body": "//\n// Written by Maxim Khitrov (November 2012)\n//\n\npackage flow\n\nimport (\n\t\"math\"\n\t\"strconv\"\n\t\"time\"\n)\n\n// clockRate is the resolution and precision of clock().\nconst clockRate = 20 * time.Millisecond\n\n// czero is the process start time rounded down to the nearest clockRate\n// increment.\nvar czero = time.Now().Round(clockRate)\n\n// clock returns a low resolution timestamp relative to the process start time.\nfunc clock() time.Duration {\n\treturn time.Now().Round(clockRate).Sub(czero)\n}\n\n// clockToTime converts a clock() timestamp to an absolute time.Time value.\nfunc clockToTime(c time.Duration) time.Time {\n\treturn czero.Add(c)\n}\n\n// clockRound returns d rounded to the nearest clockRate increment.\nfunc clockRound(d time.Duration) time.Duration {\n\treturn (d + clockRate\u003e\u003e1) / clockRate * clockRate\n}\n\n// round returns x rounded to the nearest int64 (non-negative values only).\nfunc round(x float64) int64 {\n\tif _, frac := math.Modf(x); frac \u003e= 0.5 {\n\t\treturn int64(math.Ceil(x))\n\t}\n\treturn int64(math.Floor(x))\n}\n\n// Percent represents a percentage in increments of 1/1000th of a percent.\ntype Percent uint32\n\n// percentOf calculates what percent of the total is x.\nfunc percentOf(x, total float64) Percent {\n\tif x \u003c 0 || total \u003c= 0 {\n\t\treturn 0\n\t} else if p := round(x / total * 1e5); p \u003c= math.MaxUint32 {\n\t\treturn Percent(p)\n\t}\n\treturn Percent(math.MaxUint32)\n}\n\nfunc (p Percent) Float() float64 {\n\treturn float64(p) * 1e-3\n}\n\nfunc (p Percent) String() string {\n\tvar buf [12]byte\n\tb := strconv.AppendUint(buf[:0], uint64(p)/1000, 10)\n\tn := len(b)\n\tb = strconv.AppendUint(b, 1000+uint64(p)%1000, 10)\n\tb[n] = '.'\n\treturn string(append(b, '%'))\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "ahPo3UXWYsUbyT4BWLUMkNr+HQ+D4CRTfNJXOlHvA69XiIRy4tgjCACJOzDXjq23uIHdAM8a+qLUch/HlpTmDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "fqname", + "path": "gno.land/p/demo/fqname", + "files": [ + { + "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/demo/avl.Tree\")\n//\tufmt.Sprintf(\"Package: %s, Name: %s\\n\", id.Package, id.Name)\n//\t// Output: Package: gno.land/p/demo/avl, 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/demo/uassert\"\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/demo/avl.Tree\", \"gno.land/p/demo/avl\", \"Tree\"},\n\t\t{\"gno.land/p/demo/avl\", \"gno.land/p/demo/avl\", \"\"},\n\t\t{\"gno.land/p/demo/avl.Tree.Node\", \"gno.land/p/demo/avl\", \"Tree.Node\"},\n\t\t{\"gno.land/p/demo/avl/nested.Package.Func\", \"gno.land/p/demo/avl/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/demo/avl\", \"Tree\", \"[gno.land/p/demo/avl](/p/demo/avl).Tree\"},\n\t\t{\"gno.land/p/demo/avl\", \"\", \"[gno.land/p/demo/avl](/p/demo/avl)\"},\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "pELxbsA5xbk/kjQxTskWdE4q0Gppj2S7JoGCnjSiNzmjl8sPwzFO4gbUM2kB94tZrSefW6fsPV/d7typ6rTvCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "gnode", + "path": "gno.land/p/demo/gnode", + "files": [ + { + "name": "gnode.gno", + "body": "package gnode\n\n// XXX what about Gnodes signing on behalf of others?\n// XXX like a multi-sig of Gnodes?\n\ntype Name string\n\ntype Gnode interface {\n\t//----------------------------------------\n\t// Basic properties\n\tGetName() Name\n\n\t//----------------------------------------\n\t// Affiliate Gnodes\n\tNumAffiliates() int\n\tGetAffiliates(Name) Affiliate\n\tAddAffiliate(Affiliate) error // must be affiliated\n\tRemAffiliate(Name) error // must have become unaffiliated\n\n\t//----------------------------------------\n\t// Signing\n\tNumSignedDocuments() int\n\tGetSignedDocument(idx int) Document\n\tSignDocument(doc Document) (int, error) // index relative to signer\n\n\t//----------------------------------------\n\t// Rendering\n\tRenderLines() []string\n}\n\ntype Affiliate struct {\n\tType string\n\tGnode Gnode\n\tTags []string\n}\n\ntype MyGnode struct {\n\tName\n\t// Owners // voting set, something that gives authority of action.\n\t// Treasury //\n\t// Affiliates //\n\t// Board // discussions\n\t// Data // XXX ?\n}\n\ntype Affiliates []*Affiliate\n\n// Documents are equal if they compare equal.\n// NOTE: requires all fields to be comparable.\ntype Document struct {\n\tAuthors string\n\t// Timestamp\n\t// Body\n\t// Attachments\n}\n\n// ACTIONS\n\n// * Lend tokens\n// * Pay tokens\n// * Administrate transferrable and non-transferrable tokens\n// * Sum tokens\n// * Passthrough dependencies\n// * Code\n// * ...\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "FSZgfxgYrhfchzSkJ/cq52GenW1KgbLHuy8Cx1KYLYrp59nN2zLa8rBwLzEnraF2X1EfJyfG+Hh6d3FuNM+yAA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "agent", + "path": "gno.land/p/demo/gnorkle/agent", + "files": [ + { + "name": "whitelist.gno", + "body": "package agent\n\nimport \"gno.land/p/demo/avl\"\n\n// Whitelist manages whitelisted agent addresses.\ntype Whitelist struct {\n\tstore *avl.Tree\n}\n\n// ClearAddresses removes all addresses from the whitelist and puts into a state\n// that indicates it is moot and has no whitelist defined.\nfunc (m *Whitelist) ClearAddresses() {\n\tm.store = nil\n}\n\n// AddAddresses adds the given addresses to the whitelist.\nfunc (m *Whitelist) AddAddresses(addresses []string) {\n\tif m.store == nil {\n\t\tm.store = avl.NewTree()\n\t}\n\n\tfor _, address := range addresses {\n\t\tm.store.Set(address, struct{}{})\n\t}\n}\n\n// RemoveAddress removes the given address from the whitelist if it exists.\nfunc (m *Whitelist) RemoveAddress(address string) {\n\tif m.store == nil {\n\t\treturn\n\t}\n\n\tm.store.Remove(address)\n}\n\n// HasDefinition returns true if the whitelist has a definition. It retuns false if\n// `ClearAddresses` has been called without any subsequent `AddAddresses` calls, or\n// if `AddAddresses` has never been called.\nfunc (m Whitelist) HasDefinition() bool {\n\treturn m.store != nil\n}\n\n// HasAddress returns true if the given address is in the whitelist.\nfunc (m Whitelist) HasAddress(address string) bool {\n\tif m.store == nil {\n\t\treturn false\n\t}\n\n\treturn m.store.Has(address)\n}\n" + }, + { + "name": "whitelist_test.gno", + "body": "package agent_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/agent\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestWhitelist(t *testing.T) {\n\tvar whitelist agent.Whitelist\n\n\tuassert.False(t, whitelist.HasDefinition(), \"whitelist should not be defined initially\")\n\n\twhitelist.AddAddresses([]string{\"a\", \"b\"})\n\tuassert.True(t, whitelist.HasAddress(\"a\"), `whitelist should have address \"a\"`)\n\tuassert.True(t, whitelist.HasAddress(\"b\"), `whitelist should have address \"b\"`)\n\tuassert.True(t, whitelist.HasDefinition(), \"whitelist should be defined after adding addresses\")\n\n\twhitelist.RemoveAddress(\"a\")\n\tuassert.False(t, whitelist.HasAddress(\"a\"), `whitelist should not have address \"a\"`)\n\tuassert.True(t, whitelist.HasAddress(\"b\"), `whitelist should still have address \"b\"`)\n\n\twhitelist.ClearAddresses()\n\tuassert.False(t, whitelist.HasAddress(\"a\"), `whitelist cleared; should not have address \"a\"`)\n\tuassert.False(t, whitelist.HasAddress(\"b\"), `whitelist cleared; should still have address \"b\"`)\n\tuassert.False(t, whitelist.HasDefinition(), \"whitelist cleared; should not be defined\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "ji8ZSvyDoyPFt8KYMITivPqxmtQ7fEi41b7/DGQ+XobQWYAWNaEmshUE+bNmCKagGHesEtkcoJ4Hh9dXllaUAQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "feed", + "path": "gno.land/p/demo/gnorkle/feed", + "files": [ + { + "name": "errors.gno", + "body": "package feed\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"undefined feed\")\n" + }, + { + "name": "task.gno", + "body": "package feed\n\n// Task is a unit of work that can be part of a `Feed` definition. Tasks\n// are executed by agents.\ntype Task interface {\n\tMarshalJSON() ([]byte, error)\n}\n" + }, + { + "name": "type.gno", + "body": "package feed\n\n// Type indicates the type of a feed.\ntype Type int\n\nconst (\n\t// TypeStatic indicates a feed cannot be changed once the first value is committed.\n\tTypeStatic Type = iota\n\t// TypeContinuous indicates a feed can continuously ingest values and will publish\n\t// a new value on request using the values it has ingested.\n\tTypeContinuous\n\t// TypePeriodic indicates a feed can accept one or more values within a certain period\n\t// and will proceed to commit these values at the end up each period to produce an\n\t// aggregate value before starting a new period.\n\tTypePeriodic\n)\n" + }, + { + "name": "value.gno", + "body": "package feed\n\nimport \"time\"\n\n// Value represents a value published by a feed. The `Time` is when the value was published.\ntype Value struct {\n\tString string\n\tTime time.Time\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Zcxf5e97UPYz1qn50iFQPGYZgUQa7E1T7T5FAWLYMi6ulJE28Npdkw+z8NxPEHzYxsfBnyJhDYdWfRjYIi+sDw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "ingester", + "path": "gno.land/p/demo/gnorkle/ingester", + "files": [ + { + "name": "errors.gno", + "body": "package ingester\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"ingester undefined\")\n" + }, + { + "name": "type.gno", + "body": "package ingester\n\n// Type indicates an ingester type.\ntype Type int\n\nconst (\n\t// TypeSingle indicates an ingester that can only ingest a single within a given period or no period.\n\tTypeSingle Type = iota\n\t// TypeMulti indicates an ingester that can ingest multiple within a given period or no period\n\tTypeMulti\n)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "lnr2J1fkQAX4+4QX0qEo9aPOiHpWteSZqJcJwx2iWPO7egbq3zzmEW8v4TDeAP94C6yOo1PC4L8lId8A+PdHDg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "message", + "path": "gno.land/p/demo/gnorkle/message", + "files": [ + { + "name": "parse.gno", + "body": "package message\n\nimport \"strings\"\n\n// ParseFunc parses a raw message and returns the message function\n// type extracted from the remainder of the message.\nfunc ParseFunc(rawMsg string) (FuncType, string) {\n\tfuncType, remainder := parseFirstToken(rawMsg)\n\treturn FuncType(funcType), remainder\n}\n\n// ParseID parses a raw message and returns the ID extracted from\n// the remainder of the message.\nfunc ParseID(rawMsg string) (string, string) {\n\treturn parseFirstToken(rawMsg)\n}\n\nfunc parseFirstToken(rawMsg string) (string, string) {\n\tmsgParts := strings.SplitN(rawMsg, \",\", 2)\n\tif len(msgParts) \u003c 2 {\n\t\treturn msgParts[0], \"\"\n\t}\n\n\treturn msgParts[0], msgParts[1]\n}\n" + }, + { + "name": "parse_test.gno", + "body": "package message_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestParseFunc(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput string\n\t\texpFuncType message.FuncType\n\t\texpRemainder string\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t},\n\t\t{\n\t\t\tname: \"func only\",\n\t\t\tinput: \"ingest\",\n\t\t\texpFuncType: message.FuncTypeIngest,\n\t\t},\n\t\t{\n\t\t\tname: \"func with short remainder\",\n\t\t\tinput: \"commit,asdf\",\n\t\t\texpFuncType: message.FuncTypeCommit,\n\t\t\texpRemainder: \"asdf\",\n\t\t},\n\t\t{\n\t\t\tname: \"func with long remainder\",\n\t\t\tinput: \"request,hello,world,goodbye\",\n\t\t\texpFuncType: message.FuncTypeRequest,\n\t\t\texpRemainder: \"hello,world,goodbye\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tfuncType, remainder := message.ParseFunc(tt.input)\n\n\t\t\tuassert.Equal(t, string(tt.expFuncType), string(funcType))\n\t\t\tuassert.Equal(t, tt.expRemainder, remainder)\n\t\t})\n\t}\n}\n" + }, + { + "name": "type.gno", + "body": "package message\n\n// FuncType is the type of function that is being called by the agent.\ntype FuncType string\n\nconst (\n\t// FuncTypeIngest means the agent is sending data for ingestion.\n\tFuncTypeIngest FuncType = \"ingest\"\n\t// FuncTypeCommit means the agent is requesting a feed commit the transitive data\n\t// being held by its ingester.\n\tFuncTypeCommit FuncType = \"commit\"\n\t// FuncTypeRequest means the agent is requesting feed definitions for all those\n\t// that it is whitelisted to provide data for.\n\tFuncTypeRequest FuncType = \"request\"\n)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "DCM/ZXrmgiKVU+8JNWLepNP88ogMEAPnxhWO8kS2oDrHsw6lzlnlVXpOnhlR7hKG1GgLGUl9zmMzz4tAnDvoAA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "gnorkle", + "path": "gno.land/p/demo/gnorkle/gnorkle", + "files": [ + { + "name": "feed.gno", + "body": "package gnorkle\n\nimport (\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/message\"\n)\n\n// Feed is an abstraction used by a gnorkle `Instance` to ingest data from\n// agents and provide data feeds to consumers.\ntype Feed interface {\n\tID() string\n\tType() feed.Type\n\tValue() (value feed.Value, dataType string, consumable bool)\n\tIngest(funcType message.FuncType, rawMessage, providerAddress string) error\n\tMarshalJSON() ([]byte, error)\n\tTasks() []feed.Task\n\tIsActive() bool\n}\n\n// FeedWithWhitelist associates a `Whitelist` with a `Feed`.\ntype FeedWithWhitelist struct {\n\tFeed\n\tWhitelist\n}\n" + }, + { + "name": "ingester.gno", + "body": "package gnorkle\n\nimport \"gno.land/p/demo/gnorkle/ingester\"\n\n// Ingester is the abstraction that allows a `Feed` to ingest data from agents\n// and commit it to storage using zero or more intermediate aggregation steps.\ntype Ingester interface {\n\tType() ingester.Type\n\tIngest(value, providerAddress string) (canAutoCommit bool, err error)\n\tCommitValue(storage Storage, providerAddress string) error\n}\n" + }, + { + "name": "instance.gno", + "body": "package gnorkle\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/gnorkle/agent\"\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/message\"\n)\n\n// Instance is a single instance of an oracle.\ntype Instance struct {\n\tfeeds *avl.Tree\n\twhitelist agent.Whitelist\n}\n\n// NewInstance creates a new instance of an oracle.\nfunc NewInstance() *Instance {\n\treturn \u0026Instance{\n\t\tfeeds: avl.NewTree(),\n\t}\n}\n\nfunc assertValidID(id string) error {\n\tif len(id) == 0 {\n\t\treturn errors.New(\"feed ids cannot be empty\")\n\t}\n\n\tif strings.Contains(id, \",\") {\n\t\treturn errors.New(\"feed ids cannot contain commas\")\n\t}\n\n\treturn nil\n}\n\nfunc (i *Instance) assertFeedDoesNotExist(id string) error {\n\tif i.feeds.Has(id) {\n\t\treturn errors.New(\"feed already exists\")\n\t}\n\n\treturn nil\n}\n\n// AddFeeds adds feeds to the instance with empty whitelists.\nfunc (i *Instance) AddFeeds(feeds ...Feed) error {\n\tfor _, feed := range feeds {\n\t\tif err := assertValidID(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := i.assertFeedDoesNotExist(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ti.feeds.Set(\n\t\t\tfeed.ID(),\n\t\t\tFeedWithWhitelist{\n\t\t\t\tWhitelist: new(agent.Whitelist),\n\t\t\t\tFeed: feed,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// AddFeedsWithWhitelists adds feeds to the instance with the given whitelists.\nfunc (i *Instance) AddFeedsWithWhitelists(feeds ...FeedWithWhitelist) error {\n\tfor _, feed := range feeds {\n\t\tif err := i.assertFeedDoesNotExist(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := assertValidID(feed.ID()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ti.feeds.Set(\n\t\t\tfeed.ID(),\n\t\t\tFeedWithWhitelist{\n\t\t\t\tWhitelist: feed.Whitelist,\n\t\t\t\tFeed: feed,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// RemoveFeed removes a feed from the instance.\nfunc (i *Instance) RemoveFeed(id string) {\n\ti.feeds.Remove(id)\n}\n\n// PostMessageHandler is a type that allows for post-processing of feed state after a feed\n// ingests a message from an agent.\ntype PostMessageHandler interface {\n\tHandle(i *Instance, funcType message.FuncType, feed Feed) error\n}\n\n// HandleMessage handles a message from an agent and routes to either the logic that returns\n// feed definitions or the logic that allows a feed to ingest a message.\n//\n// TODO: Consider further message types that could allow administrative action such as modifying\n// a feed's whitelist without the owner of this oracle having to maintain a reference to it.\nfunc (i *Instance) HandleMessage(msg string, postHandler PostMessageHandler) (string, error) {\n\tcaller := string(std.OriginCaller())\n\n\tfuncType, msg := message.ParseFunc(msg)\n\n\tswitch funcType {\n\tcase message.FuncTypeRequest:\n\t\treturn i.GetFeedDefinitions(caller)\n\n\tdefault:\n\t\tid, msg := message.ParseID(msg)\n\t\tif err := assertValidID(id); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tfeedWithWhitelist, err := i.getFeedWithWhitelist(id)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif !addressIsWhitelisted(\u0026i.whitelist, feedWithWhitelist, caller, nil) {\n\t\t\treturn \"\", errors.New(\"caller not whitelisted\")\n\t\t}\n\n\t\tif err := feedWithWhitelist.Ingest(funcType, msg, caller); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif postHandler != nil {\n\t\t\tpostHandler.Handle(i, funcType, feedWithWhitelist)\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\nfunc (i *Instance) getFeed(id string) (Feed, error) {\n\tuntypedFeed, ok := i.feeds.Get(id)\n\tif !ok {\n\t\treturn nil, errors.New(\"invalid ingest id: \" + id)\n\t}\n\n\tfeed, ok := untypedFeed.(Feed)\n\tif !ok {\n\t\treturn nil, errors.New(\"invalid feed type\")\n\t}\n\n\treturn feed, nil\n}\n\nfunc (i *Instance) getFeedWithWhitelist(id string) (FeedWithWhitelist, error) {\n\tuntypedFeedWithWhitelist, ok := i.feeds.Get(id)\n\tif !ok {\n\t\treturn FeedWithWhitelist{}, errors.New(\"invalid ingest id: \" + id)\n\t}\n\n\tfeedWithWhitelist, ok := untypedFeedWithWhitelist.(FeedWithWhitelist)\n\tif !ok {\n\t\treturn FeedWithWhitelist{}, errors.New(\"invalid feed with whitelist type\")\n\t}\n\n\treturn feedWithWhitelist, nil\n}\n\n// GetFeedValue returns the most recently published value of a feed along with a string\n// representation of the value's type and boolean indicating whether the value is\n// okay for consumption.\nfunc (i *Instance) GetFeedValue(id string) (feed.Value, string, bool, error) {\n\tfoundFeed, err := i.getFeed(id)\n\tif err != nil {\n\t\treturn feed.Value{}, \"\", false, err\n\t}\n\n\tvalue, valueType, consumable := foundFeed.Value()\n\treturn value, valueType, consumable, nil\n}\n\n// GetFeedDefinitions returns a JSON string representing the feed definitions for which the given\n// agent address is whitelisted to provide values for ingestion.\nfunc (i *Instance) GetFeedDefinitions(forAddress string) (string, error) {\n\tinstanceHasAddressWhitelisted := !i.whitelist.HasDefinition() || i.whitelist.HasAddress(forAddress)\n\n\tbuf := new(strings.Builder)\n\tbuf.WriteString(\"[\")\n\tfirst := true\n\tvar err error\n\n\t// The boolean value returned by this callback function indicates whether to stop iterating.\n\ti.feeds.Iterate(\"\", \"\", func(_ string, value any) bool {\n\t\tfeedWithWhitelist, ok := value.(FeedWithWhitelist)\n\t\tif !ok {\n\t\t\terr = errors.New(\"invalid feed type\")\n\t\t\treturn true\n\t\t}\n\n\t\t// Don't give agents the ability to try to publish to inactive feeds.\n\t\tif !feedWithWhitelist.IsActive() {\n\t\t\treturn false\n\t\t}\n\n\t\t// Skip feeds the address is not whitelisted for.\n\t\tif !addressIsWhitelisted(\u0026i.whitelist, feedWithWhitelist, forAddress, \u0026instanceHasAddressWhitelisted) {\n\t\t\treturn false\n\t\t}\n\n\t\tvar taskBytes []byte\n\t\tif taskBytes, err = feedWithWhitelist.Feed.MarshalJSON(); err != nil {\n\t\t\treturn true\n\t\t}\n\n\t\t// Guard against any tasks that shouldn't be returned; maybe they are not active because they have\n\t\t// already been completed.\n\t\tif len(taskBytes) == 0 {\n\t\t\treturn false\n\t\t}\n\n\t\tif !first {\n\t\t\tbuf.WriteString(\",\")\n\t\t}\n\n\t\tfirst = false\n\t\tbuf.Write(taskBytes)\n\t\treturn false\n\t})\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbuf.WriteString(\"]\")\n\treturn buf.String(), nil\n}\n" + }, + { + "name": "storage.gno", + "body": "package gnorkle\n\nimport \"gno.land/p/demo/gnorkle/feed\"\n\n// Storage defines how published feed values should be read\n// and written.\ntype Storage interface {\n\tPut(value string) error\n\tGetLatest() feed.Value\n\tGetHistory() []feed.Value\n}\n" + }, + { + "name": "whitelist.gno", + "body": "package gnorkle\n\n// Whitelist is used to manage which agents are allowed to interact.\ntype Whitelist interface {\n\tClearAddresses()\n\tAddAddresses(addresses []string)\n\tRemoveAddress(address string)\n\tHasDefinition() bool\n\tHasAddress(address string) bool\n}\n\n// ClearWhitelist clears the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) ClearWhitelist(feedID string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.ClearAddresses()\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.ClearAddresses()\n\treturn nil\n}\n\n// AddToWhitelist adds the given addresses to the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) AddToWhitelist(feedID string, addresses []string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.AddAddresses(addresses)\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.AddAddresses(addresses)\n\treturn nil\n}\n\n// RemoveFromWhitelist removes the given address from the whitelist of the instance or feed depending on the feed ID.\nfunc (i *Instance) RemoveFromWhitelist(feedID string, address string) error {\n\tif feedID == \"\" {\n\t\ti.whitelist.RemoveAddress(address)\n\t\treturn nil\n\t}\n\n\tfeedWithWhitelist, err := i.getFeedWithWhitelist(feedID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfeedWithWhitelist.RemoveAddress(address)\n\treturn nil\n}\n\n// addressWhiteListed returns true if:\n// - the feed has a white list and the address is whitelisted, or\n// - the feed has no white list and the instance has a white list and the address is whitelisted, or\n// - the feed has no white list and the instance has no white list.\nfunc addressIsWhitelisted(instanceWhitelist, feedWhitelist Whitelist, address string, instanceWhitelistedOverride *bool) bool {\n\t// A feed whitelist takes priority, so it will return false if the feed has a whitelist and the caller is\n\t// not a part of it. An empty whitelist defers to the instance whitelist.\n\tif feedWhitelist != nil {\n\t\tif feedWhitelist.HasDefinition() \u0026\u0026 !feedWhitelist.HasAddress(address) {\n\t\t\treturn false\n\t\t}\n\n\t\t// Getting to this point means that one of the following is true:\n\t\t// - the feed has no defined whitelist (so it can't possibly have the address whitelisted)\n\t\t// - the feed has a defined whitelist and the caller is a part of it\n\t\t//\n\t\t// In this case, we can be sure that the boolean indicating whether the feed has this address whitelisted\n\t\t// is equivalent to the boolean indicating whether the feed has a defined whitelist.\n\t\tif feedWhitelist.HasDefinition() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tif instanceWhitelistedOverride != nil {\n\t\treturn *instanceWhitelistedOverride\n\t}\n\n\t// We were unable able to determine whether this address is allowed after looking at the feed whitelist,\n\t// so fall back to the instance whitelist. A complete absence of values in the instance whitelist means\n\t// that the instance has no whitelist so we can return true because everything is allowed by default.\n\tif instanceWhitelist == nil || !instanceWhitelist.HasDefinition() {\n\t\treturn true\n\t}\n\n\t// The instance whitelist is defined so if the address is present then it is allowed.\n\treturn instanceWhitelist.HasAddress(address)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "8q8eOUUioCjkXPlkL9Jc6kDA6mpkW+afYL/dvf4CMlauXnyynvpzTse75PEy67VDDZH2JDel8Vd9VIEi4PzpBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "storage", + "path": "gno.land/p/demo/gnorkle/storage", + "files": [ + { + "name": "errors.gno", + "body": "package storage\n\nimport \"errors\"\n\nvar ErrUndefined = errors.New(\"undefined storage\")\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "eJCl67SS2ejZPDCGlPws2B9RGGnL0oOr40zeya/HvqTSfPn/R7akMLFy3E70z8rrmp+GyEQip9ct86620cCpCA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "simple", + "path": "gno.land/p/demo/gnorkle/storage/simple", + "files": [ + { + "name": "storage.gno", + "body": "package simple\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/storage\"\n)\n\n// Storage is simple, bounded storage for published feed values.\ntype Storage struct {\n\tvalues []feed.Value\n\tmaxValues uint\n}\n\n// NewStorage creates a new Storage with the given maximum number of values.\n// If maxValues is 0, the storage is bounded to a size of one. If this is not desirable,\n// then don't provide a value of 0.\nfunc NewStorage(maxValues uint) *Storage {\n\tif maxValues == 0 {\n\t\tmaxValues = 1\n\t}\n\n\treturn \u0026Storage{\n\t\tmaxValues: maxValues,\n\t}\n}\n\n// Put adds a new value to the storage. If the storage is full, the oldest value\n// is removed. If maxValues is 0, the storage is bounded to a size of one.\nfunc (s *Storage) Put(value string) error {\n\tif s == nil {\n\t\treturn storage.ErrUndefined\n\t}\n\n\ts.values = append(s.values, feed.Value{String: value, Time: time.Now()})\n\tif uint(len(s.values)) \u003e s.maxValues {\n\t\ts.values = s.values[1:]\n\t}\n\n\treturn nil\n}\n\n// GetLatest returns the most recently added value, or an empty value if none exist.\nfunc (s Storage) GetLatest() feed.Value {\n\tif len(s.values) == 0 {\n\t\treturn feed.Value{}\n\t}\n\n\treturn s.values[len(s.values)-1]\n}\n\n// GetHistory returns all values in the storage, from oldest to newest.\nfunc (s Storage) GetHistory() []feed.Value {\n\treturn s.values\n}\n" + }, + { + "name": "storage_test.gno", + "body": "package simple_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/storage\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestStorage(t *testing.T) {\n\tvar undefinedStorage *simple.Storage\n\terr := undefinedStorage.Put(\"\")\n\tuassert.ErrorIs(t, err, storage.ErrUndefined, \"expected storage.ErrUndefined on undefined storage\")\n\n\ttests := []struct {\n\t\tname string\n\t\tvaluesToPut []string\n\t\texpLatestValueString string\n\t\texpLatestValueTimeIsZero bool\n\t\texpHistoricalValueStrings []string\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\texpLatestValueTimeIsZero: true,\n\t\t},\n\t\t{\n\t\t\tname: \"one value\",\n\t\t\tvaluesToPut: []string{\"one\"},\n\t\t\texpLatestValueString: \"one\",\n\t\t\texpHistoricalValueStrings: []string{\"one\"},\n\t\t},\n\t\t{\n\t\t\tname: \"two values\",\n\t\t\tvaluesToPut: []string{\"one\", \"two\"},\n\t\t\texpLatestValueString: \"two\",\n\t\t\texpHistoricalValueStrings: []string{\"one\", \"two\"},\n\t\t},\n\t\t{\n\t\t\tname: \"three values\",\n\t\t\tvaluesToPut: []string{\"one\", \"two\", \"three\"},\n\t\t\texpLatestValueString: \"three\",\n\t\t\texpHistoricalValueStrings: []string{\"two\", \"three\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsimpleStorage := simple.NewStorage(2)\n\t\t\tfor _, value := range tt.valuesToPut {\n\t\t\t\terr := simpleStorage.Put(value)\n\t\t\t\turequire.NoError(t, err, \"unexpected error putting value in storage\")\n\t\t\t}\n\n\t\t\tlatestValue := simpleStorage.GetLatest()\n\t\t\tuassert.Equal(t, tt.expLatestValueString, latestValue.String)\n\t\t\tuassert.Equal(t, tt.expLatestValueTimeIsZero, latestValue.Time.IsZero())\n\n\t\t\thistoricalValues := simpleStorage.GetHistory()\n\t\t\turequire.Equal(t, len(tt.expHistoricalValueStrings), len(historicalValues), \"historical values length does not match\")\n\n\t\t\tfor i, expValue := range tt.expHistoricalValueStrings {\n\t\t\t\tuassert.Equal(t, historicalValues[i].String, expValue)\n\t\t\t\turequire.False(t, historicalValues[i].Time.IsZero(), ufmt.Sprintf(\"unexpeced zero time for historical value at index %d\", i))\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "PCAqCdgiYOMLb0/HHpaGP2it1iM8c6ynn392k450j/j6D8TfPwR+cIyWmiV5iIfxaGMqXEHFkSJFaXQAlgi+CA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "single", + "path": "gno.land/p/demo/gnorkle/ingesters/single", + "files": [ + { + "name": "ingester.gno", + "body": "package single\n\nimport (\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingester\"\n)\n\n// ValueIngester is an ingester that ingests a single value.\ntype ValueIngester struct {\n\tvalue string\n}\n\n// Type returns the type of the ingester.\nfunc (i *ValueIngester) Type() ingester.Type {\n\treturn ingester.TypeSingle\n}\n\n// Ingest ingests a value provided by the given agent address.\nfunc (i *ValueIngester) Ingest(value, providerAddress string) (bool, error) {\n\tif i == nil {\n\t\treturn false, ingester.ErrUndefined\n\t}\n\n\ti.value = value\n\treturn true, nil\n}\n\n// CommitValue commits the ingested value to the given storage instance.\nfunc (i *ValueIngester) CommitValue(valueStorer gnorkle.Storage, providerAddress string) error {\n\tif i == nil {\n\t\treturn ingester.ErrUndefined\n\t}\n\n\treturn valueStorer.Put(i.value)\n}\n" + }, + { + "name": "ingester_test.gno", + "body": "package single_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/ingester\"\n\t\"gno.land/p/demo/gnorkle/ingesters/single\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestValueIngester(t *testing.T) {\n\tstorage := simple.NewStorage(1)\n\n\tvar undefinedIngester *single.ValueIngester\n\t_, err := undefinedIngester.Ingest(\"asdf\", \"gno11111\")\n\tuassert.ErrorIs(t, err, ingester.ErrUndefined, \"undefined ingester call to Ingest should return ingester.ErrUndefined\")\n\n\terr = undefinedIngester.CommitValue(storage, \"gno11111\")\n\tuassert.ErrorIs(t, err, ingester.ErrUndefined, \"undefined ingester call to CommitValue should return ingester.ErrUndefined\")\n\n\tvar valueIngester single.ValueIngester\n\ttyp := valueIngester.Type()\n\tuassert.Equal(t, int(ingester.TypeSingle), int(typ), \"single value ingester should return type ingester.TypeSingle\")\n\n\tingestValue := \"value\"\n\tautocommit, err := valueIngester.Ingest(ingestValue, \"gno11111\")\n\tuassert.True(t, autocommit, \"single value ingester should return autocommit true\")\n\tuassert.NoError(t, err)\n\n\terr = valueIngester.CommitValue(storage, \"gno11111\")\n\tuassert.NoError(t, err)\n\n\tlatestValue := storage.GetLatest()\n\tuassert.Equal(t, ingestValue, latestValue.String)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "aliZzVJnm3UXH3VOmLUcsTd7pGchs+l9uDIyGD5Qbc1zNOag3wZGy+FoeLvSY5n9zzG3/GDI7rR5hmYN1UTkBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "static", + "path": "gno.land/p/demo/gnorkle/feeds/static", + "files": [ + { + "name": "feed.gno", + "body": "package static\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingesters/single\"\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Feed is a static feed.\ntype Feed struct {\n\tid string\n\tisLocked bool\n\tvalueDataType string\n\tingester gnorkle.Ingester\n\tstorage gnorkle.Storage\n\ttasks []feed.Task\n}\n\n// NewFeed creates a new static feed.\nfunc NewFeed(\n\tid string,\n\tvalueDataType string,\n\tingester gnorkle.Ingester,\n\tstorage gnorkle.Storage,\n\ttasks ...feed.Task,\n) *Feed {\n\treturn \u0026Feed{\n\t\tid: id,\n\t\tvalueDataType: valueDataType,\n\t\tingester: ingester,\n\t\tstorage: storage,\n\t\ttasks: tasks,\n\t}\n}\n\n// NewSingleValueFeed is a convenience function for creating a static feed\n// that autocommits a value after a single ingestion.\nfunc NewSingleValueFeed(\n\tid string,\n\tvalueDataType string,\n\ttasks ...feed.Task,\n) *Feed {\n\treturn NewFeed(\n\t\tid,\n\t\tvalueDataType,\n\t\t\u0026single.ValueIngester{},\n\t\tsimple.NewStorage(1),\n\t\ttasks...,\n\t)\n}\n\n// ID returns the feed's ID.\nfunc (f Feed) ID() string {\n\treturn f.id\n}\n\n// Type returns the feed's type.\nfunc (f Feed) Type() feed.Type {\n\treturn feed.TypeStatic\n}\n\n// Ingest ingests a message into the feed. It either adds the value to the ingester's\n// pending values or commits the value to the storage.\nfunc (f *Feed) Ingest(funcType message.FuncType, msg, providerAddress string) error {\n\tif f == nil {\n\t\treturn feed.ErrUndefined\n\t}\n\n\tif f.isLocked {\n\t\treturn errors.New(\"feed locked\")\n\t}\n\n\tswitch funcType {\n\tcase message.FuncTypeIngest:\n\t\t// Autocommit the ingester's value if it's a single value ingester\n\t\t// because this is a static feed and this is the only value it will ever have.\n\t\tif canAutoCommit, err := f.ingester.Ingest(msg, providerAddress); canAutoCommit \u0026\u0026 err == nil {\n\t\t\tif err := f.ingester.CommitValue(f.storage, providerAddress); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tf.isLocked = true\n\t\t} else if err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase message.FuncTypeCommit:\n\t\tif err := f.ingester.CommitValue(f.storage, providerAddress); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tf.isLocked = true\n\n\tdefault:\n\t\treturn errors.New(\"invalid message function \" + string(funcType))\n\t}\n\n\treturn nil\n}\n\n// Value returns the feed's latest value, it's data type, and whether or not it can\n// be safely consumed. In this case it uses `f.isLocked` because, this being a static\n// feed, it will only ever have one value; once that value is committed the feed is locked\n// and there is a valid, non-empty value to consume.\nfunc (f Feed) Value() (feed.Value, string, bool) {\n\treturn f.storage.GetLatest(), f.valueDataType, f.isLocked\n}\n\n// MarshalJSON marshals the components of the feed that are needed for\n// an agent to execute tasks and send values for ingestion.\nfunc (f Feed) MarshalJSON() ([]byte, error) {\n\tbuf := new(bytes.Buffer)\n\tw := bufio.NewWriter(buf)\n\n\tw.Write([]byte(\n\t\t`{\"id\":\"` + f.id +\n\t\t\t`\",\"type\":\"` + ufmt.Sprintf(\"%d\", int(f.Type())) +\n\t\t\t`\",\"value_type\":\"` + f.valueDataType +\n\t\t\t`\",\"tasks\":[`),\n\t)\n\n\tfirst := true\n\tfor _, task := range f.tasks {\n\t\tif !first {\n\t\t\tw.WriteString(\",\")\n\t\t}\n\n\t\ttaskJSON, err := task.MarshalJSON()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tw.Write(taskJSON)\n\t\tfirst = false\n\t}\n\n\tw.Write([]byte(\"]}\"))\n\tw.Flush()\n\n\treturn buf.Bytes(), nil\n}\n\n// Tasks returns the feed's tasks. This allows task consumers to extract task\n// contents without having to marshal the entire feed.\nfunc (f Feed) Tasks() []feed.Task {\n\treturn f.tasks\n}\n\n// IsActive returns true if the feed is accepting ingestion requests from agents.\nfunc (f Feed) IsActive() bool {\n\treturn !f.isLocked\n}\n" + }, + { + "name": "feed_test.gno", + "body": "package static_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/gnorkle/feed\"\n\t\"gno.land/p/demo/gnorkle/feeds/static\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/ingester\"\n\t\"gno.land/p/demo/gnorkle/message\"\n\t\"gno.land/p/demo/gnorkle/storage/simple\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\ntype mockIngester struct {\n\tcanAutoCommit bool\n\tingestErr error\n\tcommitErr error\n\tvalue string\n\tproviderAddress string\n}\n\nfunc (i mockIngester) Type() ingester.Type {\n\treturn ingester.Type(0)\n}\n\nfunc (i *mockIngester) Ingest(value, providerAddress string) (bool, error) {\n\tif i.ingestErr != nil {\n\t\treturn false, i.ingestErr\n\t}\n\n\ti.value = value\n\ti.providerAddress = providerAddress\n\treturn i.canAutoCommit, nil\n}\n\nfunc (i *mockIngester) CommitValue(storage gnorkle.Storage, providerAddress string) error {\n\tif i.commitErr != nil {\n\t\treturn i.commitErr\n\t}\n\n\treturn storage.Put(i.value)\n}\n\nfunc TestNewSingleValueFeed(t *testing.T) {\n\tstaticFeed := static.NewSingleValueFeed(\"1\", \"\")\n\n\tuassert.Equal(t, \"1\", staticFeed.ID())\n\tuassert.Equal(t, int(feed.TypeStatic), int(staticFeed.Type()))\n}\n\nfunc TestFeed_Ingest(t *testing.T) {\n\tvar undefinedFeed *static.Feed\n\terr := undefinedFeed.Ingest(\"\", \"\", \"\")\n\tuassert.ErrorIs(t, err, feed.ErrUndefined)\n\n\ttests := []struct {\n\t\tname string\n\t\tingester *mockIngester\n\t\tverifyIsLocked bool\n\t\tdoCommit bool\n\t\tfuncType message.FuncType\n\t\tmsg string\n\t\tproviderAddress string\n\t\texpFeedValueString string\n\t\texpErrText string\n\t\texpIsActive bool\n\t}{\n\t\t{\n\t\t\tname: \"func invalid error\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncType(\"derp\"),\n\t\t\texpErrText: \"invalid message function derp\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func ingest ingest error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tingestErr: errors.New(\"ingest error\"),\n\t\t\t},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\texpErrText: \"ingest error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func ingest commit error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tcommitErr: errors.New(\"commit error\"),\n\t\t\t\tcanAutoCommit: true,\n\t\t\t},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\texpErrText: \"commit error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"func commit commit error\",\n\t\t\tingester: \u0026mockIngester{\n\t\t\t\tcommitErr: errors.New(\"commit error\"),\n\t\t\t\tcanAutoCommit: true,\n\t\t\t},\n\t\t\tfuncType: message.FuncTypeCommit,\n\t\t\texpErrText: \"commit error\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"only ingest\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\tmsg: \"still active feed\",\n\t\t\tproviderAddress: \"gno1234\",\n\t\t\texpIsActive: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ingest autocommit\",\n\t\t\tingester: \u0026mockIngester{canAutoCommit: true},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\tmsg: \"still active feed\",\n\t\t\tproviderAddress: \"gno1234\",\n\t\t\texpFeedValueString: \"still active feed\",\n\t\t\tverifyIsLocked: true,\n\t\t},\n\t\t{\n\t\t\tname: \"commit no value\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncTypeCommit,\n\t\t\tmsg: \"shouldn't be stored\",\n\t\t\tverifyIsLocked: true,\n\t\t},\n\t\t{\n\t\t\tname: \"ingest then commmit\",\n\t\t\tingester: \u0026mockIngester{},\n\t\t\tfuncType: message.FuncTypeIngest,\n\t\t\tmsg: \"blahblah\",\n\t\t\tdoCommit: true,\n\t\t\texpFeedValueString: \"blahblah\",\n\t\t\tverifyIsLocked: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstaticFeed := static.NewFeed(\n\t\t\t\t\"1\",\n\t\t\t\t\"string\",\n\t\t\t\ttt.ingester,\n\t\t\t\tsimple.NewStorage(1),\n\t\t\t\tnil,\n\t\t\t)\n\n\t\t\tvar errText string\n\t\t\tif err := staticFeed.Ingest(tt.funcType, tt.msg, tt.providerAddress); err != nil {\n\t\t\t\terrText = err.Error()\n\t\t\t}\n\n\t\t\turequire.Equal(t, tt.expErrText, errText)\n\n\t\t\tif tt.doCommit {\n\t\t\t\terr := staticFeed.Ingest(message.FuncTypeCommit, \"\", \"\")\n\t\t\t\turequire.NoError(t, err, \"follow up commit failed\")\n\t\t\t}\n\n\t\t\tif tt.verifyIsLocked {\n\t\t\t\terrText = \"\"\n\t\t\t\tif err := staticFeed.Ingest(tt.funcType, tt.msg, tt.providerAddress); err != nil {\n\t\t\t\t\terrText = err.Error()\n\t\t\t\t}\n\n\t\t\t\turequire.Equal(t, \"feed locked\", errText)\n\t\t\t}\n\n\t\t\tuassert.Equal(t, tt.providerAddress, tt.ingester.providerAddress)\n\n\t\t\tfeedValue, dataType, isLocked := staticFeed.Value()\n\t\t\tuassert.Equal(t, tt.expFeedValueString, feedValue.String)\n\t\t\tuassert.Equal(t, \"string\", dataType)\n\t\t\tuassert.Equal(t, tt.verifyIsLocked, isLocked)\n\t\t\tuassert.Equal(t, tt.expIsActive, staticFeed.IsActive())\n\t\t})\n\t}\n}\n\ntype mockTask struct {\n\terr error\n\tvalue string\n}\n\nfunc (t mockTask) MarshalJSON() ([]byte, error) {\n\tif t.err != nil {\n\t\treturn nil, t.err\n\t}\n\n\treturn []byte(`{\"value\":\"` + t.value + `\"}`), nil\n}\n\nfunc TestFeed_Tasks(t *testing.T) {\n\tid := \"99\"\n\tvalueDataType := \"int\"\n\n\ttests := []struct {\n\t\tname string\n\t\ttasks []feed.Task\n\t\texpErrText string\n\t\texpJSON string\n\t}{\n\t\t{\n\t\t\tname: \"no tasks\",\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"marshal error\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{err: errors.New(\"marshal error\")},\n\t\t\t},\n\t\t\texpErrText: \"marshal error\",\n\t\t},\n\t\t{\n\t\t\tname: \"one task\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{value: \"single\"},\n\t\t\t},\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[{\"value\":\"single\"}]}`,\n\t\t},\n\t\t{\n\t\t\tname: \"two tasks\",\n\t\t\ttasks: []feed.Task{\n\t\t\t\tmockTask{value: \"first\"},\n\t\t\t\tmockTask{value: \"second\"},\n\t\t\t},\n\t\t\texpJSON: `{\"id\":\"99\",\"type\":\"0\",\"value_type\":\"int\",\"tasks\":[{\"value\":\"first\"},{\"value\":\"second\"}]}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tstaticFeed := static.NewSingleValueFeed(\n\t\t\t\tid,\n\t\t\t\tvalueDataType,\n\t\t\t\ttt.tasks...,\n\t\t\t)\n\n\t\t\turequire.Equal(t, len(tt.tasks), len(staticFeed.Tasks()))\n\n\t\t\tvar errText string\n\t\t\tjson, err := staticFeed.MarshalJSON()\n\t\t\tif err != nil {\n\t\t\t\terrText = err.Error()\n\t\t\t}\n\n\t\t\turequire.Equal(t, tt.expErrText, errText)\n\t\t\turequire.Equal(t, tt.expJSON, string(json))\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "sgP6W0XZ6HNi1GlvDBa1j1/t8cD/Qk/QMi3upx759afSz8CSyvFUWH9/k4HTlBmGd1ERlv301w1pmpSP2xJqCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "exts", + "path": "gno.land/p/demo/grc/exts", + "files": [ + { + "name": "token_metadata.gno", + "body": "package exts\n\ntype TokenMetadata 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() uint\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "x9YUqsmLyEZcgxwkJ5zumgW1v/s9uhJbz2QF0nMWZCJiJLsIwz/Qj5IjdMYcFFeoxKBzAw2rhKjqsSLvtL0bCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "grc1155", + "path": "gno.land/p/demo/grc/grc1155", + "files": [ + { + "name": "README.md", + "body": "# GRC-1155 Spec: Multi Token Standard\n\nGRC1155 is a specification for managing multiple tokens based on Gnoland. The name and design is based on Ethereum's ERC1155 standard.\n\n## See also:\n\n[ERC-1155 Spec][erc-1155]\n\n[erc-1155]: https://eips.ethereum.org/EIPS/eip-1155" + }, + { + "name": "basic_grc1155_token.gno", + "body": "package grc1155\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype basicGRC1155Token struct {\n\turi string\n\tbalances avl.Tree // \"TokenId:Address\" -\u003e uint64\n\toperatorApprovals avl.Tree // \"OwnerAddress:OperatorAddress\" -\u003e bool\n}\n\nvar _ IGRC1155 = (*basicGRC1155Token)(nil)\n\n// Returns new basic GRC1155 token\nfunc NewBasicGRC1155Token(uri string) *basicGRC1155Token {\n\treturn \u0026basicGRC1155Token{\n\t\turi: uri,\n\t\tbalances: avl.Tree{},\n\t\toperatorApprovals: avl.Tree{},\n\t}\n}\n\nfunc (s *basicGRC1155Token) Uri() string { return s.uri }\n\n// BalanceOf returns the input address's balance of the token type requested\nfunc (s *basicGRC1155Token) BalanceOf(addr std.Address, tid TokenID) (uint64, error) {\n\tif !isValidAddress(addr) {\n\t\treturn 0, ErrInvalidAddress\n\t}\n\n\tkey := string(tid) + \":\" + addr.String()\n\tbalance, found := s.balances.Get(key)\n\tif !found {\n\t\treturn 0, nil\n\t}\n\n\treturn balance.(uint64), nil\n}\n\n// BalanceOfBatch returns the balance of multiple account/token pairs\nfunc (s *basicGRC1155Token) BalanceOfBatch(owners []std.Address, batch []TokenID) ([]uint64, error) {\n\tif len(owners) != len(batch) {\n\t\treturn nil, ErrMismatchLength\n\t}\n\n\tbalanceOfBatch := make([]uint64, len(owners))\n\n\tfor i := 0; i \u003c len(owners); i++ {\n\t\tbalanceOfBatch[i], _ = s.BalanceOf(owners[i], batch[i])\n\t}\n\n\treturn balanceOfBatch, nil\n}\n\n// SetApprovalForAll can approve the operator to operate on all tokens\nfunc (s *basicGRC1155Token) SetApprovalForAll(operator std.Address, approved bool) error {\n\tif !isValidAddress(operator) {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.OriginCaller()\n\treturn s.setApprovalForAll(caller, operator, approved)\n}\n\n// IsApprovedForAll returns true if operator is the owner or is approved for all by the owner.\n// Otherwise, returns false\nfunc (s *basicGRC1155Token) IsApprovedForAll(owner, operator std.Address) bool {\n\tif operator == owner {\n\t\treturn true\n\t}\n\tkey := owner.String() + \":\" + operator.String()\n\t_, found := s.operatorApprovals.Get(key)\n\tif !found {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Safely transfers `tokenId` token from `from` to `to`, checking that\n// contract recipients are aware of the GRC1155 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicGRC1155Token) SafeTransferFrom(from, to std.Address, tid TokenID, amount uint64) error {\n\tcaller := std.OriginCaller()\n\tif !s.IsApprovedForAll(caller, from) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.safeBatchTransferFrom(from, to, []TokenID{tid}, []uint64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeTransferAcceptanceCheck(caller, from, to, tid, amount) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, from, to, tid, amount})\n\n\treturn nil\n}\n\n// Safely transfers a `batch` of tokens from `from` to `to`, checking that\n// contract recipients are aware of the GRC1155 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicGRC1155Token) SafeBatchTransferFrom(from, to std.Address, batch []TokenID, amounts []uint64) error {\n\tcaller := std.OriginCaller()\n\tif !s.IsApprovedForAll(caller, from) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.safeBatchTransferFrom(from, to, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeBatchTransferAcceptanceCheck(caller, from, to, batch, amounts) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, from, to, batch, amounts})\n\n\treturn nil\n}\n\n// Creates `amount` tokens of token type `id`, and assigns them to `to`. Also checks that\n// contract recipients are using GRC1155 protocol.\nfunc (s *basicGRC1155Token) SafeMint(to std.Address, tid TokenID, amount uint64) error {\n\tcaller := std.OriginCaller()\n\n\terr := s.mintBatch(to, []TokenID{tid}, []uint64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeTransferAcceptanceCheck(caller, zeroAddress, to, tid, amount) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, zeroAddress, to, tid, amount})\n\n\treturn nil\n}\n\n// Batch version of `SafeMint()`. Also checks that\n// contract recipients are using GRC1155 protocol.\nfunc (s *basicGRC1155Token) SafeBatchMint(to std.Address, batch []TokenID, amounts []uint64) error {\n\tcaller := std.OriginCaller()\n\n\terr := s.mintBatch(to, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.doSafeBatchTransferAcceptanceCheck(caller, zeroAddress, to, batch, amounts) {\n\t\treturn ErrTransferToRejectedOrNonGRC1155Receiver\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, zeroAddress, to, batch, amounts})\n\n\treturn nil\n}\n\n// Destroys `amount` tokens of token type `id` from `from`.\nfunc (s *basicGRC1155Token) Burn(from std.Address, tid TokenID, amount uint64) error {\n\tcaller := std.OriginCaller()\n\n\terr := s.burnBatch(from, []TokenID{tid}, []uint64{amount})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\temit(\u0026TransferSingleEvent{caller, from, zeroAddress, tid, amount})\n\n\treturn nil\n}\n\n// Batch version of `Burn()`\nfunc (s *basicGRC1155Token) BatchBurn(from std.Address, batch []TokenID, amounts []uint64) error {\n\tcaller := std.OriginCaller()\n\n\terr := s.burnBatch(from, batch, amounts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\temit(\u0026TransferBatchEvent{caller, from, zeroAddress, batch, amounts})\n\n\treturn nil\n}\n\n/* Helper methods */\n\n// Helper for SetApprovalForAll(): approve `operator` to operate on all of `owner` tokens\nfunc (s *basicGRC1155Token) setApprovalForAll(owner, operator std.Address, approved bool) error {\n\tif owner == operator {\n\t\treturn nil\n\t}\n\n\tkey := owner.String() + \":\" + operator.String()\n\tif approved {\n\t\ts.operatorApprovals.Set(key, approved)\n\t} else {\n\t\ts.operatorApprovals.Remove(key)\n\t}\n\n\temit(\u0026ApprovalForAllEvent{owner, operator, approved})\n\n\treturn nil\n}\n\n// Helper for SafeTransferFrom() and SafeBatchTransferFrom()\nfunc (s *basicGRC1155Token) safeBatchTransferFrom(from, to std.Address, batch []TokenID, amounts []uint64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(from) || !isValidAddress(to) {\n\t\treturn ErrInvalidAddress\n\t}\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\tcaller := std.OriginCaller()\n\ts.beforeTokenTransfer(caller, from, to, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\tfromBalance, err := s.BalanceOf(from, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fromBalance \u003c amount {\n\t\t\treturn ErrInsufficientBalance\n\t\t}\n\t\ttoBalance, err := s.BalanceOf(to, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfromBalance -= amount\n\t\ttoBalance += amount\n\t\tfromBalanceKey := string(tid) + \":\" + from.String()\n\t\ttoBalanceKey := string(tid) + \":\" + to.String()\n\t\ts.balances.Set(fromBalanceKey, fromBalance)\n\t\ts.balances.Set(toBalanceKey, toBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, from, to, batch, amounts)\n\n\treturn nil\n}\n\n// Helper for SafeMint() and SafeBatchMint()\nfunc (s *basicGRC1155Token) mintBatch(to std.Address, batch []TokenID, amounts []uint64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(to) {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.OriginCaller()\n\ts.beforeTokenTransfer(caller, zeroAddress, to, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\ttoBalance, err := s.BalanceOf(to, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ttoBalance += amount\n\t\ttoBalanceKey := string(tid) + \":\" + to.String()\n\t\ts.balances.Set(toBalanceKey, toBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, zeroAddress, to, batch, amounts)\n\n\treturn nil\n}\n\n// Helper for Burn() and BurnBatch()\nfunc (s *basicGRC1155Token) burnBatch(from std.Address, batch []TokenID, amounts []uint64) error {\n\tif len(batch) != len(amounts) {\n\t\treturn ErrMismatchLength\n\t}\n\tif !isValidAddress(from) {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.OriginCaller()\n\ts.beforeTokenTransfer(caller, from, zeroAddress, batch, amounts)\n\n\tfor i := 0; i \u003c len(batch); i++ {\n\t\ttid := batch[i]\n\t\tamount := amounts[i]\n\t\tfromBalance, err := s.BalanceOf(from, tid)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif fromBalance \u003c amount {\n\t\t\treturn ErrBurnAmountExceedsBalance\n\t\t}\n\t\tfromBalance -= amount\n\t\tfromBalanceKey := string(tid) + \":\" + from.String()\n\t\ts.balances.Set(fromBalanceKey, fromBalance)\n\t}\n\n\ts.afterTokenTransfer(caller, from, zeroAddress, batch, amounts)\n\n\treturn nil\n}\n\nfunc (s *basicGRC1155Token) setUri(newUri string) {\n\ts.uri = newUri\n\temit(\u0026UpdateURIEvent{newUri})\n}\n\nfunc (s *basicGRC1155Token) beforeTokenTransfer(operator, from, to std.Address, batch []TokenID, amounts []uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicGRC1155Token) afterTokenTransfer(operator, from, to std.Address, batch []TokenID, amounts []uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicGRC1155Token) doSafeTransferAcceptanceCheck(operator, from, to std.Address, tid TokenID, amount uint64) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicGRC1155Token) doSafeBatchTransferAcceptanceCheck(operator, from, to std.Address, batch []TokenID, amounts []uint64) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicGRC1155Token) RenderHome() (str string) {\n\tstr += ufmt.Sprintf(\"# URI:%s\\n\", s.uri)\n\n\treturn\n}\n\nfunc (mt *basicGRC1155Token) Getter() MultiTokenGetter {\n\treturn func() IGRC1155 {\n\t\treturn mt\n\t}\n}\n" + }, + { + "name": "basic_grc1155_token_test.gno", + "body": "package grc1155\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nconst dummyURI = \"ipfs://xyz\"\n\nfunc TestNewBasicGRC1155Token(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n}\n\nfunc TestUri(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\tuassert.Equal(t, dummyURI, dummy.Uri())\n}\n\nfunc TestBalanceOf(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\t_, err := dummy.BalanceOf(zeroAddress, tid1)\n\tuassert.Error(t, err, \"should result in error\")\n\n\tbalanceAddr1OfToken1, err := dummy.BalanceOf(addr1, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(0), balanceAddr1OfToken1)\n\n\tdummy.mintBatch(addr1, []TokenID{tid1, tid2}, []uint64{10, 100})\n\tdummy.mintBatch(addr2, []TokenID{tid1}, []uint64{20})\n\n\tbalanceAddr1OfToken1, err = dummy.BalanceOf(addr1, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceAddr1OfToken2, err := dummy.BalanceOf(addr1, tid2)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceAddr2OfToken1, err := dummy.BalanceOf(addr2, tid1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tuassert.Equal(t, uint64(10), balanceAddr1OfToken1)\n\tuassert.Equal(t, uint64(100), balanceAddr1OfToken2)\n\tuassert.Equal(t, uint64(20), balanceAddr2OfToken1)\n}\n\nfunc TestBalanceOfBatch(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr1, addr2}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(0), balanceBatch[0])\n\tuassert.Equal(t, uint64(0), balanceBatch[1])\n\n\tdummy.mintBatch(addr1, []TokenID{tid1}, []uint64{10})\n\tdummy.mintBatch(addr2, []TokenID{tid2}, []uint64{20})\n\n\tbalanceBatch, err = dummy.BalanceOfBatch([]std.Address{addr1, addr2}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(10), balanceBatch[0])\n\tuassert.Equal(t, uint64(20), balanceBatch[1])\n}\n\nfunc TestIsApprovedForAll(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(addr1, addr2)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSetApprovalForAll(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.OriginCaller()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n\n\terr := dummy.SetApprovalForAll(addr, true)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.True(t, isApprovedForAll)\n\n\terr = dummy.SetApprovalForAll(addr, false)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSafeTransferFrom(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.OriginCaller()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid := TokenID(\"1\")\n\n\tdummy.mintBatch(caller, []TokenID{tid}, []uint64{100})\n\n\terr := dummy.SafeTransferFrom(caller, zeroAddress, tid, 10)\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.SafeTransferFrom(caller, addr, tid, 160)\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.SafeTransferFrom(caller, addr, tid, 60)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check balance of caller after transfer\n\tbalanceOfCaller, err := dummy.BalanceOf(caller, tid)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(40), balanceOfCaller)\n\n\t// Check balance of addr after transfer\n\tbalanceOfAddr, err := dummy.BalanceOf(addr, tid)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(60), balanceOfAddr)\n}\n\nfunc TestSafeBatchTransferFrom(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.OriginCaller()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(caller, []TokenID{tid1, tid2}, []uint64{10, 100})\n\n\terr := dummy.SafeBatchTransferFrom(caller, zeroAddress, []TokenID{tid1, tid2}, []uint64{4, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1, tid2}, []uint64{40, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1}, []uint64{40, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchTransferFrom(caller, addr, []TokenID{tid1, tid2}, []uint64{4, 60})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{caller, addr, caller, addr}, []TokenID{tid1, tid1, tid2, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of caller after batch transfer\n\tuassert.Equal(t, uint64(6), balanceBatch[0])\n\n\t// Check token1's balance of addr after batch transfer\n\tuassert.Equal(t, uint64(4), balanceBatch[1])\n\n\t// Check token2's balance of caller after batch transfer\n\tuassert.Equal(t, uint64(40), balanceBatch[2])\n\n\t// Check token2's balance of addr after batch transfer\n\tuassert.Equal(t, uint64(60), balanceBatch[3])\n}\n\nfunc TestSafeMint(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\terr := dummy.SafeMint(zeroAddress, tid1, 100)\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeMint(addr1, tid1, 100)\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeMint(addr1, tid2, 200)\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeMint(addr2, tid1, 50)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr1, addr2, addr1}, []TokenID{tid1, tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\t// Check token1's balance of addr1 after mint\n\tuassert.Equal(t, uint64(100), balanceBatch[0])\n\t// Check token1's balance of addr2 after mint\n\tuassert.Equal(t, uint64(50), balanceBatch[1])\n\t// Check token2's balance of addr1 after mint\n\tuassert.Equal(t, uint64(200), balanceBatch[2])\n}\n\nfunc TestSafeBatchMint(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\terr := dummy.SafeBatchMint(zeroAddress, []TokenID{tid1, tid2}, []uint64{100, 200})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.SafeBatchMint(addr1, []TokenID{tid1, tid2}, []uint64{100, 200})\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.SafeBatchMint(addr2, []TokenID{tid1, tid2}, []uint64{300, 400})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr1, addr2, addr1, addr2}, []TokenID{tid1, tid1, tid2, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\t// Check token1's balance of addr1 after batch mint\n\tuassert.Equal(t, uint64(100), balanceBatch[0])\n\t// Check token1's balance of addr2 after batch mint\n\tuassert.Equal(t, uint64(300), balanceBatch[1])\n\t// Check token2's balance of addr1 after batch mint\n\tuassert.Equal(t, uint64(200), balanceBatch[2])\n\t// Check token2's balance of addr2 after batch mint\n\tuassert.Equal(t, uint64(400), balanceBatch[3])\n}\n\nfunc TestBurn(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(addr, []TokenID{tid1, tid2}, []uint64{100, 200})\n\terr := dummy.Burn(zeroAddress, tid1, uint64(60))\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.Burn(addr, tid1, uint64(160))\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.Burn(addr, tid1, uint64(60))\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.Burn(addr, tid2, uint64(60))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr, addr}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of addr after burn\n\tuassert.Equal(t, uint64(40), balanceBatch[0])\n\t// Check token2's balance of addr after burn\n\tuassert.Equal(t, uint64(140), balanceBatch[1])\n}\n\nfunc TestBatchBurn(t *testing.T) {\n\tdummy := NewBasicGRC1155Token(dummyURI)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\ttid1 := TokenID(\"1\")\n\ttid2 := TokenID(\"2\")\n\n\tdummy.mintBatch(addr, []TokenID{tid1, tid2}, []uint64{100, 200})\n\terr := dummy.BatchBurn(zeroAddress, []TokenID{tid1, tid2}, []uint64{60, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.BatchBurn(addr, []TokenID{tid1, tid2}, []uint64{160, 60})\n\tuassert.Error(t, err, \"should result in error\")\n\terr = dummy.BatchBurn(addr, []TokenID{tid1, tid2}, []uint64{60, 60})\n\tuassert.NoError(t, err, \"should not result in error\")\n\tbalanceBatch, err := dummy.BalanceOfBatch([]std.Address{addr, addr}, []TokenID{tid1, tid2})\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check token1's balance of addr after batch burn\n\tuassert.Equal(t, uint64(40), balanceBatch[0])\n\t// Check token2's balance of addr after batch burn\n\tuassert.Equal(t, uint64(140), balanceBatch[1])\n}\n" + }, + { + "name": "errors.gno", + "body": "package grc1155\n\nimport \"errors\"\n\nvar (\n\tErrInvalidAddress = errors.New(\"invalid address\")\n\tErrMismatchLength = errors.New(\"accounts and ids length mismatch\")\n\tErrCannotTransferToSelf = errors.New(\"cannot send transfer to self\")\n\tErrTransferToRejectedOrNonGRC1155Receiver = errors.New(\"transfer to rejected or non GRC1155Receiver implementer\")\n\tErrCallerIsNotOwnerOrApproved = errors.New(\"caller is not token owner or approved\")\n\tErrInsufficientBalance = errors.New(\"insufficient balance for transfer\")\n\tErrBurnAmountExceedsBalance = errors.New(\"burn amount exceeds balance\")\n)\n" + }, + { + "name": "igrc1155.gno", + "body": "package grc1155\n\nimport \"std\"\n\ntype IGRC1155 interface {\n\tSafeTransferFrom(from, to std.Address, tid TokenID, amount uint64) error\n\tSafeBatchTransferFrom(from, to std.Address, batch []TokenID, amounts []uint64) error\n\tBalanceOf(owner std.Address, tid TokenID) (uint64, error)\n\tBalanceOfBatch(owners []std.Address, batch []TokenID) ([]uint64, error)\n\tSetApprovalForAll(operator std.Address, approved bool) error\n\tIsApprovedForAll(owner, operator std.Address) bool\n}\n\ntype TokenID string\n\ntype TransferSingleEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tTo std.Address\n\tTokenID TokenID\n\tAmount uint64\n}\n\ntype TransferBatchEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tTo std.Address\n\tBatch []TokenID\n\tAmounts []uint64\n}\n\ntype ApprovalForAllEvent struct {\n\tOwner std.Address\n\tOperator std.Address\n\tApproved bool\n}\n\ntype UpdateURIEvent struct {\n\tURI string\n}\n\ntype MultiTokenGetter func() IGRC1155\n" + }, + { + "name": "util.gno", + "body": "package grc1155\n\nimport (\n\t\"std\"\n)\n\nconst zeroAddress std.Address = \"\"\n\nfunc isValidAddress(addr std.Address) bool {\n\tif !addr.IsValid() {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc emit(event any) {\n\t// TODO: setup a pubsub system here?\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "GF3tMySvRYr+5XhuTCqQkTK0B4AbC3QJKVcb1hwcoMppIFMOmEv7yjEuy6sKqh+f/j6aAFANWp70iT78Rwu9DA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "grc20", + "path": "gno.land/p/demo/grc/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": "mock.gno", + "body": "package grc20\n\n// XXX: func Mock(t *Token)\n" + }, + { + "name": "tellers.gno", + "body": "package grc20\n\nimport (\n\t\"std\"\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() std.Address {\n\t\t\tcaller := std.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.\nfunc (tok *Token) RealmTeller() Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\n\tcaller := std.PreviousRealm().Address()\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func() std.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.\nfunc (tok *Token) RealmSubTeller(slug string) Teller {\n\tif tok == nil {\n\t\tpanic(\"Token cannot be nil\")\n\t}\n\n\tcaller := std.PreviousRealm().Address()\n\taccount := accountSlugAddr(caller, slug)\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func() std.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 std.Address) Teller {\n\tif ledger == nil {\n\t\tpanic(\"Ledger cannot be nil\")\n\t}\n\n\treturn \u0026fnTeller{\n\t\taccountFn: func() std.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 std.Address, amount uint64) 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 std.Address, amount uint64) 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 std.Address, amount uint64) 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 std.Address, slug string) std.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 std.DerivePkgAddr(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/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\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 uint64) {\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 uint64) {\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 uint64) {\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 uint64) {\n\t\tt.Helper()\n\t\texp := ufmt.Sprintf(\"ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s\", abEB, acEB, baEB, bcEB, caEB, cbEB)\n\t\tabGB := token.Allowance(alice, bob)\n\t\tacGB := token.Allowance(alice, carl)\n\t\tbaGB := token.Allowance(bob, alice)\n\t\tbcGB := token.Allowance(bob, carl)\n\t\tcaGB := token.Allowance(carl, alice)\n\t\tcbGB := token.Allowance(carl, bob)\n\t\tgot := ufmt.Sprintf(\"ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s\", abGB, acGB, baGB, bcGB, caGB, cbGB)\n\t\tuassert.Equal(t, got, exp, \"invalid allowances\")\n\t}\n\n\turequire.NoError(t, ledger.Mint(alice, 1000))\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(0, 0, 0, 0, 0, 0)\n\n\ttesting.SetOriginCaller(alice)\n\turequire.NoError(t, teller.Approve(bob, 600))\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(600, 0, 0, 0, 0, 0)\n\n\ttesting.SetOriginCaller(bob)\n\turequire.Error(t, teller.TransferFrom(alice, carl, 700))\n\tcheckBalances(1000, 0, 0)\n\tcheckAllowances(600, 0, 0, 0, 0, 0)\n\turequire.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\"math/overflow\"\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/ufmt\"\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 uint) (*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\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() uint { return tok.decimals }\n\n// TotalSupply returns the total supply of the token.\nfunc (tok Token) TotalSupply() uint64 { 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// BalanceOf returns the balance of the specified address.\nfunc (tok Token) BalanceOf(address std.Address) uint64 {\n\treturn tok.ledger.balanceOf(address)\n}\n\n// Allowance returns the allowance of the specified owner and spender.\nfunc (tok Token) Allowance(owner, spender std.Address) uint64 {\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// Getter returns a TokenGetter function that returns this token. This allows\n// storing indirect pointers to a token in a remote realm.\nfunc (tok *Token) Getter() TokenGetter {\n\treturn func() *Token {\n\t\treturn tok\n\t}\n}\n\n// SpendAllowance decreases the allowance of the specified owner and spender.\nfunc (led *PrivateLedger) SpendAllowance(owner, spender std.Address, amount uint64) error {\n\tif !owner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif !spender.IsValid() {\n\t\treturn ErrInvalidAddress\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 := 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 std.Address, amount uint64) error {\n\tif !from.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif !to.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\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 = toBalance + amount\n\t\tnewFromBalance = fromBalance - amount\n\t)\n\n\tled.balances.Set(string(to), newToBalance)\n\tled.balances.Set(string(from), newFromBalance)\n\n\tstd.Emit(\n\t\tTransferEvent,\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 std.Address, amount uint64) error {\n\tif led.balanceOf(owner) \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\t// allowance must be sufficient\n\tcurrentAllowance := led.allowance(owner, spender)\n\tif currentAllowance \u003c amount {\n\t\treturn ErrInsufficientAllowance\n\t}\n\n\tif err := led.Transfer(owner, to, amount); err != nil {\n\t\treturn err\n\t}\n\n\t// decrease the allowance only when transfer is successful\n\tkey := allowanceKey(owner, spender)\n\tnewAllowance := 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// Approve sets the allowance of the specified owner and spender.\nfunc (led *PrivateLedger) Approve(owner, spender std.Address, amount uint64) error {\n\tif !owner.IsValid() || !spender.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tled.allowances.Set(allowanceKey(owner, spender), amount)\n\n\tstd.Emit(\n\t\tApprovalEvent,\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(address std.Address, amount uint64) error {\n\tif !address.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\t// XXX: math/overflow is not supporting uint64.\n\t// This checks prevents overflow but makes the totalSupply limited to a uint63.\n\tsum, ok := overflow.Add64(int64(led.totalSupply), int64(amount))\n\tif !ok {\n\t\treturn ErrOverflow\n\t}\n\n\tled.totalSupply = uint64(sum)\n\tcurrentBalance := led.balanceOf(address)\n\tnewBalance := currentBalance + amount\n\n\tled.balances.Set(string(address), newBalance)\n\n\tstd.Emit(\n\t\tTransferEvent,\n\t\t\"from\", \"\",\n\t\t\"to\", string(address),\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(address std.Address, amount uint64) error {\n\tif !address.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcurrentBalance := led.balanceOf(address)\n\tif currentBalance \u003c amount {\n\t\treturn ErrInsufficientBalance\n\t}\n\n\tled.totalSupply -= amount\n\tnewBalance := currentBalance - amount\n\n\tled.balances.Set(string(address), newBalance)\n\n\tstd.Emit(\n\t\tTransferEvent,\n\t\t\"from\", string(address),\n\t\t\"to\", \"\",\n\t\t\"value\", strconv.Itoa(int(amount)),\n\t)\n\n\treturn nil\n}\n\n// balanceOf returns the balance of the specified address.\nfunc (led PrivateLedger) balanceOf(address std.Address) uint64 {\n\tbalance, found := led.balances.Get(address.String())\n\tif !found {\n\t\treturn 0\n\t}\n\treturn balance.(uint64)\n}\n\n// allowance returns the allowance of the specified owner and spender.\nfunc (led PrivateLedger) allowance(owner, spender std.Address) uint64 {\n\tallowance, found := led.allowances.Get(allowanceKey(owner, spender))\n\tif !found {\n\t\treturn 0\n\t}\n\treturn allowance.(uint64)\n}\n\n// allowanceKey returns the key for the allowance of the specified owner and spender.\nfunc allowanceKey(owner, spender std.Address) string {\n\treturn owner.String() + \":\" + spender.String()\n}\n" + }, + { + "name": "token_test.gno", + "body": "package grc20\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\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 uint64) {\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 uint64) {\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 TestOverflow(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\ttok, adm := NewToken(\"Dummy\", \"DUMMY\", 6)\n\n\turequire.NoError(t, adm.Mint(alice, 2\u003c\u003c62))\n\turequire.Equal(t, tok.BalanceOf(alice), uint64(2\u003c\u003c62))\n\turequire.Error(t, adm.Mint(bob, 2\u003c\u003c62))\n}\n\nfunc TestTransferFromAtomicity(t *testing.T) {\n\tvar (\n\t\towner = testutils.TestAddress(\"owner\")\n\t\tspender = testutils.TestAddress(\"spender\")\n\t\trecipient = testutils.TestAddress(\"recipient\")\n\n\t\tinvalidRecipient = std.Address(\"\")\n\t)\n\n\ttoken, admin := NewToken(\"Test\", \"TEST\", 6)\n\n\t// owner has 100 tokens, spender has 50 allowance\n\tinitialBalance := uint64(100)\n\tinitialAllowance := uint64(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 := uint64(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" + }, + { + "name": "types.gno", + "body": "package grc20\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/grc/exts\"\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\texts.TokenMetadata\n\n\t// Returns the amount of tokens in existence.\n\tTotalSupply() uint64\n\n\t// Returns the amount of tokens owned by `account`.\n\tBalanceOf(account std.Address) uint64\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 std.Address, amount uint64) 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 std.Address) uint64\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 std.Address, amount uint64) 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 std.Address, amount uint64) 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 uint\n\t// Pointer to the PrivateLedger that manages balances and allowances.\n\tledger *PrivateLedger\n}\n\n// TokenGetter is a function type that returns a Token pointer. This type allows\n// bypassing a limitation where we cannot directly pass Token pointers between\n// realms. Instead, we pass this function which can then be called to get the\n// Token pointer. For more details on this limitation and workaround, see:\n// https://github.com/gnolang/gno/pull/3135\ntype TokenGetter func() *Token\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 uint64\n\t// std.Address -\u003e uint64\n\tbalances avl.Tree\n\t// owner.(std.Address)+\":\"+spender.(std.Address)) -\u003e uint64\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\tErrOverflow = errors.New(\"Mint overflow\")\n)\n\nconst (\n\tMintEvent = \"Mint\"\n\tBurnEvent = \"Burn\"\n\tTransferEvent = \"Transfer\"\n\tApprovalEvent = \"Approval\"\n)\n\ntype fnTeller struct {\n\taccountFn func() std.Address\n\t*Token\n}\n\nvar _ Teller = (*fnTeller)(nil)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Ctbp94NBZWM4TIuC4s+HBYmirNR7ofhLmNs8mPQHqoEvcXOBb9MAFgrjwQlQxQDgrBXeSTDhDtlfKevZUJ0RAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "grc721", + "path": "gno.land/p/demo/grc/grc721", + "files": [ + { + "name": "basic_nft.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype basicNFT struct {\n\tname string\n\tsymbol string\n\towners avl.Tree // tokenId -\u003e OwnerAddress\n\tbalances avl.Tree // OwnerAddress -\u003e TokenCount\n\ttokenApprovals avl.Tree // TokenId -\u003e ApprovedAddress\n\ttokenURIs avl.Tree // TokenId -\u003e URIs\n\toperatorApprovals avl.Tree // \"OwnerAddress:OperatorAddress\" -\u003e bool\n}\n\n// Returns new basic NFT\nfunc NewBasicNFT(name string, symbol string) *basicNFT {\n\treturn \u0026basicNFT{\n\t\tname: name,\n\t\tsymbol: symbol,\n\n\t\towners: avl.Tree{},\n\t\tbalances: avl.Tree{},\n\t\ttokenApprovals: avl.Tree{},\n\t\ttokenURIs: avl.Tree{},\n\t\toperatorApprovals: avl.Tree{},\n\t}\n}\n\nfunc (s *basicNFT) Name() string { return s.name }\nfunc (s *basicNFT) Symbol() string { return s.symbol }\nfunc (s *basicNFT) TokenCount() uint64 { return uint64(s.owners.Size()) }\n\n// BalanceOf returns balance of input address\nfunc (s *basicNFT) BalanceOf(addr std.Address) (uint64, error) {\n\tif err := isValidAddress(addr); err != nil {\n\t\treturn 0, err\n\t}\n\n\tbalance, found := s.balances.Get(addr.String())\n\tif !found {\n\t\treturn 0, nil\n\t}\n\n\treturn balance.(uint64), nil\n}\n\n// OwnerOf returns owner of input token id\nfunc (s *basicNFT) OwnerOf(tid TokenID) (std.Address, error) {\n\towner, found := s.owners.Get(string(tid))\n\tif !found {\n\t\treturn \"\", ErrInvalidTokenId\n\t}\n\n\treturn owner.(std.Address), nil\n}\n\n// TokenURI returns the URI of input token id\nfunc (s *basicNFT) TokenURI(tid TokenID) (string, error) {\n\turi, found := s.tokenURIs.Get(string(tid))\n\tif !found {\n\t\treturn \"\", ErrInvalidTokenId\n\t}\n\n\treturn uri.(string), nil\n}\n\nfunc (s *basicNFT) SetTokenURI(tid TokenID, tURI TokenURI) (bool, error) {\n\t// check for invalid TokenID\n\tif !s.exists(tid) {\n\t\treturn false, ErrInvalidTokenId\n\t}\n\n\t// check for the right owner\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tcaller := std.PreviousRealm().Address()\n\tif caller != owner {\n\t\treturn false, ErrCallerIsNotOwner\n\t}\n\ts.tokenURIs.Set(string(tid), string(tURI))\n\treturn true, nil\n}\n\n// IsApprovedForAll returns true if operator is approved for all by the owner.\n// Otherwise, returns false\nfunc (s *basicNFT) IsApprovedForAll(owner, operator std.Address) bool {\n\tkey := owner.String() + \":\" + operator.String()\n\t_, found := s.operatorApprovals.Get(key)\n\tif !found {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// Approve approves the input address for particular token\nfunc (s *basicNFT) Approve(to std.Address, tid TokenID) error {\n\tif err := isValidAddress(to); err != nil {\n\t\treturn err\n\t}\n\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner == to {\n\t\treturn ErrApprovalToCurrentOwner\n\t}\n\n\tcaller := std.PreviousRealm().Address()\n\tif caller != owner \u0026\u0026 !s.IsApprovedForAll(owner, caller) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\ts.tokenApprovals.Set(string(tid), to.String())\n\tstd.Emit(\n\t\tApprovalEvent,\n\t\t\"owner\", string(owner),\n\t\t\"to\", string(to),\n\t\t\"tokenId\", string(tid),\n\t)\n\n\treturn nil\n}\n\n// GetApproved return the approved address for token\nfunc (s *basicNFT) GetApproved(tid TokenID) (std.Address, error) {\n\taddr, found := s.tokenApprovals.Get(string(tid))\n\tif !found {\n\t\treturn zeroAddress, ErrTokenIdNotHasApproved\n\t}\n\n\treturn std.Address(addr.(string)), nil\n}\n\n// SetApprovalForAll can approve the operator to operate on all tokens\nfunc (s *basicNFT) SetApprovalForAll(operator std.Address, approved bool) error {\n\tif err := isValidAddress(operator); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tcaller := std.PreviousRealm().Address()\n\treturn s.setApprovalForAll(caller, operator, approved)\n}\n\n// Safely transfers `tokenId` token from `from` to `to`, checking that\n// contract recipients are aware of the GRC721 protocol to prevent\n// tokens from being forever locked.\nfunc (s *basicNFT) SafeTransferFrom(from, to std.Address, tid TokenID) error {\n\tcaller := std.PreviousRealm().Address()\n\tif !s.isApprovedOrOwner(caller, tid) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.transfer(from, to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.checkOnGRC721Received(from, to, tid) {\n\t\treturn ErrTransferToNonGRC721Receiver\n\t}\n\n\treturn nil\n}\n\n// Transfers `tokenId` token from `from` to `to`.\nfunc (s *basicNFT) TransferFrom(from, to std.Address, tid TokenID) error {\n\tcaller := std.PreviousRealm().Address()\n\tif !s.isApprovedOrOwner(caller, tid) {\n\t\treturn ErrCallerIsNotOwnerOrApproved\n\t}\n\n\terr := s.transfer(from, to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Mints `tokenId` and transfers it to `to`.\nfunc (s *basicNFT) Mint(to std.Address, tid TokenID) error {\n\treturn s.mint(to, tid)\n}\n\n// Mints `tokenId` and transfers it to `to`. Also checks that\n// contract recipients are using GRC721 protocol\nfunc (s *basicNFT) SafeMint(to std.Address, tid TokenID) error {\n\terr := s.mint(to, tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !s.checkOnGRC721Received(zeroAddress, to, tid) {\n\t\treturn ErrTransferToNonGRC721Receiver\n\t}\n\n\treturn nil\n}\n\nfunc (s *basicNFT) Burn(tid TokenID) error {\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.beforeTokenTransfer(owner, zeroAddress, tid, 1)\n\n\ts.tokenApprovals.Remove(string(tid))\n\tbalance, err := s.BalanceOf(owner)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbalance -= 1\n\ts.balances.Set(owner.String(), balance)\n\ts.owners.Remove(string(tid))\n\n\tstd.Emit(\n\t\tBurnEvent,\n\t\t\"from\", string(owner),\n\t\t\"tokenId\", string(tid),\n\t)\n\n\ts.afterTokenTransfer(owner, zeroAddress, tid, 1)\n\n\treturn nil\n}\n\n/* Helper methods */\n\n// Helper for SetApprovalForAll()\nfunc (s *basicNFT) setApprovalForAll(owner, operator std.Address, approved bool) error {\n\tif owner == operator {\n\t\treturn ErrApprovalToCurrentOwner\n\t}\n\n\tkey := owner.String() + \":\" + operator.String()\n\ts.operatorApprovals.Set(key, approved)\n\n\tstd.Emit(\n\t\tApprovalForAllEvent,\n\t\t\"owner\", string(owner),\n\t\t\"to\", string(operator),\n\t\t\"approved\", strconv.FormatBool(approved),\n\t)\n\n\treturn nil\n}\n\n// Helper for TransferFrom() and SafeTransferFrom()\nfunc (s *basicNFT) transfer(from, to std.Address, tid TokenID) error {\n\tif err := isValidAddress(from); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\tif err := isValidAddress(to); err != nil {\n\t\treturn ErrInvalidAddress\n\t}\n\n\tif from == to {\n\t\treturn ErrCannotTransferToSelf\n\t}\n\n\towner, err := s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner != from {\n\t\treturn ErrTransferFromIncorrectOwner\n\t}\n\n\ts.beforeTokenTransfer(from, to, tid, 1)\n\n\t// Check that tokenId was not transferred by `beforeTokenTransfer`\n\towner, err = s.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif owner != from {\n\t\treturn ErrTransferFromIncorrectOwner\n\t}\n\n\ts.tokenApprovals.Remove(string(tid))\n\tfromBalance, err := s.BalanceOf(from)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttoBalance, err := s.BalanceOf(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfromBalance -= 1\n\ttoBalance += 1\n\ts.balances.Set(from.String(), fromBalance)\n\ts.balances.Set(to.String(), toBalance)\n\ts.owners.Set(string(tid), to)\n\n\tstd.Emit(\n\t\tTransferEvent,\n\t\t\"from\", string(from),\n\t\t\"to\", string(to),\n\t\t\"tokenId\", string(tid),\n\t)\n\n\ts.afterTokenTransfer(from, to, tid, 1)\n\n\treturn nil\n}\n\n// Helper for Mint() and SafeMint()\nfunc (s *basicNFT) mint(to std.Address, tid TokenID) error {\n\tif err := isValidAddress(to); err != nil {\n\t\treturn err\n\t}\n\n\tif s.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\ts.beforeTokenTransfer(zeroAddress, to, tid, 1)\n\n\t// Check that tokenId was not minted by `beforeTokenTransfer`\n\tif s.exists(tid) {\n\t\treturn ErrTokenIdAlreadyExists\n\t}\n\n\ttoBalance, err := s.BalanceOf(to)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttoBalance += 1\n\ts.balances.Set(to.String(), toBalance)\n\ts.owners.Set(string(tid), to)\n\n\tstd.Emit(\n\t\tMintEvent,\n\t\t\"to\", string(to),\n\t\t\"tokenId\", string(tid),\n\t)\n\n\ts.afterTokenTransfer(zeroAddress, to, tid, 1)\n\n\treturn nil\n}\n\nfunc (s *basicNFT) isApprovedOrOwner(addr std.Address, tid TokenID) bool {\n\towner, found := s.owners.Get(string(tid))\n\tif !found {\n\t\treturn false\n\t}\n\n\tif addr == owner.(std.Address) || s.IsApprovedForAll(owner.(std.Address), addr) {\n\t\treturn true\n\t}\n\n\tapproved, err := s.GetApproved(tid)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn approved == addr\n}\n\n// Checks if token id already exists\nfunc (s *basicNFT) exists(tid TokenID) bool {\n\t_, found := s.owners.Get(string(tid))\n\treturn found\n}\n\nfunc (s *basicNFT) beforeTokenTransfer(from, to std.Address, firstTokenId TokenID, batchSize uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicNFT) afterTokenTransfer(from, to std.Address, firstTokenId TokenID, batchSize uint64) {\n\t// TODO: Implementation\n}\n\nfunc (s *basicNFT) checkOnGRC721Received(from, to std.Address, tid TokenID) bool {\n\t// TODO: Implementation\n\treturn true\n}\n\nfunc (s *basicNFT) RenderHome() (str string) {\n\tstr += ufmt.Sprintf(\"# %s ($%s)\\n\\n\", s.name, s.symbol)\n\tstr += ufmt.Sprintf(\"* **Total supply**: %d\\n\", s.TokenCount())\n\tstr += ufmt.Sprintf(\"* **Known accounts**: %d\\n\", s.balances.Size())\n\n\treturn\n}\n\nfunc (n *basicNFT) Getter() NFTGetter {\n\treturn func() IGRC721 {\n\t\treturn n\n\t}\n}\n" + }, + { + "name": "basic_nft_test.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\tdummyNFTName = \"DummyNFT\"\n\tdummyNFTSymbol = \"DNFT\"\n)\n\nfunc TestNewBasicNFT(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n}\n\nfunc TestName(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tname := dummy.Name()\n\tuassert.Equal(t, dummyNFTName, name)\n}\n\nfunc TestSymbol(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tsymbol := dummy.Symbol()\n\tuassert.Equal(t, dummyNFTSymbol, symbol)\n}\n\nfunc TestTokenCount(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcount := dummy.TokenCount()\n\tuassert.Equal(t, uint64(0), count)\n\n\tdummy.mint(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\", TokenID(\"1\"))\n\tdummy.mint(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\", TokenID(\"2\"))\n\n\tcount = dummy.TokenCount()\n\tuassert.Equal(t, uint64(2), count)\n}\n\nfunc TestBalanceOf(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tbalanceAddr1, err := dummy.BalanceOf(addr1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(0), balanceAddr1)\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\tdummy.mint(addr1, TokenID(\"2\"))\n\tdummy.mint(addr2, TokenID(\"3\"))\n\n\tbalanceAddr1, err = dummy.BalanceOf(addr1)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tbalanceAddr2, err := dummy.BalanceOf(addr2)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tuassert.Equal(t, uint64(2), balanceAddr1)\n\tuassert.Equal(t, uint64(1), balanceAddr2)\n}\n\nfunc TestOwnerOf(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\towner, err := dummy.OwnerOf(TokenID(\"invalid\"))\n\tuassert.Error(t, err, \"should not result in error\")\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\tdummy.mint(addr2, TokenID(\"2\"))\n\n\t// Checking for token id \"1\"\n\towner, err = dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr1.String(), owner.String())\n\n\t// Checking for token id \"2\"\n\towner, err = dummy.OwnerOf(TokenID(\"2\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr2.String(), owner.String())\n}\n\nfunc TestIsApprovedForAll(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(addr1, addr2)\n\tuassert.False(t, isApprovedForAll)\n}\n\nfunc TestSetApprovalForAll(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PreviousRealm().Address()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tisApprovedForAll := dummy.IsApprovedForAll(caller, addr)\n\tuassert.False(t, isApprovedForAll)\n\n\terr := dummy.SetApprovalForAll(addr, true)\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tisApprovedForAll = dummy.IsApprovedForAll(caller, addr)\n\tuassert.True(t, isApprovedForAll)\n}\n\nfunc TestGetApproved(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\t_, err := dummy.GetApproved(TokenID(\"invalid\"))\n\tuassert.Error(t, err, \"should result in error\")\n}\n\nfunc TestApprove(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PreviousRealm().Address()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(caller, TokenID(\"1\"))\n\n\t_, err := dummy.GetApproved(TokenID(\"1\"))\n\tuassert.Error(t, err, \"should result in error\")\n\n\terr = dummy.Approve(addr, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\tapprovedAddr, err := dummy.GetApproved(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should result in error\")\n\tuassert.Equal(t, addr.String(), approvedAddr.String())\n}\n\nfunc TestTransferFrom(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PreviousRealm().Address()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(caller, TokenID(\"1\"))\n\tdummy.mint(caller, TokenID(\"2\"))\n\n\terr := dummy.TransferFrom(caller, addr, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should result in error\")\n\n\t// Check balance of caller after transfer\n\tbalanceOfCaller, err := dummy.BalanceOf(caller)\n\tuassert.NoError(t, err, \"should result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfCaller)\n\n\t// Check balance of addr after transfer\n\tbalanceOfAddr, err := dummy.BalanceOf(addr)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfAddr)\n\n\t// Check Owner of transferred Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should result in error\")\n\tuassert.Equal(t, addr.String(), owner.String())\n}\n\nfunc TestSafeTransferFrom(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tcaller := std.PreviousRealm().Address()\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(caller, TokenID(\"1\"))\n\tdummy.mint(caller, TokenID(\"2\"))\n\n\terr := dummy.SafeTransferFrom(caller, addr, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check balance of caller after transfer\n\tbalanceOfCaller, err := dummy.BalanceOf(caller)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfCaller)\n\n\t// Check balance of addr after transfer\n\tbalanceOfAddr, err := dummy.BalanceOf(addr)\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, uint64(1), balanceOfAddr)\n\n\t// Check Owner of transferred Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr.String(), owner.String())\n}\n\nfunc TestMint(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\n\terr := dummy.Mint(addr1, TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.Mint(addr1, TokenID(\"2\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\terr = dummy.Mint(addr2, TokenID(\"3\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Try minting duplicate token id\n\terr = dummy.Mint(addr2, TokenID(\"1\"))\n\tuassert.Error(t, err, \"should not result in error\")\n\n\t// Check Owner of Token id\n\towner, err := dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\tuassert.Equal(t, addr1.String(), owner.String())\n}\n\nfunc TestBurn(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tdummy.mint(addr, TokenID(\"1\"))\n\tdummy.mint(addr, TokenID(\"2\"))\n\n\terr := dummy.Burn(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"should not result in error\")\n\n\t// Check Owner of Token id\n\t_, err = dummy.OwnerOf(TokenID(\"1\"))\n\tuassert.Error(t, err, \"should result in error\")\n}\n\nfunc TestSetTokenURI(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\taddr2 := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\ttokenURI := \"http://example.com/token\"\n\n\ttesting.SetOriginCaller(addr1) // addr1\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\t_, derr := dummy.SetTokenURI(TokenID(\"1\"), TokenURI(tokenURI))\n\tuassert.NoError(t, derr, \"should not result in error\")\n\n\t// Test case: Invalid token ID\n\t_, err := dummy.SetTokenURI(TokenID(\"3\"), TokenURI(tokenURI))\n\tuassert.ErrorIs(t, err, ErrInvalidTokenId)\n\n\ttesting.SetOriginCaller(addr2) // addr2\n\n\t_, cerr := dummy.SetTokenURI(TokenID(\"1\"), TokenURI(tokenURI)) // addr2 trying to set URI for token 1\n\tuassert.ErrorIs(t, cerr, ErrCallerIsNotOwner)\n\n\t// Test case: Retrieving TokenURI\n\ttesting.SetOriginCaller(addr1) // addr1\n\n\tdummyTokenURI, err := dummy.TokenURI(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"TokenURI error\")\n\tuassert.Equal(t, string(tokenURI), string(dummyTokenURI))\n}\n\nfunc TestIsApprovedOrOwner(t *testing.T) {\n\tdummy := NewBasicNFT(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\tvar (\n\t\towner = testutils.TestAddress(\"owner\")\n\t\toperator = testutils.TestAddress(\"operator\")\n\t\tapproved = testutils.TestAddress(\"approved\")\n\t\tother = testutils.TestAddress(\"other\")\n\t)\n\n\ttid := TokenID(\"1\")\n\n\terr := dummy.mint(owner, tid)\n\tuassert.NoError(t, err)\n\n\t// check owner\n\tisApprovedOrOwner := dummy.isApprovedOrOwner(owner, tid)\n\tuassert.True(t, isApprovedOrOwner, \"owner should be approved\")\n\n\t// check operator\n\ttesting.SetOriginCaller(owner)\n\terr = dummy.SetApprovalForAll(operator, true)\n\tuassert.NoError(t, err)\n\tisApprovedOrOwner = dummy.isApprovedOrOwner(operator, tid)\n\tuassert.True(t, isApprovedOrOwner, \"operator should be approved\")\n\n\t// check approved\n\ttesting.SetOriginCaller(owner)\n\terr = dummy.Approve(approved, tid)\n\tuassert.NoError(t, err)\n\tisApprovedOrOwner = dummy.isApprovedOrOwner(approved, tid)\n\tuassert.True(t, isApprovedOrOwner, \"approved address should be approved\")\n\n\t// check other\n\tisApprovedOrOwner = dummy.isApprovedOrOwner(other, tid)\n\tuassert.False(t, isApprovedOrOwner, \"other address should not be approved\")\n\n\t// check non-existent token\n\tisApprovedOrOwner = dummy.isApprovedOrOwner(owner, TokenID(\"999\"))\n\tuassert.False(t, isApprovedOrOwner, \"non-existent token should not be approved\")\n}\n" + }, + { + "name": "errors.gno", + "body": "package grc721\n\nimport \"errors\"\n\nvar (\n\tErrInvalidTokenId = errors.New(\"invalid token id\")\n\tErrInvalidAddress = errors.New(\"invalid address\")\n\tErrTokenIdNotHasApproved = errors.New(\"token id not approved for anyone\")\n\tErrApprovalToCurrentOwner = errors.New(\"approval to current owner\")\n\tErrCallerIsNotOwner = errors.New(\"caller is not token owner\")\n\tErrCallerNotApprovedForAll = errors.New(\"caller is not approved for all\")\n\tErrCannotTransferToSelf = errors.New(\"cannot send transfer to self\")\n\tErrTransferFromIncorrectOwner = errors.New(\"transfer from incorrect owner\")\n\tErrTransferToNonGRC721Receiver = errors.New(\"transfer to non GRC721Receiver implementer\")\n\tErrCallerIsNotOwnerOrApproved = errors.New(\"caller is not token owner or approved\")\n\tErrTokenIdAlreadyExists = errors.New(\"token id already exists\")\n\n\t// ERC721Royalty\n\tErrInvalidRoyaltyPercentage = errors.New(\"invalid royalty percentage\")\n\tErrInvalidRoyaltyPaymentAddress = errors.New(\"invalid royalty paymentAddress\")\n\tErrCannotCalculateRoyaltyAmount = errors.New(\"cannot calculate royalty amount\")\n)\n" + }, + { + "name": "grc721_metadata.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// metadataNFT represents an NFT with metadata extensions.\ntype metadataNFT struct {\n\t*basicNFT // Embedded basicNFT struct for basic NFT functionality\n\textensions *avl.Tree // AVL tree for storing metadata extensions\n}\n\n// Ensure that metadataNFT implements the IGRC721MetadataOnchain interface.\nvar _ IGRC721MetadataOnchain = (*metadataNFT)(nil)\n\n// NewNFTWithMetadata creates a new basic NFT with metadata extensions.\nfunc NewNFTWithMetadata(name string, symbol string) *metadataNFT {\n\t// Create a new basic NFT\n\tnft := NewBasicNFT(name, symbol)\n\n\t// Return a metadataNFT with basicNFT embedded and an empty AVL tree for extensions\n\treturn \u0026metadataNFT{\n\t\tbasicNFT: nft,\n\t\textensions: avl.NewTree(),\n\t}\n}\n\n// SetTokenMetadata sets metadata for a given token ID.\nfunc (s *metadataNFT) SetTokenMetadata(tid TokenID, metadata Metadata) error {\n\t// Set the metadata for the token ID in the extensions AVL tree\n\ts.extensions.Set(string(tid), metadata)\n\treturn nil\n}\n\n// TokenMetadata retrieves metadata for a given token ID.\nfunc (s *metadataNFT) TokenMetadata(tid TokenID) (Metadata, error) {\n\t// Retrieve metadata from the extensions AVL tree\n\tmetadata, found := s.extensions.Get(string(tid))\n\tif !found {\n\t\treturn Metadata{}, ErrInvalidTokenId\n\t}\n\n\treturn metadata.(Metadata), nil\n}\n\n// Basic NFT methods forwarded to embedded basicNFT\n\nfunc (s *metadataNFT) Name() string {\n\treturn s.basicNFT.Name()\n}\n\nfunc (s *metadataNFT) Symbol() string {\n\treturn s.basicNFT.Symbol()\n}\n\nfunc (s *metadataNFT) TokenCount() uint64 {\n\treturn s.basicNFT.TokenCount()\n}\n\nfunc (s *metadataNFT) BalanceOf(addr std.Address) (uint64, error) {\n\treturn s.basicNFT.BalanceOf(addr)\n}\n\nfunc (s *metadataNFT) OwnerOf(tid TokenID) (std.Address, error) {\n\treturn s.basicNFT.OwnerOf(tid)\n}\n\nfunc (s *metadataNFT) TokenURI(tid TokenID) (string, error) {\n\treturn s.basicNFT.TokenURI(tid)\n}\n\nfunc (s *metadataNFT) SetTokenURI(tid TokenID, tURI TokenURI) (bool, error) {\n\treturn s.basicNFT.SetTokenURI(tid, tURI)\n}\n\nfunc (s *metadataNFT) IsApprovedForAll(owner, operator std.Address) bool {\n\treturn s.basicNFT.IsApprovedForAll(owner, operator)\n}\n\nfunc (s *metadataNFT) Approve(to std.Address, tid TokenID) error {\n\treturn s.basicNFT.Approve(to, tid)\n}\n\nfunc (s *metadataNFT) GetApproved(tid TokenID) (std.Address, error) {\n\treturn s.basicNFT.GetApproved(tid)\n}\n\nfunc (s *metadataNFT) SetApprovalForAll(operator std.Address, approved bool) error {\n\treturn s.basicNFT.SetApprovalForAll(operator, approved)\n}\n\nfunc (s *metadataNFT) SafeTransferFrom(from, to std.Address, tid TokenID) error {\n\treturn s.basicNFT.SafeTransferFrom(from, to, tid)\n}\n\nfunc (s *metadataNFT) TransferFrom(from, to std.Address, tid TokenID) error {\n\treturn s.basicNFT.TransferFrom(from, to, tid)\n}\n\nfunc (s *metadataNFT) Mint(to std.Address, tid TokenID) error {\n\treturn s.basicNFT.Mint(to, tid)\n}\n\nfunc (s *metadataNFT) SafeMint(to std.Address, tid TokenID) error {\n\treturn s.basicNFT.SafeMint(to, tid)\n}\n\nfunc (s *metadataNFT) Burn(tid TokenID) error {\n\treturn s.basicNFT.Burn(tid)\n}\n\nfunc (s *metadataNFT) RenderHome() string {\n\treturn s.basicNFT.RenderHome()\n}\n" + }, + { + "name": "grc721_metadata_test.gno", + "body": "package grc721\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestSetMetadata(t *testing.T) {\n\t// Create a new dummy NFT with metadata\n\tdummy := NewNFTWithMetadata(dummyNFTName, dummyNFTSymbol)\n\tif dummy == nil {\n\t\tt.Errorf(\"should not be nil\")\n\t}\n\n\t// Define addresses for testing purposes\n\taddr1 := testutils.TestAddress(\"alice\")\n\taddr2 := testutils.TestAddress(\"bob\")\n\n\t// Define metadata attributes\n\tname := \"test\"\n\tdescription := \"test\"\n\timage := \"test\"\n\timageData := \"test\"\n\texternalURL := \"test\"\n\tattributes := []Trait{}\n\tbackgroundColor := \"test\"\n\tanimationURL := \"test\"\n\tyoutubeURL := \"test\"\n\n\t// Set the original caller to addr1\n\ttesting.SetOriginCaller(addr1) // addr1\n\n\t// Mint a new token for addr1\n\tdummy.mint(addr1, TokenID(\"1\"))\n\n\t// Set metadata for token 1\n\tderr := dummy.SetTokenMetadata(TokenID(\"1\"), Metadata{\n\t\tName: name,\n\t\tDescription: description,\n\t\tImage: image,\n\t\tImageData: imageData,\n\t\tExternalURL: externalURL,\n\t\tAttributes: attributes,\n\t\tBackgroundColor: backgroundColor,\n\t\tAnimationURL: animationURL,\n\t\tYoutubeURL: youtubeURL,\n\t})\n\n\t// Check if there was an error setting metadata\n\tuassert.NoError(t, derr, \"Should not result in error\")\n\n\t// Test case: Invalid token ID\n\terr := dummy.SetTokenMetadata(TokenID(\"3\"), Metadata{\n\t\tName: name,\n\t\tDescription: description,\n\t\tImage: image,\n\t\tImageData: imageData,\n\t\tExternalURL: externalURL,\n\t\tAttributes: attributes,\n\t\tBackgroundColor: backgroundColor,\n\t\tAnimationURL: animationURL,\n\t\tYoutubeURL: youtubeURL,\n\t})\n\n\t// Check if the error returned matches the expected error\n\tuassert.ErrorIs(t, err, ErrInvalidTokenId)\n\n\t// Set the original caller to addr2\n\ttesting.SetOriginCaller(addr2) // addr2\n\n\t// Try to set metadata for token 1 from addr2 (should fail)\n\tcerr := dummy.SetTokenMetadata(TokenID(\"1\"), Metadata{\n\t\tName: name,\n\t\tDescription: description,\n\t\tImage: image,\n\t\tImageData: imageData,\n\t\tExternalURL: externalURL,\n\t\tAttributes: attributes,\n\t\tBackgroundColor: backgroundColor,\n\t\tAnimationURL: animationURL,\n\t\tYoutubeURL: youtubeURL,\n\t})\n\n\t// Check if the error returned matches the expected error\n\tuassert.ErrorIs(t, cerr, ErrCallerIsNotOwner)\n\n\t// Set the original caller back to addr1\n\ttesting.SetOriginCaller(addr1) // addr1\n\n\t// Retrieve metadata for token 1\n\tdummyMetadata, err := dummy.TokenMetadata(TokenID(\"1\"))\n\tuassert.NoError(t, err, \"Metadata error\")\n\n\t// Check if metadata attributes match expected values\n\tuassert.Equal(t, image, dummyMetadata.Image)\n\tuassert.Equal(t, imageData, dummyMetadata.ImageData)\n\tuassert.Equal(t, externalURL, dummyMetadata.ExternalURL)\n\tuassert.Equal(t, description, dummyMetadata.Description)\n\tuassert.Equal(t, name, dummyMetadata.Name)\n\tuassert.Equal(t, len(attributes), len(dummyMetadata.Attributes))\n\tuassert.Equal(t, backgroundColor, dummyMetadata.BackgroundColor)\n\tuassert.Equal(t, animationURL, dummyMetadata.AnimationURL)\n\tuassert.Equal(t, youtubeURL, dummyMetadata.YoutubeURL)\n}\n" + }, + { + "name": "grc721_royalty.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// royaltyNFT represents a non-fungible token (NFT) with royalty functionality.\ntype royaltyNFT struct {\n\t*metadataNFT // Embedding metadataNFT for NFT functionality\n\ttokenRoyaltyInfo *avl.Tree // AVL tree to store royalty information for each token\n\tmaxRoyaltyPercentage uint64 // maxRoyaltyPercentage represents the maximum royalty percentage that can be charged every sale\n}\n\n// Ensure that royaltyNFT implements the IGRC2981 interface.\nvar _ IGRC2981 = (*royaltyNFT)(nil)\n\n// NewNFTWithRoyalty creates a new royalty NFT with the specified name, symbol, and royalty calculator.\nfunc NewNFTWithRoyalty(name string, symbol string) *royaltyNFT {\n\t// Create a new NFT with metadata\n\tnft := NewNFTWithMetadata(name, symbol)\n\n\treturn \u0026royaltyNFT{\n\t\tmetadataNFT: nft,\n\t\ttokenRoyaltyInfo: avl.NewTree(),\n\t\tmaxRoyaltyPercentage: 100,\n\t}\n}\n\n// SetTokenRoyalty sets the royalty information for a specific token ID.\nfunc (r *royaltyNFT) SetTokenRoyalty(tid TokenID, royaltyInfo RoyaltyInfo) error {\n\t// Validate the payment address\n\tif err := isValidAddress(royaltyInfo.PaymentAddress); err != nil {\n\t\treturn ErrInvalidRoyaltyPaymentAddress\n\t}\n\n\t// Check if royalty percentage exceeds maxRoyaltyPercentage\n\tif royaltyInfo.Percentage \u003e r.maxRoyaltyPercentage {\n\t\treturn ErrInvalidRoyaltyPercentage\n\t}\n\n\t// Check if the caller is the owner of the token\n\towner, err := r.metadataNFT.OwnerOf(tid)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcaller := std.PreviousRealm().Address()\n\tif caller != owner {\n\t\treturn ErrCallerIsNotOwner\n\t}\n\n\t// Set royalty information for the token\n\tr.tokenRoyaltyInfo.Set(string(tid), royaltyInfo)\n\n\treturn nil\n}\n\n// RoyaltyInfo returns the royalty information for the given token ID and sale price.\nfunc (r *royaltyNFT) RoyaltyInfo(tid TokenID, salePrice uint64) (std.Address, uint64, error) {\n\t// Retrieve royalty information for the token\n\tval, found := r.tokenRoyaltyInfo.Get(string(tid))\n\tif !found {\n\t\treturn \"\", 0, ErrInvalidTokenId\n\t}\n\n\troyaltyInfo := val.(RoyaltyInfo)\n\n\t// Calculate royalty amount\n\troyaltyAmount, _ := r.calculateRoyaltyAmount(salePrice, royaltyInfo.Percentage)\n\n\treturn royaltyInfo.PaymentAddress, royaltyAmount, nil\n}\n\nfunc (r *royaltyNFT) calculateRoyaltyAmount(salePrice, percentage uint64) (uint64, error) {\n\troyaltyAmount := (salePrice * percentage) / 100\n\treturn royaltyAmount, nil\n}\n" + }, + { + "name": "grc721_royalty_test.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestSetTokenRoyalty(t *testing.T) {\n\tdummy := NewNFTWithRoyalty(dummyNFTName, dummyNFTSymbol)\n\tuassert.True(t, dummy != nil, \"should not be nil\")\n\n\taddr1 := testutils.TestAddress(\"alice\")\n\taddr2 := testutils.TestAddress(\"bob\")\n\n\tpaymentAddress := testutils.TestAddress(\"john\")\n\tpercentage := uint64(10) // 10%\n\n\tsalePrice := uint64(1000)\n\texpectRoyaltyAmount := uint64(100)\n\n\ttesting.SetOriginCaller(addr1) // addr1\n\n\tdummy.mint(addr1, TokenID(\"1\"))\n\n\tderr := dummy.SetTokenRoyalty(TokenID(\"1\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: percentage,\n\t})\n\tuassert.NoError(t, derr, \"Should not result in error\")\n\n\t// Test case: Invalid token ID\n\t_ = dummy.SetTokenRoyalty(TokenID(\"3\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: percentage,\n\t})\n\tuassert.ErrorIs(t, derr, ErrInvalidTokenId)\n\n\ttesting.SetOriginCaller(addr2) // addr2\n\n\tcerr := dummy.SetTokenRoyalty(TokenID(\"1\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: percentage,\n\t})\n\tuassert.ErrorIs(t, cerr, ErrCallerIsNotOwner)\n\n\t// Test case: Invalid payment address\n\taerr := dummy.SetTokenRoyalty(TokenID(\"4\"), RoyaltyInfo{\n\t\tPaymentAddress: std.Address(\"###\"), // invalid address\n\t\tPercentage: percentage,\n\t})\n\tuassert.ErrorIs(t, aerr, ErrInvalidRoyaltyPaymentAddress)\n\n\t// Test case: Invalid percentage\n\tperr := dummy.SetTokenRoyalty(TokenID(\"5\"), RoyaltyInfo{\n\t\tPaymentAddress: paymentAddress,\n\t\tPercentage: uint64(200), // over maxRoyaltyPercentage\n\t})\n\tuassert.ErrorIs(t, perr, ErrInvalidRoyaltyPercentage)\n\n\t// Test case: Retrieving Royalty Info\n\ttesting.SetOriginCaller(addr1) // addr1\n\n\tdummyPaymentAddress, dummyRoyaltyAmount, rerr := dummy.RoyaltyInfo(TokenID(\"1\"), salePrice)\n\tuassert.NoError(t, rerr, \"RoyaltyInfo error\")\n\tuassert.Equal(t, paymentAddress, dummyPaymentAddress)\n\tuassert.Equal(t, expectRoyaltyAmount, dummyRoyaltyAmount)\n}\n" + }, + { + "name": "igrc721.gno", + "body": "package grc721\n\nimport \"std\"\n\ntype IGRC721 interface {\n\tBalanceOf(owner std.Address) (uint64, error)\n\tOwnerOf(tid TokenID) (std.Address, error)\n\tSetTokenURI(tid TokenID, tURI TokenURI) (bool, error)\n\tSafeTransferFrom(from, to std.Address, tid TokenID) error\n\tTransferFrom(from, to std.Address, tid TokenID) error\n\tApprove(approved std.Address, tid TokenID) error\n\tSetApprovalForAll(operator std.Address, approved bool) error\n\tGetApproved(tid TokenID) (std.Address, error)\n\tIsApprovedForAll(owner, operator std.Address) bool\n}\n\ntype (\n\tTokenID string\n\tTokenURI string\n)\n\nconst (\n\tMintEvent = \"Mint\"\n\tBurnEvent = \"Burn\"\n\tTransferEvent = \"Transfer\"\n\tApprovalEvent = \"Approval\"\n\tApprovalForAllEvent = \"ApprovalForAll\"\n)\n\ntype NFTGetter func() IGRC721\n" + }, + { + "name": "igrc721_metadata.gno", + "body": "package grc721\n\n// IGRC721CollectionMetadata describes basic information about an NFT collection.\ntype IGRC721CollectionMetadata interface {\n\tName() string // Name returns the name of the collection.\n\tSymbol() string // Symbol returns the symbol of the collection.\n}\n\n// IGRC721Metadata follows the Ethereum standard\ntype IGRC721Metadata interface {\n\tIGRC721CollectionMetadata\n\tTokenURI(tid TokenID) (string, error) // TokenURI returns the URI of a specific token.\n}\n\n// IGRC721Metadata follows the OpenSea metadata standard\ntype IGRC721MetadataOnchain interface {\n\tIGRC721CollectionMetadata\n\tTokenMetadata(tid TokenID) (Metadata, error)\n}\n\ntype Trait struct {\n\tDisplayType string\n\tTraitType string\n\tValue string\n}\n\n// see: https://docs.opensea.io/docs/metadata-standards\ntype Metadata struct {\n\tImage string // URL to the image of the item. Can be any type of image (including SVGs, which will be cached into PNGs by OpenSea), IPFS or Arweave URLs or paths. We recommend using a minimum 3000 x 3000 image.\n\tImageData string // Raw SVG image data, if you want to generate images on the fly (not recommended). Only use this if you're not including the image parameter.\n\tExternalURL string // URL that will appear below the asset's image on OpenSea and will allow users to leave OpenSea and view the item on your site.\n\tDescription string // Human-readable description of the item. Markdown is supported.\n\tName string // Name of the item.\n\tAttributes []Trait // Attributes for the item, which will show up on the OpenSea page for the item.\n\tBackgroundColor string // Background color of the item on OpenSea. Must be a six-character hexadecimal without a pre-pended #\n\tAnimationURL string // URL to a multimedia attachment for the item. Supported file extensions: GLTF, GLB, WEBM, MP4, M4V, OGV, OGG, MP3, WAV, OGA, HTML (for rich experiences and interactive NFTs using JavaScript canvas, WebGL, etc.). Scripts and relative paths within the HTML page are now supported. Access to browser extensions is not supported.\n\tYoutubeURL string // URL to a YouTube video (only used if animation_url is not provided).\n}\n" + }, + { + "name": "igrc721_royalty.gno", + "body": "package grc721\n\nimport \"std\"\n\n// IGRC2981 follows the Ethereum standard\ntype IGRC2981 interface {\n\t// RoyaltyInfo retrieves royalty information for a tokenID and salePrice.\n\t// It returns the payment address, royalty amount, and an error if any.\n\tRoyaltyInfo(tokenID TokenID, salePrice uint64) (std.Address, uint64, error)\n}\n\n// RoyaltyInfo represents royalty information for a token.\ntype RoyaltyInfo struct {\n\tPaymentAddress std.Address // PaymentAddress is the address where royalty payment should be sent.\n\tPercentage uint64 // Percentage is the royalty percentage. It indicates the percentage of royalty to be paid for each sale. For example : Percentage = 10 =\u003e 10%\n}\n" + }, + { + "name": "util.gno", + "body": "package grc721\n\nimport (\n\t\"std\"\n)\n\nvar zeroAddress = std.Address(\"\")\n\nfunc isValidAddress(addr std.Address) error {\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\treturn nil\n}\n\nfunc emit(event any) {\n\t// TODO: setup a pubsub system here?\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "gklom4HSUNokMC6k8Az/efzcx2KQ18eRxSLbGUcIE3IesajpHplyES1/BEZQ44Nd1P4e4PzFOYT5CpoV+4t4BQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "grc777", + "path": "gno.land/p/demo/grc/grc777", + "files": [ + { + "name": "dummy_test.gno", + "body": "package grc777\n\nimport (\n\t\"std\"\n\t\"testing\"\n)\n\ntype dummyImpl struct{}\n\n// FIXME: this should fail.\nvar _ IGRC777 = (*dummyImpl)(nil)\n\nfunc TestInterface(t *testing.T) {\n\tvar _ IGRC777 = \u0026dummyImpl{}\n}\n\nfunc (impl *dummyImpl) GetName() string { panic(\"not implemented\") }\nfunc (impl *dummyImpl) GetSymbol() string { panic(\"not implemented\") }\nfunc (impl *dummyImpl) GetDecimals() uint { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Granularity() (granularity uint64) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) TotalSupply() (supply uint64) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) BalanceOf(address std.Address) uint64 { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Burn(amount uint64, data []byte) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) AuthorizeOperator(operator std.Address) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) RevokeOperator(operators std.Address) { panic(\"not implemented\") }\nfunc (impl *dummyImpl) DefaultOperators() []std.Address { panic(\"not implemented\") }\nfunc (impl *dummyImpl) Send(recipient std.Address, amount uint64, data []byte) {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) IsOperatorFor(operator, tokenHolder std.Address) bool {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) OperatorSend(sender, recipient std.Address, amount uint64, data, operatorData []byte) {\n\tpanic(\"not implemented\")\n}\n\nfunc (impl *dummyImpl) OperatorBurn(account std.Address, amount uint64, data, operatorData []byte) {\n\tpanic(\"not implemented\")\n}\n" + }, + { + "name": "igrc777.gno", + "body": "package grc777\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/exts\"\n)\n\n// TODO: use big.Int or a custom uint64 instead of uint64\n\ntype IGRC777 interface {\n\texts.TokenMetadata\n\n\t// Returns the smallest part of the token that is not divisible. This\n\t// means all token operations (creation, movement and destruction) must\n\t// have amounts that are a multiple of this number.\n\t//\n\t// For most token contracts, this value will equal 1.\n\tGranularity() (granularity uint64)\n\n\t// Returns the amount of tokens in existence.\n\tTotalSupply() (supply uint64)\n\n\t// Returns the amount of tokens owned by an account (`owner`).\n\tBalanceOf(address std.Address) uint64\n\n\t// Moves `amount` tokens from the caller's account to `recipient`.\n\t//\n\t// If send or receive hooks are registered for the caller and `recipient`,\n\t// the corresponding functions will be called with `data` and empty\n\t// `operatorData`. See {IERC777Sender} and {IERC777Recipient}.\n\t//\n\t// Emits a {Sent} event.\n\t//\n\t// Requirements\n\t//\n\t// - the caller must have at least `amount` tokens.\n\t// - `recipient` cannot be the zero address.\n\t// - if `recipient` is a contract, it must implement the {IERC777Recipient}\n\t// interface.\n\tSend(recipient std.Address, amount uint64, data []byte)\n\n\t// Destroys `amount` tokens from the caller's account, reducing the\n\t// total supply.\n\t//\n\t// If a send hook is registered for the caller, the corresponding function\n\t// will be called with `data` and empty `operatorData`. See {IERC777Sender}.\n\t//\n\t// Emits a {Burned} event.\n\t//\n\t// Requirements\n\t//\n\t// - the caller must have at least `amount` tokens.\n\tBurn(amount uint64, data []byte)\n\n\t// Returns true if an account is an operator of `tokenHolder`.\n\t// Operators can send and burn tokens on behalf of their owners. All\n\t// accounts are their own operator.\n\t//\n\t// See {operatorSend} and {operatorBurn}.\n\tIsOperatorFor(operator, tokenHolder std.Address) bool\n\n\t// Make an account an operator of the caller.\n\t//\n\t// See {isOperatorFor}.\n\t//\n\t// Emits an {AuthorizedOperator} event.\n\t//\n\t// Requirements\n\t//\n\t// - `operator` cannot be calling address.\n\tAuthorizeOperator(operator std.Address)\n\n\t// Revoke an account's operator status for the caller.\n\t//\n\t// See {isOperatorFor} and {defaultOperators}.\n\t//\n\t// Emits a {RevokedOperator} event.\n\t//\n\t// Requirements\n\t//\n\t// - `operator` cannot be calling address.\n\tRevokeOperator(operators std.Address)\n\n\t// Returns the list of default operators. These accounts are operators\n\t// for all token holders, even if {authorizeOperator} was never called on\n\t// them.\n\t//\n\t// This list is immutable, but individual holders may revoke these via\n\t// {revokeOperator}, in which case {isOperatorFor} will return false.\n\tDefaultOperators() []std.Address\n\n\t// Moves `amount` tokens from `sender` to `recipient`. The caller must\n\t// be an operator of `sender`.\n\t//\n\t// If send or receive hooks are registered for `sender` and `recipient`,\n\t// the corresponding functions will be called with `data` and\n\t// `operatorData`. See {IERC777Sender} and {IERC777Recipient}.\n\t//\n\t// Emits a {Sent} event.\n\t//\n\t// Requirements\n\t//\n\t// - `sender` cannot be the zero address.\n\t// - `sender` must have at least `amount` tokens.\n\t// - the caller must be an operator for `sender`.\n\t// - `recipient` cannot be the zero address.\n\t// - if `recipient` is a contract, it must implement the {IERC777Recipient}\n\t// interface.\n\tOperatorSend(sender, recipient std.Address, amount uint64, data, operatorData []byte)\n\n\t// Destroys `amount` tokens from `account`, reducing the total supply.\n\t// The caller must be an operator of `account`.\n\t//\n\t// If a send hook is registered for `account`, the corresponding function\n\t// will be called with `data` and `operatorData`. See {IERC777Sender}.\n\t//\n\t// Emits a {Burned} event.\n\t//\n\t// Requirements\n\t//\n\t// - `account` cannot be the zero address.\n\t// - `account` must have at least `amount` tokens.\n\t// - the caller must be an operator for `account`.\n\tOperatorBurn(account std.Address, amount uint64, data, operatorData []byte)\n}\n\n// Emitted when `amount` tokens are created by `operator` and assigned to `to`.\n//\n// Note that some additional user `data` and `operatorData` can be logged in the event.\ntype MintedEvent struct {\n\tOperator std.Address\n\tTo std.Address\n\tAmount uint64\n\tData []byte\n\tOperatorData []byte\n}\n\n// Emitted when `operator` destroys `amount` tokens from `account`.\n//\n// Note that some additional user `data` and `operatorData` can be logged in the event.\ntype BurnedEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tAmount uint64\n\tData []byte\n\tOperatorData []byte\n}\n\n// Emitted when `operator` is made operator for `tokenHolder`\ntype AuthorizedOperatorEvent struct {\n\tOperator std.Address\n\tTokenHolder std.Address\n}\n\n// Emitted when `operator` is revoked its operator status for `tokenHolder`.\ntype RevokedOperatorEvent struct {\n\tOperator std.Address\n\tTokenHolder std.Address\n}\n\ntype SentEvent struct {\n\tOperator std.Address\n\tFrom std.Address\n\tTo std.Address\n\tAmount uint64\n\tData []byte\n\tOperatorData []byte\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "HmfAfPd7rK1Q3s+oynf87hNC/E0WLbG8AQFyndLPX5GYltJGwpxK8gPZMxCmbRYjaE/woYYA/GJJiCj5x97rAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "rat", + "path": "gno.land/p/demo/rat", + "files": [ + { + "name": "maths.gno", + "body": "package rat\n\nconst (\n\tintSize = 32 \u003c\u003c (^uint(0) \u003e\u003e 63) // 32 or 64\n\n\tMaxInt = 1\u003c\u003c(intSize-1) - 1\n\tMinInt = -1 \u003c\u003c (intSize - 1)\n\tMaxInt8 = 1\u003c\u003c7 - 1\n\tMinInt8 = -1 \u003c\u003c 7\n\tMaxInt16 = 1\u003c\u003c15 - 1\n\tMinInt16 = -1 \u003c\u003c 15\n\tMaxInt32 = 1\u003c\u003c31 - 1\n\tMinInt32 = -1 \u003c\u003c 31\n\tMaxInt64 = 1\u003c\u003c63 - 1\n\tMinInt64 = -1 \u003c\u003c 63\n\tMaxUint = 1\u003c\u003cintSize - 1\n\tMaxUint8 = 1\u003c\u003c8 - 1\n\tMaxUint16 = 1\u003c\u003c16 - 1\n\tMaxUint32 = 1\u003c\u003c32 - 1\n\tMaxUint64 = 1\u003c\u003c64 - 1\n)\n" + }, + { + "name": "rat.gno", + "body": "package rat\n\n//----------------------------------------\n// Rat fractions\n\n// represents a fraction.\ntype Rat struct {\n\tX int32\n\tY int32 // must be positive\n}\n\nfunc NewRat(x, y int32) Rat {\n\tif y \u003c= 0 {\n\t\tpanic(\"invalid std.Rat denominator\")\n\t}\n\treturn Rat{X: x, Y: y}\n}\n\nfunc (r1 Rat) IsValid() bool {\n\tif r1.Y \u003c= 0 {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (r1 Rat) Cmp(r2 Rat) int {\n\tif !r1.IsValid() {\n\t\tpanic(\"invalid std.Rat left operand\")\n\t}\n\tif !r2.IsValid() {\n\t\tpanic(\"invalid std.Rat right operand\")\n\t}\n\tvar p1, p2 int64\n\tp1 = int64(r1.X) * int64(r2.Y)\n\tp2 = int64(r1.Y) * int64(r2.X)\n\tif p1 \u003c p2 {\n\t\treturn -1\n\t} else if p1 == p2 {\n\t\treturn 0\n\t} else {\n\t\treturn 1\n\t}\n}\n\n//func (r1 Rat) Plus(r2 Rat) Rat {\n// XXX\n//}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "zCQe9f9UfSf7sCPGM3Wjhd6NBvge9YQcpUaNP3JCdSOM46hoMHWA7VrrNasyApW8QmZ2ivE/GvtodkkGSTWyAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "groups", + "path": "gno.land/p/demo/groups", + "files": [ + { + "name": "vote_set.gno", + "body": "package groups\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/rat\"\n)\n\n//----------------------------------------\n// VoteSet\n\ntype VoteSet interface {\n\t// number of present votes in set.\n\tSize() int\n\t// add or update vote for voter.\n\tSetVote(voter std.Address, value string) error\n\t// count the number of votes for value.\n\tCountVotes(value string) int\n}\n\n//----------------------------------------\n// VoteList\n\ntype Vote struct {\n\tVoter std.Address\n\tValue string\n}\n\ntype VoteList []Vote\n\nfunc NewVoteList() *VoteList {\n\treturn \u0026VoteList{}\n}\n\nfunc (vlist *VoteList) Size() int {\n\treturn len(*vlist)\n}\n\nfunc (vlist *VoteList) SetVote(voter std.Address, value string) error {\n\t// TODO optimize with binary algorithm\n\tfor i, vote := range *vlist {\n\t\tif vote.Voter == voter {\n\t\t\t// update vote\n\t\t\t(*vlist)[i] = Vote{\n\t\t\t\tVoter: voter,\n\t\t\t\tValue: value,\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\t*vlist = append(*vlist, Vote{\n\t\tVoter: voter,\n\t\tValue: value,\n\t})\n\treturn nil\n}\n\nfunc (vlist *VoteList) CountVotes(target string) int {\n\t// TODO optimize with binary algorithm\n\tvar count int\n\tfor _, vote := range *vlist {\n\t\tif vote.Value == target {\n\t\t\tcount++\n\t\t}\n\t}\n\treturn count\n}\n\n//----------------------------------------\n// Committee\n\ntype Committee struct {\n\tQuorum rat.Rat\n\tThreshold rat.Rat\n\tAddresses std.AddressSet\n}\n\n//----------------------------------------\n// VoteSession\n// NOTE: this seems a bit too formal and\n// complicated vs what might be possible;\n// something simpler, more informal.\n\ntype SessionStatus int\n\nconst (\n\tSessionNew SessionStatus = iota\n\tSessionStarted\n\tSessionCompleted\n\tSessionCanceled\n)\n\ntype VoteSession struct {\n\tName string\n\tCreator std.Address\n\tBody string\n\tStart time.Time\n\tDeadline time.Time\n\tStatus SessionStatus\n\tCommittee *Committee\n\tVotes VoteSet\n\tChoices []string\n\tResult string\n}\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\nimport (\n\t\"gno.land/p/demo/groups\"\n\t\"gno.land/p/demo/testutils\"\n)\n\nvar vset groups.VoteSet\n\nfunc init() {\n\taddr1 := testutils.TestAddress(\"test1\")\n\taddr2 := testutils.TestAddress(\"test2\")\n\tvset = groups.NewVoteList()\n\tvset.SetVote(addr1, \"yes\")\n\tvset.SetVote(addr2, \"yes\")\n}\n\nfunc main() {\n\tprintln(vset.Size())\n\tprintln(\"yes:\", vset.CountVotes(\"yes\"))\n\tprintln(\"no:\", vset.CountVotes(\"no\"))\n}\n\n// Output:\n// 2\n// yes: 2\n// no: 0\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "jZFzRG97nuNSVU2NzH33xW/kYTQkRMo0CHhALlg31bIDgpKKpraNa5oL4BkQwf6JvZJy+IA8brTrrLcz5kYeBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "uint256", + "path": "gno.land/p/demo/uint256", + "files": [ + { + "name": "LICENSE", + "body": "BSD 3-Clause License\n\nCopyright 2020 uint256 Authors\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, + { + "name": "README.md", + "body": "# Fixed size 256-bit math library\n\nThis is a library specialized at replacing the `big.Int` library for math based on 256-bit types.\n\noriginal repository: [uint256](\u003chttps://github.com/holiman/uint256/tree/master\u003e)\n" + }, + { + "name": "arithmetic.gno", + "body": "// arithmetic provides arithmetic operations for Uint objects.\n// This includes basic binary operations such as addition, subtraction, multiplication, division, and modulo operations\n// as well as overflow checks, and negation. These functions are essential for numeric\n// calculations using 256-bit unsigned integers.\npackage uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Add sets z to the sum x+y\nfunc (z *Uint) Add(x, y *Uint) *Uint {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], _ = bits.Add64(x.arr[3], y.arr[3], carry)\n\treturn z\n}\n\n// AddOverflow sets z to the sum x+y, and returns z and whether overflow occurred\nfunc (z *Uint) AddOverflow(x, y *Uint) (*Uint, bool) {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], carry = bits.Add64(x.arr[3], y.arr[3], carry)\n\treturn z, carry != 0\n}\n\n// Sub sets z to the difference x-y\nfunc (z *Uint) Sub(x, y *Uint) *Uint {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], _ = bits.Sub64(x.arr[3], y.arr[3], carry)\n\treturn z\n}\n\n// SubOverflow sets z to the difference x-y and returns z and true if the operation underflowed\nfunc (z *Uint) SubOverflow(x, y *Uint) (*Uint, bool) {\n\tvar carry uint64\n\tz.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0)\n\tz.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry)\n\tz.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry)\n\tz.arr[3], carry = bits.Sub64(x.arr[3], y.arr[3], carry)\n\treturn z, carry != 0\n}\n\n// Neg returns -x mod 2^256.\nfunc (z *Uint) Neg(x *Uint) *Uint {\n\treturn z.Sub(new(Uint), x)\n}\n\n// commented out for possible overflow\n// Mul sets z to the product x*y\nfunc (z *Uint) Mul(x, y *Uint) *Uint {\n\tvar (\n\t\tres Uint\n\t\tcarry uint64\n\t\tres1, res2, res3 uint64\n\t)\n\n\tcarry, res.arr[0] = bits.Mul64(x.arr[0], y.arr[0])\n\tcarry, res1 = umulHop(carry, x.arr[1], y.arr[0])\n\tcarry, res2 = umulHop(carry, x.arr[2], y.arr[0])\n\tres3 = x.arr[3]*y.arr[0] + carry\n\n\tcarry, res.arr[1] = umulHop(res1, x.arr[0], y.arr[1])\n\tcarry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry)\n\tres3 = res3 + x.arr[2]*y.arr[1] + carry\n\n\tcarry, res.arr[2] = umulHop(res2, x.arr[0], y.arr[2])\n\tres3 = res3 + x.arr[1]*y.arr[2] + carry\n\n\tres.arr[3] = res3 + x.arr[0]*y.arr[3]\n\n\treturn z.Set(\u0026res)\n}\n\n// MulOverflow sets z to the product x*y, and returns z and whether overflow occurred\nfunc (z *Uint) MulOverflow(x, y *Uint) (*Uint, bool) {\n\tp := umul(x, y)\n\tcopy(z.arr[:], p[:4])\n\treturn z, (p[4] | p[5] | p[6] | p[7]) != 0\n}\n\n// commented out for possible overflow\n// Div sets z to the quotient x/y for returns z.\n// If y == 0, z is set to 0\nfunc (z *Uint) Div(x, y *Uint) *Uint {\n\tif y.IsZero() || y.Gt(x) {\n\t\treturn z.Clear()\n\t}\n\tif x.Eq(y) {\n\t\treturn z.SetOne()\n\t}\n\t// Shortcut some cases\n\tif x.IsUint64() {\n\t\treturn z.SetUint64(x.Uint64() / y.Uint64())\n\t}\n\n\t// At this point, we know\n\t// x/y ; x \u003e y \u003e 0\n\n\tvar quot Uint\n\tudivrem(quot.arr[:], x.arr[:], y)\n\treturn z.Set(\u0026quot)\n}\n\n// MulMod calculates the modulo-m multiplication of x and y and\n// returns z.\n// If m == 0, z is set to 0 (OBS: differs from the big.Int)\nfunc (z *Uint) MulMod(x, y, m *Uint) *Uint {\n\tif x.IsZero() || y.IsZero() || m.IsZero() {\n\t\treturn z.Clear()\n\t}\n\tp := umul(x, y)\n\n\tif m.arr[3] != 0 {\n\t\tmu := Reciprocal(m)\n\t\tr := reduce4(p, m, mu)\n\t\treturn z.Set(\u0026r)\n\t}\n\n\tvar (\n\t\tpl Uint\n\t\tph Uint\n\t)\n\n\tpl = Uint{arr: [4]uint64{p[0], p[1], p[2], p[3]}}\n\tph = Uint{arr: [4]uint64{p[4], p[5], p[6], p[7]}}\n\n\t// If the multiplication is within 256 bits use Mod().\n\tif ph.IsZero() {\n\t\treturn z.Mod(\u0026pl, m)\n\t}\n\n\tvar quot [8]uint64\n\trem := udivrem(quot[:], p[:], m)\n\treturn z.Set(\u0026rem)\n}\n\n// Mod sets z to the modulus x%y for y != 0 and returns z.\n// If y == 0, z is set to 0 (OBS: differs from the big.Uint)\nfunc (z *Uint) Mod(x, y *Uint) *Uint {\n\tif x.IsZero() || y.IsZero() {\n\t\treturn z.Clear()\n\t}\n\tswitch x.Cmp(y) {\n\tcase -1:\n\t\t// x \u003c y\n\t\tcopy(z.arr[:], x.arr[:])\n\t\treturn z\n\tcase 0:\n\t\t// x == y\n\t\treturn z.Clear() // They are equal\n\t}\n\n\t// At this point:\n\t// x != 0\n\t// y != 0\n\t// x \u003e y\n\n\t// Shortcut trivial case\n\tif x.IsUint64() {\n\t\treturn z.SetUint64(x.Uint64() % y.Uint64())\n\t}\n\n\tvar quot Uint\n\t*z = udivrem(quot.arr[:], x.arr[:], y)\n\treturn z\n}\n\n// DivMod sets z to the quotient x div y and m to the modulus x mod y and returns the pair (z, m) for y != 0.\n// If y == 0, both z and m are set to 0 (OBS: differs from the big.Int)\nfunc (z *Uint) DivMod(x, y, m *Uint) (*Uint, *Uint) {\n\tif y.IsZero() {\n\t\treturn z.Clear(), m.Clear()\n\t}\n\tvar quot Uint\n\t*m = udivrem(quot.arr[:], x.arr[:], y)\n\t*z = quot\n\treturn z, m\n}\n\n// Exp sets z = base**exponent mod 2**256, and returns z.\nfunc (z *Uint) Exp(base, exponent *Uint) *Uint {\n\tres := Uint{arr: [4]uint64{1, 0, 0, 0}}\n\tmultiplier := *base\n\texpBitLen := exponent.BitLen()\n\n\tcurBit := 0\n\tword := exponent.arr[0]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 64; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[1]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 128; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[2]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 192; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\n\tword = exponent.arr[3]\n\tfor ; curBit \u003c expBitLen \u0026\u0026 curBit \u003c 256; curBit++ {\n\t\tif word\u00261 == 1 {\n\t\t\tres.Mul(\u0026res, \u0026multiplier)\n\t\t}\n\t\tmultiplier.squared()\n\t\tword \u003e\u003e= 1\n\t}\n\treturn z.Set(\u0026res)\n}\n\nfunc (z *Uint) squared() {\n\tvar (\n\t\tres Uint\n\t\tcarry0, carry1, carry2 uint64\n\t\tres1, res2 uint64\n\t)\n\n\tcarry0, res.arr[0] = bits.Mul64(z.arr[0], z.arr[0])\n\tcarry0, res1 = umulHop(carry0, z.arr[0], z.arr[1])\n\tcarry0, res2 = umulHop(carry0, z.arr[0], z.arr[2])\n\n\tcarry1, res.arr[1] = umulHop(res1, z.arr[0], z.arr[1])\n\tcarry1, res2 = umulStep(res2, z.arr[1], z.arr[1], carry1)\n\n\tcarry2, res.arr[2] = umulHop(res2, z.arr[0], z.arr[2])\n\n\tres.arr[3] = 2*(z.arr[0]*z.arr[3]+z.arr[1]*z.arr[2]) + carry0 + carry1 + carry2\n\n\tz.Set(\u0026res)\n}\n\n// udivrem divides u by d and produces both quotient and remainder.\n// The quotient is stored in provided quot - len(u)-len(d)+1 words.\n// It loosely follows the Knuth's division algorithm (sometimes referenced as \"schoolbook\" division) using 64-bit words.\n// See Knuth, Volume 2, section 4.3.1, Algorithm D.\nfunc udivrem(quot, u []uint64, d *Uint) (rem Uint) {\n\tvar dLen int\n\tfor i := len(d.arr) - 1; i \u003e= 0; i-- {\n\t\tif d.arr[i] != 0 {\n\t\t\tdLen = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tshift := uint(bits.LeadingZeros64(d.arr[dLen-1]))\n\n\tvar dnStorage Uint\n\tdn := dnStorage.arr[:dLen]\n\tfor i := dLen - 1; i \u003e 0; i-- {\n\t\tdn[i] = (d.arr[i] \u003c\u003c shift) | (d.arr[i-1] \u003e\u003e (64 - shift))\n\t}\n\tdn[0] = d.arr[0] \u003c\u003c shift\n\n\tvar uLen int\n\tfor i := len(u) - 1; i \u003e= 0; i-- {\n\t\tif u[i] != 0 {\n\t\t\tuLen = i + 1\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif uLen \u003c dLen {\n\t\tcopy(rem.arr[:], u)\n\t\treturn rem\n\t}\n\n\tvar unStorage [9]uint64\n\tun := unStorage[:uLen+1]\n\tun[uLen] = u[uLen-1] \u003e\u003e (64 - shift)\n\tfor i := uLen - 1; i \u003e 0; i-- {\n\t\tun[i] = (u[i] \u003c\u003c shift) | (u[i-1] \u003e\u003e (64 - shift))\n\t}\n\tun[0] = u[0] \u003c\u003c shift\n\n\t// TODO: Skip the highest word of numerator if not significant.\n\n\tif dLen == 1 {\n\t\tr := udivremBy1(quot, un, dn[0])\n\t\trem.SetUint64(r \u003e\u003e shift)\n\t\treturn rem\n\t}\n\n\tudivremKnuth(quot, un, dn)\n\n\tfor i := 0; i \u003c dLen-1; i++ {\n\t\trem.arr[i] = (un[i] \u003e\u003e shift) | (un[i+1] \u003c\u003c (64 - shift))\n\t}\n\trem.arr[dLen-1] = un[dLen-1] \u003e\u003e shift\n\n\treturn rem\n}\n\n// umul computes full 256 x 256 -\u003e 512 multiplication.\nfunc umul(x, y *Uint) [8]uint64 {\n\tvar (\n\t\tres [8]uint64\n\t\tcarry, carry4, carry5, carry6 uint64\n\t\tres1, res2, res3, res4, res5 uint64\n\t)\n\n\tcarry, res[0] = bits.Mul64(x.arr[0], y.arr[0])\n\tcarry, res1 = umulHop(carry, x.arr[1], y.arr[0])\n\tcarry, res2 = umulHop(carry, x.arr[2], y.arr[0])\n\tcarry4, res3 = umulHop(carry, x.arr[3], y.arr[0])\n\n\tcarry, res[1] = umulHop(res1, x.arr[0], y.arr[1])\n\tcarry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry)\n\tcarry, res3 = umulStep(res3, x.arr[2], y.arr[1], carry)\n\tcarry5, res4 = umulStep(carry4, x.arr[3], y.arr[1], carry)\n\n\tcarry, res[2] = umulHop(res2, x.arr[0], y.arr[2])\n\tcarry, res3 = umulStep(res3, x.arr[1], y.arr[2], carry)\n\tcarry, res4 = umulStep(res4, x.arr[2], y.arr[2], carry)\n\tcarry6, res5 = umulStep(carry5, x.arr[3], y.arr[2], carry)\n\n\tcarry, res[3] = umulHop(res3, x.arr[0], y.arr[3])\n\tcarry, res[4] = umulStep(res4, x.arr[1], y.arr[3], carry)\n\tcarry, res[5] = umulStep(res5, x.arr[2], y.arr[3], carry)\n\tres[7], res[6] = umulStep(carry6, x.arr[3], y.arr[3], carry)\n\n\treturn res\n}\n\n// umulStep computes (hi * 2^64 + lo) = z + (x * y) + carry.\nfunc umulStep(z, x, y, carry uint64) (hi, lo uint64) {\n\thi, lo = bits.Mul64(x, y)\n\tlo, carry = bits.Add64(lo, carry, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\tlo, carry = bits.Add64(lo, z, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\treturn hi, lo\n}\n\n// umulHop computes (hi * 2^64 + lo) = z + (x * y)\nfunc umulHop(z, x, y uint64) (hi, lo uint64) {\n\thi, lo = bits.Mul64(x, y)\n\tlo, carry := bits.Add64(lo, z, 0)\n\thi, _ = bits.Add64(hi, 0, carry)\n\treturn hi, lo\n}\n\n// udivremBy1 divides u by single normalized word d and produces both quotient and remainder.\n// The quotient is stored in provided quot.\nfunc udivremBy1(quot, u []uint64, d uint64) (rem uint64) {\n\treciprocal := reciprocal2by1(d)\n\trem = u[len(u)-1] // Set the top word as remainder.\n\tfor j := len(u) - 2; j \u003e= 0; j-- {\n\t\tquot[j], rem = udivrem2by1(rem, u[j], d, reciprocal)\n\t}\n\treturn rem\n}\n\n// udivremKnuth implements the division of u by normalized multiple word d from the Knuth's division algorithm.\n// The quotient is stored in provided quot - len(u)-len(d) words.\n// Updates u to contain the remainder - len(d) words.\nfunc udivremKnuth(quot, u, d []uint64) {\n\tdh := d[len(d)-1]\n\tdl := d[len(d)-2]\n\treciprocal := reciprocal2by1(dh)\n\n\tfor j := len(u) - len(d) - 1; j \u003e= 0; j-- {\n\t\tu2 := u[j+len(d)]\n\t\tu1 := u[j+len(d)-1]\n\t\tu0 := u[j+len(d)-2]\n\n\t\tvar qhat, rhat uint64\n\t\tif u2 \u003e= dh { // Division overflows.\n\t\t\tqhat = ^uint64(0)\n\t\t\t// TODO: Add \"qhat one to big\" adjustment (not needed for correctness, but helps avoiding \"add back\" case).\n\t\t} else {\n\t\t\tqhat, rhat = udivrem2by1(u2, u1, dh, reciprocal)\n\t\t\tph, pl := bits.Mul64(qhat, dl)\n\t\t\tif ph \u003e rhat || (ph == rhat \u0026\u0026 pl \u003e u0) {\n\t\t\t\tqhat--\n\t\t\t\t// TODO: Add \"qhat one to big\" adjustment (not needed for correctness, but helps avoiding \"add back\" case).\n\t\t\t}\n\t\t}\n\n\t\t// Multiply and subtract.\n\t\tborrow := subMulTo(u[j:], d, qhat)\n\t\tu[j+len(d)] = u2 - borrow\n\t\tif u2 \u003c borrow { // Too much subtracted, add back.\n\t\t\tqhat--\n\t\t\tu[j+len(d)] += addTo(u[j:], d)\n\t\t}\n\n\t\tquot[j] = qhat // Store quotient digit.\n\t}\n}\n\n// isBitSet returns true if bit n-th is set, where n = 0 is LSB.\n// The n must be \u003c= 255.\nfunc (z *Uint) isBitSet(n uint) bool {\n\treturn (z.arr[n/64] \u0026 (1 \u003c\u003c (n % 64))) != 0\n}\n\n// addTo computes x += y.\n// Requires len(x) \u003e= len(y).\nfunc addTo(x, y []uint64) uint64 {\n\tvar carry uint64\n\tfor i := 0; i \u003c len(y); i++ {\n\t\tx[i], carry = bits.Add64(x[i], y[i], carry)\n\t}\n\treturn carry\n}\n\n// subMulTo computes x -= y * multiplier.\n// Requires len(x) \u003e= len(y).\nfunc subMulTo(x, y []uint64, multiplier uint64) uint64 {\n\tvar borrow uint64\n\tfor i := 0; i \u003c len(y); i++ {\n\t\ts, carry1 := bits.Sub64(x[i], borrow, 0)\n\t\tph, pl := bits.Mul64(y[i], multiplier)\n\t\tt, carry2 := bits.Sub64(s, pl, 0)\n\t\tx[i] = t\n\t\tborrow = ph + carry1 + carry2\n\t}\n\treturn borrow\n}\n\n// reciprocal2by1 computes \u003c^d, ^0\u003e / d.\nfunc reciprocal2by1(d uint64) uint64 {\n\treciprocal, _ := bits.Div64(^d, ^uint64(0), d)\n\treturn reciprocal\n}\n\n// udivrem2by1 divides \u003cuh, ul\u003e / d and produces both quotient and remainder.\n// It uses the provided d's reciprocal.\n// Implementation ported from https://github.com/chfast/intx and is based on\n// \"Improved division by invariant integers\", Algorithm 4.\nfunc udivrem2by1(uh, ul, d, reciprocal uint64) (quot, rem uint64) {\n\tqh, ql := bits.Mul64(reciprocal, uh)\n\tql, carry := bits.Add64(ql, ul, 0)\n\tqh, _ = bits.Add64(qh, uh, carry)\n\tqh++\n\n\tr := ul - qh*d\n\n\tif r \u003e ql {\n\t\tqh--\n\t\tr += d\n\t}\n\n\tif r \u003e= d {\n\t\tqh++\n\t\tr -= d\n\t}\n\n\treturn qh, r\n}\n" + }, + { + "name": "arithmetic_test.gno", + "body": "package uint256\n\nimport (\n\t\"testing\"\n)\n\ntype binOp2Test struct {\n\tx, y, want string\n}\n\nfunc TestAdd(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"0\", \"1\", \"1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"2\"},\n\t\t{\"1\", \"3\", \"4\"},\n\t\t{\"10\", \"10\", \"20\"},\n\t\t{\"18446744073709551615\", \"18446744073709551615\", \"36893488147419103230\"}, // uint64 overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\n\t\twant := MustFromDecimal(tt.want)\n\t\tgot := new(Uint).Add(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Add(%s, %s) = %v, want %v\", tt.x, tt.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestAddOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant string\n\t\toverflow bool\n\t}{\n\t\t{\"0\", \"1\", \"1\", false},\n\t\t{\"1\", \"0\", \"1\", false},\n\t\t{\"1\", \"1\", \"2\", false},\n\t\t{\"10\", \"10\", \"20\", false},\n\t\t{\"18446744073709551615\", \"18446744073709551615\", \"36893488147419103230\", false}, // uint64 overflow, but not Uint256 overflow\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"1\", \"0\", true}, // 2^256 - 1 + 1, should overflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819967\", \"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", false}, // (2^255 - 1) + 2^255, no overflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819967\", \"57896044618658097711785492504343953926634992332820282019728792003956564819969\", \"0\", true}, // (2^255 - 1) + (2^255 + 1), should overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant, _ := FromDecimal(tt.want)\n\n\t\tgot, overflow := new(Uint).AddOverflow(x, y)\n\n\t\tif got.Cmp(want) != 0 || overflow != tt.overflow {\n\t\t\tt.Errorf(\"AddOverflow(%s, %s) = (%s, %v), want (%s, %v)\",\n\t\t\t\ttt.x, tt.y, got.String(), overflow, tt.want, tt.overflow)\n\t\t}\n\t}\n}\n\nfunc TestSub(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"10\", \"10\", \"0\"},\n\t\t{\"31337\", \"1337\", \"30000\"},\n\t\t{\"2\", \"3\", twoPow256Sub1}, // underflow\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := MustFromDecimal(tc.x)\n\t\ty := MustFromDecimal(tc.y)\n\n\t\twant := MustFromDecimal(tc.want)\n\n\t\tgot := new(Uint).Sub(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\n\t\t\t\t\"Sub(%s, %s) = %v, want %v\",\n\t\t\t\ttc.x, tc.y, got.String(), want.String(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestSubOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant string\n\t\toverflow bool\n\t}{\n\t\t{\"1\", \"0\", \"1\", false},\n\t\t{\"1\", \"1\", \"0\", false},\n\t\t{\"10\", \"10\", \"0\", false},\n\t\t{\"31337\", \"1337\", \"30000\", false},\n\t\t{\"0\", \"1\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", true}, // 0 - 1, should underflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"1\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\", false}, // 2^255 - 1, no underflow\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"57896044618658097711785492504343953926634992332820282019728792003956564819969\", \"115792089237316195423570985008687907853269984665640564039457584007913129639935\", true}, // 2^255 - (2^255 + 1), should underflow\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := MustFromDecimal(tc.x)\n\t\ty := MustFromDecimal(tc.y)\n\t\twant := MustFromDecimal(tc.want)\n\n\t\tgot, overflow := new(Uint).SubOverflow(x, y)\n\n\t\tif got.Cmp(want) != 0 || overflow != tc.overflow {\n\t\t\tt.Errorf(\n\t\t\t\t\"SubOverflow(%s, %s) = (%s, %v), want (%s, %v)\",\n\t\t\t\ttc.x, tc.y, got.String(), overflow, tc.want, tc.overflow,\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestMul(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"1\", \"0\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"10\", \"10\", \"100\"},\n\t\t{\"18446744073709551615\", \"2\", \"36893488147419103230\"}, // uint64 overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\t\tgot := new(Uint).Mul(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mul(%s, %s) = %v, want %v\", tt.x, tt.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestMulOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\twantZ string\n\t\twantOver bool\n\t}{\n\t\t{\"0x1\", \"0x1\", \"0x1\", false},\n\t\t{\"0x0\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x0\", false},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x2\", \"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\", true},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x1\", true},\n\t\t{\"0x8000000000000000000000000000000000000000000000000000000000000000\", \"0x2\", \"0x0\", true},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x2\", \"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\", false},\n\t\t{\"0x100000000000000000\", \"0x100000000000000000\", \"0x10000000000000000000000000000000000\", false},\n\t\t{\"0x10000000000000000000000000000000\", \"0x10000000000000000000000000000000\", \"0x100000000000000000000000000000000000000000000000000000000000000\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\ty := MustFromHex(tt.y)\n\t\twantZ := MustFromHex(tt.wantZ)\n\n\t\tgotZ, gotOver := new(Uint).MulOverflow(x, y)\n\n\t\tif gotZ.Neq(wantZ) {\n\t\t\tt.Errorf(\n\t\t\t\t\"MulOverflow(%s, %s) = %s, want %s\",\n\t\t\t\ttt.x, tt.y, gotZ.String(), wantZ.String(),\n\t\t\t)\n\t\t}\n\t\tif gotOver != tt.wantOver {\n\t\t\tt.Errorf(\"MulOverflow(%s, %s) = %v, want %v\", tt.x, tt.y, gotOver, tt.wantOver)\n\t\t}\n\t}\n}\n\nfunc TestDiv(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"31337\", \"3\", \"10445\"},\n\t\t{\"31337\", \"0\", \"0\"},\n\t\t{\"0\", \"31337\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"1000000000000000000\", \"3\", \"333333333333333333\"},\n\t\t{twoPow256Sub1, \"2\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Div(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Div(%s, %s) = %v, want %v\", tt.x, tt.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestMod(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"31337\", \"3\", \"2\"},\n\t\t{\"31337\", \"0\", \"0\"},\n\t\t{\"0\", \"31337\", \"0\"},\n\t\t{\"2\", \"31337\", \"2\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"2\", \"1\"}, // 2^256 - 1 mod 2\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"3\", \"0\"}, // 2^256 - 1 mod 3\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"57896044618658097711785492504343953926634992332820282019728792003956564819968\", \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"}, // 2^256 - 1 mod 2^255\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Mod(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mod(%s, %s) = %v, want %v\", tt.x, tt.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestMulMod(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\tm string\n\t\twant string\n\t}{\n\t\t{\"0x1\", \"0x1\", \"0x2\", \"0x1\"},\n\t\t{\"0x10\", \"0x10\", \"0x7\", \"0x4\"},\n\t\t{\"0x100\", \"0x100\", \"0x17\", \"0x9\"},\n\t\t{\"0x31337\", \"0x31337\", \"0x31338\", \"0x1\"},\n\t\t{\"0x0\", \"0x31337\", \"0x31338\", \"0x0\"},\n\t\t{\"0x31337\", \"0x0\", \"0x31338\", \"0x0\"},\n\t\t{\"0x2\", \"0x3\", \"0x5\", \"0x1\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0x0\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\", \"0x1\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", \"0xffffffffffffffffffffffffffffffff\", \"0x0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\ty := MustFromHex(tt.y)\n\t\tm := MustFromHex(tt.m)\n\t\twant := MustFromHex(tt.want)\n\n\t\tgot := new(Uint).MulMod(x, y, m)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\n\t\t\t\t\"MulMod(%s, %s, %s) = %s, want %s\",\n\t\t\t\ttt.x, tt.y, tt.m, got.String(), want.String(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestDivMod(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\twantDiv string\n\t\twantMod string\n\t}{\n\t\t{\"1\", \"1\", \"1\", \"0\"},\n\t\t{\"10\", \"10\", \"1\", \"0\"},\n\t\t{\"100\", \"10\", \"10\", \"0\"},\n\t\t{\"31337\", \"3\", \"10445\", \"2\"},\n\t\t{\"31337\", \"0\", \"0\", \"0\"},\n\t\t{\"0\", \"31337\", \"0\", \"0\"},\n\t\t{\"2\", \"31337\", \"0\", \"2\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twantDiv := MustFromDecimal(tt.wantDiv)\n\t\twantMod := MustFromDecimal(tt.wantMod)\n\n\t\tgotDiv := new(Uint)\n\t\tgotMod := new(Uint)\n\t\tgotDiv.DivMod(x, y, gotMod)\n\n\t\tfor i := range gotDiv.arr {\n\t\t\tif gotDiv.arr[i] != wantDiv.arr[i] {\n\t\t\t\tt.Errorf(\"DivMod(%s, %s) got Div %v, want Div %v\", tt.x, tt.y, gotDiv, wantDiv)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tfor i := range gotMod.arr {\n\t\t\tif gotMod.arr[i] != wantMod.arr[i] {\n\t\t\t\tt.Errorf(\"DivMod(%s, %s) got Mod %v, want Mod %v\", tt.x, tt.y, gotMod, wantMod)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestNeg(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"31337\", \"115792089237316195423570985008687907853269984665640564039457584007913129608599\"},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129608599\", \"31337\"},\n\t\t{\"0\", \"0\"},\n\t\t{\"2\", \"115792089237316195423570985008687907853269984665640564039457584007913129639934\"},\n\t\t{\"1\", twoPow256Sub1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Neg(x)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Neg(%s) = %v, want %v\", tt.x, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestExp(t *testing.T) {\n\ttests := []binOp2Test{\n\t\t{\"31337\", \"3\", \"30773171189753\"},\n\t\t{\"31337\", \"0\", \"1\"},\n\t\t{\"0\", \"31337\", \"0\"},\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"2\", \"3\", \"8\"},\n\t\t{\"2\", \"64\", \"18446744073709551616\"},\n\t\t{\"2\", \"128\", \"340282366920938463463374607431768211456\"},\n\t\t{\"2\", \"255\", \"57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"2\", \"256\", \"0\"}, // overflow\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Exp(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\n\t\t\t\t\"Exp(%s, %s) = %v, want %v\",\n\t\t\t\ttt.x, tt.y, got.String(), want.String(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestExp_LargeExponent(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tbase string\n\t\texponent string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"2^129\",\n\t\t\tbase: \"2\",\n\t\t\texponent: \"680564733841876926926749214863536422912\",\n\t\t\texpected: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"2^193\",\n\t\t\tbase: \"2\",\n\t\t\texponent: \"12379400392853802746563808384000000000000000000\",\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\tbase := MustFromDecimal(tt.base)\n\t\t\texponent := MustFromDecimal(tt.exponent)\n\t\t\texpected := MustFromDecimal(tt.expected)\n\n\t\t\tresult := new(Uint).Exp(base, exponent)\n\n\t\t\tif result.Neq(expected) {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Test %s failed. Expected %s, got %s\",\n\t\t\t\t\ttt.name, expected.String(), result.String(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "bits_table.gno", + "body": "// Copyright 2017 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Code generated by go run make_tables.go. DO NOT EDIT.\n\npackage uint256\n\nconst ntz8tab = \"\" +\n\t\"\\x08\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x06\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x07\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x06\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x05\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\" +\n\t\"\\x04\\x00\\x01\\x00\\x02\\x00\\x01\\x00\\x03\\x00\\x01\\x00\\x02\\x00\\x01\\x00\"\n\nconst pop8tab = \"\" +\n\t\"\\x00\\x01\\x01\\x02\\x01\\x02\\x02\\x03\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x01\\x02\\x02\\x03\\x02\\x03\\x03\\x04\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x02\\x03\\x03\\x04\\x03\\x04\\x04\\x05\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x03\\x04\\x04\\x05\\x04\\x05\\x05\\x06\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\" +\n\t\"\\x04\\x05\\x05\\x06\\x05\\x06\\x06\\x07\\x05\\x06\\x06\\x07\\x06\\x07\\x07\\x08\"\n\nconst rev8tab = \"\" +\n\t\"\\x00\\x80\\x40\\xc0\\x20\\xa0\\x60\\xe0\\x10\\x90\\x50\\xd0\\x30\\xb0\\x70\\xf0\" +\n\t\"\\x08\\x88\\x48\\xc8\\x28\\xa8\\x68\\xe8\\x18\\x98\\x58\\xd8\\x38\\xb8\\x78\\xf8\" +\n\t\"\\x04\\x84\\x44\\xc4\\x24\\xa4\\x64\\xe4\\x14\\x94\\x54\\xd4\\x34\\xb4\\x74\\xf4\" +\n\t\"\\x0c\\x8c\\x4c\\xcc\\x2c\\xac\\x6c\\xec\\x1c\\x9c\\x5c\\xdc\\x3c\\xbc\\x7c\\xfc\" +\n\t\"\\x02\\x82\\x42\\xc2\\x22\\xa2\\x62\\xe2\\x12\\x92\\x52\\xd2\\x32\\xb2\\x72\\xf2\" +\n\t\"\\x0a\\x8a\\x4a\\xca\\x2a\\xaa\\x6a\\xea\\x1a\\x9a\\x5a\\xda\\x3a\\xba\\x7a\\xfa\" +\n\t\"\\x06\\x86\\x46\\xc6\\x26\\xa6\\x66\\xe6\\x16\\x96\\x56\\xd6\\x36\\xb6\\x76\\xf6\" +\n\t\"\\x0e\\x8e\\x4e\\xce\\x2e\\xae\\x6e\\xee\\x1e\\x9e\\x5e\\xde\\x3e\\xbe\\x7e\\xfe\" +\n\t\"\\x01\\x81\\x41\\xc1\\x21\\xa1\\x61\\xe1\\x11\\x91\\x51\\xd1\\x31\\xb1\\x71\\xf1\" +\n\t\"\\x09\\x89\\x49\\xc9\\x29\\xa9\\x69\\xe9\\x19\\x99\\x59\\xd9\\x39\\xb9\\x79\\xf9\" +\n\t\"\\x05\\x85\\x45\\xc5\\x25\\xa5\\x65\\xe5\\x15\\x95\\x55\\xd5\\x35\\xb5\\x75\\xf5\" +\n\t\"\\x0d\\x8d\\x4d\\xcd\\x2d\\xad\\x6d\\xed\\x1d\\x9d\\x5d\\xdd\\x3d\\xbd\\x7d\\xfd\" +\n\t\"\\x03\\x83\\x43\\xc3\\x23\\xa3\\x63\\xe3\\x13\\x93\\x53\\xd3\\x33\\xb3\\x73\\xf3\" +\n\t\"\\x0b\\x8b\\x4b\\xcb\\x2b\\xab\\x6b\\xeb\\x1b\\x9b\\x5b\\xdb\\x3b\\xbb\\x7b\\xfb\" +\n\t\"\\x07\\x87\\x47\\xc7\\x27\\xa7\\x67\\xe7\\x17\\x97\\x57\\xd7\\x37\\xb7\\x77\\xf7\" +\n\t\"\\x0f\\x8f\\x4f\\xcf\\x2f\\xaf\\x6f\\xef\\x1f\\x9f\\x5f\\xdf\\x3f\\xbf\\x7f\\xff\"\n\nconst len8tab = \"\" +\n\t\"\\x00\\x01\\x02\\x02\\x03\\x03\\x03\\x03\\x04\\x04\\x04\\x04\\x04\\x04\\x04\\x04\" +\n\t\"\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\\x05\" +\n\t\"\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\" +\n\t\"\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\\x06\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\\x07\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\" +\n\t\"\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\\x08\"\n" + }, + { + "name": "bitwise.gno", + "body": "// bitwise contains bitwise operations for Uint instances.\n// This file includes functions to perform bitwise AND, OR, XOR, and NOT operations, as well as bit shifting.\n// These operations are crucial for manipulating individual bits within a 256-bit unsigned integer.\npackage uint256\n\n// Or sets z = x | y and returns z.\nfunc (z *Uint) Or(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] | y.arr[0]\n\tz.arr[1] = x.arr[1] | y.arr[1]\n\tz.arr[2] = x.arr[2] | y.arr[2]\n\tz.arr[3] = x.arr[3] | y.arr[3]\n\treturn z\n}\n\n// And sets z = x \u0026 y and returns z.\nfunc (z *Uint) And(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] \u0026 y.arr[0]\n\tz.arr[1] = x.arr[1] \u0026 y.arr[1]\n\tz.arr[2] = x.arr[2] \u0026 y.arr[2]\n\tz.arr[3] = x.arr[3] \u0026 y.arr[3]\n\treturn z\n}\n\n// Not sets z = ^x and returns z.\nfunc (z *Uint) Not(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = ^x.arr[3], ^x.arr[2], ^x.arr[1], ^x.arr[0]\n\treturn z\n}\n\n// AndNot sets z = x \u0026^ y and returns z.\nfunc (z *Uint) AndNot(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] \u0026^ y.arr[0]\n\tz.arr[1] = x.arr[1] \u0026^ y.arr[1]\n\tz.arr[2] = x.arr[2] \u0026^ y.arr[2]\n\tz.arr[3] = x.arr[3] \u0026^ y.arr[3]\n\treturn z\n}\n\n// Xor sets z = x ^ y and returns z.\nfunc (z *Uint) Xor(x, y *Uint) *Uint {\n\tz.arr[0] = x.arr[0] ^ y.arr[0]\n\tz.arr[1] = x.arr[1] ^ y.arr[1]\n\tz.arr[2] = x.arr[2] ^ y.arr[2]\n\tz.arr[3] = x.arr[3] ^ y.arr[3]\n\treturn z\n}\n\n// Lsh sets z = x \u003c\u003c n and returns z.\nfunc (z *Uint) Lsh(x *Uint, n uint) *Uint {\n\t// n % 64 == 0\n\tif n\u00260x3f == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.lsh64(x)\n\t\tcase 128:\n\t\t\treturn z.lsh128(x)\n\t\tcase 192:\n\t\t\treturn z.lsh192(x)\n\t\tdefault:\n\t\t\treturn z.Clear()\n\t\t}\n\t}\n\tvar a, b uint64\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.Clear()\n\t\t}\n\t\tz.lsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.lsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.lsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\ta = z.arr[0] \u003e\u003e (64 - n)\n\tz.arr[0] = z.arr[0] \u003c\u003c n\n\nsh64:\n\tb = z.arr[1] \u003e\u003e (64 - n)\n\tz.arr[1] = (z.arr[1] \u003c\u003c n) | a\n\nsh128:\n\ta = z.arr[2] \u003e\u003e (64 - n)\n\tz.arr[2] = (z.arr[2] \u003c\u003c n) | b\n\nsh192:\n\tz.arr[3] = (z.arr[3] \u003c\u003c n) | a\n\n\treturn z\n}\n\n// Rsh sets z = x \u003e\u003e n and returns z.\nfunc (z *Uint) Rsh(x *Uint, n uint) *Uint {\n\t// n % 64 == 0\n\tif n\u00260x3f == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.rsh64(x)\n\t\tcase 128:\n\t\t\treturn z.rsh128(x)\n\t\tcase 192:\n\t\t\treturn z.rsh192(x)\n\t\tdefault:\n\t\t\treturn z.Clear()\n\t\t}\n\t}\n\tvar a, b uint64\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.Clear()\n\t\t}\n\t\tz.rsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.rsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.rsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\ta = z.arr[3] \u003c\u003c (64 - n)\n\tz.arr[3] = z.arr[3] \u003e\u003e n\n\nsh64:\n\tb = z.arr[2] \u003c\u003c (64 - n)\n\tz.arr[2] = (z.arr[2] \u003e\u003e n) | a\n\nsh128:\n\ta = z.arr[1] \u003c\u003c (64 - n)\n\tz.arr[1] = (z.arr[1] \u003e\u003e n) | b\n\nsh192:\n\tz.arr[0] = (z.arr[0] \u003e\u003e n) | a\n\n\treturn z\n}\n\n// SRsh (Signed/Arithmetic right shift)\n// considers z to be a signed integer, during right-shift\n// and sets z = x \u003e\u003e n and returns z.\nfunc (z *Uint) SRsh(x *Uint, n uint) *Uint {\n\t// If the MSB is 0, SRsh is same as Rsh.\n\tif !x.isBitSet(255) {\n\t\treturn z.Rsh(x, n)\n\t}\n\tif n%64 == 0 {\n\t\tswitch n {\n\t\tcase 0:\n\t\t\treturn z.Set(x)\n\t\tcase 64:\n\t\t\treturn z.srsh64(x)\n\t\tcase 128:\n\t\t\treturn z.srsh128(x)\n\t\tcase 192:\n\t\t\treturn z.srsh192(x)\n\t\tdefault:\n\t\t\treturn z.SetAllOne()\n\t\t}\n\t}\n\tvar a uint64 = MaxUint64 \u003c\u003c (64 - n%64)\n\t// Big swaps first\n\tswitch {\n\tcase n \u003e 192:\n\t\tif n \u003e 256 {\n\t\t\treturn z.SetAllOne()\n\t\t}\n\t\tz.srsh192(x)\n\t\tn -= 192\n\t\tgoto sh192\n\tcase n \u003e 128:\n\t\tz.srsh128(x)\n\t\tn -= 128\n\t\tgoto sh128\n\tcase n \u003e 64:\n\t\tz.srsh64(x)\n\t\tn -= 64\n\t\tgoto sh64\n\tdefault:\n\t\tz.Set(x)\n\t}\n\n\t// remaining shifts\n\tz.arr[3], a = (z.arr[3]\u003e\u003en)|a, z.arr[3]\u003c\u003c(64-n)\n\nsh64:\n\tz.arr[2], a = (z.arr[2]\u003e\u003en)|a, z.arr[2]\u003c\u003c(64-n)\n\nsh128:\n\tz.arr[1], a = (z.arr[1]\u003e\u003en)|a, z.arr[1]\u003c\u003c(64-n)\n\nsh192:\n\tz.arr[0] = (z.arr[0] \u003e\u003e n) | a\n\n\treturn z\n}\n\nfunc (z *Uint) lsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[2], x.arr[1], x.arr[0], 0\n\treturn z\n}\n\nfunc (z *Uint) lsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[1], x.arr[0], 0, 0\n\treturn z\n}\n\nfunc (z *Uint) lsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[0], 0, 0, 0\n\treturn z\n}\n\nfunc (z *Uint) rsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, x.arr[3], x.arr[2], x.arr[1]\n\treturn z\n}\n\nfunc (z *Uint) rsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, x.arr[3], x.arr[2]\n\treturn z\n}\n\nfunc (z *Uint) rsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x.arr[3]\n\treturn z\n}\n\nfunc (z *Uint) srsh64(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, x.arr[3], x.arr[2], x.arr[1]\n\treturn z\n}\n\nfunc (z *Uint) srsh128(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, x.arr[3], x.arr[2]\n\treturn z\n}\n\nfunc (z *Uint) srsh192(x *Uint) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, x.arr[3]\n\treturn z\n}\n" + }, + { + "name": "bitwise_test.gno", + "body": "package uint256\n\nimport \"testing\"\n\ntype logicOpTest struct {\n\tname string\n\tx Uint\n\ty Uint\n\twant Uint\n}\n\nfunc TestOr(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(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\tres := new(Uint).Or(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Or(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.String(), tt.y.String(), res.String(), (tt.want).String(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAnd(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tres := new(Uint).And(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"And(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.String(), tt.y.String(), res.String(), (tt.want).String(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNot(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tx Uint\n\t\twant Uint\n\t}{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(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\tres := new(Uint).Not(\u0026tt.x)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Not(%s) = %s, want %s\",\n\t\t\t\t\ttt.x.String(), res.String(), (tt.want).String(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAndNot(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{0xAAAAAAAAAAAAAAAA, 0x5555555555555555, 0x0000000000000000, ^uint64(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\tres := new(Uint).AndNot(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"AndNot(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.String(), tt.y.String(), res.String(), (tt.want).String(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestXor(t *testing.T) {\n\ttests := []logicOpTest{\n\t\t{\n\t\t\tname: \"all zeros\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 2\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed 3\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}},\n\t\t\twant: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand zero\",\n\t\t\tx: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\ty: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t\twant: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}},\n\t\t},\n\t\t{\n\t\t\tname: \"one operand all ones\",\n\t\t\tx: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\ty: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}},\n\t\t\twant: Uint{arr: [4]uint64{0xAAAAAAAAAAAAAAAA, 0x5555555555555555, 0x0000000000000000, ^uint64(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\tres := new(Uint).Xor(\u0026tt.x, \u0026tt.y)\n\t\t\tif *res != tt.want {\n\t\t\t\tt.Errorf(\n\t\t\t\t\t\"Xor(%s, %s) = %s, want %s\",\n\t\t\t\t\ttt.x.String(), tt.y.String(), res.String(), (tt.want).String(),\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint\n\t\twant string\n\t}{\n\t\t{\"0\", 0, \"0\"},\n\t\t{\"0\", 1, \"0\"},\n\t\t{\"0\", 64, \"0\"},\n\t\t{\"1\", 0, \"1\"},\n\t\t{\"1\", 1, \"2\"},\n\t\t{\"1\", 64, \"18446744073709551616\"},\n\t\t{\"1\", 128, \"340282366920938463463374607431768211456\"},\n\t\t{\"1\", 192, \"6277101735386680763835789423207666416102355444464034512896\"},\n\t\t{\"1\", 255, \"57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"1\", 256, \"0\"},\n\t\t{\"31337\", 0, \"31337\"},\n\t\t{\"31337\", 1, \"62674\"},\n\t\t{\"31337\", 64, \"578065619037836218990592\"},\n\t\t{\"31337\", 128, \"10663428532201448629551770073089320442396672\"},\n\t\t{\"31337\", 192, \"196705537081812415096322133155058642481399512563169449530621952\"},\n\t\t{\"31337\", 193, \"393411074163624830192644266310117284962799025126338899061243904\"},\n\t\t{\"31337\", 255, \"57896044618658097711785492504343953926634992332820282019728792003956564819968\"},\n\t\t{\"31337\", 256, \"0\"},\n\t\t// 64 \u003c n \u003c 128\n\t\t{\"1\", 65, \"36893488147419103232\"},\n\t\t{\"31337\", 100, \"39724366859352024754702188346867712\"},\n\n\t\t// 128 \u003c n \u003c 192\n\t\t{\"1\", 129, \"680564733841876926926749214863536422912\"},\n\t\t{\"31337\", 150, \"44725660946326664792723507424638829088826130956288\"},\n\n\t\t// 192 \u003c n \u003c 256\n\t\t{\"1\", 193, \"12554203470773361527671578846415332832204710888928069025792\"},\n\t\t{\"31337\", 200, \"50356617492943978264658466087695012475238275216171379079839219712\"},\n\n\t\t// n \u003e 256\n\t\t{\"1\", 257, \"0\"},\n\t\t{\"31337\", 300, \"0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\twant := MustFromDecimal(tt.want)\n\n\t\tgot := new(Uint).Lsh(x, tt.y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Lsh(%s, %d) = %s, want %s\", tt.x, tt.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestRsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint\n\t\twant string\n\t}{\n\t\t{\"0\", 0, \"0\"},\n\t\t{\"0\", 1, \"0\"},\n\t\t{\"0\", 64, \"0\"},\n\t\t{\"1\", 0, \"1\"},\n\t\t{\"1\", 1, \"0\"},\n\t\t{\"1\", 64, \"0\"},\n\t\t{\"1\", 128, \"0\"},\n\t\t{\"1\", 192, \"0\"},\n\t\t{\"1\", 255, \"0\"},\n\t\t{\"57896044618658097711785492504343953926634992332820282019728792003956564819968\", 255, \"1\"},\n\t\t{\"6277101735386680763835789423207666416102355444464034512896\", 192, \"1\"},\n\t\t{\"340282366920938463463374607431768211456\", 128, \"1\"},\n\t\t{\"18446744073709551616\", 64, \"1\"},\n\t\t{\"393411074163624830192644266310117284962799025126338899061243904\", 193, \"31337\"},\n\t\t{\"196705537081812415096322133155058642481399512563169449530621952\", 192, \"31337\"},\n\t\t{\"10663428532201448629551770073089320442396672\", 128, \"31337\"},\n\t\t{\"578065619037836218990592\", 64, \"31337\"},\n\t\t{twoPow256Sub1, 256, \"0\"},\n\t\t// outliers\n\t\t{\"340282366920938463463374607431768211455\", 129, \"0\"},\n\t\t{\"18446744073709551615\", 65, \"0\"},\n\t\t{twoPow256Sub1, 1, \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"},\n\n\t\t// n \u003e 256\n\t\t{\"1\", 257, \"0\"},\n\t\t{\"31337\", 300, \"0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\n\t\twant := MustFromDecimal(tt.want)\n\t\tgot := new(Uint).Rsh(x, tt.y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Rsh(%s, %d) = %s, want %s\", tt.x, tt.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestSRsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint\n\t\twant string\n\t}{\n\t\t// Positive numbers (behaves like Rsh)\n\t\t{\"0x0\", 0, \"0x0\"},\n\t\t{\"0x0\", 1, \"0x0\"},\n\t\t{\"0x1\", 0, \"0x1\"},\n\t\t{\"0x1\", 1, \"0x0\"},\n\t\t{\"0x31337\", 0, \"0x31337\"},\n\t\t{\"0x31337\", 4, \"0x3133\"},\n\t\t{\"0x31337\", 8, \"0x313\"},\n\t\t{\"0x31337\", 16, \"0x3\"},\n\t\t{\"0x10000000000000000\", 64, \"0x1\"}, // 2^64 \u003e\u003e 64\n\n\t\t// // Numbers with MSB set (negative numbers in two's complement)\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 0, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 1, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 4, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 64, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 128, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 192, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 255, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\n\t\t// Large positive number close to max value\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 1, \"0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 2, \"0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 64, \"0x7fffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 128, \"0x7fffffffffffffffffffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 192, \"0x7fffffffffffffff\"},\n\t\t{\"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 255, \"0x0\"},\n\n\t\t// Specific cases\n\t\t{\"0x8000000000000000000000000000000000000000000000000000000000000000\", 1, \"0xc000000000000000000000000000000000000000000000000000000000000000\"},\n\t\t{\"0x8000000000000000000000000000000000000000000000000000000000000001\", 1, \"0xc000000000000000000000000000000000000000000000000000000000000000\"},\n\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 65, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 127, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 129, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 193, \"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n\n\t\t// n \u003e 256\n\t\t{\"0x1\", 257, \"0x0\"},\n\t\t{\"0x31337\", 300, \"0x0\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\twant := MustFromHex(tt.want)\n\n\t\tgot := new(Uint).SRsh(x, tt.y)\n\n\t\tif !got.Eq(want) {\n\t\t\tt.Errorf(\"SRsh(%s, %d) = %s, want %s\", tt.x, tt.y, got.String(), want.String())\n\t\t}\n\t}\n}\n" + }, + { + "name": "cmp.gno", + "body": "// cmp (or, comparisons) includes methods for comparing Uint instances.\n// These comparison functions cover a range of operations including equality checks, less than/greater than\n// evaluations, and specialized comparisons such as signed greater than. These are fundamental for logical\n// decision making based on Uint values.\npackage uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Cmp compares z and x and returns:\n//\n//\t-1 if z \u003c x\n//\t 0 if z == x\n//\t+1 if z \u003e x\nfunc (z *Uint) Cmp(x *Uint) (r int) {\n\t// z \u003c x \u003c=\u003e z - x \u003c 0 i.e. when subtraction overflows.\n\td0, carry := bits.Sub64(z.arr[0], x.arr[0], 0)\n\td1, carry := bits.Sub64(z.arr[1], x.arr[1], carry)\n\td2, carry := bits.Sub64(z.arr[2], x.arr[2], carry)\n\td3, carry := bits.Sub64(z.arr[3], x.arr[3], carry)\n\tif carry == 1 {\n\t\treturn -1\n\t}\n\tif d0|d1|d2|d3 == 0 {\n\t\treturn 0\n\t}\n\treturn 1\n}\n\n// IsZero returns true if z == 0\nfunc (z *Uint) IsZero() bool {\n\treturn (z.arr[0] | z.arr[1] | z.arr[2] | z.arr[3]) == 0\n}\n\n// Sign returns:\n//\n//\t-1 if z \u003c 0\n//\t 0 if z == 0\n//\t+1 if z \u003e 0\n//\n// Where z is interpreted as a two's complement signed number\nfunc (z *Uint) Sign() int {\n\tif z.IsZero() {\n\t\treturn 0\n\t}\n\tif z.arr[3] \u003c 0x8000000000000000 {\n\t\treturn 1\n\t}\n\treturn -1\n}\n\n// LtUint64 returns true if z is smaller than n\nfunc (z *Uint) LtUint64(n uint64) bool {\n\treturn z.arr[0] \u003c n \u0026\u0026 (z.arr[1]|z.arr[2]|z.arr[3]) == 0\n}\n\n// GtUint64 returns true if z is larger than n\nfunc (z *Uint) GtUint64(n uint64) bool {\n\treturn z.arr[0] \u003e n || (z.arr[1]|z.arr[2]|z.arr[3]) != 0\n}\n\n// Lt returns true if z \u003c x\nfunc (z *Uint) Lt(x *Uint) bool {\n\t// z \u003c x \u003c=\u003e z - x \u003c 0 i.e. when subtraction overflows.\n\t_, carry := bits.Sub64(z.arr[0], x.arr[0], 0)\n\t_, carry = bits.Sub64(z.arr[1], x.arr[1], carry)\n\t_, carry = bits.Sub64(z.arr[2], x.arr[2], carry)\n\t_, carry = bits.Sub64(z.arr[3], x.arr[3], carry)\n\n\treturn carry != 0\n}\n\n// Gt returns true if z \u003e x\nfunc (z *Uint) Gt(x *Uint) bool {\n\treturn x.Lt(z)\n}\n\n// Lte returns true if z \u003c= x\nfunc (z *Uint) Lte(x *Uint) bool {\n\tcond1 := z.Lt(x)\n\tcond2 := z.Eq(x)\n\n\tif cond1 || cond2 {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Gte returns true if z \u003e= x\nfunc (z *Uint) Gte(x *Uint) bool {\n\tcond1 := z.Gt(x)\n\tcond2 := z.Eq(x)\n\n\tif cond1 || cond2 {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Eq returns true if z == x\nfunc (z *Uint) Eq(x *Uint) bool {\n\treturn (z.arr[0] == x.arr[0]) \u0026\u0026 (z.arr[1] == x.arr[1]) \u0026\u0026 (z.arr[2] == x.arr[2]) \u0026\u0026 (z.arr[3] == x.arr[3])\n}\n\n// Neq returns true if z != x\nfunc (z *Uint) Neq(x *Uint) bool {\n\treturn !z.Eq(x)\n}\n\n// Sgt interprets z and x as signed integers, and returns\n// true if z \u003e x\nfunc (z *Uint) Sgt(x *Uint) bool {\n\tzSign := z.Sign()\n\txSign := x.Sign()\n\n\tswitch {\n\tcase zSign \u003e= 0 \u0026\u0026 xSign \u003c 0:\n\t\treturn true\n\tcase zSign \u003c 0 \u0026\u0026 xSign \u003e= 0:\n\t\treturn false\n\tdefault:\n\t\treturn z.Gt(x)\n\t}\n}\n" + }, + { + "name": "cmp_test.gno", + "body": "package uint256\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestSign(t *testing.T) {\n\ttests := []struct {\n\t\tinput *Uint\n\t\texpected int\n\t}{\n\t\t{\n\t\t\tinput: NewUint(0),\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tinput: NewUint(1),\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tinput: NewUint(0x7fffffffffffffff),\n\t\t\texpected: 1,\n\t\t},\n\t\t{\n\t\t\tinput: NewUint(0x8000000000000000),\n\t\t\texpected: 1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.input.String(), func(t *testing.T) {\n\t\t\tresult := tt.input.Sign()\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Sign() = %d; want %d\", result, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCmp(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant int\n\t}{\n\t\t{\"0\", \"0\", 0},\n\t\t{\"0\", \"1\", -1},\n\t\t{\"1\", \"0\", 1},\n\t\t{\"1\", \"1\", 0},\n\t\t{\"10\", \"10\", 0},\n\t\t{\"10\", \"11\", -1},\n\t\t{\"11\", \"10\", 1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := MustFromDecimal(tc.x)\n\t\ty := MustFromDecimal(tc.y)\n\n\t\tgot := x.Cmp(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Cmp(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestIsZero(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0\", true},\n\t\t{\"1\", false},\n\t\t{\"10\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\n\t\tgot := x.IsZero()\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"IsZero(%s) = %v, want %v\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestLtUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty uint64\n\t\twant bool\n\t}{\n\t\t{\"0\", 1, true},\n\t\t{\"1\", 0, false},\n\t\t{\"10\", 10, false},\n\t\t{\"0xffffffffffffffff\", 0, false},\n\t\t{\"0x10000000000000000\", 10000000000000000, false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx := parseTestString(t, tc.x)\n\n\t\tgot := x.LtUint64(tc.y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"LtUint64(%s, %d) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestUint_GtUint64(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tz string\n\t\tn uint64\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"z \u003e n\",\n\t\t\tz: \"1\",\n\t\t\tn: 0,\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"z \u003c n\",\n\t\t\tz: \"18446744073709551615\",\n\t\t\tn: 0xFFFFFFFFFFFFFFFF,\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"z == n\",\n\t\t\tz: \"18446744073709551615\",\n\t\t\tn: 0xFFFFFFFFFFFFFFFF,\n\t\t\twant: 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\tz := MustFromDecimal(tt.z)\n\n\t\t\tif got := z.GtUint64(tt.n); got != tt.want {\n\t\t\t\tt.Errorf(\"Uint.GtUint64() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSGT(t *testing.T) {\n\tx := MustFromHex(\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\")\n\ty := MustFromHex(\"0x0\")\n\tactual := x.Sgt(y)\n\tif actual {\n\t\tt.Fatalf(\"Expected %v false\", actual)\n\t}\n\n\tx = MustFromHex(\"0x0\")\n\ty = MustFromHex(\"0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe\")\n\tactual = x.Sgt(y)\n\tif !actual {\n\t\tt.Fatalf(\"Expected %v true\", actual)\n\t}\n}\n\nfunc TestEq(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\ty string\n\t\twant bool\n\t}{\n\t\t{\"0xffffffffffffffff\", \"18446744073709551615\", true},\n\t\t{\"0x10000000000000000\", \"18446744073709551616\", true},\n\t\t{\"0\", \"0\", true},\n\t\t{twoPow256Sub1, twoPow256Sub1, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := parseTestString(t, tt.x)\n\n\t\ty, err := FromDecimal(tt.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Eq(y)\n\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"Eq(%s, %s) = %v, want %v\", tt.x, tt.y, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestUint_Lte(t *testing.T) {\n\ttests := []struct {\n\t\tz, x string\n\t\twant bool\n\t}{\n\t\t{\"10\", \"20\", true},\n\t\t{\"20\", \"10\", false},\n\t\t{\"10\", \"10\", true},\n\t\t{\"0\", \"0\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz, err := FromDecimal(tt.z)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tx, err := FromDecimal(tt.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\t\tif got := z.Lte(x); got != tt.want {\n\t\t\tt.Errorf(\"Uint.Lte(%v, %v) = %v, want %v\", tt.z, tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestUint_Gte(t *testing.T) {\n\ttests := []struct {\n\t\tz, x string\n\t\twant bool\n\t}{\n\t\t{\"20\", \"10\", true},\n\t\t{\"10\", \"20\", false},\n\t\t{\"10\", \"10\", true},\n\t\t{\"0\", \"0\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := parseTestString(t, tt.z)\n\t\tx := parseTestString(t, tt.x)\n\n\t\tif got := z.Gte(x); got != tt.want {\n\t\t\tt.Errorf(\"Uint.Gte(%v, %v) = %v, want %v\", tt.z, tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc parseTestString(_ *testing.T, s string) *Uint {\n\tvar x *Uint\n\n\tif strings.HasPrefix(s, \"0x\") {\n\t\tx = MustFromHex(s)\n\t} else {\n\t\tx = MustFromDecimal(s)\n\t}\n\n\treturn x\n}\n" + }, + { + "name": "conversion.gno", + "body": "// conversions contains methods for converting Uint instances to other types and vice versa.\n// This includes conversions to and from basic types such as uint64 and int32, as well as string representations\n// and byte slices. Additionally, it covers marshaling and unmarshaling for JSON and other text formats.\npackage uint256\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Uint64 returns the lower 64-bits of z\nfunc (z *Uint) Uint64() uint64 {\n\treturn z.arr[0]\n}\n\n// Uint64WithOverflow returns the lower 64-bits of z and bool whether overflow occurred\nfunc (z *Uint) Uint64WithOverflow() (uint64, bool) {\n\treturn z.arr[0], (z.arr[1] | z.arr[2] | z.arr[3]) != 0\n}\n\n// SetUint64 sets z to the value x\nfunc (z *Uint) SetUint64(x uint64) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x\n\treturn z\n}\n\n// IsUint64 reports whether z can be represented as a uint64.\nfunc (z *Uint) IsUint64() bool {\n\treturn (z.arr[1] | z.arr[2] | z.arr[3]) == 0\n}\n\n// Dec returns the decimal representation of z.\nfunc (z *Uint) Dec() string {\n\tif z.IsZero() {\n\t\treturn \"0\"\n\t}\n\tif z.IsUint64() {\n\t\treturn strconv.FormatUint(z.Uint64(), 10)\n\t}\n\n\t// The max uint64 value being 18446744073709551615, the largest\n\t// power-of-ten below that is 10000000000000000000.\n\t// When we do a DivMod using that number, the remainder that we\n\t// get back is the lower part of the output.\n\t//\n\t// The ascii-output of remainder will never exceed 19 bytes (since it will be\n\t// below 10000000000000000000).\n\t//\n\t// Algorithm example using 100 as divisor\n\t//\n\t// 12345 % 100 = 45 (rem)\n\t// 12345 / 100 = 123 (quo)\n\t// -\u003e output '45', continue iterate on 123\n\tvar (\n\t\t// out is 98 bytes long: 78 (max size of a string without leading zeroes,\n\t\t// plus slack so we can copy 19 bytes every iteration).\n\t\t// We init it with zeroes, because when strconv appends the ascii representations,\n\t\t// it will omit leading zeroes.\n\t\tout = []byte(\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\")\n\t\tdivisor = NewUint(10000000000000000000) // 20 digits\n\t\ty = new(Uint).Set(z) // copy to avoid modifying z\n\t\tpos = len(out) // position to write to\n\t\tbuf = make([]byte, 0, 19) // buffer to write uint64:s to\n\t)\n\tfor {\n\t\t// Obtain Q and R for divisor\n\t\tvar quot Uint\n\t\trem := udivrem(quot.arr[:], y.arr[:], divisor)\n\t\ty.Set(\u0026quot) // Set Q for next loop\n\t\t// Convert the R to ascii representation\n\t\tbuf = strconv.AppendUint(buf[:0], rem.Uint64(), 10)\n\t\t// Copy in the ascii digits\n\t\tcopy(out[pos-len(buf):], buf)\n\t\tif y.IsZero() {\n\t\t\tbreak\n\t\t}\n\t\t// Move 19 digits left\n\t\tpos -= 19\n\t}\n\t// skip leading zeroes by only using the 'used size' of buf\n\treturn string(out[pos-len(buf):])\n}\n\nfunc (z *Uint) Scan(src any) error {\n\tif src == nil {\n\t\tz.Clear()\n\t\treturn nil\n\t}\n\n\tswitch src := src.(type) {\n\tcase string:\n\t\treturn z.scanScientificFromString(src)\n\tcase []byte:\n\t\treturn z.scanScientificFromString(string(src))\n\t}\n\treturn errors.New(\"default // unsupported type: can't convert to uint256.Uint\")\n}\n\nfunc (z *Uint) scanScientificFromString(src string) error {\n\tif len(src) == 0 {\n\t\tz.Clear()\n\t\treturn nil\n\t}\n\n\tidx := strings.IndexByte(src, 'e')\n\tif idx == -1 {\n\t\treturn z.SetFromDecimal(src)\n\t}\n\tif err := z.SetFromDecimal(src[:idx]); err != nil {\n\t\treturn err\n\t}\n\tif src[(idx+1):] == \"0\" {\n\t\treturn nil\n\t}\n\texp := new(Uint)\n\tif err := exp.SetFromDecimal(src[(idx + 1):]); err != nil {\n\t\treturn err\n\t}\n\tif exp.GtUint64(77) { // 10**78 is larger than 2**256\n\t\treturn ErrBig256Range\n\t}\n\texp.Exp(NewUint(10), exp)\n\tif _, overflow := z.MulOverflow(z, exp); overflow {\n\t\treturn ErrBig256Range\n\t}\n\treturn nil\n}\n\n// ToString returns the decimal string representation of z. It returns an empty string if z is nil.\n// OBS: doesn't exist from holiman's uint256\nfunc (z *Uint) String() string {\n\tif z == nil {\n\t\treturn \"\"\n\t}\n\n\treturn z.Dec()\n}\n\n// MarshalJSON implements json.Marshaler.\n// MarshalJSON marshals using the 'decimal string' representation. This is _not_ compatible\n// with big.Uint: big.Uint marshals into JSON 'native' numeric format.\n//\n// The JSON native format is, on some platforms, (e.g. javascript), limited to 53-bit large\n// integer space. Thus, U256 uses string-format, which is not compatible with\n// big.int (big.Uint refuses to unmarshal a string representation).\nfunc (z *Uint) MarshalJSON() ([]byte, error) {\n\treturn []byte(`\"` + z.Dec() + `\"`), nil\n}\n\n// UnmarshalJSON implements json.Unmarshaler. UnmarshalJSON accepts either\n// - Quoted string: either hexadecimal OR decimal\n// - Not quoted string: only decimal\nfunc (z *Uint) UnmarshalJSON(input []byte) error {\n\tif len(input) \u003c 2 || input[0] != '\"' || input[len(input)-1] != '\"' {\n\t\t// if not quoted, it must be decimal\n\t\treturn z.fromDecimal(string(input))\n\t}\n\treturn z.UnmarshalText(input[1 : len(input)-1])\n}\n\n// MarshalText implements encoding.TextMarshaler\n// MarshalText marshals using the decimal representation (compatible with big.Uint)\nfunc (z *Uint) MarshalText() ([]byte, error) {\n\treturn []byte(z.Dec()), nil\n}\n\n// UnmarshalText implements encoding.TextUnmarshaler. This method\n// can unmarshal either hexadecimal or decimal.\n// - For hexadecimal, the input _must_ be prefixed with 0x or 0X\nfunc (z *Uint) UnmarshalText(input []byte) error {\n\tif len(input) \u003e= 2 \u0026\u0026 input[0] == '0' \u0026\u0026 (input[1] == 'x' || input[1] == 'X') {\n\t\treturn z.fromHex(string(input))\n\t}\n\treturn z.fromDecimal(string(input))\n}\n\n// SetBytes interprets buf as the bytes of a big-endian unsigned\n// integer, sets z to that value, and returns z.\n// If buf is larger than 32 bytes, the last 32 bytes is used.\nfunc (z *Uint) SetBytes(buf []byte) *Uint {\n\tswitch l := len(buf); l {\n\tcase 0:\n\t\tz.Clear()\n\tcase 1:\n\t\tz.SetBytes1(buf)\n\tcase 2:\n\t\tz.SetBytes2(buf)\n\tcase 3:\n\t\tz.SetBytes3(buf)\n\tcase 4:\n\t\tz.SetBytes4(buf)\n\tcase 5:\n\t\tz.SetBytes5(buf)\n\tcase 6:\n\t\tz.SetBytes6(buf)\n\tcase 7:\n\t\tz.SetBytes7(buf)\n\tcase 8:\n\t\tz.SetBytes8(buf)\n\tcase 9:\n\t\tz.SetBytes9(buf)\n\tcase 10:\n\t\tz.SetBytes10(buf)\n\tcase 11:\n\t\tz.SetBytes11(buf)\n\tcase 12:\n\t\tz.SetBytes12(buf)\n\tcase 13:\n\t\tz.SetBytes13(buf)\n\tcase 14:\n\t\tz.SetBytes14(buf)\n\tcase 15:\n\t\tz.SetBytes15(buf)\n\tcase 16:\n\t\tz.SetBytes16(buf)\n\tcase 17:\n\t\tz.SetBytes17(buf)\n\tcase 18:\n\t\tz.SetBytes18(buf)\n\tcase 19:\n\t\tz.SetBytes19(buf)\n\tcase 20:\n\t\tz.SetBytes20(buf)\n\tcase 21:\n\t\tz.SetBytes21(buf)\n\tcase 22:\n\t\tz.SetBytes22(buf)\n\tcase 23:\n\t\tz.SetBytes23(buf)\n\tcase 24:\n\t\tz.SetBytes24(buf)\n\tcase 25:\n\t\tz.SetBytes25(buf)\n\tcase 26:\n\t\tz.SetBytes26(buf)\n\tcase 27:\n\t\tz.SetBytes27(buf)\n\tcase 28:\n\t\tz.SetBytes28(buf)\n\tcase 29:\n\t\tz.SetBytes29(buf)\n\tcase 30:\n\t\tz.SetBytes30(buf)\n\tcase 31:\n\t\tz.SetBytes31(buf)\n\tdefault:\n\t\tz.SetBytes32(buf[l-32:])\n\t}\n\treturn z\n}\n\n// SetBytes1 is identical to SetBytes(in[:1]), but panics is input is too short\nfunc (z *Uint) SetBytes1(in []byte) *Uint {\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(in[0])\n\treturn z\n}\n\n// SetBytes2 is identical to SetBytes(in[:2]), but panics is input is too short\nfunc (z *Uint) SetBytes2(in []byte) *Uint {\n\t_ = in[1] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\treturn z\n}\n\n// SetBytes3 is identical to SetBytes(in[:3]), but panics is input is too short\nfunc (z *Uint) SetBytes3(in []byte) *Uint {\n\t_ = in[2] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\treturn z\n}\n\n// SetBytes4 is identical to SetBytes(in[:4]), but panics is input is too short\nfunc (z *Uint) SetBytes4(in []byte) *Uint {\n\t_ = in[3] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\treturn z\n}\n\n// SetBytes5 is identical to SetBytes(in[:5]), but panics is input is too short\nfunc (z *Uint) SetBytes5(in []byte) *Uint {\n\t_ = in[4] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint40(in[0:5])\n\treturn z\n}\n\n// SetBytes6 is identical to SetBytes(in[:6]), but panics is input is too short\nfunc (z *Uint) SetBytes6(in []byte) *Uint {\n\t_ = in[5] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint48(in[0:6])\n\treturn z\n}\n\n// SetBytes7 is identical to SetBytes(in[:7]), but panics is input is too short\nfunc (z *Uint) SetBytes7(in []byte) *Uint {\n\t_ = in[6] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = bigEndianUint56(in[0:7])\n\treturn z\n}\n\n// SetBytes8 is identical to SetBytes(in[:8]), but panics is input is too short\nfunc (z *Uint) SetBytes8(in []byte) *Uint {\n\t_ = in[7] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\tz.arr[0] = binary.BigEndian.Uint64(in[0:8])\n\treturn z\n}\n\n// SetBytes9 is identical to SetBytes(in[:9]), but panics is input is too short\nfunc (z *Uint) SetBytes9(in []byte) *Uint {\n\t_ = in[8] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(in[0])\n\tz.arr[0] = binary.BigEndian.Uint64(in[1:9])\n\treturn z\n}\n\n// SetBytes10 is identical to SetBytes(in[:10]), but panics is input is too short\nfunc (z *Uint) SetBytes10(in []byte) *Uint {\n\t_ = in[9] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[0] = binary.BigEndian.Uint64(in[2:10])\n\treturn z\n}\n\n// SetBytes11 is identical to SetBytes(in[:11]), but panics is input is too short\nfunc (z *Uint) SetBytes11(in []byte) *Uint {\n\t_ = in[10] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[0] = binary.BigEndian.Uint64(in[3:11])\n\treturn z\n}\n\n// SetBytes12 is identical to SetBytes(in[:12]), but panics is input is too short\nfunc (z *Uint) SetBytes12(in []byte) *Uint {\n\t_ = in[11] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[0] = binary.BigEndian.Uint64(in[4:12])\n\treturn z\n}\n\n// SetBytes13 is identical to SetBytes(in[:13]), but panics is input is too short\nfunc (z *Uint) SetBytes13(in []byte) *Uint {\n\t_ = in[12] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint40(in[0:5])\n\tz.arr[0] = binary.BigEndian.Uint64(in[5:13])\n\treturn z\n}\n\n// SetBytes14 is identical to SetBytes(in[:14]), but panics is input is too short\nfunc (z *Uint) SetBytes14(in []byte) *Uint {\n\t_ = in[13] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint48(in[0:6])\n\tz.arr[0] = binary.BigEndian.Uint64(in[6:14])\n\treturn z\n}\n\n// SetBytes15 is identical to SetBytes(in[:15]), but panics is input is too short\nfunc (z *Uint) SetBytes15(in []byte) *Uint {\n\t_ = in[14] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = bigEndianUint56(in[0:7])\n\tz.arr[0] = binary.BigEndian.Uint64(in[7:15])\n\treturn z\n}\n\n// SetBytes16 is identical to SetBytes(in[:16]), but panics is input is too short\nfunc (z *Uint) SetBytes16(in []byte) *Uint {\n\t_ = in[15] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3], z.arr[2] = 0, 0\n\tz.arr[1] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[0] = binary.BigEndian.Uint64(in[8:16])\n\treturn z\n}\n\n// SetBytes17 is identical to SetBytes(in[:17]), but panics is input is too short\nfunc (z *Uint) SetBytes17(in []byte) *Uint {\n\t_ = in[16] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(in[0])\n\tz.arr[1] = binary.BigEndian.Uint64(in[1:9])\n\tz.arr[0] = binary.BigEndian.Uint64(in[9:17])\n\treturn z\n}\n\n// SetBytes18 is identical to SetBytes(in[:18]), but panics is input is too short\nfunc (z *Uint) SetBytes18(in []byte) *Uint {\n\t_ = in[17] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[1] = binary.BigEndian.Uint64(in[2:10])\n\tz.arr[0] = binary.BigEndian.Uint64(in[10:18])\n\treturn z\n}\n\n// SetBytes19 is identical to SetBytes(in[:19]), but panics is input is too short\nfunc (z *Uint) SetBytes19(in []byte) *Uint {\n\t_ = in[18] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[1] = binary.BigEndian.Uint64(in[3:11])\n\tz.arr[0] = binary.BigEndian.Uint64(in[11:19])\n\treturn z\n}\n\n// SetBytes20 is identical to SetBytes(in[:20]), but panics is input is too short\nfunc (z *Uint) SetBytes20(in []byte) *Uint {\n\t_ = in[19] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[1] = binary.BigEndian.Uint64(in[4:12])\n\tz.arr[0] = binary.BigEndian.Uint64(in[12:20])\n\treturn z\n}\n\n// SetBytes21 is identical to SetBytes(in[:21]), but panics is input is too short\nfunc (z *Uint) SetBytes21(in []byte) *Uint {\n\t_ = in[20] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint40(in[0:5])\n\tz.arr[1] = binary.BigEndian.Uint64(in[5:13])\n\tz.arr[0] = binary.BigEndian.Uint64(in[13:21])\n\treturn z\n}\n\n// SetBytes22 is identical to SetBytes(in[:22]), but panics is input is too short\nfunc (z *Uint) SetBytes22(in []byte) *Uint {\n\t_ = in[21] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint48(in[0:6])\n\tz.arr[1] = binary.BigEndian.Uint64(in[6:14])\n\tz.arr[0] = binary.BigEndian.Uint64(in[14:22])\n\treturn z\n}\n\n// SetBytes23 is identical to SetBytes(in[:23]), but panics is input is too short\nfunc (z *Uint) SetBytes23(in []byte) *Uint {\n\t_ = in[22] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = bigEndianUint56(in[0:7])\n\tz.arr[1] = binary.BigEndian.Uint64(in[7:15])\n\tz.arr[0] = binary.BigEndian.Uint64(in[15:23])\n\treturn z\n}\n\n// SetBytes24 is identical to SetBytes(in[:24]), but panics is input is too short\nfunc (z *Uint) SetBytes24(in []byte) *Uint {\n\t_ = in[23] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = 0\n\tz.arr[2] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[1] = binary.BigEndian.Uint64(in[8:16])\n\tz.arr[0] = binary.BigEndian.Uint64(in[16:24])\n\treturn z\n}\n\n// SetBytes25 is identical to SetBytes(in[:25]), but panics is input is too short\nfunc (z *Uint) SetBytes25(in []byte) *Uint {\n\t_ = in[24] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(in[0])\n\tz.arr[2] = binary.BigEndian.Uint64(in[1:9])\n\tz.arr[1] = binary.BigEndian.Uint64(in[9:17])\n\tz.arr[0] = binary.BigEndian.Uint64(in[17:25])\n\treturn z\n}\n\n// SetBytes26 is identical to SetBytes(in[:26]), but panics is input is too short\nfunc (z *Uint) SetBytes26(in []byte) *Uint {\n\t_ = in[25] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint16(in[0:2]))\n\tz.arr[2] = binary.BigEndian.Uint64(in[2:10])\n\tz.arr[1] = binary.BigEndian.Uint64(in[10:18])\n\tz.arr[0] = binary.BigEndian.Uint64(in[18:26])\n\treturn z\n}\n\n// SetBytes27 is identical to SetBytes(in[:27]), but panics is input is too short\nfunc (z *Uint) SetBytes27(in []byte) *Uint {\n\t_ = in[26] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])\u003c\u003c16\n\tz.arr[2] = binary.BigEndian.Uint64(in[3:11])\n\tz.arr[1] = binary.BigEndian.Uint64(in[11:19])\n\tz.arr[0] = binary.BigEndian.Uint64(in[19:27])\n\treturn z\n}\n\n// SetBytes28 is identical to SetBytes(in[:28]), but panics is input is too short\nfunc (z *Uint) SetBytes28(in []byte) *Uint {\n\t_ = in[27] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = uint64(binary.BigEndian.Uint32(in[0:4]))\n\tz.arr[2] = binary.BigEndian.Uint64(in[4:12])\n\tz.arr[1] = binary.BigEndian.Uint64(in[12:20])\n\tz.arr[0] = binary.BigEndian.Uint64(in[20:28])\n\treturn z\n}\n\n// SetBytes29 is identical to SetBytes(in[:29]), but panics is input is too short\nfunc (z *Uint) SetBytes29(in []byte) *Uint {\n\t_ = in[23] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint40(in[0:5])\n\tz.arr[2] = binary.BigEndian.Uint64(in[5:13])\n\tz.arr[1] = binary.BigEndian.Uint64(in[13:21])\n\tz.arr[0] = binary.BigEndian.Uint64(in[21:29])\n\treturn z\n}\n\n// SetBytes30 is identical to SetBytes(in[:30]), but panics is input is too short\nfunc (z *Uint) SetBytes30(in []byte) *Uint {\n\t_ = in[29] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint48(in[0:6])\n\tz.arr[2] = binary.BigEndian.Uint64(in[6:14])\n\tz.arr[1] = binary.BigEndian.Uint64(in[14:22])\n\tz.arr[0] = binary.BigEndian.Uint64(in[22:30])\n\treturn z\n}\n\n// SetBytes31 is identical to SetBytes(in[:31]), but panics is input is too short\nfunc (z *Uint) SetBytes31(in []byte) *Uint {\n\t_ = in[30] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = bigEndianUint56(in[0:7])\n\tz.arr[2] = binary.BigEndian.Uint64(in[7:15])\n\tz.arr[1] = binary.BigEndian.Uint64(in[15:23])\n\tz.arr[0] = binary.BigEndian.Uint64(in[23:31])\n\treturn z\n}\n\n// SetBytes32 sets z to the value of the big-endian 256-bit unsigned integer in.\nfunc (z *Uint) SetBytes32(in []byte) *Uint {\n\t_ = in[31] // bounds check hint to compiler; see golang.org/issue/14808\n\tz.arr[3] = binary.BigEndian.Uint64(in[0:8])\n\tz.arr[2] = binary.BigEndian.Uint64(in[8:16])\n\tz.arr[1] = binary.BigEndian.Uint64(in[16:24])\n\tz.arr[0] = binary.BigEndian.Uint64(in[24:32])\n\treturn z\n}\n\n// Utility methods that are \"missing\" among the bigEndian.UintXX methods.\n\n// bigEndianUint40 returns the uint64 value represented by the 5 bytes in big-endian order.\nfunc bigEndianUint40(b []byte) uint64 {\n\t_ = b[4] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[4]) | uint64(b[3])\u003c\u003c8 | uint64(b[2])\u003c\u003c16 | uint64(b[1])\u003c\u003c24 |\n\t\tuint64(b[0])\u003c\u003c32\n}\n\n// bigEndianUint56 returns the uint64 value represented by the 7 bytes in big-endian order.\nfunc bigEndianUint56(b []byte) uint64 {\n\t_ = b[6] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[6]) | uint64(b[5])\u003c\u003c8 | uint64(b[4])\u003c\u003c16 | uint64(b[3])\u003c\u003c24 |\n\t\tuint64(b[2])\u003c\u003c32 | uint64(b[1])\u003c\u003c40 | uint64(b[0])\u003c\u003c48\n}\n\n// bigEndianUint48 returns the uint64 value represented by the 6 bytes in big-endian order.\nfunc bigEndianUint48(b []byte) uint64 {\n\t_ = b[5] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[5]) | uint64(b[4])\u003c\u003c8 | uint64(b[3])\u003c\u003c16 | uint64(b[2])\u003c\u003c24 |\n\t\tuint64(b[1])\u003c\u003c32 | uint64(b[0])\u003c\u003c40\n}\n" + }, + { + "name": "conversion_test.gno", + "body": "package uint256\n\nimport \"testing\"\n\nfunc TestIsUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0x0\", true},\n\t\t{\"0x1\", true},\n\t\t{\"0x10\", true},\n\t\t{\"0xffffffffffffffff\", true},\n\t\t{\"0x10000000000000000\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromHex(tt.x)\n\t\tgot := x.IsUint64()\n\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"IsUint64(%s) = %v, want %v\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestDec(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tz Uint\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"zero\",\n\t\t\tz: Uint{arr: [4]uint64{0, 0, 0, 0}},\n\t\t\twant: \"0\",\n\t\t},\n\t\t{\n\t\t\tname: \"less than 20 digits\",\n\t\t\tz: Uint{arr: [4]uint64{1234567890, 0, 0, 0}},\n\t\t\twant: \"1234567890\",\n\t\t},\n\t\t{\n\t\t\tname: \"max possible value\",\n\t\t\tz: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}},\n\t\t\twant: twoPow256Sub1,\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 := tt.z.Dec()\n\t\t\tif result != tt.want {\n\t\t\t\tt.Errorf(\"Dec(%v) = %s, want %s\", tt.z, result, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUint_Scan(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput any\n\t\twant *Uint\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"nil\",\n\t\t\tinput: nil,\n\t\t\twant: NewUint(0),\n\t\t},\n\t\t{\n\t\t\tname: \"valid scientific notation\",\n\t\t\tinput: \"1e4\",\n\t\t\twant: NewUint(10000),\n\t\t},\n\t\t{\n\t\t\tname: \"valid decimal string\",\n\t\t\tinput: \"12345\",\n\t\t\twant: NewUint(12345),\n\t\t},\n\t\t{\n\t\t\tname: \"valid byte slice\",\n\t\t\tinput: []byte(\"12345\"),\n\t\t\twant: NewUint(12345),\n\t\t},\n\t\t{\n\t\t\tname: \"invalid string\",\n\t\t\tinput: \"invalid\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"out of range\",\n\t\t\tinput: \"115792089237316195423570985008687907853269984665640564039457584007913129639936\", // 2^256\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"unsupported type\",\n\t\t\tinput: 123,\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\tz := new(Uint)\n\t\t\terr := z.Scan(tt.input)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil {\n\t\t\t\t\tt.Errorf(\"Scan() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"Scan() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\t}\n\t\t\t\tif !z.Eq(tt.want) {\n\t\t\t\t\tt.Errorf(\"Scan() = %v, want %v\", z, tt.want)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSetBytes(t *testing.T) {\n\ttests := []struct {\n\t\tinput []byte\n\t\texpected string\n\t}{\n\t\t{[]byte{}, \"0\"},\n\t\t{[]byte{0x01}, \"1\"},\n\t\t{[]byte{0x12, 0x34}, \"4660\"},\n\t\t{[]byte{0x12, 0x34, 0x56}, \"1193046\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78}, \"305419896\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a}, \"78187493530\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"20015998343868\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"5124095576030430\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"1311768467463790320\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, \"335812727670730321938\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, \"85968058283706962416180\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, \"22007822920628982378542166\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, \"5634002667681019488906794616\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, \"1442304682926340989160139421850\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"369229998829143293224995691993788\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"94522879700260683065598897150409950\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"24197857203266734864793317670504947440\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, \"6194651444036284125387089323649266544658\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, \"1585830769673288736099094866854212235432500\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, \"405972677036361916441368285914678332270720086\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, \"103929005321308650608990281194157653061304342136\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, \"26605825362255014555901511985704359183693911586970\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"6811091292737283726310787068340315951025641366264508\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"1743639370940744633935561489495120883462564189763714270\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"446371678960830626287503741310750946166416432579510853360\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, \"114271149813972640329600957775552242218602606740354778460178\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, \"29253414352376995924377845190541374007962267325530823285805620\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, \"7488874074208510956640728368778591746038340435335890761166238806\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, \"1917151762997378804900026462407319486985815151445988034858557134456\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, \"490790851327328974054406774376273788668368678770172936923790626420890\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, \"125642457939796217357928134240326089899102381765164271852490400363748028\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, \"32164469232587831643629602365523479014170209731882053594237542493119495390\"},\n\t\t{[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, \"8234104123542484900769178205574010627627573691361805720124810878238590820080\"},\n\t\t// over 32 bytes (last 32 bytes are used)\n\t\t{append([]byte{0xff}, []byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}...), \"8234104123542484900769178205574010627627573691361805720124810878238590820080\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tz := new(Uint)\n\t\tz.SetBytes(test.input)\n\t\texpected := MustFromDecimal(test.expected)\n\t\tif z.Cmp(expected) != 0 {\n\t\t\tt.Errorf(\"SetBytes(%x) = %s, expected %s\", test.input, z.String(), test.expected)\n\t\t}\n\t}\n}\n" + }, + { + "name": "error.gno", + "body": "package uint256\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\tErrEmptyString = errors.New(\"empty hex string\")\n\tErrSyntax = errors.New(\"invalid hex string\")\n\tErrRange = errors.New(\"number out of range\")\n\tErrMissingPrefix = errors.New(\"hex string without 0x prefix\")\n\tErrEmptyNumber = errors.New(\"hex string \\\"0x\\\"\")\n\tErrLeadingZero = errors.New(\"hex number with leading zero digits\")\n\tErrBig256Range = errors.New(\"hex number \u003e 256 bits\")\n\tErrBadBufferLength = errors.New(\"bad ssz buffer length\")\n\tErrBadEncodedLength = errors.New(\"bad ssz encoded length\")\n\tErrInvalidBase = errors.New(\"invalid base\")\n\tErrInvalidBitSize = errors.New(\"invalid bit size\")\n)\n\ntype u256Error struct {\n\tfn string // function name\n\tinput string\n\terr error\n}\n\nfunc (e *u256Error) Error() string {\n\treturn e.fn + \": \" + e.input + \": \" + e.err.Error()\n}\n\nfunc (e *u256Error) Unwrap() error {\n\treturn e.err\n}\n\nfunc errEmptyString(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrEmptyString}\n}\n\nfunc errSyntax(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrSyntax}\n}\n\nfunc errMissingPrefix(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrMissingPrefix}\n}\n\nfunc errEmptyNumber(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrEmptyNumber}\n}\n\nfunc errLeadingZero(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrLeadingZero}\n}\n\nfunc errRange(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrRange}\n}\n\nfunc errBig256Range(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrBig256Range}\n}\n\nfunc errBadBufferLength(fn, input string) error {\n\treturn \u0026u256Error{fn: fn, input: input, err: ErrBadBufferLength}\n}\n\nfunc errInvalidBase(fn string, base int) error {\n\treturn \u0026u256Error{fn: fn, input: string(base), err: ErrInvalidBase}\n}\n\nfunc errInvalidBitSize(fn string, bitSize int) error {\n\treturn \u0026u256Error{fn: fn, input: string(bitSize), err: ErrInvalidBitSize}\n}\n" + }, + { + "name": "mod.gno", + "body": "package uint256\n\nimport (\n\t\"math/bits\"\n)\n\n// Some utility functions\n\n// Reciprocal computes a 320-bit value representing 1/m\n//\n// Notes:\n// - specialized for m.arr[3] != 0, hence limited to 2^192 \u003c= m \u003c 2^256\n// - returns zero if m.arr[3] == 0\n// - starts with a 32-bit division, refines with newton-raphson iterations\nfunc Reciprocal(m *Uint) (mu [5]uint64) {\n\tif m.arr[3] == 0 {\n\t\treturn mu\n\t}\n\n\ts := bits.LeadingZeros64(m.arr[3]) // Replace with leadingZeros(m) for general case\n\tp := 255 - s // floor(log_2(m)), m\u003e0\n\n\t// 0 or a power of 2?\n\n\t// Check if at least one bit is set in m.arr[2], m.arr[1] or m.arr[0],\n\t// or at least two bits in m.arr[3]\n\n\tif m.arr[0]|m.arr[1]|m.arr[2]|(m.arr[3]\u0026(m.arr[3]-1)) == 0 {\n\n\t\tmu[4] = ^uint64(0) \u003e\u003e uint(p\u002663)\n\t\tmu[3] = ^uint64(0)\n\t\tmu[2] = ^uint64(0)\n\t\tmu[1] = ^uint64(0)\n\t\tmu[0] = ^uint64(0)\n\n\t\treturn mu\n\t}\n\n\t// Maximise division precision by left-aligning divisor\n\n\tvar (\n\t\ty Uint // left-aligned copy of m\n\t\tr0 uint32 // estimate of 2^31/y\n\t)\n\n\ty.Lsh(m, uint(s)) // 1/2 \u003c y \u003c 1\n\n\t// Extract most significant 32 bits\n\n\tyh := uint32(y.arr[3] \u003e\u003e 32)\n\n\tif yh == 0x80000000 { // Avoid overflow in division\n\t\tr0 = 0xffffffff\n\t} else {\n\t\tr0, _ = bits.Div32(0x80000000, 0, yh)\n\t}\n\n\t// First iteration: 32 -\u003e 64\n\n\tt1 := uint64(r0) // 2^31/y\n\tt1 *= t1 // 2^62/y^2\n\tt1, _ = bits.Mul64(t1, y.arr[3]) // 2^62/y^2 * 2^64/y / 2^64 = 2^62/y\n\n\tr1 := uint64(r0) \u003c\u003c 32 // 2^63/y\n\tr1 -= t1 // 2^63/y - 2^62/y = 2^62/y\n\tr1 *= 2 // 2^63/y\n\n\tif (r1 | (y.arr[3] \u003c\u003c 1)) == 0 {\n\t\tr1 = ^uint64(0)\n\t}\n\n\t// Second iteration: 64 -\u003e 128\n\n\t// square: 2^126/y^2\n\ta2h, a2l := bits.Mul64(r1, r1)\n\n\t// multiply by y: e2h:e2l:b2h = 2^126/y^2 * 2^128/y / 2^128 = 2^126/y\n\tb2h, _ := bits.Mul64(a2l, y.arr[2])\n\tc2h, c2l := bits.Mul64(a2l, y.arr[3])\n\td2h, d2l := bits.Mul64(a2h, y.arr[2])\n\te2h, e2l := bits.Mul64(a2h, y.arr[3])\n\n\tb2h, c := bits.Add64(b2h, c2l, 0)\n\te2l, c = bits.Add64(e2l, c2h, c)\n\te2h, _ = bits.Add64(e2h, 0, c)\n\n\t_, c = bits.Add64(b2h, d2l, 0)\n\te2l, c = bits.Add64(e2l, d2h, c)\n\te2h, _ = bits.Add64(e2h, 0, c)\n\n\t// subtract: t2h:t2l = 2^127/y - 2^126/y = 2^126/y\n\tt2l, b := bits.Sub64(0, e2l, 0)\n\tt2h, _ := bits.Sub64(r1, e2h, b)\n\n\t// double: r2h:r2l = 2^127/y\n\tr2l, c := bits.Add64(t2l, t2l, 0)\n\tr2h, _ := bits.Add64(t2h, t2h, c)\n\n\tif (r2h | r2l | (y.arr[3] \u003c\u003c 1)) == 0 {\n\t\tr2h = ^uint64(0)\n\t\tr2l = ^uint64(0)\n\t}\n\n\t// Third iteration: 128 -\u003e 192\n\n\t// square r2 (keep 256 bits): 2^190/y^2\n\ta3h, a3l := bits.Mul64(r2l, r2l)\n\tb3h, b3l := bits.Mul64(r2l, r2h)\n\tc3h, c3l := bits.Mul64(r2h, r2h)\n\n\ta3h, c = bits.Add64(a3h, b3l, 0)\n\tc3l, c = bits.Add64(c3l, b3h, c)\n\tc3h, _ = bits.Add64(c3h, 0, c)\n\n\ta3h, c = bits.Add64(a3h, b3l, 0)\n\tc3l, c = bits.Add64(c3l, b3h, c)\n\tc3h, _ = bits.Add64(c3h, 0, c)\n\n\t// multiply by y: q = 2^190/y^2 * 2^192/y / 2^192 = 2^190/y\n\n\tx0 := a3l\n\tx1 := a3h\n\tx2 := c3l\n\tx3 := c3h\n\n\tvar q0, q1, q2, q3, q4, t0 uint64\n\n\tq0, _ = bits.Mul64(x2, y.arr[0])\n\tq1, t0 = bits.Mul64(x3, y.arr[0])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, _ = bits.Add64(q1, 0, c)\n\n\tt1, _ = bits.Mul64(x1, y.arr[1])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tq2, t0 = bits.Mul64(x3, y.arr[1])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x2, y.arr[1])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[2])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq3, t0 = bits.Mul64(x3, y.arr[2])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x0, y.arr[2])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x2, y.arr[2])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[3])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq4, t0 = bits.Mul64(x3, y.arr[3])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[3])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[3])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\t// subtract: t3 = 2^191/y - 2^190/y = 2^190/y\n\t_, b = bits.Sub64(0, q0, 0)\n\t_, b = bits.Sub64(0, q1, b)\n\tt3l, b := bits.Sub64(0, q2, b)\n\tt3m, b := bits.Sub64(r2l, q3, b)\n\tt3h, _ := bits.Sub64(r2h, q4, b)\n\n\t// double: r3 = 2^191/y\n\tr3l, c := bits.Add64(t3l, t3l, 0)\n\tr3m, c := bits.Add64(t3m, t3m, c)\n\tr3h, _ := bits.Add64(t3h, t3h, c)\n\n\t// Fourth iteration: 192 -\u003e 320\n\n\t// square r3\n\n\ta4h, a4l := bits.Mul64(r3l, r3l)\n\tb4h, b4l := bits.Mul64(r3l, r3m)\n\tc4h, c4l := bits.Mul64(r3l, r3h)\n\td4h, d4l := bits.Mul64(r3m, r3m)\n\te4h, e4l := bits.Mul64(r3m, r3h)\n\tf4h, f4l := bits.Mul64(r3h, r3h)\n\n\tb4h, c = bits.Add64(b4h, c4l, 0)\n\te4l, c = bits.Add64(e4l, c4h, c)\n\te4h, _ = bits.Add64(e4h, 0, c)\n\n\ta4h, c = bits.Add64(a4h, b4l, 0)\n\td4l, c = bits.Add64(d4l, b4h, c)\n\td4h, c = bits.Add64(d4h, e4l, c)\n\tf4l, c = bits.Add64(f4l, e4h, c)\n\tf4h, _ = bits.Add64(f4h, 0, c)\n\n\ta4h, c = bits.Add64(a4h, b4l, 0)\n\td4l, c = bits.Add64(d4l, b4h, c)\n\td4h, c = bits.Add64(d4h, e4l, c)\n\tf4l, c = bits.Add64(f4l, e4h, c)\n\tf4h, _ = bits.Add64(f4h, 0, c)\n\n\t// multiply by y\n\n\tx1, x0 = bits.Mul64(d4h, y.arr[0])\n\tx3, x2 = bits.Mul64(f4h, y.arr[0])\n\tt1, t0 = bits.Mul64(f4l, y.arr[0])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tx3, _ = bits.Add64(x3, 0, c)\n\n\tt1, t0 = bits.Mul64(d4h, y.arr[1])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tx4, t0 := bits.Mul64(f4h, y.arr[1])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, _ = bits.Add64(x4, 0, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[1])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[1])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tx4, _ = bits.Add64(x4, 0, c)\n\n\tt1, t0 = bits.Mul64(a4h, y.arr[2])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(d4h, y.arr[2])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tx5, t0 := bits.Mul64(f4h, y.arr[2])\n\tx4, c = bits.Add64(x4, t0, c)\n\tx5, _ = bits.Add64(x5, 0, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[2])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[2])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, c = bits.Add64(x4, t1, c)\n\tx5, _ = bits.Add64(x5, 0, c)\n\n\tt1, t0 = bits.Mul64(a4h, y.arr[3])\n\tx1, c = bits.Add64(x1, t0, 0)\n\tx2, c = bits.Add64(x2, t1, c)\n\tt1, t0 = bits.Mul64(d4h, y.arr[3])\n\tx3, c = bits.Add64(x3, t0, c)\n\tx4, c = bits.Add64(x4, t1, c)\n\tx6, t0 := bits.Mul64(f4h, y.arr[3])\n\tx5, c = bits.Add64(x5, t0, c)\n\tx6, _ = bits.Add64(x6, 0, c)\n\tt1, t0 = bits.Mul64(a4l, y.arr[3])\n\tx0, c = bits.Add64(x0, t0, 0)\n\tx1, c = bits.Add64(x1, t1, c)\n\tt1, t0 = bits.Mul64(d4l, y.arr[3])\n\tx2, c = bits.Add64(x2, t0, c)\n\tx3, c = bits.Add64(x3, t1, c)\n\tt1, t0 = bits.Mul64(f4l, y.arr[3])\n\tx4, c = bits.Add64(x4, t0, c)\n\tx5, c = bits.Add64(x5, t1, c)\n\tx6, _ = bits.Add64(x6, 0, c)\n\n\t// subtract\n\t_, b = bits.Sub64(0, x0, 0)\n\t_, b = bits.Sub64(0, x1, b)\n\tr4l, b := bits.Sub64(0, x2, b)\n\tr4k, b := bits.Sub64(0, x3, b)\n\tr4j, b := bits.Sub64(r3l, x4, b)\n\tr4i, b := bits.Sub64(r3m, x5, b)\n\tr4h, _ := bits.Sub64(r3h, x6, b)\n\n\t// Multiply candidate for 1/4y by y, with full precision\n\n\tx0 = r4l\n\tx1 = r4k\n\tx2 = r4j\n\tx3 = r4i\n\tx4 = r4h\n\n\tq1, q0 = bits.Mul64(x0, y.arr[0])\n\tq3, q2 = bits.Mul64(x2, y.arr[0])\n\tq5, q4 := bits.Mul64(x4, y.arr[0])\n\n\tt1, t0 = bits.Mul64(x1, y.arr[0])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[0])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[1])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[1])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq6, t0 := bits.Mul64(x4, y.arr[1])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, _ = bits.Add64(q6, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[1])\n\tq2, c = bits.Add64(q2, t0, 0)\n\tq3, c = bits.Add64(q3, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[1])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, c = bits.Add64(q5, t1, c)\n\tq6, _ = bits.Add64(q6, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[2])\n\tq2, c = bits.Add64(q2, t0, 0)\n\tq3, c = bits.Add64(q3, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[2])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, c = bits.Add64(q5, t1, c)\n\tq7, t0 := bits.Mul64(x4, y.arr[2])\n\tq6, c = bits.Add64(q6, t0, c)\n\tq7, _ = bits.Add64(q7, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[2])\n\tq3, c = bits.Add64(q3, t0, 0)\n\tq4, c = bits.Add64(q4, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[2])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, c = bits.Add64(q6, t1, c)\n\tq7, _ = bits.Add64(q7, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, y.arr[3])\n\tq3, c = bits.Add64(q3, t0, 0)\n\tq4, c = bits.Add64(q4, t1, c)\n\tt1, t0 = bits.Mul64(x2, y.arr[3])\n\tq5, c = bits.Add64(q5, t0, c)\n\tq6, c = bits.Add64(q6, t1, c)\n\tq8, t0 := bits.Mul64(x4, y.arr[3])\n\tq7, c = bits.Add64(q7, t0, c)\n\tq8, _ = bits.Add64(q8, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, y.arr[3])\n\tq4, c = bits.Add64(q4, t0, 0)\n\tq5, c = bits.Add64(q5, t1, c)\n\tt1, t0 = bits.Mul64(x3, y.arr[3])\n\tq6, c = bits.Add64(q6, t0, c)\n\tq7, c = bits.Add64(q7, t1, c)\n\tq8, _ = bits.Add64(q8, 0, c)\n\n\t// Final adjustment\n\n\t// subtract q from 1/4\n\t_, b = bits.Sub64(0, q0, 0)\n\t_, b = bits.Sub64(0, q1, b)\n\t_, b = bits.Sub64(0, q2, b)\n\t_, b = bits.Sub64(0, q3, b)\n\t_, b = bits.Sub64(0, q4, b)\n\t_, b = bits.Sub64(0, q5, b)\n\t_, b = bits.Sub64(0, q6, b)\n\t_, b = bits.Sub64(0, q7, b)\n\t_, b = bits.Sub64(uint64(1)\u003c\u003c62, q8, b)\n\n\t// decrement the result\n\tx0, t := bits.Sub64(r4l, 1, 0)\n\tx1, t = bits.Sub64(r4k, 0, t)\n\tx2, t = bits.Sub64(r4j, 0, t)\n\tx3, t = bits.Sub64(r4i, 0, t)\n\tx4, _ = bits.Sub64(r4h, 0, t)\n\n\t// commit the decrement if the subtraction underflowed (reciprocal was too large)\n\tif b != 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t}\n\n\t// Shift to correct bit alignment, truncating excess bits\n\n\tp = (p \u0026 63) - 1\n\n\tx0, c = bits.Add64(r4l, r4l, 0)\n\tx1, c = bits.Add64(r4k, r4k, c)\n\tx2, c = bits.Add64(r4j, r4j, c)\n\tx3, c = bits.Add64(r4i, r4i, c)\n\tx4, _ = bits.Add64(r4h, r4h, c)\n\n\tif p \u003c 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t\tp = 0 // avoid negative shift below\n\t}\n\n\t{\n\t\tr := uint(p) // right shift\n\t\tl := uint(64 - r) // left shift\n\n\t\tx0 = (r4l \u003e\u003e r) | (r4k \u003c\u003c l)\n\t\tx1 = (r4k \u003e\u003e r) | (r4j \u003c\u003c l)\n\t\tx2 = (r4j \u003e\u003e r) | (r4i \u003c\u003c l)\n\t\tx3 = (r4i \u003e\u003e r) | (r4h \u003c\u003c l)\n\t\tx4 = (r4h \u003e\u003e r)\n\t}\n\n\tif p \u003e 0 {\n\t\tr4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0\n\t}\n\n\tmu[0] = r4l\n\tmu[1] = r4k\n\tmu[2] = r4j\n\tmu[3] = r4i\n\tmu[4] = r4h\n\n\treturn mu\n}\n\n// reduce4 computes the least non-negative residue of x modulo m\n//\n// requires a four-word modulus (m.arr[3] \u003e 1) and its inverse (mu)\nfunc reduce4(x [8]uint64, m *Uint, mu [5]uint64) (z Uint) {\n\t// NB: Most variable names in the comments match the pseudocode for\n\t// \tBarrett reduction in the Handbook of Applied Cryptography.\n\n\t// q1 = x/2^192\n\n\tx0 := x[3]\n\tx1 := x[4]\n\tx2 := x[5]\n\tx3 := x[6]\n\tx4 := x[7]\n\n\t// q2 = q1 * mu; q3 = q2 / 2^320\n\n\tvar q0, q1, q2, q3, q4, q5, t0, t1, c uint64\n\n\tq0, _ = bits.Mul64(x3, mu[0])\n\tq1, t0 = bits.Mul64(x4, mu[0])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, _ = bits.Add64(q1, 0, c)\n\n\tt1, _ = bits.Mul64(x2, mu[1])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tq2, t0 = bits.Mul64(x4, mu[1])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x3, mu[1])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq2, _ = bits.Add64(q2, 0, c)\n\n\tt1, t0 = bits.Mul64(x2, mu[2])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tq3, t0 = bits.Mul64(x4, mu[2])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x1, mu[2])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x3, mu[2])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq3, _ = bits.Add64(q3, 0, c)\n\n\tt1, _ = bits.Mul64(x0, mu[3])\n\tq0, c = bits.Add64(q0, t1, 0)\n\tt1, t0 = bits.Mul64(x2, mu[3])\n\tq1, c = bits.Add64(q1, t0, c)\n\tq2, c = bits.Add64(q2, t1, c)\n\tq4, t0 = bits.Mul64(x4, mu[3])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, mu[3])\n\tq0, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x3, mu[3])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq4, _ = bits.Add64(q4, 0, c)\n\n\tt1, t0 = bits.Mul64(x0, mu[4])\n\t_, c = bits.Add64(q0, t0, 0)\n\tq1, c = bits.Add64(q1, t1, c)\n\tt1, t0 = bits.Mul64(x2, mu[4])\n\tq2, c = bits.Add64(q2, t0, c)\n\tq3, c = bits.Add64(q3, t1, c)\n\tq5, t0 = bits.Mul64(x4, mu[4])\n\tq4, c = bits.Add64(q4, t0, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\tt1, t0 = bits.Mul64(x1, mu[4])\n\tq1, c = bits.Add64(q1, t0, 0)\n\tq2, c = bits.Add64(q2, t1, c)\n\tt1, t0 = bits.Mul64(x3, mu[4])\n\tq3, c = bits.Add64(q3, t0, c)\n\tq4, c = bits.Add64(q4, t1, c)\n\tq5, _ = bits.Add64(q5, 0, c)\n\n\t// Drop the fractional part of q3\n\n\tq0 = q1\n\tq1 = q2\n\tq2 = q3\n\tq3 = q4\n\tq4 = q5\n\n\t// r1 = x mod 2^320\n\n\tx0 = x[0]\n\tx1 = x[1]\n\tx2 = x[2]\n\tx3 = x[3]\n\tx4 = x[4]\n\n\t// r2 = q3 * m mod 2^320\n\n\tvar r0, r1, r2, r3, r4 uint64\n\n\tr4, r3 = bits.Mul64(q0, m.arr[3])\n\t_, t0 = bits.Mul64(q1, m.arr[3])\n\tr4, _ = bits.Add64(r4, t0, 0)\n\n\tt1, r2 = bits.Mul64(q0, m.arr[2])\n\tr3, c = bits.Add64(r3, t1, 0)\n\t_, t0 = bits.Mul64(q2, m.arr[2])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[2])\n\tr3, c = bits.Add64(r3, t0, 0)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\tt1, r1 = bits.Mul64(q0, m.arr[1])\n\tr2, c = bits.Add64(r2, t1, 0)\n\tt1, t0 = bits.Mul64(q2, m.arr[1])\n\tr3, c = bits.Add64(r3, t0, c)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[1])\n\tr2, c = bits.Add64(r2, t0, 0)\n\tr3, c = bits.Add64(r3, t1, c)\n\t_, t0 = bits.Mul64(q3, m.arr[1])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, r0 = bits.Mul64(q0, m.arr[0])\n\tr1, c = bits.Add64(r1, t1, 0)\n\tt1, t0 = bits.Mul64(q2, m.arr[0])\n\tr2, c = bits.Add64(r2, t0, c)\n\tr3, c = bits.Add64(r3, t1, c)\n\t_, t0 = bits.Mul64(q4, m.arr[0])\n\tr4, _ = bits.Add64(r4, t0, c)\n\n\tt1, t0 = bits.Mul64(q1, m.arr[0])\n\tr1, c = bits.Add64(r1, t0, 0)\n\tr2, c = bits.Add64(r2, t1, c)\n\tt1, t0 = bits.Mul64(q3, m.arr[0])\n\tr3, c = bits.Add64(r3, t0, c)\n\tr4, _ = bits.Add64(r4, t1, c)\n\n\t// r = r1 - r2\n\n\tvar b uint64\n\n\tr0, b = bits.Sub64(x0, r0, 0)\n\tr1, b = bits.Sub64(x1, r1, b)\n\tr2, b = bits.Sub64(x2, r2, b)\n\tr3, b = bits.Sub64(x3, r3, b)\n\tr4, b = bits.Sub64(x4, r4, b)\n\n\t// if r\u003c0 then r+=m\n\n\tif b != 0 {\n\t\tr0, c = bits.Add64(r0, m.arr[0], 0)\n\t\tr1, c = bits.Add64(r1, m.arr[1], c)\n\t\tr2, c = bits.Add64(r2, m.arr[2], c)\n\t\tr3, c = bits.Add64(r3, m.arr[3], c)\n\t\tr4, _ = bits.Add64(r4, 0, c)\n\t}\n\n\t// while (r\u003e=m) r-=m\n\n\tfor {\n\t\t// q = r - m\n\t\tq0, b = bits.Sub64(r0, m.arr[0], 0)\n\t\tq1, b = bits.Sub64(r1, m.arr[1], b)\n\t\tq2, b = bits.Sub64(r2, m.arr[2], b)\n\t\tq3, b = bits.Sub64(r3, m.arr[3], b)\n\t\tq4, b = bits.Sub64(r4, 0, b)\n\n\t\t// if borrow break\n\t\tif b != 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// r = q\n\t\tr4, r3, r2, r1, r0 = q4, q3, q2, q1, q0\n\t}\n\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = r3, r2, r1, r0\n\n\treturn z\n}\n" + }, + { + "name": "uint256.gno", + "body": "// Ported from https://github.com/holiman/uint256\n// This package provides a 256-bit unsigned integer type, Uint256, and associated functions.\npackage uint256\n\nimport (\n\t\"errors\"\n\t\"math/bits\"\n\t\"strconv\"\n)\n\nconst (\n\tMaxUint64 = 1\u003c\u003c64 - 1\n\tuintSize = 32 \u003c\u003c (^uint(0) \u003e\u003e 63)\n)\n\n// Uint is represented as an array of 4 uint64, in little-endian order,\n// so that Uint[3] is the most significant, and Uint[0] is the least significant\ntype Uint struct {\n\tarr [4]uint64\n}\n\n// NewUint returns a new initialized Uint.\nfunc NewUint(val uint64) *Uint {\n\tz := \u0026Uint{arr: [4]uint64{val, 0, 0, 0}}\n\treturn z\n}\n\n// Zero returns a new Uint initialized to zero.\nfunc Zero() *Uint {\n\treturn NewUint(0)\n}\n\n// One returns a new Uint initialized to one.\nfunc One() *Uint {\n\treturn NewUint(1)\n}\n\n// SetAllOne sets all the bits of z to 1\nfunc (z *Uint) SetAllOne() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, MaxUint64\n\treturn z\n}\n\n// Set sets z to x and returns z.\nfunc (z *Uint) Set(x *Uint) *Uint {\n\t*z = *x\n\n\treturn z\n}\n\n// SetOne sets z to 1\nfunc (z *Uint) SetOne() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 1\n\treturn z\n}\n\nconst twoPow256Sub1 = \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"\n\n// SetFromDecimal sets z from the given string, interpreted as a decimal number.\n// OBS! This method is _not_ strictly identical to the (*big.Uint).SetString(..., 10) method.\n// Notable differences:\n// - This method does not accept underscore input, e.g. \"100_000\",\n// - This method does not accept negative zero as valid, e.g \"-0\",\n// - (this method does not accept any negative input as valid))\nfunc (z *Uint) SetFromDecimal(s string) (err error) {\n\t// Remove max one leading +\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '+' {\n\t\ts = s[1:]\n\t}\n\t// Remove any number of leading zeroes\n\tif len(s) \u003e 0 \u0026\u0026 s[0] == '0' {\n\t\tvar i int\n\t\tvar c rune\n\t\tfor i, c = range s {\n\t\t\tif c != '0' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ts = s[i:]\n\t}\n\tif len(s) \u003c len(twoPow256Sub1) {\n\t\treturn z.fromDecimal(s)\n\t}\n\tif len(s) == len(twoPow256Sub1) {\n\t\tif s \u003e twoPow256Sub1 {\n\t\t\treturn ErrBig256Range\n\t\t}\n\t\treturn z.fromDecimal(s)\n\t}\n\treturn ErrBig256Range\n}\n\n// FromDecimal is a convenience-constructor to create an Uint from a\n// decimal (base 10) string. Numbers larger than 256 bits are not accepted.\nfunc FromDecimal(decimal string) (*Uint, error) {\n\tvar z Uint\n\tif err := z.SetFromDecimal(decimal); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026z, nil\n}\n\n// MustFromDecimal is a convenience-constructor to create an Uint from a\n// decimal (base 10) string.\n// Returns a new Uint and panics if any error occurred.\nfunc MustFromDecimal(decimal string) *Uint {\n\tvar z Uint\n\tif err := z.SetFromDecimal(decimal); err != nil {\n\t\tpanic(err)\n\t}\n\treturn \u0026z\n}\n\n// multipliers holds the values that are needed for fromDecimal\nvar multipliers = [5]*Uint{\n\tnil, // represents first round, no multiplication needed\n\t{[4]uint64{10000000000000000000, 0, 0, 0}}, // 10 ^ 19\n\t{[4]uint64{687399551400673280, 5421010862427522170, 0, 0}}, // 10 ^ 38\n\t{[4]uint64{5332261958806667264, 17004971331911604867, 2938735877055718769, 0}}, // 10 ^ 57\n\t{[4]uint64{0, 8607968719199866880, 532749306367912313, 1593091911132452277}}, // 10 ^ 76\n}\n\n// fromDecimal is a helper function to only ever be called via SetFromDecimal\n// this function takes a string and chunks it up, calling ParseUint on it up to 5 times\n// these chunks are then multiplied by the proper power of 10, then added together.\nfunc (z *Uint) fromDecimal(bs string) error {\n\t// first clear the input\n\tz.Clear()\n\t// the maximum value of uint64 is 18446744073709551615, which is 20 characters\n\t// one less means that a string of 19 9's is always within the uint64 limit\n\tvar (\n\t\tnum uint64\n\t\terr error\n\t\tremaining = len(bs)\n\t)\n\tif remaining == 0 {\n\t\treturn errors.New(\"EOF\")\n\t}\n\t// We proceed in steps of 19 characters (nibbles), from least significant to most significant.\n\t// This means that the first (up to) 19 characters do not need to be multiplied.\n\t// In the second iteration, our slice of 19 characters needs to be multipleied\n\t// by a factor of 10^19. Et cetera.\n\tfor i, mult := range multipliers {\n\t\tif remaining \u003c= 0 {\n\t\t\treturn nil // Done\n\t\t} else if remaining \u003e 19 {\n\t\t\tnum, err = strconv.ParseUint(bs[remaining-19:remaining], 10, 64)\n\t\t} else {\n\t\t\t// Final round\n\t\t\tnum, err = strconv.ParseUint(bs, 10, 64)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// add that number to our running total\n\t\tif i == 0 {\n\t\t\tz.SetUint64(num)\n\t\t} else {\n\t\t\tbase := NewUint(num)\n\t\t\tz.Add(z, base.Mul(base, mult))\n\t\t}\n\t\t// Chop off another 19 characters\n\t\tif remaining \u003e 19 {\n\t\t\tbs = bs[0 : remaining-19]\n\t\t}\n\t\tremaining -= 19\n\t}\n\treturn nil\n}\n\n// Byte sets z to the value of the byte at position n,\n// with 'z' considered as a big-endian 32-byte integer\n// if 'n' \u003e 32, f is set to 0\n// Example: f = '5', n=31 =\u003e 5\nfunc (z *Uint) Byte(n *Uint) *Uint {\n\t// in z, z.arr[0] is the least significant\n\tif number, overflow := n.Uint64WithOverflow(); !overflow {\n\t\tif number \u003c 32 {\n\t\t\tnumber := z.arr[4-1-number/8]\n\t\t\toffset := (n.arr[0] \u0026 0x7) \u003c\u003c 3 // 8*(n.d % 8)\n\t\t\tz.arr[0] = (number \u0026 (0xff00000000000000 \u003e\u003e offset)) \u003e\u003e (56 - offset)\n\t\t\tz.arr[3], z.arr[2], z.arr[1] = 0, 0, 0\n\t\t\treturn z\n\t\t}\n\t}\n\n\treturn z.Clear()\n}\n\n// BitLen returns the number of bits required to represent z\nfunc (z *Uint) BitLen() int {\n\tswitch {\n\tcase z.arr[3] != 0:\n\t\treturn 192 + bits.Len64(z.arr[3])\n\tcase z.arr[2] != 0:\n\t\treturn 128 + bits.Len64(z.arr[2])\n\tcase z.arr[1] != 0:\n\t\treturn 64 + bits.Len64(z.arr[1])\n\tdefault:\n\t\treturn bits.Len64(z.arr[0])\n\t}\n}\n\n// ByteLen returns the number of bytes required to represent z\nfunc (z *Uint) ByteLen() int {\n\treturn (z.BitLen() + 7) / 8\n}\n\n// Clear sets z to 0\nfunc (z *Uint) Clear() *Uint {\n\tz.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 0\n\treturn z\n}\n\nconst (\n\t// hextable = \"0123456789abcdef\"\n\tbintable = \"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\a\\b\\t\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\n\\v\\f\\r\\x0e\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\n\\v\\f\\r\\x0e\\x0f\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\"\n\tbadNibble = 0xff\n)\n\n// SetFromHex sets z from the given string, interpreted as a hexadecimal number.\n// OBS! This method is _not_ strictly identical to the (*big.Int).SetString(..., 16) method.\n// Notable differences:\n// - This method _require_ \"0x\" or \"0X\" prefix.\n// - This method does not accept zero-prefixed hex, e.g. \"0x0001\"\n// - This method does not accept underscore input, e.g. \"100_000\",\n// - This method does not accept negative zero as valid, e.g \"-0x0\",\n// - (this method does not accept any negative input as valid)\nfunc (z *Uint) SetFromHex(hex string) error {\n\treturn z.fromHex(hex)\n}\n\n// fromHex is the internal implementation of parsing a hex-string.\nfunc (z *Uint) fromHex(hex string) error {\n\tif err := checkNumberS(hex); err != nil {\n\t\treturn err\n\t}\n\tif len(hex) \u003e 66 {\n\t\treturn ErrBig256Range\n\t}\n\tz.Clear()\n\tend := len(hex)\n\tfor i := 0; i \u003c 4; i++ {\n\t\tstart := end - 16\n\t\tif start \u003c 2 {\n\t\t\tstart = 2\n\t\t}\n\t\tfor ri := start; ri \u003c end; ri++ {\n\t\t\tnib := bintable[hex[ri]]\n\t\t\tif nib == badNibble {\n\t\t\t\treturn ErrSyntax\n\t\t\t}\n\t\t\tz.arr[i] = z.arr[i] \u003c\u003c 4\n\t\t\tz.arr[i] += uint64(nib)\n\t\t}\n\t\tend = start\n\t}\n\treturn nil\n}\n\n// FromHex is a convenience-constructor to create an Uint from\n// a hexadecimal string. The string is required to be '0x'-prefixed\n// Numbers larger than 256 bits are not accepted.\nfunc FromHex(hex string) (*Uint, error) {\n\tvar z Uint\n\tif err := z.fromHex(hex); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026z, nil\n}\n\n// MustFromHex is a convenience-constructor to create an Uint from\n// a hexadecimal string.\n// Returns a new Uint and panics if any error occurred.\nfunc MustFromHex(hex string) *Uint {\n\tvar z Uint\n\tif err := z.fromHex(hex); err != nil {\n\t\tpanic(err)\n\t}\n\treturn \u0026z\n}\n\n// Clone creates a new Uint identical to z\nfunc (z *Uint) Clone() *Uint {\n\tvar x Uint\n\tx.arr[0] = z.arr[0]\n\tx.arr[1] = z.arr[1]\n\tx.arr[2] = z.arr[2]\n\tx.arr[3] = z.arr[3]\n\n\treturn \u0026x\n}\n" + }, + { + "name": "uint256_test.gno", + "body": "package uint256\n\nimport (\n\t\"testing\"\n)\n\nfunc TestSetAllOne(t *testing.T) {\n\tz := Zero()\n\tz.SetAllOne()\n\tif z.String() != twoPow256Sub1 {\n\t\tt.Errorf(\"Expected all ones, got %s\", z.String())\n\t}\n}\n\nfunc TestByte(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\tposition uint64\n\t\texpected byte\n\t}{\n\t\t{\"0x1000000000000000000000000000000000000000000000000000000000000000\", 0, 16},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 0, 255},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 31, 255},\n\t}\n\n\tfor i, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tn := NewUint(tt.position)\n\t\tresult := z.Byte(n)\n\n\t\tif result.arr[0] != uint64(tt.expected) {\n\t\t\tt.Errorf(\"Test case %d failed. Input: %s, Position: %d, Expected: %d, Got: %d\",\n\t\t\t\ti, tt.input, tt.position, tt.expected, result.arr[0])\n\t\t}\n\n\t\t// check other array elements are 0\n\t\tif result.arr[1] != 0 || result.arr[2] != 0 || result.arr[3] != 0 {\n\t\t\tt.Errorf(\"Test case %d failed. Non-zero values in upper bytes\", i)\n\t\t}\n\t}\n\n\t// overflow\n\tz, _ := FromHex(\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\")\n\tn := NewUint(32)\n\tresult := z.Byte(n)\n\n\tif !result.IsZero() {\n\t\tt.Errorf(\"Expected zero for position \u003e= 32, got %v\", result)\n\t}\n}\n\nfunc TestBitLen(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected int\n\t}{\n\t\t{\"0x0\", 0},\n\t\t{\"0x1\", 1},\n\t\t{\"0xff\", 8},\n\t\t{\"0x100\", 9},\n\t\t{\"0xffff\", 16},\n\t\t{\"0x10000\", 17},\n\t\t{\"0xffffffffffffffff\", 64},\n\t\t{\"0x10000000000000000\", 65},\n\t\t{\"0xffffffffffffffffffffffffffffffff\", 128},\n\t\t{\"0x100000000000000000000000000000000\", 129},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 256},\n\t}\n\n\tfor i, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tresult := z.BitLen()\n\n\t\tif result != tt.expected {\n\t\t\tt.Errorf(\"Test case %d failed. Input: %s, Expected: %d, Got: %d\",\n\t\t\t\ti, tt.input, tt.expected, result)\n\t\t}\n\t}\n}\n\nfunc TestByteLen(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected int\n\t}{\n\t\t{\"0x0\", 0},\n\t\t{\"0x1\", 1},\n\t\t{\"0xff\", 1},\n\t\t{\"0x100\", 2},\n\t\t{\"0xffff\", 2},\n\t\t{\"0x10000\", 3},\n\t\t{\"0xffffffffffffffff\", 8},\n\t\t{\"0x10000000000000000\", 9},\n\t\t{\"0xffffffffffffffffffffffffffffffff\", 16},\n\t\t{\"0x100000000000000000000000000000000\", 17},\n\t\t{\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\", 32},\n\t}\n\n\tfor i, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tresult := z.ByteLen()\n\n\t\tif result != tt.expected {\n\t\t\tt.Errorf(\"Test case %d failed. Input: %s, Expected: %d, Got: %d\",\n\t\t\t\ti, tt.input, tt.expected, result)\n\t\t}\n\t}\n}\n\nfunc TestClone(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected string\n\t}{\n\t\t{\"0x1\", \"1\"},\n\t\t{\"0x100\", \"256\"},\n\t\t{\"0x10000000000000000\", \"18446744073709551616\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz, _ := FromHex(tt.input)\n\t\tresult := z.Clone()\n\t\tif result.String() != tt.expected {\n\t\t\tt.Errorf(\"Test %s failed. Expected %s, got %s\", tt.input, tt.expected, result.String())\n\t\t}\n\t}\n}\n" + }, + { + "name": "utils.gno", + "body": "package uint256\n\nfunc checkNumberS(input string) error {\n\tconst fn = \"UnmarshalText\"\n\tl := len(input)\n\tif l == 0 {\n\t\treturn errEmptyString(fn, input)\n\t}\n\tif l \u003c 2 || input[0] != '0' ||\n\t\t(input[1] != 'x' \u0026\u0026 input[1] != 'X') {\n\t\treturn errMissingPrefix(fn, input)\n\t}\n\tif l == 2 {\n\t\treturn errEmptyNumber(fn, input)\n\t}\n\tif len(input) \u003e 3 \u0026\u0026 input[2] == '0' {\n\t\treturn errLeadingZero(fn, input)\n\t}\n\treturn nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Pr1jEwUM70VNmUEDmd+OF+P52/vyVi9ucyk6MjG6rqk/Z+9AzhEqyqZGpky/uCHirRwICPkYt5dpYsR5s87eCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "int256", + "path": "gno.land/p/demo/int256", + "files": [ + { + "name": "arithmetic.gno", + "body": "package int256\n\nimport (\n\t\"gno.land/p/demo/uint256\"\n)\n\nconst divisionByZeroError = \"division by zero\"\n\n// Add adds two int256 values and saves the result in z.\nfunc (z *Int) Add(x, y *Int) *Int {\n\tz.value.Add(\u0026x.value, \u0026y.value)\n\treturn z\n}\n\n// AddUint256 adds int256 and uint256 values and saves the result in z.\nfunc (z *Int) AddUint256(x *Int, y *uint256.Uint) *Int {\n\tz.value.Add(\u0026x.value, y)\n\treturn z\n}\n\n// Sub subtracts two int256 values and saves the result in z.\nfunc (z *Int) Sub(x, y *Int) *Int {\n\tz.value.Sub(\u0026x.value, \u0026y.value)\n\treturn z\n}\n\n// SubUint256 subtracts uint256 and int256 values and saves the result in z.\nfunc (z *Int) SubUint256(x *Int, y *uint256.Uint) *Int {\n\tz.value.Sub(\u0026x.value, y)\n\treturn z\n}\n\n// Mul multiplies two int256 values and saves the result in z.\n//\n// It considers the signs of the operands to determine the sign of the result.\nfunc (z *Int) Mul(x, y *Int) *Int {\n\txAbs, xSign := x.Abs(), x.Sign()\n\tyAbs, ySign := y.Abs(), y.Sign()\n\n\tz.value.Mul(xAbs, yAbs)\n\n\tif xSign != ySign {\n\t\tz.value.Neg(\u0026z.value)\n\t}\n\n\treturn z\n}\n\n// Abs returns the absolute value of z.\nfunc (z *Int) Abs() *uint256.Uint {\n\tif z.Sign() \u003e= 0 {\n\t\treturn \u0026z.value\n\t}\n\n\tvar absValue uint256.Uint\n\tabsValue.Sub(uint0, \u0026z.value).Neg(\u0026z.value)\n\n\treturn \u0026absValue\n}\n\n// Div performs integer division z = x / y and returns z.\n// If y == 0, it panics with a \"division by zero\" error.\n//\n// This function handles signed division using two's complement representation:\n// 1. Determine the sign of the quotient based on the signs of x and y.\n// 2. Perform unsigned division on the absolute values.\n// 3. Adjust the result's sign if necessary.\n//\n// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity):\n//\n// Let x = -6 (11111010 in two's complement) and y = 3 (00000011)\n//\n// Step 2: Determine signs\n//\n//\tx: negative (MSB is 1)\n//\ty: positive (MSB is 0)\n//\n// Step 3: Calculate absolute values\n//\n//\t|x| = 6: 11111010 -\u003e 00000110\n//\t NOT: 00000101\n//\t +1: 00000110\n//\n//\t|y| = 3: 00000011 (already positive)\n//\n// Step 4: Unsigned division\n//\n//\t6 / 3 = 2: 00000010\n//\n// Step 5: Adjust sign (x and y have different signs)\n//\n//\t-2: 00000010 -\u003e 11111110\n//\t NOT: 11111101\n//\t +1: 11111110\n//\n// Note: This implementation rounds towards zero, as is standard in Go.\nfunc (z *Int) Div(x, y *Int) *Int {\n\t// Step 1: Check for division by zero\n\tif y.IsZero() {\n\t\tpanic(divisionByZeroError)\n\t}\n\n\t// Step 2, 3: Calculate the absolute values of x and y\n\txAbs, xSign := x.Abs(), x.Sign()\n\tyAbs, ySign := y.Abs(), y.Sign()\n\n\t// Step 4: Perform unsigned division on the absolute values\n\tz.value.Div(xAbs, yAbs)\n\n\t// Step 5: Adjust the sign of the result\n\t// if x and y have different signs, the result must be negative\n\tif xSign != ySign {\n\t\tz.value.Neg(\u0026z.value)\n\t}\n\n\treturn z\n}\n\n// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity):\n//\n// Let x = -7 (11111001 in two's complement) and y = 3 (00000011)\n//\n// Step 2: Determine signs\n//\n//\tx: negative (MSB is 1)\n//\ty: positive (MSB is 0)\n//\n// Step 3: Calculate absolute values\n//\n//\t|x| = 7: 11111001 -\u003e 00000111\n//\t NOT: 00000110\n//\t +1: 00000111\n//\n//\t|y| = 3: 00000011 (already positive)\n//\n// Step 4: Unsigned division\n//\n//\t7 / 3 = 2: 00000010\n//\n// Step 5: Adjust sign (x and y have different signs)\n//\n//\t-2: 00000010 -\u003e 11111110\n//\t NOT: 11111101\n//\t +1: 11111110\n//\n// Final result: -2 (11111110 in two's complement)\n//\n// Note: This implementation rounds towards zero, as is standard in Go.\nfunc (z *Int) Quo(x, y *Int) *Int {\n\t// Step 1: Check for division by zero\n\tif y.IsZero() {\n\t\tpanic(divisionByZeroError)\n\t}\n\n\t// Step 2, 3: Calculate the absolute values of x and y\n\txAbs, xSign := x.Abs(), x.Sign()\n\tyAbs, ySign := y.Abs(), y.Sign()\n\n\t// perform unsigned division on the absolute values\n\tz.value.Div(xAbs, yAbs)\n\n\t// Step 5: Adjust the sign of the result\n\t// if x and y have different signs, the result must be negative\n\tif xSign != ySign {\n\t\tz.value.Neg(\u0026z.value)\n\t}\n\n\treturn z\n}\n\n// Rem sets z to the remainder x%y for y != 0 and returns z.\n//\n// The function performs the following steps:\n// 1. Check for division by zero\n// 2. Determine the signs of x and y\n// 3. Calculate the absolute values of x and y\n// 4. Perform unsigned division and get the remainder\n// 5. Adjust the sign of the remainder\n//\n// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity):\n//\n// Let x = -7 (11111001 in two's complement) and y = 3 (00000011)\n//\n// Step 2: Determine signs\n//\n//\tx: negative (MSB is 1)\n//\ty: positive (MSB is 0)\n//\n// Step 3: Calculate absolute values\n//\n//\t|x| = 7: 11111001 -\u003e 00000111\n//\t NOT: 00000110\n//\t +1: 00000111\n//\n//\t|y| = 3: 00000011 (already positive)\n//\n// Step 4: Unsigned division\n//\n//\t7 / 3 = 2 remainder 1\n//\tq = 2: 00000010 (not used in result)\n//\tr = 1: 00000001\n//\n// Step 5: Adjust sign of remainder (x is negative)\n//\n//\t-1: 00000001 -\u003e 11111111\n//\t NOT: 11111110\n//\t +1: 11111111\n//\n// Final result: -1 (11111111 in two's complement)\n//\n// Note: The sign of the remainder is always the same as the sign of the dividend (x).\nfunc (z *Int) Rem(x, y *Int) *Int {\n\t// Step 1: Check for division by zero\n\tif y.IsZero() {\n\t\tpanic(divisionByZeroError)\n\t}\n\n\t// Step 2, 3\n\txAbs, xSign := x.Abs(), x.Sign()\n\tyAbs := y.Abs()\n\n\t// Step 4: Perform unsigned division and get the remainder\n\tvar q, r uint256.Uint\n\tq.DivMod(xAbs, yAbs, \u0026r)\n\n\t// Step 5: Adjust the sign of the remainder\n\tif xSign \u003c 0 {\n\t\tr.Neg(\u0026r)\n\t}\n\n\tz.value.Set(\u0026r)\n\treturn z\n}\n\n// Mod sets z to the modulus x%y for y != 0 and returns z.\n// The result (z) has the same sign as the divisor y.\nfunc (z *Int) Mod(x, y *Int) *Int {\n\treturn z.ModE(x, y)\n}\n\n// DivE performs Euclidean division of x by y, setting z to the quotient and returning z.\n// If y == 0, it panics with a \"division by zero\" error.\n//\n// Euclidean division satisfies the following properties:\n// 1. The remainder is always non-negative: 0 \u003c= x mod y \u003c |y|\n// 2. It follows the identity: x = y * (x div y) + (x mod y)\nfunc (z *Int) DivE(x, y *Int) *Int {\n\tif y.IsZero() {\n\t\tpanic(divisionByZeroError)\n\t}\n\n\t// Compute the truncated division quotient\n\tz.Quo(x, y)\n\n\t// Compute the remainder\n\tr := new(Int).Rem(x, y)\n\n\t// If the remainder is negative, adjust the quotient\n\tif r.Sign() \u003c 0 {\n\t\tif y.Sign() \u003e 0 {\n\t\t\tz.Sub(z, NewInt(1))\n\t\t} else {\n\t\t\tz.Add(z, NewInt(1))\n\t\t}\n\t}\n\n\treturn z\n}\n\n// ModE computes the Euclidean modulus of x by y, setting z to the result and returning z.\n// If y == 0, it panics with a \"division by zero\" error.\n//\n// The Euclidean modulus is always non-negative and satisfies:\n//\n//\t0 \u003c= x mod y \u003c |y|\n//\n// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity):\n//\n// Case 1: Let x = -7 (11111001 in two's complement) and y = 3 (00000011)\n//\n// Step 1: Compute remainder (using Rem)\n//\n//\tResult of Rem: -1 (11111111 in two's complement)\n//\n// Step 2: Adjust sign (result is negative, y is positive)\n//\n//\t-1 + 3 = 2\n//\t11111111 + 00000011 = 00000010\n//\n// Final result: 2 (00000010)\n//\n// Case 2: Let x = -7 (11111001 in two's complement) and y = -3 (11111101 in two's complement)\n//\n// Step 1: Compute remainder (using Rem)\n//\n//\tResult of Rem: -1 (11111111 in two's complement)\n//\n// Step 2: Adjust sign (result is negative, y is negative)\n//\n//\tNo adjustment needed\n//\n// Final result: -1 (11111111 in two's complement)\n//\n// Note: This implementation ensures that the result always has the same sign as y,\n// which is different from the Rem operation.\nfunc (z *Int) ModE(x, y *Int) *Int {\n\tif y.IsZero() {\n\t\tpanic(divisionByZeroError)\n\t}\n\n\t// Perform T-division to get the remainder\n\tz.Rem(x, y)\n\n\t// Adjust the remainder if necessary\n\tif z.Sign() \u003e= 0 {\n\t\treturn z\n\t}\n\tif y.Sign() \u003e 0 {\n\t\treturn z.Add(z, y)\n\t}\n\n\treturn z.Sub(z, y)\n}\n\n// Sets z to the sum x + y, where z and x are uint256s and y is an int256.\n//\n// If the y is positive, it adds y.value to x. otherwise, it subtracts y.Abs() from x.\nfunc AddDelta(z, x *uint256.Uint, y *Int) {\n\tif y.Sign() \u003e= 0 {\n\t\tz.Add(x, \u0026y.value)\n\t} else {\n\t\tz.Sub(x, y.Abs())\n\t}\n}\n\n// Sets z to the sum x + y, where z and x are uint256s and y is an int256.\n//\n// This function returns true if the addition overflows, false otherwise.\nfunc AddDeltaOverflow(z, x *uint256.Uint, y *Int) bool {\n\tvar overflow bool\n\tif y.Sign() \u003e= 0 {\n\t\t_, overflow = z.AddOverflow(x, \u0026y.value)\n\t} else {\n\t\tvar absY uint256.Uint\n\t\tabsY.Sub(uint0, \u0026y.value) // absY = -y.value\n\t\t_, overflow = z.SubOverflow(x, \u0026absY)\n\t}\n\n\treturn overflow\n}\n" + }, + { + "name": "arithmetic_test.gno", + "body": "package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nconst (\n\t// 2^255 - 1\n\tMAX_INT256 = \"57896044618658097711785492504343953926634992332820282019728792003956564819967\"\n\t// -(2^255 - 1)\n\tMINUS_MAX_INT256 = \"-57896044618658097711785492504343953926634992332820282019728792003956564819967\"\n\n\t// 2^255 - 1\n\tMAX_UINT256 = \"115792089237316195423570985008687907853269984665640564039457584007913129639935\"\n\tMAX_UINT256_MINUS_1 = \"115792089237316195423570985008687907853269984665640564039457584007913129639934\"\n\n\tMINUS_MAX_UINT256 = \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\"\n\tMINUS_MAX_UINT256_PLUS_1 = \"-115792089237316195423570985008687907853269984665640564039457584007913129639934\"\n\n\tTWO_POW_128 = \"340282366920938463463374607431768211456\"\n\tMINUS_TWO_POW_128 = \"-340282366920938463463374607431768211456\"\n\tMINUS_TWO_POW_128_MINUS_1 = \"-340282366920938463463374607431768211457\"\n\tTWO_POW_128_MINUS_1 = \"340282366920938463463374607431768211455\"\n\n\tTWO_POW_129_MINUS_1 = \"680564733841876926926749214863536422911\"\n\n\tTWO_POW_254 = \"28948022309329048855892746252171976963317496166410141009864396001978282409984\"\n\tMINUS_TWO_POW_254 = \"-28948022309329048855892746252171976963317496166410141009864396001978282409984\"\n\tHALF_MAX_INT256 = \"28948022309329048855892746252171976963317496166410141009864396001978282409983\"\n\tMINUS_HALF_MAX_INT256 = \"-28948022309329048855892746252171976963317496166410141009864396001978282409983\"\n\n\tTWO_POW_255 = \"57896044618658097711785492504343953926634992332820282019728792003956564819968\"\n\tMIN_INT256 = \"-57896044618658097711785492504343953926634992332820282019728792003956564819968\"\n\tMIN_INT256_MINUS_1 = \"-57896044618658097711785492504343953926634992332820282019728792003956564819969\"\n)\n\nfunc TestAdd(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"2\"},\n\t\t{\"1\", \"2\", \"3\"},\n\t\t// NEGATIVE\n\t\t{\"-1\", \"1\", \"0\"},\n\t\t{\"1\", \"-1\", \"0\"},\n\t\t{\"3\", \"-3\", \"0\"},\n\t\t{\"-1\", \"-1\", \"-2\"},\n\t\t{\"-1\", \"-2\", \"-3\"},\n\t\t{\"-1\", \"3\", \"2\"},\n\t\t{\"3\", \"-1\", \"2\"},\n\t\t// OVERFLOW\n\t\t{MAX_UINT256, \"1\", \"0\"},\n\t\t{MAX_INT256, \"1\", MIN_INT256},\n\t\t{MIN_INT256, \"-1\", MAX_INT256},\n\t\t{MAX_INT256, MAX_INT256, \"-2\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Add(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Add(%s, %s) = %v, want %v\", tc.x, tc.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestAddUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"2\"},\n\t\t{\"1\", \"2\", \"3\"},\n\t\t{\"-1\", \"1\", \"0\"},\n\t\t{\"-1\", \"3\", \"2\"},\n\t\t{MINUS_MAX_UINT256_PLUS_1, MAX_UINT256, \"1\"},\n\t\t{MINUS_MAX_UINT256, MAX_UINT256_MINUS_1, \"-1\"},\n\t\t// OVERFLOW\n\t\t{MINUS_MAX_UINT256, MAX_UINT256, \"0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.AddUint256(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"AddUint256(%s, %s) = %v, want %v\", tc.x, tc.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestAddDelta(t *testing.T) {\n\ttests := []struct {\n\t\tz, x, y, want string\n\t}{\n\t\t{\"0\", \"0\", \"0\", \"0\"},\n\t\t{\"0\", \"0\", \"1\", \"1\"},\n\t\t{\"0\", \"1\", \"0\", \"1\"},\n\t\t{\"0\", \"1\", \"1\", \"2\"},\n\t\t{\"1\", \"2\", \"3\", \"5\"},\n\t\t{\"5\", \"10\", \"-3\", \"7\"},\n\t\t// underflow\n\t\t{\"1\", \"2\", \"-3\", MAX_UINT256},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz, err := uint256.FromDecimal(tc.z)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tx, err := uint256.FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := uint256.FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tAddDelta(z, x, y)\n\n\t\tif z.Neq(want) {\n\t\t\tt.Errorf(\"AddDelta(%s, %s, %s) = %v, want %v\", tc.z, tc.x, tc.y, z.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestAddDeltaOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tz, x, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", \"0\", false},\n\t\t// underflow\n\t\t{\"1\", \"2\", \"-3\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tz, err := uint256.FromDecimal(tc.z)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tx, err := uint256.FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tresult := AddDeltaOverflow(z, x, y)\n\t\tif result != tc.want {\n\t\t\tt.Errorf(\"AddDeltaOverflow(%s, %s, %s) = %v, want %v\", tc.z, tc.x, tc.y, result, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestSub(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"-1\", \"1\", \"-2\"},\n\t\t{\"1\", \"-1\", \"2\"},\n\t\t{\"-1\", \"-1\", \"0\"},\n\t\t{MINUS_MAX_UINT256, MINUS_MAX_UINT256, \"0\"},\n\t\t{MINUS_MAX_UINT256, \"0\", MINUS_MAX_UINT256},\n\t\t{MAX_INT256, MIN_INT256, \"-1\"},\n\t\t{MIN_INT256, MIN_INT256, \"0\"},\n\t\t{MAX_INT256, MAX_INT256, \"0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Sub(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Sub(%s, %s) = %v, want %v\", tc.x, tc.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestSubUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"-1\"},\n\t\t{\"1\", \"0\", \"1\"},\n\t\t{\"1\", \"1\", \"0\"},\n\t\t{\"1\", \"2\", \"-1\"},\n\t\t{\"-1\", \"1\", \"-2\"},\n\t\t{\"-1\", \"3\", \"-4\"},\n\t\t// underflow\n\t\t{MINUS_MAX_UINT256, \"1\", \"0\"},\n\t\t{MINUS_MAX_UINT256, \"2\", \"-1\"},\n\t\t{MINUS_MAX_UINT256, \"3\", \"-2\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := uint256.FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.SubUint256(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"SubUint256(%s, %s) = %v, want %v\", tc.x, tc.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestMul(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"5\", \"3\", \"15\"},\n\t\t{\"-5\", \"3\", \"-15\"},\n\t\t{\"5\", \"-3\", \"-15\"},\n\t\t{\"0\", \"3\", \"0\"},\n\t\t{\"3\", \"0\", \"0\"},\n\t\t{\"-5\", \"-3\", \"15\"},\n\t\t{MAX_UINT256, \"1\", MAX_UINT256},\n\t\t{MAX_INT256, \"2\", \"-2\"},\n\t\t{TWO_POW_254, \"2\", MIN_INT256},\n\t\t{MINUS_TWO_POW_254, \"2\", MIN_INT256},\n\t\t{MAX_INT256, \"1\", MAX_INT256},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Mul(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mul(%s, %s) = %v, want %v\", tc.x, tc.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestDiv(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, expected string\n\t}{\n\t\t{\"1\", \"1\", \"1\"},\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"-1\", \"1\", \"-1\"},\n\t\t{\"1\", \"-1\", \"-1\"},\n\t\t{\"-1\", \"-1\", \"1\"},\n\t\t{\"-6\", \"3\", \"-2\"},\n\t\t{\"10\", \"-2\", \"-5\"},\n\t\t{\"-10\", \"3\", \"-3\"},\n\t\t{\"7\", \"3\", \"2\"},\n\t\t{\"-7\", \"3\", \"-2\"},\n\t\t// the maximum value of a positive number in int256 is less than the maximum value of a uint256\n\t\t{MAX_INT256, \"2\", HALF_MAX_INT256},\n\t\t{MINUS_MAX_INT256, \"2\", MINUS_HALF_MAX_INT256},\n\t\t{MAX_INT256, \"-1\", MINUS_MAX_INT256},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.x+\"/\"+tt.y, func(t *testing.T) {\n\t\t\tx := MustFromDecimal(tt.x)\n\t\t\ty := MustFromDecimal(tt.y)\n\t\t\tresult := Zero().Div(x, y)\n\t\t\tif result.String() != tt.expected {\n\t\t\t\tt.Errorf(\"Div(%s, %s) = %s, want %s\", tt.x, tt.y, result.String(), tt.expected)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"Division by zero\", func(t *testing.T) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Errorf(\"Div(1, 0) did not panic\")\n\t\t\t}\n\t\t}()\n\t\tx := MustFromDecimal(\"1\")\n\t\ty := MustFromDecimal(\"0\")\n\t\tZero().Div(x, y)\n\t})\n}\n\nfunc TestQuo(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"0\", \"-1\", \"0\"},\n\t\t{\"10\", \"1\", \"10\"},\n\t\t{\"10\", \"-1\", \"-10\"},\n\t\t{\"-10\", \"1\", \"-10\"},\n\t\t{\"-10\", \"-1\", \"10\"},\n\t\t{\"10\", \"-3\", \"-3\"},\n\t\t{\"-10\", \"3\", \"-3\"},\n\t\t{\"10\", \"3\", \"3\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Quo(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Quo(%s, %s) = %v, want %v\", tc.x, tc.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestRem(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"0\", \"-1\", \"0\"},\n\t\t{\"10\", \"1\", \"0\"},\n\t\t{\"10\", \"-1\", \"0\"},\n\t\t{\"-10\", \"1\", \"0\"},\n\t\t{\"-10\", \"-1\", \"0\"},\n\t\t{\"10\", \"3\", \"1\"},\n\t\t{\"10\", \"-3\", \"1\"},\n\t\t{\"-10\", \"3\", \"-1\"},\n\t\t{\"-10\", \"-3\", \"-1\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Rem(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Rem(%s, %s) = %v, want %v\", tc.x, tc.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestMod(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"0\", \"1\", \"0\"},\n\t\t{\"0\", \"-1\", \"0\"},\n\t\t{\"10\", \"1\", \"0\"},\n\t\t{\"10\", \"-1\", \"0\"},\n\t\t{\"-10\", \"1\", \"0\"},\n\t\t{\"-10\", \"-1\", \"0\"},\n\t\t{\"10\", \"3\", \"1\"},\n\t\t{\"10\", \"-3\", \"1\"},\n\t\t{\"-10\", \"3\", \"2\"},\n\t\t{\"-10\", \"-3\", \"2\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\twant, err := FromDecimal(tc.want)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := New()\n\t\tgot.Mod(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mod(%s, %s) = %v, want %v\", tc.x, tc.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestModeOverflow(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{MIN_INT256, \"2\", \"0\"}, // MIN_INT256 % 2 = 0\n\t\t{MAX_INT256, \"2\", \"1\"}, // MAX_INT256 % 2 = 1\n\t\t{MIN_INT256, \"-1\", \"0\"}, // MIN_INT256 % -1 = 0\n\t\t{MAX_INT256, \"-1\", \"0\"}, // MAX_INT256 % -1 = 0\n\t}\n\n\tfor _, tt := range tests {\n\t\tx := MustFromDecimal(tt.x)\n\t\ty := MustFromDecimal(tt.y)\n\t\twant := MustFromDecimal(tt.want)\n\t\tgot := New().Mod(x, y)\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Mod(%s, %s) = %v, want %v\", tt.x, tt.y, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestModPanic(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t}{\n\t\t{\"10\", \"0\"},\n\t\t{\"10\", \"-0\"},\n\t\t{\"-10\", \"0\"},\n\t\t{\"-10\", \"-0\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Errorf(\"Mod(%s, %s) did not panic\", tc.x, tc.y)\n\t\t\t}\n\t\t}()\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tresult := New().Mod(x, y)\n\t\tt.Errorf(\"Mod(%s, %s) = %v, want %v\", tc.x, tc.y, result.String(), \"0\")\n\t}\n}\n\nfunc TestDivE(t *testing.T) {\n\ttestCases := []struct {\n\t\tx, y int64\n\t\twant int64\n\t}{\n\t\t{8, 3, 2},\n\t\t{8, -3, -2},\n\t\t{-8, 3, -3},\n\t\t{-8, -3, 3},\n\t\t{1, 2, 0},\n\t\t{1, -2, 0},\n\t\t{-1, 2, -1},\n\t\t{-1, -2, 1},\n\t\t{0, 1, 0},\n\t\t{0, -1, 0},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tx := NewInt(tc.x)\n\t\ty := NewInt(tc.y)\n\t\twant := NewInt(tc.want)\n\t\tgot := new(Int).DivE(x, y)\n\t\tif got.Cmp(want) != 0 {\n\t\t\tt.Errorf(\"DivE(%v, %v) = %v, want %v\", tc.x, tc.y, got, want)\n\t\t}\n\t}\n}\n\nfunc TestDivEByZero(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"DivE did not panic on division by zero\")\n\t\t}\n\t}()\n\n\tx := NewInt(1)\n\ty := NewInt(0)\n\tnew(Int).DivE(x, y)\n}\n\nfunc TestModEByZero(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"ModE did not panic on division by zero\")\n\t\t}\n\t}()\n\n\tx := NewInt(1)\n\ty := NewInt(0)\n\tnew(Int).ModE(x, y)\n}\n\nfunc TestLargeNumbers(t *testing.T) {\n\tx, _ := new(Int).SetString(\"123456789012345678901234567890\")\n\ty, _ := new(Int).SetString(\"987654321098765432109876543210\")\n\n\t// Expected results (calculated separately)\n\texpectedQ, _ := new(Int).SetString(\"0\")\n\texpectedR, _ := new(Int).SetString(\"123456789012345678901234567890\")\n\n\tgotQ := new(Int).DivE(x, y)\n\tgotR := new(Int).ModE(x, y)\n\n\tif gotQ.Cmp(expectedQ) != 0 {\n\t\tt.Errorf(\"DivE with large numbers: got %v, want %v\", gotQ, expectedQ)\n\t}\n\n\tif gotR.Cmp(expectedR) != 0 {\n\t\tt.Errorf(\"ModE with large numbers: got %v, want %v\", gotR, expectedR)\n\t}\n}\n\nfunc TestAbs(t *testing.T) {\n\ttests := []struct {\n\t\tx, want string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"1\"},\n\t\t{\"-1\", \"1\"},\n\t\t{\"-2\", \"2\"},\n\t\t{\"-100000000000\", \"100000000000\"},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Abs()\n\n\t\tif got.String() != tc.want {\n\t\t\tt.Errorf(\"Abs(%s) = %v, want %v\", tc.x, got.String(), tc.want)\n\t\t}\n\t}\n}\n" + }, + { + "name": "bitwise.gno", + "body": "package int256\n\n// Not sets z to the bitwise NOT of x and returns z.\n//\n// The bitwise NOT operation flips each bit of the operand.\nfunc (z *Int) Not(x *Int) *Int {\n\tz.value.Not(\u0026x.value)\n\treturn z\n}\n\n// And sets z to the bitwise AND of x and y and returns z.\n//\n// The bitwise AND operation results in a value that has a bit set\n// only if both corresponding bits of the operands are set.\nfunc (z *Int) And(x, y *Int) *Int {\n\tz.value.And(\u0026x.value, \u0026y.value)\n\treturn z\n}\n\n// Or sets z to the bitwise OR of x and y and returns z.\n//\n// The bitwise OR operation results in a value that has a bit set\n// if at least one of the corresponding bits of the operands is set.\nfunc (z *Int) Or(x, y *Int) *Int {\n\tz.value.Or(\u0026x.value, \u0026y.value)\n\treturn z\n}\n\n// Xor sets z to the bitwise XOR of x and y and returns z.\n//\n// The bitwise XOR operation results in a value that has a bit set\n// only if the corresponding bits of the operands are different.\nfunc (z *Int) Xor(x, y *Int) *Int {\n\tz.value.Xor(\u0026x.value, \u0026y.value)\n\treturn z\n}\n\n// Rsh sets z to the result of right-shifting x by n bits and returns z.\n//\n// Right shift operation moves all bits in the operand to the right by the specified number of positions.\n// Bits shifted out on the right are discarded, and zeros are shifted in on the left.\nfunc (z *Int) Rsh(x *Int, n uint) *Int {\n\tz.value.Rsh(\u0026x.value, n)\n\treturn z\n}\n\n// Lsh sets z to the result of left-shifting x by n bits and returns z.\n//\n// Left shift operation moves all bits in the operand to the left by the specified number of positions.\n// Bits shifted out on the left are discarded, and zeros are shifted in on the right.\nfunc (z *Int) Lsh(x *Int, n uint) *Int {\n\tz.value.Lsh(\u0026x.value, n)\n\treturn z\n}\n" + }, + { + "name": "bitwise_test.gno", + "body": "package int256\n\nimport (\n\t\"testing\"\n)\n\nfunc TestBitwise_And(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"5\", \"1\", \"1\"}, // 0101 \u0026 0001 = 0001\n\t\t{\"-1\", \"1\", \"1\"}, // 1111 \u0026 0001 = 0001\n\t\t{\"-5\", \"3\", \"3\"}, // 1111...1011 \u0026 0000...0011 = 0000...0011\n\t\t{MAX_UINT256, MAX_UINT256, MAX_UINT256},\n\t\t{TWO_POW_128, TWO_POW_128_MINUS_1, \"0\"}, // 2^128 \u0026 (2^128 - 1) = 0\n\t\t{TWO_POW_128, MAX_UINT256, TWO_POW_128}, // 2^128 \u0026 MAX_INT256\n\t\t{MAX_UINT256, TWO_POW_128, TWO_POW_128}, // MAX_INT256 \u0026 2^128\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, _ := FromDecimal(tc.x)\n\t\ty, _ := FromDecimal(tc.y)\n\t\twant, _ := FromDecimal(tc.want)\n\n\t\tgot := new(Int).And(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"And(%s, %s) = %s, want %s\", x.String(), y.String(), got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestBitwise_Or(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"5\", \"1\", \"5\"}, // 0101 | 0001 = 0101\n\t\t{\"-1\", \"1\", \"-1\"}, // 1111 | 0001 = 1111\n\t\t{\"-5\", \"3\", \"-5\"}, // 1111...1011 | 0000...0011 = 1111...1011\n\t\t{TWO_POW_128, TWO_POW_128_MINUS_1, TWO_POW_129_MINUS_1},\n\t\t{TWO_POW_128, MAX_UINT256, MAX_UINT256},\n\t\t{\"0\", TWO_POW_128, TWO_POW_128}, // 0 | 2^128 = 2^128\n\t\t{MAX_UINT256, TWO_POW_128, MAX_UINT256}, // MAX_INT256 | 2^128 = MAX_INT256\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, _ := FromDecimal(tc.x)\n\t\ty, _ := FromDecimal(tc.y)\n\t\twant, _ := FromDecimal(tc.want)\n\n\t\tgot := new(Int).Or(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\n\t\t\t\t\"Or(%s, %s) = %s, want %s\",\n\t\t\t\tx.String(), y.String(), got.String(), want.String(),\n\t\t\t)\n\t\t}\n\t}\n}\n\nfunc TestBitwise_Not(t *testing.T) {\n\ttests := []struct {\n\t\tx, want string\n\t}{\n\t\t{\"5\", \"-6\"}, // 0101 -\u003e 1111...1010\n\t\t{\"-1\", \"0\"}, // 1111...1111 -\u003e 0000...0000\n\t\t{TWO_POW_128, MINUS_TWO_POW_128_MINUS_1}, // NOT 2^128\n\t\t{TWO_POW_255, MIN_INT256_MINUS_1}, // NOT 2^255\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, _ := FromDecimal(tc.x)\n\t\twant, _ := FromDecimal(tc.want)\n\n\t\tgot := new(Int).Not(x)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Not(%s) = %s, want %s\", x.String(), got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestBitwise_Xor(t *testing.T) {\n\ttests := []struct {\n\t\tx, y, want string\n\t}{\n\t\t{\"5\", \"1\", \"4\"}, // 0101 ^ 0001 = 0100\n\t\t{\"-1\", \"1\", \"-2\"}, // 1111...1111 ^ 0000...0001 = 1111...1110\n\t\t{\"-5\", \"3\", \"-8\"}, // 1111...1011 ^ 0000...0011 = 1111...1000\n\t\t{TWO_POW_128, TWO_POW_128, \"0\"}, // 2^128 ^ 2^128 = 0\n\t\t{MAX_UINT256, TWO_POW_128, MINUS_TWO_POW_128_MINUS_1}, // MAX_INT256 ^ 2^128\n\t\t{TWO_POW_255, MAX_UINT256, MIN_INT256_MINUS_1}, // 2^255 ^ MAX_INT256\n\t}\n\n\tfor _, tt := range tests {\n\t\tx, _ := FromDecimal(tt.x)\n\t\ty, _ := FromDecimal(tt.y)\n\t\twant, _ := FromDecimal(tt.want)\n\n\t\tgot := new(Int).Xor(x, y)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Xor(%s, %s) = %s, want %s\", x.String(), y.String(), got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestBitwise_Rsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\tn uint\n\t\twant string\n\t}{\n\t\t{\"5\", 1, \"2\"}, // 0101 \u003e\u003e 1 = 0010\n\t\t{\"42\", 3, \"5\"}, // 00101010 \u003e\u003e 3 = 00000101\n\t\t{TWO_POW_128, 128, \"1\"},\n\t\t{MAX_UINT256, 255, \"1\"},\n\t\t{TWO_POW_255, 254, \"2\"},\n\t\t{MINUS_TWO_POW_128, 128, TWO_POW_128_MINUS_1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx, _ := FromDecimal(tt.x)\n\t\twant, _ := FromDecimal(tt.want)\n\n\t\tgot := new(Int).Rsh(x, tt.n)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Rsh(%s, %d) = %s, want %s\", x.String(), tt.n, got.String(), want.String())\n\t\t}\n\t}\n}\n\nfunc TestBitwise_Lsh(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\tn uint\n\t\twant string\n\t}{\n\t\t{\"5\", 2, \"20\"}, // 0101 \u003c\u003c 2 = 10100\n\t\t{\"42\", 5, \"1344\"}, // 00101010 \u003c\u003c 5 = 10101000000\n\t\t{\"1\", 128, TWO_POW_128}, // 1 \u003c\u003c 128 = 2^128\n\t\t{\"2\", 254, TWO_POW_255},\n\t\t{\"1\", 255, MIN_INT256}, // 1 \u003c\u003c 255 = MIN_INT256 (overflow)\n\t}\n\n\tfor _, tt := range tests {\n\t\tx, _ := FromDecimal(tt.x)\n\t\twant, _ := FromDecimal(tt.want)\n\n\t\tgot := new(Int).Lsh(x, tt.n)\n\n\t\tif got.Neq(want) {\n\t\t\tt.Errorf(\"Lsh(%s, %d) = %s, want %s\", x.String(), tt.n, got.String(), want.String())\n\t\t}\n\t}\n}\n" + }, + { + "name": "cmp.gno", + "body": "package int256\n\nfunc (z *Int) Eq(x *Int) bool {\n\treturn z.value.Eq(\u0026x.value)\n}\n\nfunc (z *Int) Neq(x *Int) bool {\n\treturn !z.Eq(x)\n}\n\n// Cmp compares z and x and returns:\n//\n// - 1 if z \u003e x\n// - 0 if z == x\n// - -1 if z \u003c x\nfunc (z *Int) Cmp(x *Int) int {\n\tzSign, xSign := z.Sign(), x.Sign()\n\n\tif zSign == xSign {\n\t\treturn z.value.Cmp(\u0026x.value)\n\t}\n\n\tif zSign == 0 {\n\t\treturn -xSign\n\t}\n\n\treturn zSign\n}\n\n// IsZero returns true if z == 0\nfunc (z *Int) IsZero() bool {\n\treturn z.value.IsZero()\n}\n\n// IsNeg returns true if z \u003c 0\nfunc (z *Int) IsNeg() bool {\n\treturn z.Sign() \u003c 0\n}\n\nfunc (z *Int) Lt(x *Int) bool {\n\treturn z.Cmp(x) \u003c 0\n}\n\nfunc (z *Int) Gt(x *Int) bool {\n\treturn z.Cmp(x) \u003e 0\n}\n\nfunc (z *Int) Le(x *Int) bool {\n\treturn z.Cmp(x) \u003c= 0\n}\n\nfunc (z *Int) Ge(x *Int) bool {\n\treturn z.Cmp(x) \u003e= 0\n}\n\n// Clone creates a new Int identical to z\nfunc (z *Int) Clone() *Int {\n\treturn New().FromUint256(\u0026z.value)\n}\n" + }, + { + "name": "cmp_test.gno", + "body": "package int256\n\nimport \"testing\"\n\nfunc TestEq(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", true},\n\t\t{\"0\", \"1\", false},\n\t\t{\"1\", \"0\", false},\n\t\t{\"-1\", \"0\", false},\n\t\t{\"0\", \"-1\", false},\n\t\t{\"1\", \"1\", true},\n\t\t{\"-1\", \"-1\", true},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", false},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Eq(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Eq(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestNeq(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", false},\n\t\t{\"0\", \"1\", true},\n\t\t{\"1\", \"0\", true},\n\t\t{\"-1\", \"0\", true},\n\t\t{\"0\", \"-1\", true},\n\t\t{\"1\", \"1\", false},\n\t\t{\"-1\", \"-1\", false},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", true},\n\t\t{\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Neq(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Neq(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestCmp(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant int\n\t}{\n\t\t{\"0\", \"0\", 0},\n\t\t{\"0\", \"1\", -1},\n\t\t{\"1\", \"0\", 1},\n\t\t{\"-1\", \"0\", -1},\n\t\t{\"0\", \"-1\", 1},\n\t\t{\"1\", \"1\", 0},\n\t\t{\"115792089237316195423570985008687907853269984665640564039457584007913129639935\", \"-115792089237316195423570985008687907853269984665640564039457584007913129639935\", -1},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Cmp(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Cmp(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestIsZero(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0\", true},\n\t\t{\"-0\", true},\n\t\t{\"1\", false},\n\t\t{\"-1\", false},\n\t\t{\"10\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.IsZero()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"IsZero(%s) = %v, want %v\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestIsNeg(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant bool\n\t}{\n\t\t{\"0\", false},\n\t\t{\"-0\", false},\n\t\t{\"1\", false},\n\t\t{\"-1\", true},\n\t\t{\"10\", false},\n\t\t{\"-10\", true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.IsNeg()\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"IsNeg(%s) = %v, want %v\", tc.x, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestLt(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", false},\n\t\t{\"0\", \"1\", true},\n\t\t{\"1\", \"0\", false},\n\t\t{\"-1\", \"0\", true},\n\t\t{\"0\", \"-1\", false},\n\t\t{\"1\", \"1\", false},\n\t\t{\"-1\", \"-1\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Lt(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Lt(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestGt(t *testing.T) {\n\ttests := []struct {\n\t\tx, y string\n\t\twant bool\n\t}{\n\t\t{\"0\", \"0\", false},\n\t\t{\"0\", \"1\", false},\n\t\t{\"1\", \"0\", true},\n\t\t{\"-1\", \"0\", false},\n\t\t{\"0\", \"-1\", true},\n\t\t{\"1\", \"1\", false},\n\t\t{\"-1\", \"-1\", false},\n\t}\n\n\tfor _, tc := range tests {\n\t\tx, err := FromDecimal(tc.x)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty, err := FromDecimal(tc.y)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot := x.Gt(y)\n\t\tif got != tc.want {\n\t\t\tt.Errorf(\"Gt(%s, %s) = %v, want %v\", tc.x, tc.y, got, tc.want)\n\t\t}\n\t}\n}\n\nfunc TestClone(t *testing.T) {\n\ttests := []string{\n\t\t\"0\",\n\t\t\"-0\",\n\t\t\"1\",\n\t\t\"-1\",\n\t\t\"10\",\n\t\t\"-10\",\n\t\t\"115792089237316195423570985008687907853269984665640564039457584007913129639935\",\n\t\t\"-115792089237316195423570985008687907853269984665640564039457584007913129639935\",\n\t}\n\n\tfor _, xStr := range tests {\n\t\tx, err := FromDecimal(xStr)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\ty := x.Clone()\n\n\t\tif x.Neq(y) {\n\t\t\tt.Errorf(\"cloned value is not equal to original value\")\n\t\t}\n\t}\n}\n" + }, + { + "name": "conversion.gno", + "body": "package int256\n\nimport (\n\t\"math\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\n// SetInt64 sets the Int to the value of the provided int64.\n//\n// This method allows for easy conversion from standard Go integer types\n// to Int, correctly handling both positive and negative values.\nfunc (z *Int) SetInt64(v int64) *Int {\n\tif v \u003e= 0 {\n\t\tz.value.SetUint64(uint64(v))\n\t} else {\n\t\tz.value.SetUint64(uint64(-v)).Neg(\u0026z.value)\n\t}\n\treturn z\n}\n\n// SetUint64 sets the Int to the value of the provided uint64.\nfunc (z *Int) SetUint64(v uint64) *Int {\n\tz.value.SetUint64(v)\n\treturn z\n}\n\n// Uint64 returns the lower 64-bits of z\nfunc (z *Int) Uint64() uint64 {\n\tif z.Sign() \u003c 0 {\n\t\tpanic(\"cannot convert negative int256 to uint64\")\n\t}\n\tif z.value.Gt(uint256.NewUint(0).SetUint64(math.MaxUint64)) {\n\t\tpanic(\"overflow: int256 does not fit in uint64 type\")\n\t}\n\treturn z.value.Uint64()\n}\n\n// Int64 returns the lower 64-bits of z\nfunc (z *Int) Int64() int64 {\n\tif z.Sign() \u003e= 0 {\n\t\tif z.value.BitLen() \u003e 64 {\n\t\t\tpanic(\"overflow: int256 does not fit in int64 type\")\n\t\t}\n\t\treturn int64(z.value.Uint64())\n\t}\n\tvar temp uint256.Uint\n\ttemp.Sub(uint256.NewUint(0), \u0026z.value) // temp = -z.value\n\tif temp.BitLen() \u003e 64 {\n\t\tpanic(\"overflow: int256 does not fit in int64 type\")\n\t}\n\treturn -int64(temp.Uint64())\n}\n\n// Neg sets z to -x and returns z.)\nfunc (z *Int) Neg(x *Int) *Int {\n\tif x.IsZero() {\n\t\tz.value.Clear()\n\t} else {\n\t\tz.value.Neg(\u0026x.value)\n\t}\n\treturn z\n}\n\n// Set sets z to x and returns z.\nfunc (z *Int) Set(x *Int) *Int {\n\tz.value.Set(\u0026x.value)\n\treturn z\n}\n\n// SetFromUint256 converts a uint256.Uint to Int and sets the value to z.\nfunc (z *Int) SetUint256(x *uint256.Uint) *Int {\n\tz.value.Set(x)\n\treturn z\n}\n\n// ToString returns a string representation of z in base 10.\n// The string is prefixed with a minus sign if z is negative.\nfunc (z *Int) String() string {\n\tif z.value.IsZero() {\n\t\treturn \"0\"\n\t}\n\tsign := z.Sign()\n\tvar temp uint256.Uint\n\tif sign \u003e= 0 {\n\t\ttemp.Set(\u0026z.value)\n\t} else {\n\t\t// temp = -z.value\n\t\ttemp.Sub(uint256.NewUint(0), \u0026z.value)\n\t}\n\ts := temp.Dec()\n\tif sign \u003c 0 {\n\t\treturn \"-\" + s\n\t}\n\treturn s\n}\n\n// NilToZero returns the Int if it's not nil, or a new zero-valued Int otherwise.\n//\n// This method is useful for safely handling potentially nil Int pointers,\n// ensuring that operations always have a valid Int to work with.\nfunc (z *Int) NilToZero() *Int {\n\tif z == nil {\n\t\treturn Zero()\n\t}\n\treturn z\n}\n" + }, + { + "name": "conversion_test.gno", + "body": "package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nfunc TestSetInt64(t *testing.T) {\n\ttests := []struct {\n\t\tv int64\n\t\texpect int\n\t}{\n\t\t{0, 0},\n\t\t{1, 1},\n\t\t{-1, -1},\n\t\t{9223372036854775807, 1}, // overflow (max int64)\n\t\t{-9223372036854775808, -1}, // underflow (min int64)\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := New().SetInt64(tt.v)\n\t\tif z.Sign() != tt.expect {\n\t\t\tt.Errorf(\"SetInt64(%d) = %d, want %d\", tt.v, z.Sign(), tt.expect)\n\t\t}\n\t}\n}\n\nfunc TestUint64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant uint64\n\t}{\n\t\t{\"0\", 0},\n\t\t{\"1\", 1},\n\t\t{\"9223372036854775807\", 9223372036854775807},\n\t\t{\"9223372036854775808\", 9223372036854775808},\n\t\t{\"18446744073709551615\", 18446744073709551615},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := MustFromDecimal(tt.x)\n\n\t\tgot := z.Uint64()\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"Uint64(%s) = %d, want %d\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestUint64_Panic(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t}{\n\t\t{\"-1\"},\n\t\t{\"18446744073709551616\"},\n\t\t{\"18446744073709551617\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Errorf(\"Uint64(%s) did not panic\", tt.x)\n\t\t\t}\n\t\t}()\n\n\t\tz := MustFromDecimal(tt.x)\n\t\tz.Uint64()\n\t}\n}\n\nfunc TestInt64(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant int64\n\t}{\n\t\t{\"0\", 0},\n\t\t{\"1\", 1},\n\t\t{\"9223372036854775807\", 9223372036854775807},\n\t\t{\"-1\", -1},\n\t\t{\"-9223372036854775808\", -9223372036854775808},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := MustFromDecimal(tt.x)\n\n\t\tgot := z.Int64()\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"Uint64(%s) = %d, want %d\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestInt64_Panic(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t}{\n\t\t{\"18446744073709551616\"},\n\t\t{\"18446744073709551617\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Errorf(\"Int64(%s) did not panic\", tt.x)\n\t\t\t}\n\t\t}()\n\n\t\tz := MustFromDecimal(tt.x)\n\t\tz.Int64()\n\t}\n}\n\nfunc TestNeg(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"-1\"},\n\t\t{\"-1\", \"1\"},\n\t\t{\"9223372036854775807\", \"-9223372036854775807\"},\n\t\t{\"-18446744073709551615\", \"18446744073709551615\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := MustFromDecimal(tt.x)\n\t\tz.Neg(z)\n\n\t\tgot := z.String()\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"Neg(%s) = %s, want %s\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestSet(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"1\"},\n\t\t{\"-1\", \"-1\"},\n\t\t{\"9223372036854775807\", \"9223372036854775807\"},\n\t\t{\"-18446744073709551615\", \"-18446744073709551615\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := MustFromDecimal(tt.x)\n\t\tz.Set(z)\n\n\t\tgot := z.String()\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"Set(%s) = %s, want %s\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestSetUint256(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"1\"},\n\t\t{\"9223372036854775807\", \"9223372036854775807\"},\n\t\t{\"18446744073709551615\", \"18446744073709551615\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tgot := New()\n\n\t\tz := uint256.MustFromDecimal(tt.x)\n\t\tgot.SetUint256(z)\n\n\t\tif got.String() != tt.want {\n\t\t\tt.Errorf(\"SetUint256(%s) = %s, want %s\", tt.x, got.String(), tt.want)\n\t\t}\n\t}\n}\n\nfunc TestString(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected string\n\t}{\n\t\t{\"0\", \"0\"},\n\t\t{\"1\", \"1\"},\n\t\t{\"-1\", \"-1\"},\n\t\t{\"123456789\", \"123456789\"},\n\t\t{\"-123456789\", \"-123456789\"},\n\t\t{\"18446744073709551615\", \"18446744073709551615\"}, // max uint64\n\t\t{\"-18446744073709551615\", \"-18446744073709551615\"},\n\t\t{TWO_POW_128_MINUS_1, TWO_POW_128_MINUS_1},\n\t\t{MINUS_TWO_POW_128, MINUS_TWO_POW_128},\n\t\t{MIN_INT256, MIN_INT256},\n\t\t{MAX_INT256, MAX_INT256},\n\t}\n\n\tfor _, tt := range tests {\n\t\tx, err := FromDecimal(tt.input)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Failed to parse input (%s): %v\", tt.input, err)\n\t\t\tcontinue\n\t\t}\n\n\t\toutput := x.String()\n\n\t\tif output != tt.expected {\n\t\t\tt.Errorf(\"String(%s) = %s, want %s\", tt.input, output, tt.expected)\n\t\t}\n\t}\n}\n\nfunc TestNilToZero(t *testing.T) {\n\tz := New().NilToZero()\n\tif z.Sign() != 0 {\n\t\tt.Errorf(\"NilToZero() = %d, want %d\", z.Sign(), 0)\n\t}\n}\n" + }, + { + "name": "doc.gno", + "body": "// The int256 package provides a 256-bit signed interger type for gno,\n// supporting arithmetic operations and bitwise manipulation.\n//\n// It designed for applications that require high-precision arithmetic\n// beyond the standard 64-bit range.\n//\n// ## Features\n//\n// - 256-bit Signed Integers: Support for large integer ranging from -2^255 to 2^255-1.\n// - Two's Complement Representation: Efficient storage and computation using two's complement.\n// - Arithmetic Operations: Add, Sub, Mul, Div, Mod, Inc, Dec, etc.\n// - Bitwise Operations: And, Or, Xor, Not, etc.\n// - Comparison Operations: Cmp, Eq, Lt, Gt, etc.\n// - Conversion Functions: Int to Uint, Uint to Int, etc.\n// - String Parsing and Formatting: Convert to and from decimal string representation.\n//\n// ## Notes\n//\n// - Some methods may panic when encountering invalid inputs or overflows.\n// - The `int256.Int` type can interact with `uint256.Uint` from the `p/demo/uint256` package.\n// - Unlike `math/big.Int`, the `int256.Int` type has fixed size (256-bit) and does not support\n// arbitrary precision arithmetic.\n//\n// # Division and modulus operations\n//\n// This package provides three different division and modulus operations:\n//\n// - Div and Rem: Truncated division (T-division)\n// - Quo and Mod: Floored division (F-division)\n// - DivE and ModE: Euclidean division (E-division)\n//\n// Truncated division (Div, Rem) is the most common implementation in modern processors\n// and programming languages. It rounds quotients towards zero and the remainder\n// always has the same sign as the dividend.\n//\n// Floored division (Quo, Mod) always rounds quotients towards negative infinity.\n// This ensures that the modulus is always non-negative for a positive divisor,\n// which can be useful in certain algorithms.\n//\n// Euclidean division (DivE, ModE) ensures that the remainder is always non-negative,\n// regardless of the signs of the dividend and divisor. This has several mathematical\n// advantages:\n//\n// 1. It satisfies the unique division with remainder theorem.\n// 2. It preserves division and modulus properties for negative divisors.\n// 3. It allows for optimizations in divisions by powers of two.\n//\n// [+] Currently, ModE and Mod are shared the same implementation.\n//\n// ## Performance considerations:\n//\n// - For most operations, the performance difference between these division types is negligible.\n// - Euclidean division may require an extra comparison and potentially an addition,\n// which could impact performance in extremely performance-critical scenarios.\n// - For divisions by powers of two, Euclidean division can be optimized to use\n// bitwise operations, potentially offering better performance.\n//\n// ## Usage guidelines:\n//\n// - Use Div and Rem for general-purpose division that matches most common expectations.\n// - Use Quo and Mod when you need a non-negative remainder for positive divisors,\n// or when implementing algorithms that assume floored division.\n// - Use DivE and ModE when you need the mathematical properties of Euclidean division,\n// or when working with algorithms that specifically require it.\n//\n// Note: When working with negative numbers, be aware of the differences in behavior\n// between these division types, especially at the boundaries of integer ranges.\n//\n// ## References\n//\n// Daan Leijen, “Division and Modulus for Computer Scientists”:\n// https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/divmodnote-letter.pdf\npackage int256\n" + }, + { + "name": "int256.gno", + "body": "package int256\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nvar (\n\tint1 = NewInt(1)\n\tuint0 = uint256.NewUint(0)\n\tuint1 = uint256.NewUint(1)\n)\n\ntype Int struct {\n\tvalue uint256.Uint\n}\n\n// New creates and returns a new Int initialized to zero.\nfunc New() *Int {\n\treturn \u0026Int{}\n}\n\n// NewInt allocates and returns a new Int set to the value of the provided int64.\nfunc NewInt(x int64) *Int {\n\treturn New().SetInt64(x)\n}\n\n// Zero returns a new Int initialized to 0.\n//\n// This function is useful for creating a starting point for calculations or\n// when an explicit zero value is needed.\nfunc Zero() *Int { return \u0026Int{} }\n\n// One returns a new Int initialized to one.\n//\n// This function is convenient for operations that require a unit value,\n// such as incrementing or serving as an identity element in multiplication.\nfunc One() *Int {\n\treturn \u0026Int{\n\t\tvalue: *uint256.NewUint(1),\n\t}\n}\n\n// Sign determines the sign of the Int.\n//\n// It returns -1 for negative numbers, 0 for zero, and +1 for positive numbers.\nfunc (z *Int) Sign() int {\n\tif z == nil || z.IsZero() {\n\t\treturn 0\n\t}\n\t// Right shift the value by 255 bits to check the sign bit.\n\t// In two's complement representation, the most significant bit (MSB) is the sign bit.\n\t// If the MSB is 0, the number is positive; if it is 1, the number is negative.\n\t//\n\t// Example:\n\t// Original value: 1 0 1 0 ... 0 1 (256 bits)\n\t// After Rsh 255: 0 0 0 0 ... 0 1 (1 bit)\n\t//\n\t// This approach is highly efficient as it avoids the need for comparisons\n\t// or arithmetic operations on the full 256-bit number. Instead it reduces\n\t// the problem to checking a single bit.\n\t//\n\t// Additionally, this method will work correctly for all values,\n\t// including the minimum possible negative number (which in two's complement\n\t// doesn't have a positive counterpart in the same bit range).\n\tvar temp uint256.Uint\n\tif temp.Rsh(\u0026z.value, 255).IsZero() {\n\t\treturn 1\n\t}\n\treturn -1\n}\n\n// FromDecimal creates a new Int from a decimal string representation.\n// It handles both positive and negative values.\n//\n// This function is useful for parsing user input or reading numeric data\n// from text-based formats.\nfunc FromDecimal(s string) (*Int, error) {\n\treturn New().SetString(s)\n}\n\n// MustFromDecimal is similar to FromDecimal but panics if the input string\n// is not a valid decimal representation.\nfunc MustFromDecimal(s string) *Int {\n\tz, err := FromDecimal(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn z\n}\n\n// SetString sets the Int to the value represented by the input string.\n// This method supports decimal string representations of integers and handles\n// both positive and negative values.\nfunc (z *Int) SetString(s string) (*Int, error) {\n\tif len(s) == 0 {\n\t\treturn nil, errors.New(\"cannot set int256 from empty string\")\n\t}\n\n\t// Check for negative sign\n\tneg := s[0] == '-'\n\tif neg || s[0] == '+' {\n\t\ts = s[1:]\n\t}\n\n\t// Convert string to uint256\n\ttemp, err := uint256.FromDecimal(s)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// If negative, negate the uint256 value\n\tif neg {\n\t\ttemp.Neg(temp)\n\t}\n\n\tz.value.Set(temp)\n\treturn z, nil\n}\n\n// FromUint256 sets the Int to the value of the provided Uint256.\n//\n// This method allows for conversion from unsigned 256-bit integers\n// to signed integers.\nfunc (z *Int) FromUint256(v *uint256.Uint) *Int {\n\tz.value.Set(v)\n\treturn z\n}\n" + }, + { + "name": "int256_test.gno", + "body": "package int256\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uint256\"\n)\n\nfunc TestInitializers(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tfn func() *Int\n\t\twantSign int\n\t\twantStr string\n\t}{\n\t\t{\"Zero\", Zero, 0, \"0\"},\n\t\t{\"New\", New, 0, \"0\"},\n\t\t{\"One\", One, 1, \"1\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tz := tt.fn()\n\t\t\tif z.Sign() != tt.wantSign {\n\t\t\t\tt.Errorf(\"%s() = %d, want %d\", tt.name, z.Sign(), tt.wantSign)\n\t\t\t}\n\t\t\tif z.String() != tt.wantStr {\n\t\t\t\tt.Errorf(\"%s() = %s, want %s\", tt.name, z.String(), tt.wantStr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewInt(t *testing.T) {\n\ttests := []struct {\n\t\tinput int64\n\t\texpected int\n\t}{\n\t\t{0, 0},\n\t\t{1, 1},\n\t\t{-1, -1},\n\t\t{9223372036854775807, 1}, // max int64\n\t\t{-9223372036854775808, -1}, // min int64\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := NewInt(tt.input)\n\t\tif z.Sign() != tt.expected {\n\t\t\tt.Errorf(\"NewInt(%d) = %d, want %d\", tt.input, z.Sign(), tt.expected)\n\t\t}\n\t}\n}\n\nfunc TestFromDecimal(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected int\n\t\tisError bool\n\t}{\n\t\t{\"0\", 0, false},\n\t\t{\"1\", 1, false},\n\t\t{\"-1\", -1, false},\n\t\t{\"123456789\", 1, false},\n\t\t{\"-123456789\", -1, false},\n\t\t{\"invalid\", 0, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz, err := FromDecimal(tt.input)\n\t\tif tt.isError {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"FromDecimal(%s) expected error, but got nil\", tt.input)\n\t\t\t}\n\t\t} else {\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"FromDecimal(%s) unexpected error: %v\", tt.input, err)\n\t\t\t} else if z.Sign() != tt.expected {\n\t\t\t\tt.Errorf(\"FromDecimal(%s) sign is incorrect. Expected: %d, Actual: %d\", tt.input, tt.expected, z.Sign())\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestMustFromDecimal(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected int\n\t\tshouldPanic bool\n\t}{\n\t\t{\"0\", 0, false},\n\t\t{\"1\", 1, false},\n\t\t{\"-1\", -1, false},\n\t\t{\"123\", 1, false},\n\t\t{\"invalid\", 0, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tif tt.shouldPanic {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\tt.Errorf(\"MustFromDecimal(%q) expected panic, but got nil\", tt.input)\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\n\t\tz := MustFromDecimal(tt.input)\n\t\tif !tt.shouldPanic \u0026\u0026 z.Sign() != tt.expected {\n\t\t\tt.Errorf(\"MustFromDecimal(%q) sign is incorrect. Expected: %d, Actual: %d\", tt.input, tt.expected, z.Sign())\n\t\t}\n\t}\n}\n\nfunc TestSetUint64(t *testing.T) {\n\ttests := []uint64{\n\t\t0,\n\t\t1,\n\t\t18446744073709551615, // max uint64\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := New().SetUint64(tt)\n\t\tif z.Sign() \u003c 0 {\n\t\t\tt.Errorf(\"SetUint64(%d) result is negative\", tt)\n\t\t}\n\t\tif tt == 0 \u0026\u0026 z.Sign() != 0 {\n\t\t\tt.Errorf(\"SetUint64(0) result is not zero\")\n\t\t}\n\t\tif tt \u003e 0 \u0026\u0026 z.Sign() != 1 {\n\t\t\tt.Errorf(\"SetUint64(%d) result is not positive\", tt)\n\t\t}\n\t}\n}\n\nfunc TestFromUint256(t *testing.T) {\n\ttests := []struct {\n\t\tinput *uint256.Uint\n\t\texpected int\n\t}{\n\t\t{uint256.NewUint(0), 0},\n\t\t{uint256.NewUint(1), 1},\n\t\t{uint256.NewUint(18446744073709551615), 1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := New().FromUint256(tt.input)\n\t\tif z.Sign() != tt.expected {\n\t\t\tt.Errorf(\"FromUint256(%v) = %d, want %d\", tt.input, z.Sign(), tt.expected)\n\t\t}\n\t}\n}\n\nfunc TestSign(t *testing.T) {\n\ttests := []struct {\n\t\tx string\n\t\twant int\n\t}{\n\t\t{\"0\", 0},\n\t\t{\"-0\", 0},\n\t\t{\"+0\", 0},\n\t\t{\"1\", 1},\n\t\t{\"-1\", -1},\n\t\t{\"9223372036854775807\", 1},\n\t\t{\"-9223372036854775808\", -1},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz := MustFromDecimal(tt.x)\n\t\tgot := z.Sign()\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"Sign(%s) = %d, want %d\", tt.x, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc BenchmarkSign(b *testing.B) {\n\tz := New()\n\tfor i := 0; i \u003c b.N; i++ {\n\t\tz.SetUint64(uint64(i))\n\t\tz.Sign()\n\t}\n}\n\nfunc TestSetAndToString(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected int\n\t\tisError bool\n\t}{\n\t\t{\"0\", 0, false},\n\t\t{\"1\", 1, false},\n\t\t{\"-1\", -1, false},\n\t\t{\"123456789\", 1, false},\n\t\t{\"-123456789\", -1, false},\n\t\t{\"invalid\", 0, true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tz, err := New().SetString(tt.input)\n\t\tif tt.isError {\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"SetString(%s) expected error, but got nil\", tt.input)\n\t\t\t}\n\t\t} else {\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"SetString(%s) unexpected error: %v\", tt.input, err)\n\t\t\t} else if z.Sign() != tt.expected {\n\t\t\t\tt.Errorf(\"SetString(%s) sign is incorrect. Expected: %d, Actual: %d\", tt.input, tt.expected, z.Sign())\n\t\t\t} else if z.String() != tt.input {\n\t\t\t\tt.Errorf(\"SetString(%s) string representation is incorrect. Expected: %s, Actual: %s\", tt.input, tt.input, z.String())\n\t\t\t}\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "pxsS+mSczaQmlloR64uSPga2p3Q/CqUi0+pTEu4tQG6gR0vvQPqsvZsh3I7DMQiDEJjIx1DMTEBMXSL5yfhzCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "int32", + "path": "gno.land/p/demo/math_eval/int32", + "files": [ + { + "name": "int32.gno", + "body": "// eval/int32 is a evaluator for int32 expressions.\n// This code is heavily forked from https://github.com/dengsgo/math-engine\n// which is licensed under Apache 2.0:\n// https://raw.githubusercontent.com/dengsgo/math-engine/298e2b57b7e7350d0f67bd036916efd5709abe25/LICENSE\npackage int32\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tIdentifier = iota\n\tNumber // numbers\n\tOperator // +, -, *, /, etc.\n\tVariable // x, y, z, etc. (one-letter only)\n)\n\ntype expression interface {\n\tString() string\n}\n\ntype expressionRaw struct {\n\texpression string\n\tType int\n\tFlag int\n\tOffset int\n}\n\ntype parser struct {\n\tInput string\n\tch byte\n\toffset int\n\terr error\n}\n\ntype expressionNumber struct {\n\tVal int\n\tStr string\n}\n\ntype expressionVariable struct {\n\tVal int\n\tStr string\n}\n\ntype expressionOperation struct {\n\tOp string\n\tLhs,\n\tRhs expression\n}\n\ntype ast struct {\n\trawexpressions []*expressionRaw\n\tsource string\n\tcurrentexpression *expressionRaw\n\tcurrentIndex int\n\tdepth int\n\terr error\n}\n\n// Parse takes an expression string, e.g. \"1+2\" and returns\n// a parsed expression. If there is an error it will return.\nfunc Parse(s string) (ar expression, err error) {\n\ttoks, err := lexer(s)\n\tif err != nil {\n\t\treturn\n\t}\n\tast, err := newAST(toks, s)\n\tif err != nil {\n\t\treturn\n\t}\n\tar, err = ast.parseExpression()\n\treturn\n}\n\n// Eval takes a parsed expression and a map of variables (or nil). The parsed\n// expression is evaluated using any variables and returns the\n// resulting int and/or error.\nfunc Eval(expr expression, variables map[string]int) (res int, err error) {\n\tif err != nil {\n\t\treturn\n\t}\n\tvar l, r int\n\tswitch expr.(type) {\n\tcase expressionVariable:\n\t\tast := expr.(expressionVariable)\n\t\tok := false\n\t\tif variables != nil {\n\t\t\tres, ok = variables[ast.Str]\n\t\t}\n\t\tif !ok {\n\t\t\terr = ufmt.Errorf(\"variable '%s' not found\", ast.Str)\n\t\t}\n\t\treturn\n\tcase expressionOperation:\n\t\tast := expr.(expressionOperation)\n\t\tl, err = Eval(ast.Lhs, variables)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tr, err = Eval(ast.Rhs, variables)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tswitch ast.Op {\n\t\tcase \"+\":\n\t\t\tres = l + r\n\t\tcase \"-\":\n\t\t\tres = l - r\n\t\tcase \"*\":\n\t\t\tres = l * r\n\t\tcase \"/\":\n\t\t\tif r == 0 {\n\t\t\t\terr = ufmt.Errorf(\"violation of arithmetic specification: a division by zero in Eval: [%d/%d]\", l, r)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tres = l / r\n\t\tcase \"%\":\n\t\t\tif r == 0 {\n\t\t\t\tres = 0\n\t\t\t} else {\n\t\t\t\tres = l % r\n\t\t\t}\n\t\tcase \"^\":\n\t\t\tres = l ^ r\n\t\tcase \"\u003e\u003e\":\n\t\t\tres = l \u003e\u003e r\n\t\tcase \"\u003c\u003c\":\n\t\t\tres = l \u003c\u003c r\n\t\tcase \"\u003e\":\n\t\t\tif l \u003e r {\n\t\t\t\tres = 1\n\t\t\t} else {\n\t\t\t\tres = 0\n\t\t\t}\n\t\tcase \"\u003c\":\n\t\t\tif l \u003c r {\n\t\t\t\tres = 1\n\t\t\t} else {\n\t\t\t\tres = 0\n\t\t\t}\n\t\tcase \"\u0026\":\n\t\t\tres = l \u0026 r\n\t\tcase \"|\":\n\t\t\tres = l | r\n\t\tdefault:\n\n\t\t}\n\tcase expressionNumber:\n\t\tres = expr.(expressionNumber).Val\n\t}\n\n\treturn\n}\n\nfunc expressionError(s string, pos int) string {\n\tr := strings.Repeat(\"-\", len(s)) + \"\\n\"\n\ts += \"\\n\"\n\tfor i := 0; i \u003c pos; i++ {\n\t\ts += \" \"\n\t}\n\ts += \"^\\n\"\n\treturn r + s + r\n}\n\nfunc (n expressionVariable) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionVariable: %s\",\n\t\tn.Str,\n\t)\n}\n\nfunc (n expressionNumber) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionNumber: %s\",\n\t\tn.Str,\n\t)\n}\n\nfunc (b expressionOperation) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"expressionOperation: (%s %s %s)\",\n\t\tb.Op,\n\t\tb.Lhs.String(),\n\t\tb.Rhs.String(),\n\t)\n}\n\nfunc newAST(toks []*expressionRaw, s string) (*ast, error) {\n\ta := \u0026ast{\n\t\trawexpressions: toks,\n\t\tsource: s,\n\t}\n\tif a.rawexpressions == nil || len(a.rawexpressions) == 0 {\n\t\treturn a, errors.New(\"empty token\")\n\t} else {\n\t\ta.currentIndex = 0\n\t\ta.currentexpression = a.rawexpressions[0]\n\t}\n\treturn a, nil\n}\n\nfunc (a *ast) parseExpression() (expression, error) {\n\ta.depth++ // called depth\n\tlhs := a.parsePrimary()\n\tr := a.parseBinOpRHS(0, lhs)\n\ta.depth--\n\tif a.depth == 0 \u0026\u0026 a.currentIndex != len(a.rawexpressions) \u0026\u0026 a.err == nil {\n\t\treturn r, ufmt.Errorf(\"bad expression, reaching the end or missing the operator\\n%s\",\n\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t}\n\treturn r, nil\n}\n\nfunc (a *ast) getNextexpressionRaw() *expressionRaw {\n\ta.currentIndex++\n\tif a.currentIndex \u003c len(a.rawexpressions) {\n\t\ta.currentexpression = a.rawexpressions[a.currentIndex]\n\t\treturn a.currentexpression\n\t}\n\treturn nil\n}\n\nfunc (a *ast) getTokPrecedence() int {\n\tswitch a.currentexpression.expression {\n\tcase \"/\", \"%\", \"*\":\n\t\treturn 100\n\tcase \"\u003c\u003c\", \"\u003e\u003e\":\n\t\treturn 80\n\tcase \"+\", \"-\":\n\t\treturn 75\n\tcase \"\u003c\", \"\u003e\":\n\t\treturn 70\n\tcase \"\u0026\":\n\t\treturn 60\n\tcase \"^\":\n\t\treturn 50\n\tcase \"|\":\n\t\treturn 40\n\t}\n\treturn -1\n}\n\nfunc (a *ast) parseNumber() expressionNumber {\n\tf64, err := strconv.Atoi(a.currentexpression.expression)\n\tif err != nil {\n\t\ta.err = ufmt.Errorf(\"%v\\nwant '(' or '0-9' but get '%s'\\n%s\",\n\t\t\terr.Error(),\n\t\t\ta.currentexpression.expression,\n\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\treturn expressionNumber{}\n\t}\n\tn := expressionNumber{\n\t\tVal: f64,\n\t\tStr: a.currentexpression.expression,\n\t}\n\ta.getNextexpressionRaw()\n\treturn n\n}\n\nfunc (a *ast) parseVariable() expressionVariable {\n\tn := expressionVariable{\n\t\tVal: 0,\n\t\tStr: a.currentexpression.expression,\n\t}\n\ta.getNextexpressionRaw()\n\treturn n\n}\n\nfunc (a *ast) parsePrimary() expression {\n\tswitch a.currentexpression.Type {\n\tcase Variable:\n\t\treturn a.parseVariable()\n\tcase Number:\n\t\treturn a.parseNumber()\n\tcase Operator:\n\t\tif a.currentexpression.expression == \"(\" {\n\t\t\tt := a.getNextexpressionRaw()\n\t\t\tif t == nil {\n\t\t\t\ta.err = ufmt.Errorf(\"want '(' or '0-9' but get EOF\\n%s\",\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\te, _ := a.parseExpression()\n\t\t\tif e == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tif a.currentexpression.expression != \")\" {\n\t\t\t\ta.err = ufmt.Errorf(\"want ')' but get %s\\n%s\",\n\t\t\t\t\ta.currentexpression.expression,\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\ta.getNextexpressionRaw()\n\t\t\treturn e\n\t\t} else if a.currentexpression.expression == \"-\" {\n\t\t\tif a.getNextexpressionRaw() == nil {\n\t\t\t\ta.err = ufmt.Errorf(\"want '0-9' but get '-'\\n%s\",\n\t\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tbin := expressionOperation{\n\t\t\t\tOp: \"-\",\n\t\t\t\tLhs: expressionNumber{},\n\t\t\t\tRhs: a.parsePrimary(),\n\t\t\t}\n\t\t\treturn bin\n\t\t} else {\n\t\t\treturn a.parseNumber()\n\t\t}\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc (a *ast) parseBinOpRHS(execPrec int, lhs expression) expression {\n\tfor {\n\t\ttokPrec := a.getTokPrecedence()\n\t\tif tokPrec \u003c execPrec {\n\t\t\treturn lhs\n\t\t}\n\t\tbinOp := a.currentexpression.expression\n\t\tif a.getNextexpressionRaw() == nil {\n\t\t\ta.err = ufmt.Errorf(\"want '(' or '0-9' but get EOF\\n%s\",\n\t\t\t\texpressionError(a.source, a.currentexpression.Offset))\n\t\t\treturn nil\n\t\t}\n\t\trhs := a.parsePrimary()\n\t\tif rhs == nil {\n\t\t\treturn nil\n\t\t}\n\t\tnextPrec := a.getTokPrecedence()\n\t\tif tokPrec \u003c nextPrec {\n\t\t\trhs = a.parseBinOpRHS(tokPrec+1, rhs)\n\t\t\tif rhs == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\tlhs = expressionOperation{\n\t\t\tOp: binOp,\n\t\t\tLhs: lhs,\n\t\t\tRhs: rhs,\n\t\t}\n\t}\n}\n\nfunc lexer(s string) ([]*expressionRaw, error) {\n\tp := \u0026parser{\n\t\tInput: s,\n\t\terr: nil,\n\t\tch: s[0],\n\t}\n\ttoks := p.parse()\n\tif p.err != nil {\n\t\treturn nil, p.err\n\t}\n\treturn toks, nil\n}\n\nfunc (p *parser) parse() []*expressionRaw {\n\ttoks := make([]*expressionRaw, 0)\n\tfor {\n\t\ttok := p.nextTok()\n\t\tif tok == nil {\n\t\t\tbreak\n\t\t}\n\t\ttoks = append(toks, tok)\n\t}\n\treturn toks\n}\n\nfunc (p *parser) nextTok() *expressionRaw {\n\tif p.offset \u003e= len(p.Input) || p.err != nil {\n\t\treturn nil\n\t}\n\tvar err error\n\tfor p.isWhitespace(p.ch) \u0026\u0026 err == nil {\n\t\terr = p.nextCh()\n\t}\n\tstart := p.offset\n\tvar tok *expressionRaw\n\tswitch p.ch {\n\tcase\n\t\t'(',\n\t\t')',\n\t\t'+',\n\t\t'-',\n\t\t'*',\n\t\t'/',\n\t\t'^',\n\t\t'\u0026',\n\t\t'|',\n\t\t'%':\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: string(p.ch),\n\t\t\tType: Operator,\n\t\t}\n\t\ttok.Offset = start\n\t\terr = p.nextCh()\n\tcase '\u003e', '\u003c':\n\t\ttokS := string(p.ch)\n\t\tbb, be := p.nextChPeek()\n\t\tif be == nil \u0026\u0026 string(bb) == tokS {\n\t\t\ttokS += string(p.ch)\n\t\t}\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: tokS,\n\t\t\tType: Operator,\n\t\t}\n\t\ttok.Offset = start\n\t\tif len(tokS) \u003e 1 {\n\t\t\tp.nextCh()\n\t\t}\n\t\terr = p.nextCh()\n\tcase\n\t\t'0',\n\t\t'1',\n\t\t'2',\n\t\t'3',\n\t\t'4',\n\t\t'5',\n\t\t'6',\n\t\t'7',\n\t\t'8',\n\t\t'9':\n\t\tfor p.isDigitNum(p.ch) \u0026\u0026 p.nextCh() == nil {\n\t\t\tif (p.ch == '-' || p.ch == '+') \u0026\u0026 p.Input[p.offset-1] != 'e' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ttok = \u0026expressionRaw{\n\t\t\texpression: strings.ReplaceAll(p.Input[start:p.offset], \"_\", \"\"),\n\t\t\tType: Number,\n\t\t}\n\t\ttok.Offset = start\n\tdefault:\n\t\tif p.isChar(p.ch) {\n\t\t\ttok = \u0026expressionRaw{\n\t\t\t\texpression: string(p.ch),\n\t\t\t\tType: Variable,\n\t\t\t}\n\t\t\ttok.Offset = start\n\t\t\terr = p.nextCh()\n\t\t} else if p.ch != ' ' {\n\t\t\tp.err = ufmt.Errorf(\"symbol error: unknown '%v', pos [%v:]\\n%s\",\n\t\t\t\tstring(p.ch),\n\t\t\t\tstart,\n\t\t\t\texpressionError(p.Input, start))\n\t\t}\n\t}\n\treturn tok\n}\n\nfunc (p *parser) nextChPeek() (byte, error) {\n\toffset := p.offset + 1\n\tif offset \u003c len(p.Input) {\n\t\treturn p.Input[offset], nil\n\t}\n\treturn byte(0), errors.New(\"no byte\")\n}\n\nfunc (p *parser) nextCh() error {\n\tp.offset++\n\tif p.offset \u003c len(p.Input) {\n\t\tp.ch = p.Input[p.offset]\n\t\treturn nil\n\t}\n\treturn errors.New(\"EOF\")\n}\n\nfunc (p *parser) isWhitespace(c byte) bool {\n\treturn c == ' ' ||\n\t\tc == '\\t' ||\n\t\tc == '\\n' ||\n\t\tc == '\\v' ||\n\t\tc == '\\f' ||\n\t\tc == '\\r'\n}\n\nfunc (p *parser) isDigitNum(c byte) bool {\n\treturn '0' \u003c= c \u0026\u0026 c \u003c= '9' || c == '.' || c == '_' || c == 'e' || c == '-' || c == '+'\n}\n\nfunc (p *parser) isChar(c byte) bool {\n\treturn 'a' \u003c= c \u0026\u0026 c \u003c= 'z' || 'A' \u003c= c \u0026\u0026 c \u003c= 'Z'\n}\n\nfunc (p *parser) isWordChar(c byte) bool {\n\treturn p.isChar(c) || '0' \u003c= c \u0026\u0026 c \u003c= '9'\n}\n" + }, + { + "name": "int32_test.gno", + "body": "package int32\n\nimport \"testing\"\n\nfunc TestOne(t *testing.T) {\n\tttt := []struct {\n\t\texp string\n\t\tres int\n\t}{\n\t\t{\"1\", 1},\n\t\t{\"--1\", 1},\n\t\t{\"1+2\", 3},\n\t\t{\"-1+2\", 1},\n\t\t{\"-(1+2)\", -3},\n\t\t{\"-(1+2)*5\", -15},\n\t\t{\"-(1+2)*5/3\", -5},\n\t\t{\"1+(-(1+2)*5/3)\", -4},\n\t\t{\"3^4\", 3 ^ 4},\n\t\t{\"8%2\", 8 % 2},\n\t\t{\"8%3\", 8 % 3},\n\t\t{\"8|3\", 8 | 3},\n\t\t{\"10%2\", 0},\n\t\t{\"(4 + 3)/2-1+11*15\", (4+3)/2 - 1 + 11*15},\n\t\t{\n\t\t\t\"(30099\u003e\u003e10^30099\u003e\u003e11)%5*((30099\u003e\u003e14\u00263^30099\u003e\u003e15\u00261)+1)*30099%99 + ((3 + (30099 \u003e\u003e 14 \u0026 3) - (30099 \u003e\u003e 16 \u0026 1)) / 3 * 30099 % 99 \u0026 64)\",\n\t\t\t(30099\u003e\u003e10^30099\u003e\u003e11)%5*((30099\u003e\u003e14\u00263^30099\u003e\u003e15\u00261)+1)*30099%99 + ((3 + (30099 \u003e\u003e 14 \u0026 3) - (30099 \u003e\u003e 16 \u0026 1)) / 3 * 30099 % 99 \u0026 64),\n\t\t},\n\t\t{\n\t\t\t\"(1023850\u003e\u003e10^1023850\u003e\u003e11)%5*((1023850\u003e\u003e14\u00263^1023850\u003e\u003e15\u00261)+1)*1023850%99 + ((3 + (1023850 \u003e\u003e 14 \u0026 3) - (1023850 \u003e\u003e 16 \u0026 1)) / 3 * 1023850 % 99 \u0026 64)\",\n\t\t\t(1023850\u003e\u003e10^1023850\u003e\u003e11)%5*((1023850\u003e\u003e14\u00263^1023850\u003e\u003e15\u00261)+1)*1023850%99 + ((3 + (1023850 \u003e\u003e 14 \u0026 3) - (1023850 \u003e\u003e 16 \u0026 1)) / 3 * 1023850 % 99 \u0026 64),\n\t\t},\n\t\t{\"((0000+1)*0000)\", 0},\n\t}\n\tfor _, tc := range ttt {\n\t\tt.Run(tc.exp, func(t *testing.T) {\n\t\t\texp, err := Parse(tc.exp)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"%s:\\n%s\", tc.exp, err.Error())\n\t\t\t} else {\n\t\t\t\tres, errEval := Eval(exp, nil)\n\t\t\t\tif errEval != nil {\n\t\t\t\t\tt.Errorf(\"eval error: %s\", errEval.Error())\n\t\t\t\t} else if res != tc.res {\n\t\t\t\t\tt.Errorf(\"%s:\\nexpected %d, got %d\", tc.exp, tc.res, res)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestVariables(t *testing.T) {\n\tfn := func(x, y int) int {\n\t\treturn 1 + ((x*3+1)*(x*2))\u003e\u003ey + 1\n\t}\n\texpr := \"1 + ((x*3+1)*(x*2))\u003e\u003ey + 1\"\n\texp, err := Parse(expr)\n\tif err != nil {\n\t\tt.Errorf(\"could not parse: %s\", err.Error())\n\t}\n\tvariables := make(map[string]int)\n\tfor i := 0; i \u003c 10; i++ {\n\t\tvariables[\"x\"] = i\n\t\tvariables[\"y\"] = 2\n\t\tres, errEval := Eval(exp, variables)\n\t\tif errEval != nil {\n\t\t\tt.Errorf(\"could not evaluate: %s\", err.Error())\n\t\t}\n\t\texpected := fn(variables[\"x\"], variables[\"y\"])\n\t\tif res != expected {\n\t\t\tt.Errorf(\"expected: %d, actual: %d\", expected, res)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "ggrqUIBDqJRBe1szIfS2RtSgdPK7KvA1fSQCGinMug/y+qfRsRHxA8Z728hRg0w86M4KJVbkpYs7lKPx4hTzDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "membstore", + "path": "gno.land/p/demo/membstore", + "files": [ + { + "name": "members.gno", + "body": "package membstore\n\nimport (\n\t\"std\"\n)\n\n// MemberStore defines the member storage abstraction\ntype MemberStore interface {\n\t// Members returns all members in the store\n\tMembers(offset, count uint64) []Member\n\n\t// Size returns the current size of the store\n\tSize() int\n\n\t// IsMember returns a flag indicating if the given address\n\t// belongs to a member\n\tIsMember(address std.Address) bool\n\n\t// TotalPower returns the total voting power of the member store\n\tTotalPower() uint64\n\n\t// Member returns the requested member\n\tMember(address std.Address) (Member, error)\n\n\t// AddMember adds a member to the store\n\tAddMember(member Member) error\n\n\t// UpdateMember updates the member in the store.\n\t// If updating a member's voting power to 0,\n\t// the member will be removed\n\tUpdateMember(address std.Address, member Member) error\n}\n\n// Member holds the relevant member information\ntype Member struct {\n\tAddress std.Address // bech32 gno address of the member (unique)\n\tVotingPower uint64 // the voting power of the member\n}\n" + }, + { + "name": "membstore.gno", + "body": "package membstore\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tErrAlreadyMember = errors.New(\"address is already a member\")\n\tErrMissingMember = errors.New(\"address is not a member\")\n\tErrInvalidAddressUpdate = errors.New(\"invalid address update\")\n\tErrNotGovDAO = errors.New(\"caller not correct govdao instance\")\n)\n\n// maxRequestMembers is the maximum number of\n// paginated members that can be requested\nconst maxRequestMembers = 50\n\ntype Option func(*MembStore)\n\n// WithInitialMembers initializes the member store\n// with an initial member list\nfunc WithInitialMembers(members []Member) Option {\n\treturn func(store *MembStore) {\n\t\tfor _, m := range members {\n\t\t\tmemberAddr := m.Address.String()\n\n\t\t\t// Check if the member already exists\n\t\t\tif store.members.Has(memberAddr) {\n\t\t\t\tpanic(ufmt.Errorf(\"%s, %s\", memberAddr, ErrAlreadyMember))\n\t\t\t}\n\n\t\t\tstore.members.Set(memberAddr, m)\n\t\t\tstore.totalVotingPower += m.VotingPower\n\t\t}\n\t}\n}\n\n// WithDAOPkgPath initializes the member store\n// with a dao package path guard\nfunc WithDAOPkgPath(daoPkgPath string) Option {\n\treturn func(store *MembStore) {\n\t\tstore.daoPkgPath = daoPkgPath\n\t}\n}\n\n// MembStore implements the dao.MembStore abstraction\ntype MembStore struct {\n\tdaoPkgPath string // active dao pkg path, if any\n\tmembers *avl.Tree // std.Address -\u003e Member\n\ttotalVotingPower uint64 // cached value for quick lookups\n}\n\n// NewMembStore creates a new member store\nfunc NewMembStore(opts ...Option) *MembStore {\n\tm := \u0026MembStore{\n\t\tmembers: avl.NewTree(), // empty set\n\t\tdaoPkgPath: \"\", // no dao guard\n\t\ttotalVotingPower: 0,\n\t}\n\n\t// Apply the options\n\tfor _, opt := range opts {\n\t\topt(m)\n\t}\n\n\treturn m\n}\n\n// AddMember adds member to the member store `m`.\n// It fails if the caller is not GovDAO or\n// if the member is already present\nfunc (m *MembStore) AddMember(member Member) error {\n\tif !m.isCallerDAORealm() {\n\t\treturn ErrNotGovDAO\n\t}\n\n\t// Check if the member exists\n\tif m.IsMember(member.Address) {\n\t\treturn ErrAlreadyMember\n\t}\n\n\t// Add the member\n\tm.members.Set(member.Address.String(), member)\n\n\t// Update the total voting power\n\tm.totalVotingPower += member.VotingPower\n\n\treturn nil\n}\n\n// UpdateMember updates the member with the given address.\n// Updating fails if the caller is not GovDAO.\nfunc (m *MembStore) UpdateMember(address std.Address, member Member) error {\n\tif !m.isCallerDAORealm() {\n\t\treturn ErrNotGovDAO\n\t}\n\n\t// Get the member\n\toldMember, err := m.Member(address)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Check if this is a removal request\n\tif member.VotingPower == 0 {\n\t\tm.members.Remove(address.String())\n\n\t\t// Update the total voting power\n\t\tm.totalVotingPower -= oldMember.VotingPower\n\n\t\treturn nil\n\t}\n\n\t// Check that the member wouldn't be\n\t// overwriting an existing one\n\tisAddressUpdate := address != member.Address\n\tif isAddressUpdate \u0026\u0026 m.IsMember(member.Address) {\n\t\treturn ErrInvalidAddressUpdate\n\t}\n\n\t// Remove the old member info\n\t// in case the address changed\n\tif address != member.Address {\n\t\tm.members.Remove(address.String())\n\t}\n\n\t// Save the new member info\n\tm.members.Set(member.Address.String(), member)\n\n\t// Update the total voting power\n\tdifference := member.VotingPower - oldMember.VotingPower\n\tm.totalVotingPower += difference\n\n\treturn nil\n}\n\n// IsMember returns a flag indicating if the given\n// address belongs to a member of the member store\nfunc (m *MembStore) IsMember(address std.Address) bool {\n\t_, exists := m.members.Get(address.String())\n\n\treturn exists\n}\n\n// Member returns the member associated with the given address\nfunc (m *MembStore) Member(address std.Address) (Member, error) {\n\tmember, exists := m.members.Get(address.String())\n\tif !exists {\n\t\treturn Member{}, ErrMissingMember\n\t}\n\n\treturn member.(Member), nil\n}\n\n// Members returns a paginated list of members from\n// the member store. If the store is empty, an empty slice\n// is returned instead\nfunc (m *MembStore) Members(offset, count uint64) []Member {\n\t// Calculate the left and right bounds\n\tif count \u003c 1 || offset \u003e= uint64(m.members.Size()) {\n\t\treturn []Member{}\n\t}\n\n\t// Limit the maximum number of returned members\n\tif count \u003e maxRequestMembers {\n\t\tcount = maxRequestMembers\n\t}\n\n\t// Gather the members\n\tmembers := make([]Member, 0)\n\tm.members.IterateByOffset(\n\t\tint(offset),\n\t\tint(count),\n\t\tfunc(_ string, val any) bool {\n\t\t\tmember := val.(Member)\n\n\t\t\t// Save the member\n\t\t\tmembers = append(members, member)\n\n\t\t\treturn false\n\t\t})\n\n\treturn members\n}\n\n// Size returns the number of active members in the member store\nfunc (m *MembStore) Size() int {\n\treturn m.members.Size()\n}\n\n// TotalPower returns the total voting power\n// of the member store\nfunc (m *MembStore) TotalPower() uint64 {\n\treturn m.totalVotingPower\n}\n\n// isCallerDAORealm returns a flag indicating if the\n// current caller context is the active DAO Realm.\n// We need to include a dao guard, even if the\n// executor guarantees it, because\n// the API of the member store is public and callable\n// by anyone who has a reference to the member store instance.\nfunc (m *MembStore) isCallerDAORealm() bool {\n\treturn m.daoPkgPath != \"\" \u0026\u0026 std.CurrentRealm().PkgPath() == m.daoPkgPath\n}\n" + }, + { + "name": "membstore_test.gno", + "body": "package membstore\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// generateMembers generates dummy govdao members\nfunc generateMembers(t *testing.T, count int) []Member {\n\tt.Helper()\n\n\tmembers := make([]Member, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tmembers = append(members, Member{\n\t\t\tAddress: testutils.TestAddress(ufmt.Sprintf(\"member %d\", i)),\n\t\t\tVotingPower: 10,\n\t\t})\n\t}\n\n\treturn members\n}\n\nfunc TestMembStore_GetMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"member not found\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\t_, err := m.Member(testutils.TestAddress(\"random\"))\n\t\tuassert.ErrorIs(t, err, ErrMissingMember)\n\t})\n\n\tt.Run(\"valid member fetched\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\n\t\tm := NewMembStore(WithInitialMembers(members))\n\n\t\t_, err := m.Member(members[0].Address)\n\t\tuassert.NoError(t, err)\n\t})\n}\n\nfunc TestMembStore_GetMembers(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no members\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\tmembers := m.Members(0, 10)\n\t\tuassert.Equal(t, 0, len(members))\n\t})\n\n\tt.Run(\"proper pagination\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tnumMembers = maxRequestMembers * 2\n\t\t\thalfRange = numMembers / 2\n\n\t\t\tmembers = generateMembers(t, numMembers)\n\t\t\tm = NewMembStore(WithInitialMembers(members))\n\n\t\t\tverifyMembersPresent = func(members, fetchedMembers []Member) {\n\t\t\t\tfor _, fetchedMember := range fetchedMembers {\n\t\t\t\t\tfor _, member := range members {\n\t\t\t\t\t\tif member.Address != fetchedMember.Address {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tuassert.Equal(t, member.VotingPower, fetchedMember.VotingPower)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\turequire.Equal(t, numMembers, m.Size())\n\n\t\tfetchedMembers := m.Members(0, uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedMembers))\n\n\t\t// Verify the members\n\t\tverifyMembersPresent(members, fetchedMembers)\n\n\t\t// Fetch the other half\n\t\tfetchedMembers = m.Members(uint64(halfRange), uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedMembers))\n\n\t\t// Verify the members\n\t\tverifyMembersPresent(members, fetchedMembers)\n\t})\n}\n\nfunc TestMembStore_IsMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"non-existing member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\tuassert.False(t, m.IsMember(testutils.TestAddress(\"random\")))\n\t})\n\n\tt.Run(\"existing member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 50)\n\n\t\tm := NewMembStore(WithInitialMembers(members))\n\n\t\tfor _, member := range members {\n\t\t\tuassert.True(t, m.IsMember(member.Address))\n\t\t}\n\t})\n}\n\nfunc TestMembStore_AddMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"caller not govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore(WithDAOPkgPath(\"gno.land/r/gov/dao\"))\n\n\t\t// Attempt to add a member\n\t\tmember := generateMembers(t, 1)[0]\n\t\tuassert.ErrorIs(t, m.AddMember(member), ErrNotGovDAO)\n\t})\n\n\tt.Run(\"member already exists\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\ttesting.SetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\t// Attempt to add a member\n\t\tuassert.ErrorIs(t, m.AddMember(members[0]), ErrAlreadyMember)\n\t})\n\n\tt.Run(\"new member added\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\ttesting.SetRealm(r)\n\n\t\t// Create an empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath))\n\n\t\t// Attempt to add a member\n\t\turequire.NoError(t, m.AddMember(members[0]))\n\n\t\t// Make sure the member is added\n\t\tuassert.True(t, m.IsMember(members[0].Address))\n\t})\n}\n\nfunc TestMembStore_Size(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore()\n\n\t\tuassert.Equal(t, 0, m.Size())\n\t})\n\n\tt.Run(\"non-empty govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 50)\n\t\tm := NewMembStore(WithInitialMembers(members))\n\n\t\tuassert.Equal(t, len(members), m.Size())\n\t})\n}\n\nfunc TestMembStore_UpdateMember(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"caller not govdao\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create an empty store\n\t\tm := NewMembStore(WithDAOPkgPath(\"gno.land/r/gov/dao\"))\n\n\t\t// Attempt to update a member\n\t\tmember := generateMembers(t, 1)[0]\n\t\tuassert.ErrorIs(t, m.UpdateMember(member.Address, member), ErrNotGovDAO)\n\t})\n\n\tt.Run(\"non-existing member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\ttesting.SetRealm(r)\n\n\t\t// Create an empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath))\n\n\t\t// Attempt to update a member\n\t\tuassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[0]), ErrMissingMember)\n\t})\n\n\tt.Run(\"overwrite member attempt\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\ttesting.SetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 2)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\t// Attempt to update a member\n\t\tuassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[1]), ErrInvalidAddressUpdate)\n\t})\n\n\tt.Run(\"successful update\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\ttesting.SetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\toldVotingPower := m.totalVotingPower\n\t\turequire.Equal(t, members[0].VotingPower, oldVotingPower)\n\n\t\tvotingPower := uint64(300)\n\t\tmembers[0].VotingPower = votingPower\n\n\t\t// Attempt to update a member\n\t\tuassert.NoError(t, m.UpdateMember(members[0].Address, members[0]))\n\t\tuassert.Equal(t, votingPower, m.Members(0, 10)[0].VotingPower)\n\t\turequire.Equal(t, votingPower, m.totalVotingPower)\n\t})\n\n\tt.Run(\"member removed\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\t// Execute as the /r/gov/dao caller\n\t\t\tdaoPkgPath = \"gno.land/r/gov/dao\"\n\t\t\tr = std.NewCodeRealm(daoPkgPath)\n\t\t)\n\n\t\ttesting.SetRealm(r)\n\n\t\t// Create a non-empty store\n\t\tmembers := generateMembers(t, 1)\n\t\tm := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members))\n\n\t\tvotingPower := uint64(0)\n\t\tmembers[0].VotingPower = votingPower\n\n\t\t// Attempt to update a member\n\t\tuassert.NoError(t, m.UpdateMember(members[0].Address, members[0]))\n\n\t\t// Make sure the member was removed\n\t\tuassert.False(t, m.IsMember(members[0].Address))\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "J+jcM50iJVo/OZnCA2FKWFoBufGk57tldpAqhajcgX9NluWYkwpPFi3mpdQcQOo1KJrhifZJKxgJolP/8JMlDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "ownable", + "path": "gno.land/p/demo/ownable", + "files": [ + { + "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": "ownable.gno", + "body": "package ownable\n\nimport \"std\"\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\ntype Ownable struct {\n\towner std.Address\n}\n\nfunc New() *Ownable {\n\treturn \u0026Ownable{\n\t\towner: std.PreviousRealm().Address(),\n\t}\n}\n\nfunc NewWithAddress(addr std.Address) *Ownable {\n\treturn \u0026Ownable{\n\t\towner: addr,\n\t}\n}\n\n// TransferOwnership transfers ownership of the Ownable struct to a new address\nfunc (o *Ownable) TransferOwnership(newOwner std.Address) error {\n\tif !o.CallerIsOwner() {\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\tstd.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// Top-level usage: disables all only-owner actions/functions,\n// Embedded usage: behaves like a burn functionality, removing the owner from the struct\nfunc (o *Ownable) DropOwnership() error {\n\tif !o.CallerIsOwner() {\n\t\treturn ErrUnauthorized\n\t}\n\n\tprevOwner := o.owner\n\to.owner = \"\"\n\n\tstd.Emit(\n\t\tOwnershipTransferEvent,\n\t\t\"from\", prevOwner.String(),\n\t\t\"to\", \"\",\n\t)\n\n\treturn nil\n}\n\n// Owner returns the owner address from Ownable\nfunc (o *Ownable) Owner() std.Address {\n\tif o == nil {\n\t\treturn std.Address(\"\")\n\t}\n\treturn o.owner\n}\n\n// CallerIsOwner checks if the caller of the function is the Realm's owner\nfunc (o *Ownable) CallerIsOwner() bool {\n\tif o == nil {\n\t\treturn false\n\t}\n\treturn std.PreviousRealm().Address() == o.owner\n}\n\n// AssertCallerIsOwner panics if the caller is not the owner\nfunc (o *Ownable) AssertCallerIsOwner() {\n\tif o == nil {\n\t\tpanic(ErrUnauthorized)\n\t}\n\tcaller := std.PreviousRealm().Address()\n\tif caller != o.owner {\n\t\tpanic(ErrUnauthorized)\n\t}\n}\n" + }, + { + "name": "ownable_test.gno", + "body": "package ownable\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n)\n\nfunc TestNew(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\tgot := o.Owner()\n\tuassert.Equal(t, got, alice)\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 TestTransferOwnership(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\n\terr := o.TransferOwnership(bob)\n\turequire.NoError(t, err)\n\n\tgot := o.Owner()\n\n\tuassert.Equal(t, got, bob)\n}\n\nfunc TestCallerIsOwner(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\n\to := New()\n\tunauthorizedCaller := bob\n\n\ttesting.SetOriginCaller(unauthorizedCaller)\n\n\tuassert.False(t, o.CallerIsOwner())\n}\n\nfunc TestDropOwnership(t *testing.T) {\n\ttesting.SetRealm(std.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\n// Errors\n\nfunc TestErrUnauthorized(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\n\ttesting.SetOriginCaller(bob)\n\n\tuassert.ErrorContains(t, o.TransferOwnership(alice), ErrUnauthorized.Error())\n\tuassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error())\n}\n\nfunc TestErrInvalidAddress(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\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 TestAssertCallerIsOwner(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\n\t// Should not panic when caller is owner\n\to.AssertCallerIsOwner()\n\n\t// Should panic when caller is not owner\n\ttesting.SetRealm(std.NewUserRealm(bob))\n\ttesting.SetOriginCaller(bob)\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.AssertCallerIsOwner()\n}\n\nfunc TestNilReceiver(t *testing.T) {\n\tvar o *Ownable\n\n\towner := o.Owner()\n\tif owner != std.Address(\"\") {\n\t\tt.Errorf(\"expected empty address but got %v\", owner)\n\t}\n\n\tisOwner := o.CallerIsOwner()\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.AssertCallerIsOwner()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "kITYqbcZoqj1ilwg7XLTCMcHKXPE7P+IQG2J1mVOLMJBma460U+AJB5TtXh/mh1lk/q5TSAKxgm+GX2cjmL1Aw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "memeland", + "path": "gno.land/p/demo/memeland", + "files": [ + { + "name": "memeland.gno", + "body": "package memeland\n\nimport (\n\t\"sort\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/seqid\"\n)\n\nconst (\n\tDATE_CREATED = \"DATE_CREATED\"\n\tUPVOTES = \"UPVOTES\"\n)\n\ntype Post struct {\n\tID string\n\tData string\n\tAuthor std.Address\n\tTimestamp time.Time\n\tUpvoteTracker *avl.Tree // address \u003e struct{}{}\n}\n\ntype Memeland struct {\n\t*ownable.Ownable\n\tPosts []*Post\n\tMemeCounter seqid.ID\n}\n\nfunc NewMemeland() *Memeland {\n\treturn \u0026Memeland{\n\t\tOwnable: ownable.New(),\n\t\tPosts: make([]*Post, 0),\n\t}\n}\n\n// PostMeme - Adds a new post\nfunc (m *Memeland) PostMeme(data string, timestamp int64) string {\n\tif data == \"\" || timestamp \u003c= 0 {\n\t\tpanic(\"timestamp or data cannot be empty\")\n\t}\n\n\t// Generate ID\n\tid := m.MemeCounter.Next().String()\n\n\tnewPost := \u0026Post{\n\t\tID: id,\n\t\tData: data,\n\t\tAuthor: std.PreviousRealm().Address(),\n\t\tTimestamp: time.Unix(timestamp, 0),\n\t\tUpvoteTracker: avl.NewTree(),\n\t}\n\n\tm.Posts = append(m.Posts, newPost)\n\treturn id\n}\n\nfunc (m *Memeland) Upvote(id string) string {\n\tpost := m.getPost(id)\n\tif post == nil {\n\t\tpanic(\"post with specified ID does not exist\")\n\t}\n\n\tcaller := std.PreviousRealm().Address().String()\n\n\tif _, exists := post.UpvoteTracker.Get(caller); exists {\n\t\tpanic(\"user has already upvoted this post\")\n\t}\n\n\tpost.UpvoteTracker.Set(caller, struct{}{})\n\n\treturn \"upvote successful\"\n}\n\n// GetPostsInRange returns a JSON string of posts within the given timestamp range, supporting pagination\nfunc (m *Memeland) GetPostsInRange(startTimestamp, endTimestamp int64, page, pageSize int, sortBy string) string {\n\tif len(m.Posts) == 0 {\n\t\treturn \"[]\"\n\t}\n\n\tif page \u003c 1 {\n\t\tpanic(\"page number cannot be less than 1\")\n\t}\n\n\t// No empty pages\n\tif pageSize \u003c 1 {\n\t\tpanic(\"page size cannot be less than 1\")\n\t}\n\n\t// No pages larger than 10\n\tif pageSize \u003e 10 {\n\t\tpanic(\"page size cannot be larger than 10\")\n\t}\n\n\t// Need to pass in a sort parameter\n\tif sortBy == \"\" {\n\t\tpanic(\"sort order cannot be empty\")\n\t}\n\n\tvar filteredPosts []*Post\n\n\tstart := time.Unix(startTimestamp, 0)\n\tend := time.Unix(endTimestamp, 0)\n\n\t// Filtering posts\n\tfor _, p := range m.Posts {\n\t\tif !p.Timestamp.Before(start) \u0026\u0026 !p.Timestamp.After(end) {\n\t\t\tfilteredPosts = append(filteredPosts, p)\n\t\t}\n\t}\n\n\tswitch sortBy {\n\t// Sort by upvote descending\n\tcase UPVOTES:\n\t\tdateSorter := PostSorter{\n\t\t\tPosts: filteredPosts,\n\t\t\tLessF: func(i, j int) bool {\n\t\t\t\treturn filteredPosts[i].UpvoteTracker.Size() \u003e filteredPosts[j].UpvoteTracker.Size()\n\t\t\t},\n\t\t}\n\t\tsort.Sort(dateSorter)\n\tcase DATE_CREATED:\n\t\t// Sort by timestamp, beginning with newest\n\t\tdateSorter := PostSorter{\n\t\t\tPosts: filteredPosts,\n\t\t\tLessF: func(i, j int) bool {\n\t\t\t\treturn filteredPosts[i].Timestamp.After(filteredPosts[j].Timestamp)\n\t\t\t},\n\t\t}\n\t\tsort.Sort(dateSorter)\n\tdefault:\n\t\tpanic(\"sort order can only be \\\"UPVOTES\\\" or \\\"DATE_CREATED\\\"\")\n\t}\n\n\t// Pagination\n\tstartIndex := (page - 1) * pageSize\n\tendIndex := startIndex + pageSize\n\n\t// If page does not contain any posts\n\tif startIndex \u003e= len(filteredPosts) {\n\t\treturn \"[]\"\n\t}\n\n\t// If page contains fewer posts than the page size\n\tif endIndex \u003e len(filteredPosts) {\n\t\tendIndex = len(filteredPosts)\n\t}\n\n\t// Return JSON representation of paginated and sorted posts\n\treturn PostsToJSONString(filteredPosts[startIndex:endIndex])\n}\n\n// RemovePost allows the owner to remove a post with a specific ID\nfunc (m *Memeland) RemovePost(id string) string {\n\tif id == \"\" {\n\t\tpanic(\"id cannot be empty\")\n\t}\n\n\tif !m.CallerIsOwner() {\n\t\tpanic(ownable.ErrUnauthorized)\n\t}\n\n\tfor i, post := range m.Posts {\n\t\tif post.ID == id {\n\t\t\tm.Posts = append(m.Posts[:i], m.Posts[i+1:]...)\n\t\t\treturn id\n\t\t}\n\t}\n\n\tpanic(\"post with specified id does not exist\")\n}\n\n// PostsToJSONString converts a slice of Post structs into a JSON string\nfunc PostsToJSONString(posts []*Post) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"[\")\n\n\tfor i, post := range posts {\n\t\tif i \u003e 0 {\n\t\t\tsb.WriteString(\",\")\n\t\t}\n\n\t\tsb.WriteString(PostToJSONString(post))\n\t}\n\tsb.WriteString(\"]\")\n\n\treturn sb.String()\n}\n\n// PostToJSONString returns a Post formatted as a JSON string\nfunc PostToJSONString(post *Post) string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(\"{\")\n\tsb.WriteString(`\"id\":\"` + post.ID + `\",`)\n\tsb.WriteString(`\"data\":\"` + escapeString(post.Data) + `\",`)\n\tsb.WriteString(`\"author\":\"` + escapeString(post.Author.String()) + `\",`)\n\tsb.WriteString(`\"timestamp\":\"` + strconv.Itoa(int(post.Timestamp.Unix())) + `\",`)\n\tsb.WriteString(`\"upvotes\":` + strconv.Itoa(post.UpvoteTracker.Size()))\n\tsb.WriteString(\"}\")\n\n\treturn sb.String()\n}\n\n// escapeString escapes quotes in a string for JSON compatibility.\nfunc escapeString(s string) string {\n\treturn strings.ReplaceAll(s, `\"`, `\\\"`)\n}\n\nfunc (m *Memeland) getPost(id string) *Post {\n\tfor _, p := range m.Posts {\n\t\tif p.ID == id {\n\t\t\treturn p\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// PostSorter is a flexible sorter for the *Post slice\ntype PostSorter struct {\n\tPosts []*Post\n\tLessF func(i, j int) bool\n}\n\nfunc (p PostSorter) Len() int {\n\treturn len(p.Posts)\n}\n\nfunc (p PostSorter) Swap(i, j int) {\n\tp.Posts[i], p.Posts[j] = p.Posts[j], p.Posts[i]\n}\n\nfunc (p PostSorter) Less(i, j int) bool {\n\treturn p.LessF(i, j)\n}\n" + }, + { + "name": "memeland_test.gno", + "body": "package memeland\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestPostMeme(t *testing.T) {\n\tm := NewMemeland()\n\tid := m.PostMeme(\"Test meme data\", time.Now().Unix())\n\tuassert.NotEqual(t, \"\", string(id), \"Expected valid ID, got empty string\")\n}\n\nfunc TestGetPostsInRangePagination(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tm.PostMeme(data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\ttestCases := []struct {\n\t\tpage int\n\t\tpageSize int\n\t\texpectedNumOfPosts int\n\t}{\n\t\t{page: 1, pageSize: 1, expectedNumOfPosts: 1}, // one per page\n\t\t{page: 2, pageSize: 1, expectedNumOfPosts: 1}, // one on second page\n\t\t{page: 1, pageSize: numOfPosts, expectedNumOfPosts: numOfPosts}, // all posts on single page\n\t\t{page: 12, pageSize: 1, expectedNumOfPosts: 0}, // empty page\n\t\t{page: 1, pageSize: numOfPosts + 1, expectedNumOfPosts: numOfPosts}, // page with fewer posts than its size\n\t\t{page: 5, pageSize: numOfPosts / 5, expectedNumOfPosts: 1}, // evenly distribute posts per page\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(ufmt.Sprintf(\"Page%d_Size%d\", tc.page, tc.pageSize), func(t *testing.T) {\n\t\t\tresult := m.GetPostsInRange(beforeEarliest.Unix(), afterLatest.Unix(), tc.page, tc.pageSize, \"DATE_CREATED\")\n\n\t\t\t// Count posts by how many times id: shows up in JSON string\n\t\t\tpostCount := strings.Count(result, `\"id\":\"`)\n\t\t\tuassert.Equal(t, tc.expectedNumOfPosts, postCount)\n\t\t})\n\t}\n}\n\nfunc TestGetPostsInRangeByTimestamp(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tm.PostMeme(data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\t// Default sort is by addition order/timestamp\n\tjsonStr := m.GetPostsInRange(\n\t\tbeforeEarliest.Unix(), // start at earliest post\n\t\tafterLatest.Unix(), // end at latest post\n\t\t1, // first page\n\t\tnumOfPosts, // all memes on the page\n\t\t\"DATE_CREATED\", // sort by newest first\n\t)\n\n\tuassert.NotEmpty(t, jsonStr, \"Expected non-empty JSON string, got empty string\")\n\n\t// Count the number of posts returned in the JSON string as a rudimentary check for correct pagination/filtering\n\tpostCount := strings.Count(jsonStr, `\"id\":\"`)\n\tuassert.Equal(t, uint64(m.MemeCounter), uint64(postCount))\n\n\t// Check if data is there\n\tfor _, expData := range memeData {\n\t\tcheck := strings.Contains(jsonStr, expData)\n\t\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s in the JSON string, but counld't find it\", expData))\n\t}\n\n\t// Check if ordering is correct, sort by created date\n\tfor i := 0; i \u003c len(memeData)-2; i++ {\n\t\tcheck := strings.Index(jsonStr, memeData[i]) \u003e= strings.Index(jsonStr, memeData[i+1])\n\t\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s to be before %s, but was at %d, and %d\", memeData[i], memeData[i+1], i, i+1))\n\t}\n}\n\nfunc TestGetPostsInRangeByUpvote(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tmemeData1 := \"Meme #1\"\n\tmemeData2 := \"Meme #2\"\n\n\t// Create posts at specific times for testing\n\tid1 := m.PostMeme(memeData1, now.Unix())\n\tid2 := m.PostMeme(memeData2, now.Add(time.Minute).Unix())\n\n\tm.Upvote(id1)\n\tm.Upvote(id2)\n\n\t// Change caller so avoid double upvote panic\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\tm.Upvote(id1)\n\n\t// Final upvote count:\n\t// Meme #1 - 2 upvote\n\t// Meme #2 - 1 upvotes\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-time.Minute)\n\tafterLatest := now.Add(time.Hour)\n\n\t// Default sort is by addition order/timestamp\n\tjsonStr := m.GetPostsInRange(\n\t\tbeforeEarliest.Unix(), // start at earliest post\n\t\tafterLatest.Unix(), // end at latest post\n\t\t1, // first page\n\t\t2, // all memes on the page\n\t\t\"UPVOTES\", // sort by upvote\n\t)\n\n\tuassert.NotEmpty(t, jsonStr, \"Expected non-empty JSON string, got empty string\")\n\n\t// Count the number of posts returned in the JSON string as a rudimentary check for correct pagination/filtering\n\tpostCount := strings.Count(jsonStr, `\"id\":\"`)\n\tuassert.Equal(t, uint64(m.MemeCounter), uint64(postCount))\n\n\t// Check if ordering is correct\n\tcheck := strings.Index(jsonStr, \"Meme #1\") \u003c= strings.Index(jsonStr, \"Meme #2\")\n\tuassert.True(t, check, ufmt.Sprintf(\"Expected %s to be before %s\", memeData1, memeData2))\n}\n\nfunc TestBadSortBy(t *testing.T) {\n\tm := NewMemeland()\n\tnow := time.Now()\n\n\tnumOfPosts := 5\n\tvar memeData []string\n\tfor i := 1; i \u003c= numOfPosts; i++ {\n\t\t// Prepare meme data\n\t\tnextTime := now.Add(time.Duration(i) * time.Minute)\n\t\tdata := ufmt.Sprintf(\"Meme #%d\", i)\n\t\tmemeData = append(memeData, data)\n\n\t\tm.PostMeme(data, nextTime.Unix())\n\t}\n\n\t// Get timestamps\n\tbeforeEarliest := now.Add(-1 * time.Minute)\n\tafterLatest := now.Add(time.Duration(numOfPosts)*time.Minute + time.Minute)\n\n\ttests := []struct {\n\t\tname string\n\t\tsortBy string\n\t\twantPanic string\n\t}{\n\t\t{\n\t\t\tname: \"Empty sortBy\",\n\t\t\tsortBy: \"\",\n\t\t\twantPanic: \"runtime error: index out of range\",\n\t\t},\n\t\t{\n\t\t\tname: \"Wrong sortBy\",\n\t\t\tsortBy: \"random string\",\n\t\t\twantPanic: \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdefer func() {\n\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\tt.Errorf(\"code did not panic when it should have\")\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\t// Panics should be caught\n\t\t\t_ = m.GetPostsInRange(beforeEarliest.Unix(), afterLatest.Unix(), 1, 1, tc.sortBy)\n\t\t})\n\t}\n}\n\nfunc TestNoPosts(t *testing.T) {\n\tm := NewMemeland()\n\n\t// Add a post to Memeland\n\tnow := time.Now().Unix()\n\n\tjsonStr := m.GetPostsInRange(0, now, 1, 1, \"DATE_CREATED\")\n\n\tuassert.Equal(t, jsonStr, \"[]\")\n}\n\nfunc TestUpvote(t *testing.T) {\n\tm := NewMemeland()\n\n\t// Add a post to Memeland\n\tnow := time.Now().Unix()\n\tpostID := m.PostMeme(\"Test meme data\", now)\n\n\t// Initial upvote count should be 0\n\tpost := m.getPost(postID)\n\tuassert.Equal(t, 0, post.UpvoteTracker.Size())\n\n\t// Upvote the post\n\tupvoteResult := m.Upvote(postID)\n\tuassert.Equal(t, \"upvote successful\", upvoteResult)\n\n\t// Retrieve the post again and check the upvote count\n\tpost = m.getPost(postID)\n\tuassert.Equal(t, 1, post.UpvoteTracker.Size())\n}\n\nfunc TestDelete(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\n\t// Alice is admin\n\tm := NewMemeland()\n\n\t// Set caller to Bob\n\tbob := testutils.TestAddress(\"bob\")\n\ttesting.SetOriginCaller(bob)\n\n\t// Bob adds post to Memeland\n\tnow := time.Now()\n\tpostID := m.PostMeme(\"Meme #1\", now.Unix())\n\n\t// Alice removes Bob's post\n\ttesting.SetOriginCaller(alice)\n\n\tid := m.RemovePost(postID)\n\tuassert.Equal(t, postID, id, \"post IDs not matching\")\n\tuassert.Equal(t, 0, len(m.Posts), \"there should be 0 posts after removing\")\n}\n\nfunc TestDeleteByNonAdmin(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\n\tm := NewMemeland()\n\n\t// Add a post to Memeland\n\tnow := time.Now()\n\tpostID := m.PostMeme(\"Meme #1\", now.Unix())\n\n\t// Bob will try to delete meme posted by Alice, which should fail\n\tbob := testutils.TestAddress(\"bob\")\n\ttesting.SetOriginCaller(bob)\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"code did not panic when it should have\")\n\t\t}\n\t}()\n\n\t// Should panic - caught by defer\n\tm.RemovePost(postID)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "d3y0tcFAMEWOnH15ZqWXGXgM4NtbIGXi9uU4rDFgy/5Ew7xbQAs/Y1gCeq5e8gXhsETtftRI0GTmZHguoLUBBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "merkle", + "path": "gno.land/p/demo/merkle", + "files": [ + { + "name": "README.md", + "body": "# p/demo/merkle\n\nThis package implement a merkle tree that is complient with [merkletreejs](https://github.com/merkletreejs/merkletreejs)\n\n## [merkletreejs](https://github.com/merkletreejs/merkletreejs)\n\n```javascript\nconst { MerkleTree } = require(\"merkletreejs\");\nconst SHA256 = require(\"crypto-js/sha256\");\n\nlet leaves = [];\nfor (let i = 0; i \u003c 10; i++) {\n leaves.push(SHA256(`node_${i}`));\n}\n\nconst tree = new MerkleTree(leaves, SHA256);\nconst root = tree.getRoot().toString(\"hex\");\n\nconsole.log(root); // cd8a40502b0b92bf58e7432a5abb2d8b60121cf2b7966d6ebaf103f907a1bc21\n```\n" + }, + { + "name": "merkle.gno", + "body": "package merkle\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n)\n\ntype Hashable interface {\n\tBytes() []byte\n}\n\ntype nodes []Node\n\ntype Node struct {\n\thash []byte\n\n\tposition uint8\n}\n\nfunc NewNode(hash []byte, position uint8) Node {\n\treturn Node{\n\t\thash: hash,\n\t\tposition: position,\n\t}\n}\n\nfunc (n Node) Position() uint8 {\n\treturn n.position\n}\n\nfunc (n Node) Hash() string {\n\treturn hex.EncodeToString(n.hash[:])\n}\n\ntype Tree struct {\n\tlayers []nodes\n}\n\n// Root return the merkle root of the tree\nfunc (t *Tree) Root() string {\n\tfor _, l := range t.layers {\n\t\tif len(l) == 1 {\n\t\t\treturn l[0].Hash()\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// NewTree create a new Merkle Tree\nfunc NewTree(data []Hashable) *Tree {\n\ttree := \u0026Tree{}\n\n\tleaves := make([]Node, len(data))\n\n\tfor i, d := range data {\n\t\thash := sha256.Sum256(d.Bytes())\n\t\tleaves[i] = Node{hash: hash[:]}\n\t}\n\n\ttree.layers = []nodes{nodes(leaves)}\n\n\tvar buff bytes.Buffer\n\tfor len(leaves) \u003e 1 {\n\t\tlevel := make([]Node, 0, len(leaves)/2+1)\n\t\tfor i := 0; i \u003c len(leaves); i += 2 {\n\t\t\tbuff.Reset()\n\n\t\t\tif i \u003c len(leaves)-1 {\n\t\t\t\tbuff.Write(leaves[i].hash)\n\t\t\t\tbuff.Write(leaves[i+1].hash)\n\t\t\t\thash := sha256.Sum256(buff.Bytes())\n\t\t\t\tlevel = append(level, Node{\n\t\t\t\t\thash: hash[:],\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tlevel = append(level, leaves[i])\n\t\t\t}\n\t\t}\n\t\tleaves = level\n\t\ttree.layers = append(tree.layers, level)\n\t}\n\treturn tree\n}\n\n// Proof return a MerkleProof\nfunc (t *Tree) Proof(data Hashable) ([]Node, error) {\n\ttargetHash := sha256.Sum256(data.Bytes())\n\ttargetIndex := -1\n\n\tfor i, layer := range t.layers[0] {\n\t\tif bytes.Equal(targetHash[:], layer.hash) {\n\t\t\ttargetIndex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif targetIndex == -1 {\n\t\treturn nil, errors.New(\"target not found\")\n\t}\n\n\tproofs := make([]Node, 0, len(t.layers))\n\n\tfor _, layer := range t.layers {\n\t\tvar pairIndex int\n\n\t\tif targetIndex%2 == 0 {\n\t\t\tpairIndex = targetIndex + 1\n\t\t} else {\n\t\t\tpairIndex = targetIndex - 1\n\t\t}\n\t\tif pairIndex \u003c len(layer) {\n\t\t\tproofs = append(proofs, Node{\n\t\t\t\thash: layer[pairIndex].hash,\n\t\t\t\tposition: uint8(targetIndex) % 2,\n\t\t\t})\n\t\t}\n\t\ttargetIndex /= 2\n\t}\n\treturn proofs, nil\n}\n\n// Verify if a merkle proof is valid\nfunc (t *Tree) Verify(leaf Hashable, proofs []Node) bool {\n\treturn Verify(t.Root(), leaf, proofs)\n}\n\n// Verify if a merkle proof is valid\nfunc Verify(root string, leaf Hashable, proofs []Node) bool {\n\thash := sha256.Sum256(leaf.Bytes())\n\n\tfor i := 0; i \u003c len(proofs); i += 1 {\n\t\tvar h []byte\n\t\tif proofs[i].position == 0 {\n\t\t\th = append(hash[:], proofs[i].hash...)\n\t\t} else {\n\t\t\th = append(proofs[i].hash, hash[:]...)\n\t\t}\n\t\thash = sha256.Sum256(h)\n\t}\n\treturn hex.EncodeToString(hash[:]) == root\n}\n" + }, + { + "name": "merkle_test.gno", + "body": "package merkle\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\ntype testData struct {\n\tcontent string\n}\n\nfunc (d testData) Bytes() []byte {\n\treturn []byte(d.content)\n}\n\nfunc TestMerkleTree(t *testing.T) {\n\ttests := []struct {\n\t\tsize int\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tsize: 1,\n\t\t\texpected: \"cf9f824bce7f5bc63d557b23591f58577f53fe29f974a615bdddbd0140f912f4\",\n\t\t},\n\t\t{\n\t\t\tsize: 3,\n\t\t\texpected: \"1a4a5f0fa267244bf9f74a63fdf2a87eed5e97e4bd104a9e94728c8fb5442177\",\n\t\t},\n\t\t{\n\t\t\tsize: 10,\n\t\t\texpected: \"cd8a40502b0b92bf58e7432a5abb2d8b60121cf2b7966d6ebaf103f907a1bc21\",\n\t\t},\n\t\t{\n\t\t\tsize: 1000,\n\t\t\texpected: \"fa533d2efdf12be26bc410dfa42936ac63361324e35e9b1ff54d422a1dd2388b\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tvar leaves []Hashable\n\t\tfor i := 0; i \u003c test.size; i++ {\n\t\t\tleaves = append(leaves, testData{fmt.Sprintf(\"node_%d\", i)})\n\t\t}\n\n\t\ttree := NewTree(leaves)\n\n\t\tif tree == nil {\n\t\t\tt.Error(\"Merkle tree creation failed\")\n\t\t}\n\n\t\troot := tree.Root()\n\n\t\tif root != test.expected {\n\t\t\tt.Fatalf(\"merkle.Tree.Root(), expected: %s; got: %s\", test.expected, root)\n\t\t}\n\n\t\tfor _, leaf := range leaves {\n\t\t\tproofs, err := tree.Proof(leaf)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"failed to proof leaf: %v, on tree: %v\", leaf, test)\n\t\t\t}\n\n\t\t\tok := Verify(root, leaf, proofs)\n\t\t\tif !ok {\n\t\t\t\tt.Fatal(\"failed to verify leaf: %v, on tree: %v\", leaf, tree)\n\t\t\t}\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "hmlfA+7mo1jvdcmVkN+b3ioJUWEQ5G1ucePMUqt4yXZE33mnqk1oj6o4Mzpb0QBqxrbPIJ6D/tCBIgm1FsArCA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "microblog", + "path": "gno.land/p/demo/microblog", + "files": [ + { + "name": "microblog.gno", + "body": "package microblog\n\nimport (\n\t\"errors\"\n\t\"sort\"\n\t\"std\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tErrNotFound = errors.New(\"not found\")\n\tStatusNotFound = \"404\"\n)\n\ntype Microblog struct {\n\tTitle string\n\tPrefix string // i.e. r/gnoland/blog:\n\tPages avl.Tree // author (string) -\u003e Page\n}\n\nfunc NewMicroblog(title string, prefix string) (m *Microblog) {\n\treturn \u0026Microblog{\n\t\tTitle: title,\n\t\tPrefix: prefix,\n\t\tPages: avl.Tree{},\n\t}\n}\n\nfunc (m *Microblog) GetPages() []*Page {\n\tvar (\n\t\tpages = make([]*Page, m.Pages.Size())\n\t\tindex = 0\n\t)\n\n\tm.Pages.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tpages[index] = value.(*Page)\n\t\tindex++\n\t\treturn false\n\t})\n\n\tsort.Sort(byLastPosted(pages))\n\n\treturn pages\n}\n\nfunc (m *Microblog) NewPost(text string) error {\n\tauthor := std.OriginCaller()\n\t_, found := m.Pages.Get(author.String())\n\tif !found {\n\t\t// make a new page for the new author\n\t\tm.Pages.Set(author.String(), \u0026Page{\n\t\t\tAuthor: author,\n\t\t\tCreatedAt: time.Now(),\n\t\t})\n\t}\n\n\tpage, err := m.GetPage(author.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn page.NewPost(text)\n}\n\nfunc (m *Microblog) GetPage(author string) (*Page, error) {\n\tsilo, found := m.Pages.Get(author)\n\tif !found {\n\t\treturn nil, ErrNotFound\n\t}\n\treturn silo.(*Page), nil\n}\n\ntype Page struct {\n\tID int\n\tAuthor std.Address\n\tCreatedAt time.Time\n\tLastPosted time.Time\n\tPosts avl.Tree // time -\u003e Post\n}\n\n// byLastPosted implements sort.Interface for []Page based on\n// the LastPosted field.\ntype byLastPosted []*Page\n\nfunc (a byLastPosted) Len() int { return len(a) }\nfunc (a byLastPosted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a byLastPosted) Less(i, j int) bool { return a[i].LastPosted.After(a[j].LastPosted) }\n\nfunc (p *Page) NewPost(text string) error {\n\tnow := time.Now()\n\tp.LastPosted = now\n\tp.Posts.Set(ufmt.Sprintf(\"%s%d\", now.Format(time.RFC3339), p.Posts.Size()), \u0026Post{\n\t\tID: p.Posts.Size(),\n\t\tText: text,\n\t\tCreatedAt: now,\n\t})\n\treturn nil\n}\n\nfunc (p *Page) GetPosts() []*Post {\n\tposts := make([]*Post, p.Posts.Size())\n\ti := 0\n\tp.Posts.ReverseIterate(\"\", \"\", func(key string, value any) bool {\n\t\tpostParsed := value.(*Post)\n\t\tposts[i] = postParsed\n\t\ti++\n\t\treturn false\n\t})\n\treturn posts\n}\n\n// Post lists the specific update\ntype Post struct {\n\tID int\n\tCreatedAt time.Time\n\tText string\n}\n\nfunc (p *Post) String() string {\n\treturn \"\u003e \" + strings.ReplaceAll(p.Text, \"\\n\", \"\\n\u003e\\n\u003e\") + \"\\n\u003e\\n\u003e *\" + p.CreatedAt.Format(time.RFC1123) + \"*\"\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "6T0LtHYBOOlv2lwMtuMTCJoyqMbtAlI9Mb00ey3HDV34Ty58Td0A0jd/nHZgW1w1z+zyfftibBPvTvT08tdjDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "nestedpkg", + "path": "gno.land/p/demo/nestedpkg", + "files": [ + { + "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 std.CurrentRealm/PreviousRealm work correctly,\n// this file is tested from gno.land/r/demo/tests/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\"std\"\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 = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.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 = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.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 = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.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 = std.CurrentRealm().PkgPath() + \"/\"\n\t\tprev = std.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(std.CurrentRealm().PkgPath()) + \"/\"\n\t\tprev = nsFromPath(std.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(std.CurrentRealm().PkgPath()) + \"/\"\n\t\tprev = nsFromPath(std.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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "5PsxxKVtiN2fE5oNCXJec2rP09sUS9EDHQNX5N+mueiElbqF8QlH4ArhmMwkCtpFLY9x1eVEB55wNsvLH4tnAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "authorizable", + "path": "gno.land/p/demo/ownable/exts/authorizable", + "files": [ + { + "name": "authorizable.gno", + "body": "// Package authorizable is an extension of p/demo/ownable;\n// It allows the user to instantiate an Authorizable struct, which extends\n// p/demo/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\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Authorizable struct {\n\t*ownable.Ownable // owner in ownable is superuser\n\tauthorized *avl.Tree // std.Addr \u003e struct{}{}\n}\n\nfunc NewAuthorizable() *Authorizable {\n\ta := \u0026Authorizable{\n\t\townable.New(),\n\t\tavl.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 NewAuthorizableWithAddress(addr std.Address) *Authorizable {\n\ta := \u0026Authorizable{\n\t\townable.NewWithAddress(addr),\n\t\tavl.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 std.Address) error {\n\tif !a.CallerIsOwner() {\n\t\treturn ErrNotSuperuser\n\t}\n\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 std.Address) error {\n\tif !a.CallerIsOwner() {\n\t\treturn ErrNotSuperuser\n\t}\n\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) CallerOnAuthList() error {\n\tcaller := std.PreviousRealm().Address()\n\n\tif !a.authorized.Has(caller.String()) {\n\t\treturn ErrNotInAuthList\n\t}\n\n\treturn nil\n}\n\nfunc (a Authorizable) AssertOnAuthList() {\n\tcaller := std.PreviousRealm().Address()\n\n\tif !a.authorized.Has(caller.String()) {\n\t\tpanic(ErrNotInAuthList)\n\t}\n}\n" + }, + { + "name": "authorizable_test.gno", + "body": "package authorizable\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestNewAuthorizable(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\ta := NewAuthorizable()\n\tgot := a.Owner()\n\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestNewAuthorizableWithAddress(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\n\tgot := a.Owner()\n\n\tif alice != got {\n\t\tt.Fatalf(\"Expected %s, got: %s\", alice, got)\n\t}\n}\n\nfunc TestCallerOnAuthList(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\ttesting.SetOriginCaller(alice)\n\n\tif err := a.CallerOnAuthList(); err == ErrNotInAuthList {\n\t\tt.Fatalf(\"expected alice to be on the list\")\n\t}\n}\n\nfunc TestNotCallerOnAuthList(t *testing.T) {\n\ta := NewAuthorizableWithAddress(alice)\n\ttesting.SetOriginCaller(bob)\n\n\tif err := a.CallerOnAuthList(); 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 := NewAuthorizableWithAddress(alice)\n\ttesting.SetOriginCaller(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.SetOriginCaller(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 := NewAuthorizableWithAddress(alice)\n\ttesting.SetOriginCaller(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.SetOriginCaller(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.SetOriginCaller(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\ttesting.SetOriginCaller(alice)\n\n\ta := NewAuthorizableWithAddress(alice)\n\n\ttesting.SetOriginCaller(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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "eZcXD2xHZvtSeEz/j7VPD7uiuMx/308B3QSIH53tVzD1IRiXtG5H7xbdpnCgvzIOGN9tdcugavqfrWCW7yl4Cw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "pausable", + "path": "gno.land/p/demo/pausable", + "files": [ + { + "name": "pausable.gno", + "body": "// Package pausable provides a mechanism to programmatically pause and unpause\n// functionality. This package allows an owner, defined via an Ownable object,\n// to restrict operations or methods when the contract is in a \"paused\" state.\npackage pausable\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n)\n\ntype Pausable struct {\n\to *ownable.Ownable\n\tpaused bool\n}\n\nvar ErrPaused = errors.New(\"pausable: realm is currently paused\")\n\n// NewFromOwnable is the same as New, but with a pre-existing top-level ownable\nfunc NewFromOwnable(ownable *ownable.Ownable) *Pausable {\n\treturn \u0026Pausable{\n\t\to: ownable,\n\t\tpaused: false,\n\t}\n}\n\n// IsPaused checks if Pausable is paused\nfunc (p Pausable) IsPaused() bool {\n\treturn p.paused\n}\n\n// Pause sets the state of Pausable to true, meaning all pausable functions are paused\nfunc (p *Pausable) Pause() error {\n\tif !p.o.CallerIsOwner() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\n\tp.paused = true\n\tstd.Emit(\"Paused\", \"by\", p.o.Owner().String())\n\n\treturn nil\n}\n\n// Unpause sets the state of Pausable to false, meaning all pausable functions are resumed\nfunc (p *Pausable) Unpause() error {\n\tif !p.o.CallerIsOwner() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\n\tp.paused = false\n\tstd.Emit(\"Unpaused\", \"by\", p.o.Owner().String())\n\n\treturn nil\n}\n\n// Ownable returns the underlying ownable\nfunc (p *Pausable) Ownable() *ownable.Ownable {\n\treturn p.o\n}\n" + }, + { + "name": "pausable_test.gno", + "body": "package pausable\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\tfirstCaller = std.Address(\"g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de\")\n\to = ownable.NewWithAddress(firstCaller)\n)\n\nfunc TestNewFromOwnable(t *testing.T) {\n\ttesting.SetOriginCaller(firstCaller)\n\tresult := NewFromOwnable(o)\n\n\turequire.Equal(t, firstCaller.String(), result.Ownable().Owner().String())\n}\n\nfunc TestSetUnpaused(t *testing.T) {\n\ttesting.SetOriginCaller(firstCaller)\n\tresult := NewFromOwnable(o)\n\n\tresult.Unpause()\n\tuassert.False(t, result.IsPaused(), \"Expected result to be unpaused\")\n}\n\nfunc TestSetPaused(t *testing.T) {\n\ttesting.SetOriginCaller(firstCaller)\n\tresult := NewFromOwnable(o)\n\n\tresult.Pause()\n\tuassert.True(t, result.IsPaused(), \"Expected result to be paused\")\n}\n\nfunc TestIsPaused(t *testing.T) {\n\tresult := NewFromOwnable(o)\n\turequire.False(t, result.IsPaused(), \"Expected result to be unpaused\")\n\n\ttesting.SetOriginCaller(firstCaller)\n\tresult.Pause()\n\tuassert.True(t, result.IsPaused(), \"Expected result to be paused\")\n}\n\nfunc TestOwnable(t *testing.T) {\n\tresult := NewFromOwnable(o)\n\n\tuassert.Equal(t, result.Ownable().Owner(), o.Owner())\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "tEr7FTFXd+uJI2kbQo0wndRF3QL9VdYSpXde+eH+aXPmjjj4gLZKnslKNNFSF7L9DcnHXF91ZrGN46tOxGVoDg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "md", + "path": "gno.land/p/moul/md", + "files": [ + { + "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.\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// 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" + }, + { + "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{\"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 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" + }, + { + "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\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "GRiMrlBRJl9fWvVvpL1J0Xgh1o/Ey+35BAKOOznnzIzmPBkBzI9N70szcpaxUzSgRujdEjHFDaNPPWZeSY7ADA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "mdtable", + "path": "gno.land/p/moul/mdtable", + "files": [ + { + "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/demo/urequire\"\n\t\"gno.land/p/moul/mdtable\"\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "XoGQYLbGaAnJi0S+bsJLj4UspSsUSssT0tvbC5/nA0FSYx+Lr6G57+6+BgcD2Hqb6xpPXHRRwGi0emnrmxhgAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "releases", + "path": "gno.land/p/demo/releases", + "files": [ + { + "name": "changelog.gno", + "body": "package releases\n\nimport (\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n)\n\ntype changelog struct {\n\tname string\n\treleases []release\n}\n\nfunc NewChangelog(name string) *changelog {\n\treturn \u0026changelog{\n\t\tname: name,\n\t\treleases: make([]release, 0),\n\t}\n}\n\nfunc (c *changelog) NewRelease(version, url, notes string) {\n\tif latest := c.Latest(); latest != nil {\n\t\tlatest.isLatest = false\n\t}\n\n\trelease := release{\n\t\t// manual\n\t\tversion: version,\n\t\turl: url,\n\t\tnotes: notes,\n\n\t\t// internal\n\t\tchangelog: c,\n\t\tisLatest: true,\n\t}\n\n\tc.releases = append(c.releases, release)\n}\n\nfunc (c changelog) RenderAsTable(max int) string {\n\tout := md.H1(c.name)\n\n\tif len(c.releases) == 0 {\n\t\tout += \"No releases.\"\n\t\treturn out\n\t}\n\n\tout += ufmt.Sprintf(\"See the %s changelog below.\\n\\n\", c.name)\n\ttable := mdtable.Table{\n\t\tHeaders: []string{\"Version\", \"Link\", \"Notes\"},\n\t}\n\n\tif max \u003e len(c.releases) {\n\t\tmax = len(c.releases)\n\t}\n\n\tfor i := len(c.releases) - 1; i \u003e= len(c.releases)-max; i-- {\n\t\tr := c.releases[i]\n\t\ttable.Append([]string{r.version, r.Link(), r.notes})\n\t}\n\n\tout += table.String()\n\treturn out\n}\n\nfunc (c *changelog) Render(path string) string {\n\tif path == \"\" {\n\t\toutput := \"# \" + c.name + \"\\n\\n\"\n\t\tmax := len(c.releases) - 1\n\t\tmin := 0\n\t\tif max-min \u003e 10 {\n\t\t\tmin = max - 10\n\t\t}\n\t\tfor i := max; i \u003e= min; i-- {\n\t\t\trelease := c.releases[i]\n\t\t\toutput += release.Render()\n\t\t}\n\t\treturn output\n\t}\n\n\trelease := c.ByVersion(path)\n\tif release != nil {\n\t\treturn release.Render()\n\t}\n\n\treturn \"no such release\"\n}\n\nfunc (c *changelog) Latest() *release {\n\tif len(c.releases) \u003e 0 {\n\t\tpos := len(c.releases) - 1\n\t\treturn \u0026c.releases[pos]\n\t}\n\treturn nil\n}\n\nfunc (c *changelog) ByVersion(version string) *release {\n\tfor _, release := range c.releases {\n\t\tif release.version == version {\n\t\t\treturn \u0026release\n\t\t}\n\t}\n\treturn nil\n}\n" + }, + { + "name": "release.gno", + "body": "package releases\n\ntype release struct {\n\t// manual\n\tversion string\n\turl string\n\tnotes string\n\n\t// internal\n\tisLatest bool\n\tchangelog *changelog\n}\n\nfunc (r *release) URL() string { return r.url }\nfunc (r *release) Version() string { return r.version }\nfunc (r *release) Notes() string { return r.notes }\nfunc (r *release) IsLatest() bool { return r.isLatest }\n\nfunc (r *release) Title() string {\n\toutput := r.changelog.name + \" \" + r.version\n\tif r.isLatest {\n\t\toutput += \" (latest)\"\n\t}\n\treturn output\n}\n\nfunc (r *release) Link() string {\n\treturn \"[\" + r.Title() + \"](\" + r.url + \")\"\n}\n\nfunc (r *release) Render() string {\n\toutput := \"\"\n\toutput += \"## \" + r.Link() + \"\\n\\n\"\n\tif r.notes != \"\" {\n\t\toutput += r.notes + \"\\n\\n\"\n\t}\n\treturn output\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "o14As7zs2bDW3tC8OAAcwj6EB9L0wnQxgfj2ip9XO13TODPS+OWpOJEqbNzkhd1ktahqEdTIqrFf6PqxVyP6Cg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "simpledao", + "path": "gno.land/p/demo/simpledao", + "files": [ + { + "name": "dao.gno", + "body": "package simpledao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tErrInvalidExecutor = errors.New(\"invalid executor provided\")\n\tErrInvalidTitle = errors.New(\"invalid proposal title provided\")\n\tErrInsufficientProposalFunds = errors.New(\"insufficient funds for proposal\")\n\tErrInsufficientExecuteFunds = errors.New(\"insufficient funds for executing proposal\")\n\tErrProposalExecuted = errors.New(\"proposal already executed\")\n\tErrProposalInactive = errors.New(\"proposal is inactive\")\n\tErrProposalNotAccepted = errors.New(\"proposal is not accepted\")\n)\n\nvar (\n\tminProposalFeeValue int64 = 100 * 1_000_000 // minimum gnot required for a govdao proposal (100 GNOT)\n\tminExecuteFeeValue int64 = 500 * 1_000_000 // minimum gnot required for a govdao proposal (500 GNOT)\n\n\tminProposalFee = std.NewCoin(\"ugnot\", minProposalFeeValue)\n\tminExecuteFee = std.NewCoin(\"ugnot\", minExecuteFeeValue)\n)\n\n// SimpleDAO is a simple DAO implementation\ntype SimpleDAO struct {\n\tproposals *avl.Tree // seqid.ID -\u003e proposal\n\tmembStore membstore.MemberStore\n}\n\n// New creates a new instance of the simpledao DAO\nfunc New(membStore membstore.MemberStore) *SimpleDAO {\n\treturn \u0026SimpleDAO{\n\t\tproposals: avl.NewTree(),\n\t\tmembStore: membStore,\n\t}\n}\n\nfunc (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) {\n\t// Make sure the executor is set\n\tif request.Executor == nil {\n\t\treturn 0, ErrInvalidExecutor\n\t}\n\n\t// Make sure the title is set\n\tif strings.TrimSpace(request.Title) == \"\" {\n\t\treturn 0, ErrInvalidTitle\n\t}\n\n\tvar (\n\t\tcaller = getDAOCaller()\n\t\tsentCoins = std.OriginSend() // Get the sent coins, if any\n\t\tcanCoverFee = sentCoins.AmountOf(\"ugnot\") \u003e= minProposalFee.Amount\n\t)\n\n\t// Check if the proposal is valid\n\tif !s.membStore.IsMember(caller) \u0026\u0026 !canCoverFee {\n\t\treturn 0, ErrInsufficientProposalFunds\n\t}\n\n\t// Create the wrapped proposal\n\tprop := \u0026proposal{\n\t\tauthor: caller,\n\t\ttitle: request.Title,\n\t\tdescription: request.Description,\n\t\texecutor: request.Executor,\n\t\tstatus: dao.Active,\n\t\ttally: newTally(),\n\t\tgetTotalVotingPowerFn: s.membStore.TotalPower,\n\t}\n\n\t// Add the proposal\n\tid, err := s.addProposal(prop)\n\tif err != nil {\n\t\treturn 0, ufmt.Errorf(\"unable to add proposal, %s\", err.Error())\n\t}\n\n\t// Emit the proposal added event\n\tdao.EmitProposalAdded(id, caller)\n\n\treturn id, nil\n}\n\nfunc (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error {\n\t// Verify the GOVDAO member\n\tcaller := getDAOCaller()\n\n\tmember, err := s.membStore.Member(caller)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"unable to get govdao member, %s\", err.Error())\n\t}\n\n\t// Check if the proposal exists\n\tpropRaw, err := s.ProposalByID(id)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"unable to get proposal %d, %s\", id, err.Error())\n\t}\n\n\tprop := propRaw.(*proposal)\n\n\t// Check the proposal status\n\tif prop.Status() == dao.ExecutionSuccessful ||\n\t\tprop.Status() == dao.ExecutionFailed {\n\t\t// Proposal was already executed, nothing to vote on anymore.\n\t\t//\n\t\t// In fact, the proposal should stop accepting\n\t\t// votes as soon as a 2/3+ majority is reached\n\t\t// on either option, but leaving the ability to vote still,\n\t\t// even if a proposal is accepted, or not accepted,\n\t\t// leaves room for \"principle\" vote decisions to be recorded\n\t\treturn ErrProposalInactive\n\t}\n\n\t// Cast the vote\n\tif err = prop.tally.castVote(member, option); err != nil {\n\t\treturn ufmt.Errorf(\"unable to vote on proposal %d, %s\", id, err.Error())\n\t}\n\n\t// Emit the vote cast event\n\tdao.EmitVoteAdded(id, caller, option)\n\n\t// Check the votes to see if quorum is reached\n\tvar (\n\t\ttotalPower = s.membStore.TotalPower()\n\t\tmajorityPower = (2 * totalPower) / 3\n\t)\n\n\tacceptProposal := func() {\n\t\tprop.status = dao.Accepted\n\n\t\tdao.EmitProposalAccepted(id)\n\t}\n\n\tdeclineProposal := func() {\n\t\tprop.status = dao.NotAccepted\n\n\t\tdao.EmitProposalNotAccepted(id)\n\t}\n\n\tswitch {\n\tcase prop.tally.yays \u003e majorityPower:\n\t\t// 2/3+ voted YES\n\t\tacceptProposal()\n\tcase prop.tally.nays \u003e majorityPower:\n\t\t// 2/3+ voted NO\n\t\tdeclineProposal()\n\tcase prop.tally.abstains \u003e majorityPower:\n\t\t// 2/3+ voted ABSTAIN\n\t\tdeclineProposal()\n\tcase prop.tally.yays+prop.tally.nays+prop.tally.abstains \u003e= totalPower:\n\t\t// Everyone voted, but it's undecided,\n\t\t// hence the proposal can't go through\n\t\tdeclineProposal()\n\tdefault:\n\t\t// Quorum not reached\n\t}\n\n\treturn nil\n}\n\nfunc (s *SimpleDAO) ExecuteProposal(id uint64) error {\n\tvar (\n\t\tcaller = getDAOCaller()\n\t\tsentCoins = std.OriginSend() // Get the sent coins, if any\n\t\tcanCoverFee = sentCoins.AmountOf(\"ugnot\") \u003e= minExecuteFee.Amount\n\t)\n\n\t// Check if the non-DAO member can cover the execute fee\n\tif !s.membStore.IsMember(caller) \u0026\u0026 !canCoverFee {\n\t\treturn ErrInsufficientExecuteFunds\n\t}\n\n\t// Check if the proposal exists\n\tpropRaw, err := s.ProposalByID(id)\n\tif err != nil {\n\t\treturn ufmt.Errorf(\"unable to get proposal %d, %s\", id, err.Error())\n\t}\n\n\tprop := propRaw.(*proposal)\n\n\t// Check if the proposal is executed\n\tif prop.Status() == dao.ExecutionSuccessful ||\n\t\tprop.Status() == dao.ExecutionFailed {\n\t\t// Proposal is already executed\n\t\treturn ErrProposalExecuted\n\t}\n\n\t// Check the proposal status\n\tif prop.Status() != dao.Accepted {\n\t\t// Proposal is not accepted, cannot be executed\n\t\treturn ErrProposalNotAccepted\n\t}\n\n\t// Emit an event when the execution finishes\n\tdefer dao.EmitProposalExecuted(id, prop.status)\n\n\t// Attempt to execute the proposal\n\tif err = prop.executor.Execute(); err != nil {\n\t\tprop.status = dao.ExecutionFailed\n\n\t\treturn ufmt.Errorf(\"error during proposal %d execution, %s\", id, err.Error())\n\t}\n\n\t// Update the proposal status\n\tprop.status = dao.ExecutionSuccessful\n\n\treturn nil\n}\n\n// getDAOCaller returns the DAO caller.\n// XXX: This is not a great way to determine the caller, and it is very unsafe.\n// However, the current MsgRun context does not persist escaping the main() scope.\n// Until a better solution is developed, this enables proposals to be made through a package deployment + init()\nfunc getDAOCaller() std.Address {\n\treturn std.OriginCaller()\n}\n" + }, + { + "name": "dao_test.gno", + "body": "package simpledao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// generateMembers generates dummy govdao members\nfunc generateMembers(t *testing.T, count int) []membstore.Member {\n\tt.Helper()\n\n\tmembers := make([]membstore.Member, 0, count)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tmembers = append(members, membstore.Member{\n\t\t\tAddress: testutils.TestAddress(ufmt.Sprintf(\"member %d\", i)),\n\t\t\tVotingPower: 10,\n\t\t})\n\t}\n\n\treturn members\n}\n\nfunc TestSimpleDAO_Propose(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"invalid executor\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := New(nil)\n\n\t\t_, err := s.Propose(dao.ProposalRequest{})\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\terr,\n\t\t\tErrInvalidExecutor,\n\t\t)\n\t})\n\n\tt.Run(\"invalid title\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tex = \u0026mockExecutor{\n\t\t\t\texecuteFn: cb,\n\t\t\t}\n\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminProposalFeeValue,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\ttesting.SetOriginSend(sentCoins)\n\n\t\t_, err := s.Propose(dao.ProposalRequest{\n\t\t\tExecutor: ex,\n\t\t\tTitle: \"\", // Set invalid title\n\t\t})\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\terr,\n\t\t\tErrInvalidTitle,\n\t\t)\n\n\t\tuassert.False(t, called)\n\t})\n\n\tt.Run(\"caller cannot cover fee\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tex = \u0026mockExecutor{\n\t\t\t\texecuteFn: cb,\n\t\t\t}\n\t\t\ttitle = \"Proposal title\"\n\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminProposalFeeValue-1,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be lower\n\t\t// than the proposal fee\n\t\ttesting.SetOriginSend(sentCoins)\n\n\t\t_, err := s.Propose(dao.ProposalRequest{\n\t\t\tExecutor: ex,\n\t\t\tTitle: title,\n\t\t})\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\terr,\n\t\t\tErrInsufficientProposalFunds,\n\t\t)\n\n\t\tuassert.False(t, called)\n\t})\n\n\tt.Run(\"proposal added\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tex = \u0026mockExecutor{\n\t\t\t\texecuteFn: cb,\n\t\t\t}\n\t\t\tdescription = \"Proposal description\"\n\t\t\ttitle = \"Proposal title\"\n\n\t\t\tproposer = testutils.TestAddress(\"proposer\")\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminProposalFeeValue, // enough to cover\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(addr std.Address) bool {\n\t\t\t\t\treturn addr == proposer\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be enough\n\t\t// to cover the fee\n\t\ttesting.SetOriginSend(sentCoins)\n\t\ttesting.SetOriginCaller(proposer)\n\n\t\t// Make sure the proposal was added\n\t\tid, err := s.Propose(dao.ProposalRequest{\n\t\t\tTitle: title,\n\t\t\tDescription: description,\n\t\t\tExecutor: ex,\n\t\t})\n\t\tuassert.NoError(t, err)\n\t\tuassert.False(t, called)\n\n\t\t// Make sure the proposal exists\n\t\tprop, err := s.ProposalByID(id)\n\t\tuassert.NoError(t, err)\n\n\t\tuassert.Equal(t, proposer.String(), prop.Author().String())\n\t\tuassert.Equal(t, description, prop.Description())\n\t\tuassert.Equal(t, title, prop.Title())\n\t\tuassert.Equal(t, dao.Active.String(), prop.Status().String())\n\n\t\tstats := prop.Stats()\n\n\t\tuassert.Equal(t, uint64(0), stats.YayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.NayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.AbstainVotes)\n\t\tuassert.Equal(t, uint64(0), stats.TotalVotingPower)\n\t})\n}\n\nfunc TestSimpleDAO_VoteOnProposal(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"not govdao member\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\t\t\tfetchErr = errors.New(\"fetch error\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(_ std.Address) (membstore.Member, error) {\n\t\t\t\t\treturn membstore.Member{\n\t\t\t\t\t\tAddress: voter,\n\t\t\t\t\t}, fetchErr\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\ttesting.SetOriginCaller(voter)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(0, dao.YesVote),\n\t\t\tfetchErr.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"missing proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(a std.Address) (membstore.Member, error) {\n\t\t\t\t\tif a != voter {\n\t\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{\n\t\t\t\t\t\tAddress: voter,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts = New(ms)\n\t\t)\n\n\t\ttesting.SetOriginCaller(voter)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(0, dao.YesVote),\n\t\t\tErrMissingProposal.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"proposal executed\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(a std.Address) (membstore.Member, error) {\n\t\t\t\t\tif a != voter {\n\t\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{\n\t\t\t\t\t\tAddress: voter,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.ExecutionSuccessful,\n\t\t\t}\n\t\t)\n\n\t\ttesting.SetOriginCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\tErrProposalInactive,\n\t\t)\n\t})\n\n\tt.Run(\"double vote on proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\t\t\tmember = membstore.Member{\n\t\t\t\tAddress: voter,\n\t\t\t\tVotingPower: 10,\n\t\t\t}\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(a std.Address) (membstore.Member, error) {\n\t\t\t\t\tif a != voter {\n\t\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn member, nil\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\ttesting.SetOriginCaller(voter)\n\n\t\t// Cast the initial vote\n\t\turequire.NoError(t, prop.tally.castVote(member, dao.YesVote))\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\tErrAlreadyVoted.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"majority accepted\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\tmajorityIndex := (len(members)*2)/3 + 1 // 2/3+\n\t\tfor _, m := range members[:majorityIndex] {\n\t\t\ttesting.SetOriginCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal was accepted\n\t\tuassert.Equal(t, dao.Accepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"majority rejected\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\tmajorityIndex := (len(members)*2)/3 + 1 // 2/3+\n\t\tfor _, m := range members[:majorityIndex] {\n\t\t\ttesting.SetOriginCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.NoVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal was not accepted\n\t\tuassert.Equal(t, dao.NotAccepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"majority abstained\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\tmajorityIndex := (len(members)*2)/3 + 1 // 2/3+\n\t\tfor _, m := range members[:majorityIndex] {\n\t\t\ttesting.SetOriginCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.AbstainVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal was not accepted\n\t\tuassert.Equal(t, dao.NotAccepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"everyone voted, undecided\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// The first half votes yes\n\t\tfor _, m := range members[:len(members)/2] {\n\t\t\ttesting.SetOriginCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\t)\n\t\t}\n\n\t\t// The other half votes no\n\t\tfor _, m := range members[len(members)/2:] {\n\t\t\ttesting.SetOriginCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.NoVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal is not active,\n\t\t// since everyone voted, and it was undecided\n\t\tuassert.Equal(t, dao.NotAccepted.String(), prop.status.String())\n\t})\n\n\tt.Run(\"proposal undecided\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tmemberFn: func(address std.Address) (membstore.Member, error) {\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tif m.Address == address {\n\t\t\t\t\t\t\treturn m, nil\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn membstore.Member{}, errors.New(\"member not found\")\n\t\t\t\t},\n\n\t\t\t\ttotalPowerFn: func() uint64 {\n\t\t\t\t\tpower := uint64(0)\n\n\t\t\t\t\tfor _, m := range members {\n\t\t\t\t\t\tpower += m.VotingPower\n\t\t\t\t\t}\n\n\t\t\t\t\treturn power\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Active,\n\t\t\t\texecutor: \u0026mockExecutor{},\n\t\t\t\ttally: newTally(),\n\t\t\t}\n\t\t)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// The first quarter votes yes\n\t\tfor _, m := range members[:len(members)/4] {\n\t\t\ttesting.SetOriginCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.YesVote),\n\t\t\t)\n\t\t}\n\n\t\t// The second quarter votes no\n\t\tfor _, m := range members[len(members)/4 : len(members)/2] {\n\t\t\ttesting.SetOriginCaller(m.Address)\n\n\t\t\t// Attempt to vote on the proposal\n\t\t\turequire.NoError(\n\t\t\t\tt,\n\t\t\t\ts.VoteOnProposal(id, dao.NoVote),\n\t\t\t)\n\t\t}\n\n\t\t// Make sure the proposal is still active,\n\t\t// since there wasn't quorum reached on any decision\n\t\tuassert.Equal(t, dao.Active.String(), prop.status.String())\n\t})\n}\n\nfunc TestSimpleDAO_ExecuteProposal(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"caller cannot cover fee\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminExecuteFeeValue-1,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn false\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be lower\n\t\t// than the execute fee\n\t\ttesting.SetOriginSend(sentCoins)\n\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(0),\n\t\t\tErrInsufficientExecuteFunds,\n\t\t)\n\t})\n\n\tt.Run(\"missing proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tsentCoins = std.NewCoins(\n\t\t\t\tstd.NewCoin(\n\t\t\t\t\t\"ugnot\",\n\t\t\t\t\tminExecuteFeeValue,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts = New(ms)\n\t\t)\n\n\t\t// Set the sent coins to be enough\n\t\t// so the execution can take place\n\t\ttesting.SetOriginSend(sentCoins)\n\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(0),\n\t\t\tErrMissingProposal.Error(),\n\t\t)\n\t})\n\n\tt.Run(\"proposal not accepted\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.NotAccepted,\n\t\t\t}\n\t\t)\n\n\t\ttesting.SetOriginCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorIs(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(id),\n\t\t\tErrProposalNotAccepted,\n\t\t)\n\t})\n\n\tt.Run(\"proposal already executed\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestTable := []struct {\n\t\t\tname string\n\t\t\tstatus dao.ProposalStatus\n\t\t}{\n\t\t\t{\n\t\t\t\t\"execution was successful\",\n\t\t\t\tdao.ExecutionSuccessful,\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"execution not succeeded\",\n\t\t\t\tdao.ExecutionFailed,\n\t\t\t},\n\t\t}\n\n\t\tfor _, testCase := range testTable {\n\t\t\tt.Run(testCase.name, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\tvar (\n\t\t\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\t\t\tms = \u0026mockMemberStore{\n\t\t\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ts = New(ms)\n\n\t\t\t\t\tprop = \u0026proposal{\n\t\t\t\t\t\tstatus: testCase.status,\n\t\t\t\t\t}\n\t\t\t\t)\n\n\t\t\t\ttesting.SetOriginCaller(voter)\n\n\t\t\t\t// Add an initial proposal\n\t\t\t\tid, err := s.addProposal(prop)\n\t\t\t\turequire.NoError(t, err)\n\n\t\t\t\t// Attempt to vote on the proposal\n\t\t\t\tuassert.ErrorIs(\n\t\t\t\t\tt,\n\t\t\t\t\ts.ExecuteProposal(id),\n\t\t\t\t\tErrProposalExecuted,\n\t\t\t\t)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"execution error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts = New(ms)\n\n\t\t\texecError = errors.New(\"exec error\")\n\n\t\t\tmockExecutor = \u0026mockExecutor{\n\t\t\t\texecuteFn: func() error {\n\t\t\t\t\treturn execError\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Accepted,\n\t\t\t\texecutor: mockExecutor,\n\t\t\t}\n\t\t)\n\n\t\ttesting.SetOriginCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.ErrorContains(\n\t\t\tt,\n\t\t\ts.ExecuteProposal(id),\n\t\t\texecError.Error(),\n\t\t)\n\n\t\tuassert.Equal(t, dao.ExecutionFailed.String(), prop.status.String())\n\t})\n\n\tt.Run(\"successful execution\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tvoter = testutils.TestAddress(\"voter\")\n\n\t\t\tms = \u0026mockMemberStore{\n\t\t\t\tisMemberFn: func(_ std.Address) bool {\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t}\n\t\t\ts = New(ms)\n\n\t\t\tcalled = false\n\t\t\tmockExecutor = \u0026mockExecutor{\n\t\t\t\texecuteFn: func() error {\n\t\t\t\t\tcalled = true\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tprop = \u0026proposal{\n\t\t\t\tstatus: dao.Accepted,\n\t\t\t\texecutor: mockExecutor,\n\t\t\t}\n\t\t)\n\n\t\ttesting.SetOriginCaller(voter)\n\n\t\t// Add an initial proposal\n\t\tid, err := s.addProposal(prop)\n\t\turequire.NoError(t, err)\n\n\t\t// Attempt to vote on the proposal\n\t\tuassert.NoError(t, s.ExecuteProposal(id))\n\t\tuassert.Equal(t, dao.ExecutionSuccessful.String(), prop.status.String())\n\t\tuassert.True(t, called)\n\t})\n}\n" + }, + { + "name": "mock_test.gno", + "body": "package simpledao\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/membstore\"\n)\n\ntype executeDelegate func() error\n\ntype mockExecutor struct {\n\texecuteFn executeDelegate\n}\n\nfunc (m *mockExecutor) Execute() error {\n\tif m.executeFn != nil {\n\t\treturn m.executeFn()\n\t}\n\n\treturn nil\n}\n\ntype (\n\tmembersDelegate func(uint64, uint64) []membstore.Member\n\tsizeDelegate func() int\n\tisMemberDelegate func(std.Address) bool\n\ttotalPowerDelegate func() uint64\n\tmemberDelegate func(std.Address) (membstore.Member, error)\n\taddMemberDelegate func(membstore.Member) error\n\tupdateMemberDelegate func(std.Address, membstore.Member) error\n)\n\ntype mockMemberStore struct {\n\tmembersFn membersDelegate\n\tsizeFn sizeDelegate\n\tisMemberFn isMemberDelegate\n\ttotalPowerFn totalPowerDelegate\n\tmemberFn memberDelegate\n\taddMemberFn addMemberDelegate\n\tupdateMemberFn updateMemberDelegate\n}\n\nfunc (m *mockMemberStore) Members(offset, count uint64) []membstore.Member {\n\tif m.membersFn != nil {\n\t\treturn m.membersFn(offset, count)\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockMemberStore) Size() int {\n\tif m.sizeFn != nil {\n\t\treturn m.sizeFn()\n\t}\n\n\treturn 0\n}\n\nfunc (m *mockMemberStore) IsMember(address std.Address) bool {\n\tif m.isMemberFn != nil {\n\t\treturn m.isMemberFn(address)\n\t}\n\n\treturn false\n}\n\nfunc (m *mockMemberStore) TotalPower() uint64 {\n\tif m.totalPowerFn != nil {\n\t\treturn m.totalPowerFn()\n\t}\n\n\treturn 0\n}\n\nfunc (m *mockMemberStore) Member(address std.Address) (membstore.Member, error) {\n\tif m.memberFn != nil {\n\t\treturn m.memberFn(address)\n\t}\n\n\treturn membstore.Member{}, nil\n}\n\nfunc (m *mockMemberStore) AddMember(member membstore.Member) error {\n\tif m.addMemberFn != nil {\n\t\treturn m.addMemberFn(member)\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockMemberStore) UpdateMember(address std.Address, member membstore.Member) error {\n\tif m.updateMemberFn != nil {\n\t\treturn m.updateMemberFn(address, member)\n\t}\n\n\treturn nil\n}\n" + }, + { + "name": "propstore.gno", + "body": "package simpledao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar ErrMissingProposal = errors.New(\"proposal is missing\")\n\n// maxRequestProposals is the maximum number of\n// paginated proposals that can be requested\nconst maxRequestProposals = 10\n\n// proposal is the internal simpledao proposal implementation\ntype proposal struct {\n\tauthor std.Address // initiator of the proposal\n\ttitle string // title of the proposal\n\tdescription string // description of the proposal\n\n\texecutor dao.Executor // executor for the proposal\n\tstatus dao.ProposalStatus // status of the proposal\n\n\ttally *tally // voting tally\n\tgetTotalVotingPowerFn func() uint64 // callback for the total voting power\n}\n\nfunc (p *proposal) Author() std.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) Status() dao.ProposalStatus {\n\treturn p.status\n}\n\nfunc (p *proposal) Executor() dao.Executor {\n\treturn p.executor\n}\n\nfunc (p *proposal) Stats() dao.Stats {\n\t// Get the total voting power of the body\n\ttotalPower := p.getTotalVotingPowerFn()\n\n\treturn dao.Stats{\n\t\tYayVotes: p.tally.yays,\n\t\tNayVotes: p.tally.nays,\n\t\tAbstainVotes: p.tally.abstains,\n\t\tTotalVotingPower: totalPower,\n\t}\n}\n\nfunc (p *proposal) IsExpired() bool {\n\treturn false // this proposal never expires\n}\n\nfunc (p *proposal) Render() string {\n\t// Fetch the voting stats\n\tstats := p.Stats()\n\n\tvar out string\n\n\tout += \"## Description\\n\\n\"\n\tif strings.TrimSpace(p.description) != \"\" {\n\t\tout += ufmt.Sprintf(\"%s\\n\\n\", p.description)\n\t} else {\n\t\tout += \"No description provided.\\n\\n\"\n\t}\n\n\tout += \"## Proposal information\\n\\n\"\n\tout += ufmt.Sprintf(\"**Status: %s**\\n\\n\", strings.ToUpper(p.Status().String()))\n\n\tout += ufmt.Sprintf(\n\t\t\"**Voting stats:**\\n- YES %d (%d%%)\\n- NO %d (%d%%)\\n- ABSTAIN %d (%d%%)\\n- MISSING VOTES %d (%d%%)\\n\",\n\t\tstats.YayVotes,\n\t\tstats.YayPercent(),\n\t\tstats.NayVotes,\n\t\tstats.NayPercent(),\n\t\tstats.AbstainVotes,\n\t\tstats.AbstainPercent(),\n\t\tstats.MissingVotes(),\n\t\tstats.MissingVotesPercent(),\n\t)\n\n\tout += \"\\n\\n\"\n\tthresholdOut := strings.ToUpper(ufmt.Sprintf(\"%t\", stats.YayVotes \u003e (2*stats.TotalVotingPower)/3))\n\n\tout += ufmt.Sprintf(\"**Threshold met: %s**\\n\\n\", thresholdOut)\n\n\treturn out\n}\n\n// addProposal adds a new simpledao proposal to the store\nfunc (s *SimpleDAO) addProposal(proposal *proposal) (uint64, error) {\n\t// See what the next proposal number should be\n\tnextID := uint64(s.proposals.Size())\n\n\t// Save the proposal\n\ts.proposals.Set(getProposalID(nextID), proposal)\n\n\treturn nextID, nil\n}\n\nfunc (s *SimpleDAO) Proposals(offset, count uint64) []dao.Proposal {\n\t// Check the requested count\n\tif count \u003c 1 {\n\t\treturn []dao.Proposal{}\n\t}\n\n\t// Limit the maximum number of returned proposals\n\tif count \u003e maxRequestProposals {\n\t\tcount = maxRequestProposals\n\t}\n\n\tvar (\n\t\tstartIndex = offset\n\t\tendIndex = startIndex + count\n\n\t\tnumProposals = uint64(s.proposals.Size())\n\t)\n\n\t// Check if the current offset has any proposals\n\tif startIndex \u003e= numProposals {\n\t\treturn []dao.Proposal{}\n\t}\n\n\t// Check if the right bound is good\n\tif endIndex \u003e numProposals {\n\t\tendIndex = numProposals\n\t}\n\n\tprops := make([]dao.Proposal, 0)\n\ts.proposals.Iterate(\n\t\tgetProposalID(startIndex),\n\t\tgetProposalID(endIndex),\n\t\tfunc(_ string, val any) bool {\n\t\t\tprop := val.(*proposal)\n\n\t\t\t// Save the proposal\n\t\t\tprops = append(props, prop)\n\n\t\t\treturn false\n\t\t},\n\t)\n\n\treturn props\n}\n\nfunc (s *SimpleDAO) ProposalByID(id uint64) (dao.Proposal, error) {\n\tprop, exists := s.proposals.Get(getProposalID(id))\n\tif !exists {\n\t\treturn nil, ErrMissingProposal\n\t}\n\n\treturn prop.(*proposal), nil\n}\n\nfunc (s *SimpleDAO) Size() int {\n\treturn s.proposals.Size()\n}\n\n// getProposalID generates a sequential proposal ID\n// from the given ID number\nfunc getProposalID(id uint64) string {\n\treturn seqid.ID(id).String()\n}\n" + }, + { + "name": "propstore_test.gno", + "body": "package simpledao\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// generateProposals generates dummy proposals\nfunc generateProposals(t *testing.T, count int) []*proposal {\n\tt.Helper()\n\n\tvar (\n\t\tmembers = generateMembers(t, count)\n\t\tproposals = make([]*proposal, 0, count)\n\t)\n\n\tfor i := 0; i \u003c count; i++ {\n\t\tproposal := \u0026proposal{\n\t\t\tauthor: members[i].Address,\n\t\t\tdescription: ufmt.Sprintf(\"proposal %d\", i),\n\t\t\tstatus: dao.Active,\n\t\t\ttally: newTally(),\n\t\t\tgetTotalVotingPowerFn: func() uint64 {\n\t\t\t\treturn 0\n\t\t\t},\n\t\t\texecutor: nil,\n\t\t}\n\n\t\tproposals = append(proposals, proposal)\n\t}\n\n\treturn proposals\n}\n\nfunc equalProposals(t *testing.T, p1, p2 dao.Proposal) {\n\tt.Helper()\n\n\tuassert.Equal(\n\t\tt,\n\t\tp1.Author().String(),\n\t\tp2.Author().String(),\n\t)\n\n\tuassert.Equal(\n\t\tt,\n\t\tp1.Description(),\n\t\tp2.Description(),\n\t)\n\n\tuassert.Equal(\n\t\tt,\n\t\tp1.Status().String(),\n\t\tp2.Status().String(),\n\t)\n\n\tp1Stats := p1.Stats()\n\tp2Stats := p2.Stats()\n\n\tuassert.Equal(t, p1Stats.YayVotes, p2Stats.YayVotes)\n\tuassert.Equal(t, p1Stats.NayVotes, p2Stats.NayVotes)\n\tuassert.Equal(t, p1Stats.AbstainVotes, p2Stats.AbstainVotes)\n\tuassert.Equal(t, p1Stats.TotalVotingPower, p2Stats.TotalVotingPower)\n}\n\nfunc TestProposal_Data(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"author\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\tauthor: testutils.TestAddress(\"address\"),\n\t\t}\n\n\t\tuassert.Equal(t, p.author, p.Author())\n\t})\n\n\tt.Run(\"description\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\tdescription: \"example proposal description\",\n\t\t}\n\n\t\tuassert.Equal(t, p.description, p.Description())\n\t})\n\n\tt.Run(\"status\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\tstatus: dao.ExecutionSuccessful,\n\t\t}\n\n\t\tuassert.Equal(t, p.status.String(), p.Status().String())\n\t})\n\n\tt.Run(\"executor\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tnumCalled = 0\n\t\t\tcb = func() error {\n\t\t\t\tnumCalled++\n\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tex = \u0026mockExecutor{\n\t\t\t\texecuteFn: cb,\n\t\t\t}\n\n\t\t\tp = \u0026proposal{\n\t\t\t\texecutor: ex,\n\t\t\t}\n\t\t)\n\n\t\turequire.NoError(t, p.executor.Execute())\n\t\turequire.NoError(t, p.Executor().Execute())\n\n\t\tuassert.Equal(t, 2, numCalled)\n\t})\n\n\tt.Run(\"no votes\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tp := \u0026proposal{\n\t\t\ttally: newTally(),\n\t\t\tgetTotalVotingPowerFn: func() uint64 {\n\t\t\t\treturn 0\n\t\t\t},\n\t\t}\n\n\t\tstats := p.Stats()\n\n\t\tuassert.Equal(t, uint64(0), stats.YayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.NayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.AbstainVotes)\n\t\tuassert.Equal(t, uint64(0), stats.TotalVotingPower)\n\t})\n\n\tt.Run(\"existing votes\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tmembers = generateMembers(t, 50)\n\t\t\ttotalPower = uint64(len(members)) * 10\n\n\t\t\tp = \u0026proposal{\n\t\t\t\ttally: newTally(),\n\t\t\t\tgetTotalVotingPowerFn: func() uint64 {\n\t\t\t\t\treturn totalPower\n\t\t\t\t},\n\t\t\t}\n\t\t)\n\n\t\tfor _, m := range members {\n\t\t\turequire.NoError(t, p.tally.castVote(m, dao.YesVote))\n\t\t}\n\n\t\tstats := p.Stats()\n\n\t\tuassert.Equal(t, totalPower, stats.YayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.NayVotes)\n\t\tuassert.Equal(t, uint64(0), stats.AbstainVotes)\n\t\tuassert.Equal(t, totalPower, stats.TotalVotingPower)\n\t})\n}\n\nfunc TestSimpleDAO_GetProposals(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no proposals\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := New(nil)\n\n\t\tuassert.Equal(t, 0, s.Size())\n\t\tproposals := s.Proposals(0, 0)\n\n\t\tuassert.Equal(t, 0, len(proposals))\n\t})\n\n\tt.Run(\"proper pagination\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tnumProposals = 20\n\t\t\thalfRange = numProposals / 2\n\n\t\t\ts = New(nil)\n\t\t\tproposals = generateProposals(t, numProposals)\n\t\t)\n\n\t\t// Add initial proposals\n\t\tfor _, proposal := range proposals {\n\t\t\t_, err := s.addProposal(proposal)\n\n\t\t\turequire.NoError(t, err)\n\t\t}\n\n\t\tuassert.Equal(t, numProposals, s.Size())\n\n\t\tfetchedProposals := s.Proposals(0, uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedProposals))\n\n\t\tfor index, fetchedProposal := range fetchedProposals {\n\t\t\tequalProposals(t, proposals[index], fetchedProposal)\n\t\t}\n\n\t\t// Fetch the other half\n\t\tfetchedProposals = s.Proposals(uint64(halfRange), uint64(halfRange))\n\t\turequire.Equal(t, halfRange, len(fetchedProposals))\n\n\t\tfor index, fetchedProposal := range fetchedProposals {\n\t\t\tequalProposals(t, proposals[index+halfRange], fetchedProposal)\n\t\t}\n\t})\n}\n\nfunc TestSimpleDAO_GetProposalByID(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"missing proposal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts := New(nil)\n\n\t\t_, err := s.ProposalByID(0)\n\t\tuassert.ErrorIs(t, err, ErrMissingProposal)\n\t})\n\n\tt.Run(\"proposal found\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\ts = New(nil)\n\t\t\tproposal = generateProposals(t, 1)[0]\n\t\t)\n\n\t\t// Add the initial proposal\n\t\t_, err := s.addProposal(proposal)\n\t\turequire.NoError(t, err)\n\n\t\t// Fetch the proposal\n\t\tfetchedProposal, err := s.ProposalByID(0)\n\t\turequire.NoError(t, err)\n\n\t\tequalProposals(t, proposal, fetchedProposal)\n\t})\n}\n" + }, + { + "name": "votestore.gno", + "body": "package simpledao\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n)\n\nvar ErrAlreadyVoted = errors.New(\"vote already cast\")\n\n// tally is a simple vote tally system\ntype tally struct {\n\t// tally cache to keep track of active\n\t// yes / no / abstain votes\n\tyays uint64\n\tnays uint64\n\tabstains uint64\n\n\tvoters *avl.Tree // std.Address -\u003e dao.VoteOption\n}\n\n// newTally creates a new tally system instance\nfunc newTally() *tally {\n\treturn \u0026tally{\n\t\tvoters: avl.NewTree(),\n\t}\n}\n\n// castVote casts a single vote in the name of the given member\nfunc (t *tally) castVote(member membstore.Member, option dao.VoteOption) error {\n\t// Check if the member voted already\n\taddress := member.Address.String()\n\n\t_, voted := t.voters.Get(address)\n\tif voted {\n\t\treturn ErrAlreadyVoted\n\t}\n\n\t// convert option to upper-case, like the constants are.\n\toption = dao.VoteOption(strings.ToUpper(string(option)))\n\n\t// Update the tally\n\tswitch option {\n\tcase dao.YesVote:\n\t\tt.yays += member.VotingPower\n\tcase dao.AbstainVote:\n\t\tt.abstains += member.VotingPower\n\tcase dao.NoVote:\n\t\tt.nays += member.VotingPower\n\tdefault:\n\t\tpanic(\"invalid voting option: \" + option)\n\t}\n\n\t// Save the voting status\n\tt.voters.Set(address, option)\n\n\treturn nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "BwNhxMENp6dkApHxDLFZyYjrAVhcU1eMF3JSzbzMpDdMgeRbL0dB0Wu+6tB+D89j3SfH69yH0SHRU0mDmH1rAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "stack", + "path": "gno.land/p/demo/stack", + "files": [ + { + "name": "stack.gno", + "body": "package stack\n\ntype Stack struct {\n\ttop *node\n\tlength int\n}\n\ntype node struct {\n\tvalue any\n\tprev *node\n}\n\nfunc New() *Stack {\n\treturn \u0026Stack{nil, 0}\n}\n\nfunc (s *Stack) Len() int {\n\treturn s.length\n}\n\nfunc (s *Stack) Top() any {\n\tif s.length == 0 {\n\t\treturn nil\n\t}\n\treturn s.top.value\n}\n\nfunc (s *Stack) Pop() any {\n\tif s.length == 0 {\n\t\treturn nil\n\t}\n\n\tnode := s.top\n\ts.top = node.prev\n\ts.length -= 1\n\treturn node.value\n}\n\nfunc (s *Stack) Push(value any) {\n\tnode := \u0026node{value, s.top}\n\ts.top = node\n\ts.length += 1\n}\n" + }, + { + "name": "stack_test.gno", + "body": "package stack\n\nimport \"testing\"\n\nfunc TestStack(t *testing.T) {\n\ts := New() // Empty stack\n\n\tif s.Len() != 0 {\n\t\tt.Errorf(\"s.Len(): expected 0; got %d\", s.Len())\n\t}\n\n\ts.Push(1)\n\n\tif s.Len() != 1 {\n\t\tt.Errorf(\"s.Len(): expected 1; got %d\", s.Len())\n\t}\n\n\tif top := s.Top(); top.(int) != 1 {\n\t\tt.Errorf(\"s.Top(): expected 1; got %v\", top.(int))\n\t}\n\n\tif elem := s.Pop(); elem.(int) != 1 {\n\t\tt.Errorf(\"s.Pop(): expected 1; got %v\", elem.(int))\n\t}\n\tif s.Len() != 0 {\n\t\tt.Errorf(\"s.Len(): expected 0; got %d\", s.Len())\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "xI+NnRoBM2ggQ2AC0tC2meAARaPQhDRRz/O8QEbWgEkWnlFANxgpLI4SZ+Qh8GQcUkbnUfPPV5X51LrOr4GUCQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "subscription", + "path": "gno.land/p/demo/subscription", + "files": [ + { + "name": "doc.gno", + "body": "// Package subscription provides a flexible system for managing both recurring and\n// lifetime subscriptions in Gno applications. It enables developers to handle\n// payment-based access control for services or products. The library supports\n// both subscriptions requiring periodic payments (recurring) and one-time payments\n// (lifetime). Subscriptions are tracked using an AVL tree for efficient management\n// of subscription statuses.\n//\n// Usage:\n//\n// Import the required sub-packages (`recurring` and/or `lifetime`) to manage specific\n// subscription types. The methods provided allow users to subscribe, check subscription\n// status, and manage payments.\n//\n// Recurring Subscription:\n//\n// Recurring subscriptions require periodic payments to maintain access.\n// Users pay to extend their access for a specific duration.\n//\n// Example:\n//\n//\t// Create a recurring subscription requiring 100 ugnot every 30 days\n//\trecSub := recurring.NewRecurringSubscription(time.Hour * 24 * 30, 100)\n//\n//\t// Process payment for the recurring subscription\n//\trecSub.Subscribe()\n//\n//\t// Gift a recurring subscription to another user\n//\trecSub.GiftSubscription(recipientAddress)\n//\n//\t// Check if a user has a valid subscription\n//\trecSub.HasValidSubscription(addr)\n//\n//\t// Get the expiration date of the subscription\n//\trecSub.GetExpiration(caller)\n//\n//\t// Update the subscription amount to 200 ugnot\n//\trecSub.UpdateAmount(200)\n//\n//\t// Get the current subscription amount\n//\trecSub.GetAmount()\n//\n// Lifetime Subscription:\n//\n// Lifetime subscriptions require a one-time payment for permanent access.\n// Once paid, users have indefinite access without further payments.\n//\n// Example:\n//\n//\t// Create a lifetime subscription costing 500 ugnot\n//\tlifeSub := lifetime.NewLifetimeSubscription(500)\n//\n//\t// Process payment for lifetime access\n//\tlifeSub.Subscribe()\n//\n//\t// Gift a lifetime subscription to another user\n//\tlifeSub.GiftSubscription(recipientAddress)\n//\n//\t// Check if a user has a valid subscription\n//\tlifeSub.HasValidSubscription(addr)\n//\n//\t// Update the lifetime subscription amount to 1000 ugnot\n//\tlifeSub.UpdateAmount(1000)\n//\n//\t// Get the current lifetime subscription amount\n//\tlifeSub.GetAmount()\npackage subscription\n" + }, + { + "name": "subscription.gno", + "body": "package subscription\n\nimport (\n\t\"std\"\n)\n\n// Subscription interface defines standard methods that all subscription types must implement.\ntype Subscription interface {\n\tHasValidSubscription(std.Address) error\n\tSubscribe() error\n\tUpdateAmount(newAmount int64) error\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "JTeBws49+YnAIoKBTvwCkwMTism7uj3hjZejsm0t0/Yx2fQBcbdp0Oy/YMEnMkVCpLi8I5KOku+bbUh+aZmHAA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "lifetime", + "path": "gno.land/p/demo/subscription/lifetime", + "files": [ + { + "name": "errors.gno", + "body": "package lifetime\n\nimport \"errors\"\n\nvar (\n\tErrNoSub = errors.New(\"lifetime subscription: no active subscription found\")\n\tErrAmt = errors.New(\"lifetime subscription: payment amount does not match the required subscription amount\")\n\tErrAlreadySub = errors.New(\"lifetime subscription: this address already has an active lifetime subscription\")\n\tErrNotAuthorized = errors.New(\"lifetime subscription: action not authorized\")\n)\n" + }, + { + "name": "lifetime.gno", + "body": "package lifetime\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\n// LifetimeSubscription represents a subscription that requires only a one-time payment.\n// It grants permanent access to a service or product.\ntype LifetimeSubscription struct {\n\townable.Ownable\n\tamount int64\n\tsubs *avl.Tree // std.Address -\u003e bool\n}\n\n// NewLifetimeSubscription creates and returns a new lifetime subscription.\nfunc NewLifetimeSubscription(amount int64) *LifetimeSubscription {\n\treturn \u0026LifetimeSubscription{\n\t\tOwnable: *ownable.New(),\n\t\tamount: amount,\n\t\tsubs: avl.NewTree(),\n\t}\n}\n\n// processSubscription handles the subscription process for a given receiver.\nfunc (ls *LifetimeSubscription) processSubscription(receiver std.Address) error {\n\tamount := std.OriginSend()\n\n\tif amount.AmountOf(\"ugnot\") != ls.amount {\n\t\treturn ErrAmt\n\t}\n\n\t_, exists := ls.subs.Get(receiver.String())\n\n\tif exists {\n\t\treturn ErrAlreadySub\n\t}\n\n\tls.subs.Set(receiver.String(), true)\n\n\treturn nil\n}\n\n// Subscribe processes the payment for a lifetime subscription.\nfunc (ls *LifetimeSubscription) Subscribe() error {\n\tcaller := std.PreviousRealm().Address()\n\treturn ls.processSubscription(caller)\n}\n\n// GiftSubscription allows the caller to pay for a lifetime subscription for another user.\nfunc (ls *LifetimeSubscription) GiftSubscription(receiver std.Address) error {\n\treturn ls.processSubscription(receiver)\n}\n\n// HasValidSubscription checks if the given address has an active lifetime subscription.\nfunc (ls *LifetimeSubscription) HasValidSubscription(addr std.Address) error {\n\t_, exists := ls.subs.Get(addr.String())\n\n\tif !exists {\n\t\treturn ErrNoSub\n\t}\n\n\treturn nil\n}\n\n// UpdateAmount allows the owner of the LifetimeSubscription contract to update the subscription price.\nfunc (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error {\n\tif !ls.CallerIsOwner() {\n\t\treturn ErrNotAuthorized\n\t}\n\n\tls.amount = newAmount\n\treturn nil\n}\n\n// GetAmount returns the current subscription price.\nfunc (ls *LifetimeSubscription) GetAmount() int64 {\n\treturn ls.amount\n}\n" + }, + { + "name": "lifetime_test.gno", + "body": "package lifetime\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestLifetimeSubscription(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr := ls.Subscribe()\n\tuassert.NoError(t, err, \"Expected ProcessPayment to succeed\")\n\n\terr = ls.HasValidSubscription(std.PreviousRealm().Address())\n\tuassert.NoError(t, err, \"Expected Alice to have access\")\n}\n\nfunc TestLifetimeSubscriptionGift(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\tls := NewLifetimeSubscription(1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr := ls.GiftSubscription(bob)\n\tuassert.NoError(t, err, \"Expected ProcessPaymentGift to succeed for Bob\")\n\n\terr = ls.HasValidSubscription(bob)\n\tuassert.NoError(t, err, \"Expected Bob to have access\")\n\n\terr = ls.HasValidSubscription(charlie)\n\tuassert.Error(t, err, \"Expected Charlie to fail access check\")\n}\n\nfunc TestUpdateAmountAuthorization(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\tls := NewLifetimeSubscription(1000)\n\n\terr := ls.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\ttesting.SetOriginCaller(bob)\n\n\terr = ls.UpdateAmount(3000)\n\tuassert.Error(t, err, \"Expected Bob to fail when updating amount\")\n}\n\nfunc TestIncorrectPaymentAmount(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\tls := NewLifetimeSubscription(1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 500}})\n\terr := ls.Subscribe()\n\tuassert.Error(t, err, \"Expected payment to fail with incorrect amount\")\n}\n\nfunc TestMultipleSubscriptionAttempts(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\tls := NewLifetimeSubscription(1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr := ls.Subscribe()\n\tuassert.NoError(t, err, \"Expected first subscription to succeed\")\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr = ls.Subscribe()\n\tuassert.Error(t, err, \"Expected second subscription to fail as Alice is already subscribed\")\n}\n\nfunc TestGiftSubscriptionWithIncorrectAmount(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\tls := NewLifetimeSubscription(1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 500}})\n\terr := ls.GiftSubscription(bob)\n\tuassert.Error(t, err, \"Expected gift subscription to fail with incorrect amount\")\n\n\terr = ls.HasValidSubscription(bob)\n\tuassert.Error(t, err, \"Expected Bob to not have access after incorrect gift subscription\")\n}\n\nfunc TestUpdateAmountEffectiveness(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\tls := NewLifetimeSubscription(1000)\n\n\terr := ls.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr = ls.Subscribe()\n\tuassert.Error(t, err, \"Expected subscription to fail with old amount after update\")\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 2000}})\n\terr = ls.Subscribe()\n\tuassert.NoError(t, err, \"Expected subscription to succeed with new amount\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "+XKKn46NAnGeVEJ8Wi7S5yQzK3iAenmhZ/w1UL7f0a0zQJRR600dJXHmwmF7pqBOAz+9Ulf1eh1co+DCAQ+qDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "recurring", + "path": "gno.land/p/demo/subscription/recurring", + "files": [ + { + "name": "errors.gno", + "body": "package recurring\n\nimport \"errors\"\n\nvar (\n\tErrNoSub = errors.New(\"recurring subscription: no active subscription found\")\n\tErrSubExpired = errors.New(\"recurring subscription: your subscription has expired\")\n\tErrAmt = errors.New(\"recurring subscription: payment amount does not match the required subscription amount\")\n\tErrAlreadySub = errors.New(\"recurring subscription: this address already has an active subscription\")\n\tErrNotAuthorized = errors.New(\"recurring subscription: action not authorized\")\n)\n" + }, + { + "name": "recurring.gno", + "body": "package recurring\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\n// RecurringSubscription represents a subscription that requires periodic payments.\n// It includes the duration of the subscription and the amount required per period.\ntype RecurringSubscription struct {\n\townable.Ownable\n\tduration time.Duration\n\tamount int64\n\tsubs *avl.Tree // std.Address -\u003e time.Time\n}\n\n// NewRecurringSubscription creates and returns a new recurring subscription.\nfunc NewRecurringSubscription(duration time.Duration, amount int64) *RecurringSubscription {\n\treturn \u0026RecurringSubscription{\n\t\tOwnable: *ownable.New(),\n\t\tduration: duration,\n\t\tamount: amount,\n\t\tsubs: avl.NewTree(),\n\t}\n}\n\n// HasValidSubscription verifies if the caller has an active recurring subscription.\nfunc (rs *RecurringSubscription) HasValidSubscription(addr std.Address) error {\n\texpTime, exists := rs.subs.Get(addr.String())\n\tif !exists {\n\t\treturn ErrNoSub\n\t}\n\n\tif time.Now().After(expTime.(time.Time)) {\n\t\treturn ErrSubExpired\n\t}\n\n\treturn nil\n}\n\n// processSubscription processes the payment for a given receiver and renews or adds their subscription.\nfunc (rs *RecurringSubscription) processSubscription(receiver std.Address) error {\n\tamount := std.OriginSend()\n\n\tif amount.AmountOf(\"ugnot\") != rs.amount {\n\t\treturn ErrAmt\n\t}\n\n\texpTime, exists := rs.subs.Get(receiver.String())\n\n\t// If the user is already a subscriber but his subscription has expired, authorize renewal\n\tif exists {\n\t\texpiration := expTime.(time.Time)\n\t\tif time.Now().Before(expiration) {\n\t\t\treturn ErrAlreadySub\n\t\t}\n\t}\n\n\t// Renew or add subscription\n\tnewExpiration := time.Now().Add(rs.duration)\n\trs.subs.Set(receiver.String(), newExpiration)\n\n\treturn nil\n}\n\n// Subscribe handles the payment for the caller's subscription.\nfunc (rs *RecurringSubscription) Subscribe() error {\n\tcaller := std.PreviousRealm().Address()\n\n\treturn rs.processSubscription(caller)\n}\n\n// GiftSubscription allows the user to pay for a subscription for another user (receiver).\nfunc (rs *RecurringSubscription) GiftSubscription(receiver std.Address) error {\n\treturn rs.processSubscription(receiver)\n}\n\n// GetExpiration returns the expiration date of the recurring subscription for a given caller.\nfunc (rs *RecurringSubscription) GetExpiration(addr std.Address) (time.Time, error) {\n\texpTime, exists := rs.subs.Get(addr.String())\n\tif !exists {\n\t\treturn time.Time{}, ErrNoSub\n\t}\n\n\treturn expTime.(time.Time), nil\n}\n\n// UpdateAmount allows the owner of the subscription contract to change the required subscription amount.\nfunc (rs *RecurringSubscription) UpdateAmount(newAmount int64) error {\n\tif !rs.CallerIsOwner() {\n\t\treturn ErrNotAuthorized\n\t}\n\n\trs.amount = newAmount\n\treturn nil\n}\n\n// GetAmount returns the current amount required for each subscription period.\nfunc (rs *RecurringSubscription) GetAmount() int64 {\n\treturn rs.amount\n}\n" + }, + { + "name": "recurring_test.gno", + "body": "package recurring\n\nimport (\n\t\"std\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n)\n\nfunc TestRecurringSubscription(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PreviousRealm().Address())\n\tuassert.NoError(t, err, \"Expected Alice to have access\")\n\n\t_, err = rs.GetExpiration(std.PreviousRealm().Address())\n\tuassert.NoError(t, err, \"Expected to get expiration for Alice\")\n}\n\nfunc TestRecurringSubscriptionGift(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr := rs.GiftSubscription(bob)\n\tuassert.NoError(t, err, \"Expected ProcessPaymentGift to succeed for Bob\")\n\n\terr = rs.HasValidSubscription(bob)\n\tuassert.NoError(t, err, \"Expected Bob to have access\")\n\n\terr = rs.HasValidSubscription(charlie)\n\tuassert.Error(t, err, \"Expected Charlie to fail access check\")\n}\n\nfunc TestRecurringSubscriptionExpiration(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\trs := NewRecurringSubscription(time.Hour, 1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PreviousRealm().Address())\n\tuassert.NoError(t, err, \"Expected Alice to have access\")\n\n\texpiration := time.Now().Add(-time.Hour * 2)\n\trs.subs.Set(std.PreviousRealm().Address().String(), expiration)\n\n\terr = rs.HasValidSubscription(std.PreviousRealm().Address())\n\tuassert.Error(t, err, \"Expected Alice's subscription to be expired\")\n}\n\nfunc TestUpdateAmountAuthorization(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\terr := rs.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\ttesting.SetOriginCaller(bob)\n\terr = rs.UpdateAmount(3000)\n\tuassert.Error(t, err, \"Expected Bob to fail when updating amount\")\n}\n\nfunc TestGetAmount(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\tamount := rs.GetAmount()\n\tuassert.Equal(t, amount, int64(1000), \"Expected the initial amount to be 1000 ugnot\")\n\n\terr := rs.UpdateAmount(2000)\n\tuassert.NoError(t, err, \"Expected Alice to succeed in updating amount\")\n\n\tamount = rs.GetAmount()\n\tuassert.Equal(t, amount, int64(2000), \"Expected the updated amount to be 2000 ugnot\")\n}\n\nfunc TestIncorrectPaymentAmount(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 500}})\n\terr := rs.Subscribe()\n\tuassert.Error(t, err, \"Expected payment with incorrect amount to fail\")\n}\n\nfunc TestMultiplePaymentsForSameUser(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\trs := NewRecurringSubscription(time.Hour*24, 1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected first ProcessPayment to succeed for Alice\")\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr = rs.Subscribe()\n\tuassert.Error(t, err, \"Expected second ProcessPayment to fail for Alice due to existing subscription\")\n}\n\nfunc TestRecurringSubscriptionWithMultiplePayments(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\trs := NewRecurringSubscription(time.Hour, 1000)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr := rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected first ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PreviousRealm().Address())\n\tuassert.NoError(t, err, \"Expected Alice to have access after first payment\")\n\n\texpiration := time.Now().Add(-time.Hour * 2)\n\trs.subs.Set(std.PreviousRealm().Address().String(), expiration)\n\n\ttesting.SetOriginSend([]std.Coin{{Denom: \"ugnot\", Amount: 1000}})\n\terr = rs.Subscribe()\n\tuassert.NoError(t, err, \"Expected second ProcessPayment to succeed for Alice\")\n\n\terr = rs.HasValidSubscription(std.PreviousRealm().Address())\n\tuassert.NoError(t, err, \"Expected Alice to have access after second payment\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "MmvMN4e3EP6bl+4znc5boJ4BI2oa5JAOlvsFVwhZW0D/q33xahlPKA78A3bT65MlCa8Zd0aPnPxIm5gef49CBQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "svg", + "path": "gno.land/p/demo/svg", + "files": [ + { + "name": "doc.gno", + "body": "/*\nPackage svg is a minimalist SVG generation library for Gno.\n\nThe svg package provides a simple and lightweight solution for programmatically generating SVG (Scalable Vector Graphics) markup in Gno. It allows you to create basic shapes like rectangles and circles, and output the generated SVG to a\n\nExample:\n\n\timport \"gno.land/p/demo/svg\"\"\n\n\tfunc Foo() string {\n\t canvas := svg.Canvas{Width: 200, Height: 200}\n\t canvas.DrawRectangle(50, 50, 100, 100, \"red\")\n\t canvas.DrawCircle(100, 100, 50, \"blue\")\n\t return canvas.String()\n\t}\n*/\npackage svg // import \"gno.land/p/demo/svg\"\n" + }, + { + "name": "svg.gno", + "body": "package svg\n\nimport \"gno.land/p/demo/ufmt\"\n\ntype Canvas struct {\n\tWidth int\n\tHeight int\n\tElems []Elem\n}\n\ntype Elem interface{ String() string }\n\nfunc (c Canvas) String() string {\n\toutput := \"\"\n\toutput += ufmt.Sprintf(`\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\"\u003e`, c.Width, c.Height)\n\tfor _, elem := range c.Elems {\n\t\toutput += elem.String()\n\t}\n\toutput += \"\u003c/svg\u003e\"\n\treturn output\n}\n\nfunc (c *Canvas) Append(elem Elem) {\n\tc.Elems = append(c.Elems, elem)\n}\n\ntype Circle struct {\n\tCX int // center X\n\tCY int // center Y\n\tR int // radius\n\tFill string\n}\n\nfunc (c Circle) String() string {\n\treturn ufmt.Sprintf(`\u003ccircle cx=\"%d\" cy=\"%d\" r=\"%d\" fill=\"%s\" /\u003e`, c.CX, c.CY, c.R, c.Fill)\n}\n\nfunc (c *Canvas) DrawCircle(cx, cy, r int, fill string) {\n\tc.Append(Circle{\n\t\tCX: cx,\n\t\tCY: cy,\n\t\tR: r,\n\t\tFill: fill,\n\t})\n}\n\ntype Rectangle struct {\n\tX, Y, Width, Height int\n\tFill string\n}\n\nfunc (c Rectangle) String() string {\n\treturn ufmt.Sprintf(`\u003crect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" fill=\"%s\" /\u003e`, c.X, c.Y, c.Width, c.Height, c.Fill)\n}\n\nfunc (c *Canvas) DrawRectangle(x, y, width, height int, fill string) {\n\tc.Append(Rectangle{\n\t\tX: x,\n\t\tY: y,\n\t\tWidth: width,\n\t\tHeight: height,\n\t\tFill: fill,\n\t})\n}\n\ntype Text struct {\n\tX, Y int\n\tText, Fill string\n}\n\nfunc (c Text) String() string {\n\treturn ufmt.Sprintf(`\u003ctext x=\"%d\" y=\"%d\" fill=\"%s\"\u003e%s\u003c/text\u003e`, c.X, c.Y, c.Fill, c.Text)\n}\n\nfunc (c *Canvas) DrawText(x, y int, text, fill string) {\n\tc.Append(Text{\n\t\tX: x,\n\t\tY: y,\n\t\tText: text,\n\t\tFill: fill,\n\t})\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.DrawRectangle(50, 50, 100, 100, \"red\")\n\tcanvas.DrawCircle(100, 100, 50, \"blue\")\n\tcanvas.DrawText(100, 100, \"hello world!\", \"magenta\")\n\tprintln(canvas)\n}\n\n// Output:\n// \u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"500\" height=\"500\"\u003e\u003crect x=\"50\" y=\"50\" width=\"100\" height=\"100\" fill=\"red\" /\u003e\u003ccircle cx=\"100\" cy=\"100\" r=\"50\" fill=\"blue\" /\u003e\u003ctext x=\"100\" y=\"100\" fill=\"magenta\"\u003ehello world!\u003c/text\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{50, 50, 100, 100, \"red\"},\n\t\t\tsvg.Circle{50, 50, 100, \"red\"},\n\t\t\tsvg.Text{100, 100, \"hello world!\", \"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\"\u003e\u003crect x=\"50\" y=\"50\" width=\"100\" height=\"100\" fill=\"red\" /\u003e\u003ccircle cx=\"50\" cy=\"50\" r=\"100\" fill=\"red\" /\u003e\u003ctext x=\"100\" y=\"100\" fill=\"magenta\"\u003ehello world!\u003c/text\u003e\u003c/svg\u003e\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "kPQi3hh21YyD0J5GU5itH4NBSv0ti7rVqZIE90bokLhQDDy4N1Izro2mK6MnvBe7/TKkiS03sAVsNmT5bj8VBQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "tamagotchi", + "path": "gno.land/p/demo/tamagotchi", + "files": [ + { + "name": "tamagotchi.gno", + "body": "package tamagotchi\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Tamagotchi structure\ntype Tamagotchi struct {\n\tname string\n\thunger int\n\thappiness int\n\thealth int\n\tage int\n\tmaxAge int\n\tsleepy int\n\tcreated time.Time\n\tlastUpdated time.Time\n}\n\nfunc New(name string) *Tamagotchi {\n\tnow := time.Now()\n\treturn \u0026Tamagotchi{\n\t\tname: name,\n\t\thunger: 50,\n\t\thappiness: 50,\n\t\thealth: 50,\n\t\tmaxAge: 100,\n\t\tlastUpdated: now,\n\t\tcreated: now,\n\t}\n}\n\nfunc (t *Tamagotchi) Name() string {\n\tt.update()\n\treturn t.name\n}\n\nfunc (t *Tamagotchi) Hunger() int {\n\tt.update()\n\treturn t.hunger\n}\n\nfunc (t *Tamagotchi) Happiness() int {\n\tt.update()\n\treturn t.happiness\n}\n\nfunc (t *Tamagotchi) Health() int {\n\tt.update()\n\treturn t.health\n}\n\nfunc (t *Tamagotchi) Age() int {\n\tt.update()\n\treturn t.age\n}\n\nfunc (t *Tamagotchi) Sleepy() int {\n\tt.update()\n\treturn t.sleepy\n}\n\n// Feed method for Tamagotchi\nfunc (t *Tamagotchi) Feed() {\n\tt.update()\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.hunger = bound(t.hunger-10, 0, 100)\n}\n\n// Play method for Tamagotchi\nfunc (t *Tamagotchi) Play() {\n\tt.update()\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.happiness = bound(t.happiness+10, 0, 100)\n}\n\n// Heal method for Tamagotchi\nfunc (t *Tamagotchi) Heal() {\n\tt.update()\n\n\tif t.dead() {\n\t\treturn\n\t}\n\tt.health = bound(t.health+10, 0, 100)\n}\n\nfunc (t Tamagotchi) dead() bool { return t.health == 0 }\n\n// Update applies changes based on the duration since the last update\nfunc (t *Tamagotchi) update() {\n\tif t.dead() {\n\t\treturn\n\t}\n\n\tnow := time.Now()\n\tif t.lastUpdated == now {\n\t\treturn\n\t}\n\n\tduration := now.Sub(t.lastUpdated)\n\telapsedMins := int(duration.Minutes())\n\n\tt.hunger = bound(t.hunger+elapsedMins, 0, 100)\n\tt.happiness = bound(t.happiness-elapsedMins, 0, 100)\n\tt.health = bound(t.health-elapsedMins, 0, 100)\n\tt.sleepy = bound(t.sleepy+elapsedMins, 0, 100)\n\n\t// age is hours since created\n\tt.age = int(now.Sub(t.created).Hours())\n\tif t.age \u003e t.maxAge {\n\t\tt.age = t.maxAge\n\t\tt.health = 0\n\t}\n\tif t.health == 0 {\n\t\tt.sleepy = 0\n\t\tt.happiness = 0\n\t\tt.hunger = 0\n\t}\n\n\tt.lastUpdated = now\n}\n\n// Face returns an ASCII art representation of the Tamagotchi's current state\nfunc (t *Tamagotchi) Face() string {\n\tt.update()\n\treturn t.face()\n}\n\nfunc (t *Tamagotchi) face() string {\n\tswitch {\n\tcase t.health == 0:\n\t\treturn \"😵\" // dead face\n\tcase t.health \u003c 30:\n\t\treturn \"😷\" // sick face\n\tcase t.happiness \u003c 30:\n\t\treturn \"😢\" // sad face\n\tcase t.hunger \u003e 70:\n\t\treturn \"😫\" // hungry face\n\tcase t.sleepy \u003e 70:\n\t\treturn \"😴\" // sleepy face\n\tdefault:\n\t\treturn \"😃\" // happy face\n\t}\n}\n\n// Markdown method for Tamagotchi\nfunc (t *Tamagotchi) Markdown() string {\n\tt.update()\n\treturn ufmt.Sprintf(`# %s %s\n\n* age: %d\n* hunger: %d\n* happiness: %d\n* health: %d\n* sleepy: %d`,\n\t\tt.name, t.Face(),\n\t\tt.age, t.hunger, t.happiness, t.health, t.sleepy,\n\t)\n}\n\nfunc bound(n, min, max int) int {\n\tif n \u003c min {\n\t\treturn min\n\t}\n\tif n \u003e max {\n\t\treturn max\n\t}\n\treturn n\n}\n" + }, + { + "name": "z0_filetest.gno", + "body": "package main\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"gno.land/p/demo/tamagotchi\"\n)\n\nfunc main() {\n\tt := tamagotchi.New(\"Gnome\")\n\n\tprintln(\"\\n-- INITIAL\\n\")\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 minutes\\n\")\n\tos.Sleep(20 * time.Minute)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- FEEDx3, PLAYx2, HEALx4\\n\")\n\tt.Feed()\n\tt.Feed()\n\tt.Feed()\n\tt.Play()\n\tt.Play()\n\tt.Heal()\n\tt.Heal()\n\tt.Heal()\n\tt.Heal()\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 minutes\\n\")\n\tos.Sleep(20 * time.Minute)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 hours\\n\")\n\tos.Sleep(20 * time.Hour)\n\tprintln(t.Markdown())\n\n\tprintln(\"\\n-- WAIT 20 hours\\n\")\n\tos.Sleep(20 * time.Hour)\n\tprintln(t.Markdown())\n}\n\n// Output:\n//\n// -- INITIAL\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 50\n// * happiness: 50\n// * health: 50\n// * sleepy: 0\n//\n// -- WAIT 20 minutes\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 70\n// * happiness: 30\n// * health: 30\n// * sleepy: 20\n//\n// -- FEEDx3, PLAYx2, HEALx4\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 40\n// * happiness: 50\n// * health: 70\n// * sleepy: 20\n//\n// -- WAIT 20 minutes\n//\n// # Gnome 😃\n//\n// * age: 0\n// * hunger: 60\n// * happiness: 30\n// * health: 50\n// * sleepy: 40\n//\n// -- WAIT 20 hours\n//\n// # Gnome 😵\n//\n// * age: 20\n// * hunger: 0\n// * happiness: 0\n// * health: 0\n// * sleepy: 0\n//\n// -- WAIT 20 hours\n//\n// # Gnome 😵\n//\n// * age: 20\n// * hunger: 0\n// * happiness: 0\n// * health: 0\n// * sleepy: 0\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "5ULNb1HJSXS9+7nuC6iImmaaYTv5TPJLyS5HY0JLIUZ8A6J4rtCV4r9xYIBF2XVcH1MBE14Jrg55AhwGhHgeAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "subtests", + "path": "gno.land/p/demo/tests/subtests", + "files": [ + { + "name": "subtests.gno", + "body": "package subtests\n\nimport (\n\t\"std\"\n)\n\nfunc GetCurrentRealm() std.Realm {\n\treturn std.CurrentRealm()\n}\n\nfunc GetPreviousRealm() std.Realm {\n\treturn std.PreviousRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "MSsj9cbuQ8AYA8kh65XjISXSuzPv586m/myiG5EmRUIJOVBsEAAY+fZ7fYZxN/GngLuP3HB/LyK0BJ0JkgSLDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "tests", + "path": "gno.land/p/demo/tests", + "files": [ + { + "name": "README.md", + "body": "Modules here are only useful for file realm tests.\nThey can be safely ignored for other purposes.\n" + }, + { + "name": "tests.gno", + "body": "package tests\n\nimport (\n\t\"std\"\n\n\tpsubtests \"gno.land/p/demo/tests/subtests\"\n)\n\nconst World = \"world\"\n\nfunc CurrentRealmPath() string {\n\treturn std.CurrentRealm().PkgPath()\n}\n\n//----------------------------------------\n// cross realm test vars\n\ntype TestRealmObject2 struct {\n\tField string\n}\n\nfunc (o2 *TestRealmObject2) Modify() {\n\to2.Field = \"modified\"\n}\n\nvar (\n\tsomevalue1 TestRealmObject2\n\tSomeValue2 TestRealmObject2\n\tSomeValue3 *TestRealmObject2\n)\n\nfunc init() {\n\tsomevalue1 = TestRealmObject2{Field: \"init\"}\n\tSomeValue2 = TestRealmObject2{Field: \"init\"}\n\tSomeValue3 = \u0026TestRealmObject2{Field: \"init\"}\n}\n\nfunc ModifyTestRealmObject2a() {\n\tsomevalue1.Field = \"modified\"\n}\n\nfunc ModifyTestRealmObject2b() {\n\tSomeValue2.Field = \"modified\"\n}\n\nfunc ModifyTestRealmObject2c() {\n\tSomeValue3.Field = \"modified\"\n}\n\nfunc GetPreviousRealm() std.Realm {\n\treturn std.PreviousRealm()\n}\n\nfunc GetPSubtestsPreviousRealm() std.Realm {\n\treturn psubtests.GetPreviousRealm()\n}\n\n// Warning: unsafe pattern.\nfunc Exec(fn func()) {\n\tfn()\n}\n" + }, + { + "name": "tests_test.gno", + "body": "package tests_test\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/tests\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar World = \"WORLD\"\n\nfunc TestGetHelloWorld(t *testing.T) {\n\t// tests.World is 'world'\n\ts := \"hello \" + tests.World + World\n\tconst want = \"hello worldWORLD\"\n\n\tuassert.Equal(t, want, s)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "l2s1YdMv7mJ/YYr21g3J4DHgxDA5UGWHneaTcrmSVtTobvzFpiDfH4bjE+1cyt7tIKtKdV00NVcsrv+R0rcSCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "p_crossrealm", + "path": "gno.land/p/demo/tests/p_crossrealm", + "files": [ + { + "name": "p_crossrealm.gno", + "body": "package p_crossrealm\n\ntype Stringer interface {\n\tString() string\n}\n\ntype Container struct {\n\tA int\n\tB Stringer\n}\n\nfunc (c *Container) Touch() *Container {\n\tc.A += 1\n\treturn c\n}\n\nfunc (c *Container) Print() {\n\tprintln(\"A:\", c.A)\n\tif c.B == nil {\n\t\tprintln(\"B: undefined\")\n\t} else {\n\t\tprintln(\"B:\", c.B.String())\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "hYjvM/L8Ls/eGmjeI4LI8W8M6l4moCwrZDO0yhlepuKYpy+zubbiMoeTNZAm6IFDjboxsHFu3Nj8JrU2LPrMAA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "todolist", + "path": "gno.land/p/demo/todolist", + "files": [ + { + "name": "todolist.gno", + "body": "package todolist\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype TodoList struct {\n\tTitle string\n\tTasks *avl.Tree\n\tOwner std.Address\n}\n\ntype Task struct {\n\tTitle string\n\tDone bool\n}\n\nfunc NewTodoList(title string) *TodoList {\n\treturn \u0026TodoList{\n\t\tTitle: title,\n\t\tTasks: avl.NewTree(),\n\t\tOwner: std.OriginCaller(),\n\t}\n}\n\nfunc NewTask(title string) *Task {\n\treturn \u0026Task{\n\t\tTitle: title,\n\t\tDone: false,\n\t}\n}\n\nfunc (tl *TodoList) AddTask(id int, task *Task) {\n\ttl.Tasks.Set(strconv.Itoa(id), task)\n}\n\nfunc ToggleTaskStatus(task *Task) {\n\ttask.Done = !task.Done\n}\n\nfunc (tl *TodoList) RemoveTask(taskId string) {\n\ttl.Tasks.Remove(taskId)\n}\n\nfunc (tl *TodoList) GetTasks() []*Task {\n\ttasks := make([]*Task, 0, tl.Tasks.Size())\n\ttl.Tasks.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\ttasks = append(tasks, value.(*Task))\n\t\treturn false\n\t})\n\treturn tasks\n}\n\nfunc (tl *TodoList) GetTodolistOwner() std.Address {\n\treturn tl.Owner\n}\n\nfunc (tl *TodoList) GetTodolistTitle() string {\n\treturn tl.Title\n}\n" + }, + { + "name": "todolist_test.gno", + "body": "package todolist\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestNewTodoList(t *testing.T) {\n\ttitle := \"My Todo List\"\n\ttodoList := NewTodoList(title)\n\n\tuassert.Equal(t, title, todoList.GetTodolistTitle())\n\tuassert.Equal(t, 0, len(todoList.GetTasks()))\n\tuassert.Equal(t, std.OriginCaller().String(), todoList.GetTodolistOwner().String())\n}\n\nfunc TestNewTask(t *testing.T) {\n\ttitle := \"My Task\"\n\ttask := NewTask(title)\n\n\tuassert.Equal(t, title, task.Title)\n\tuassert.False(t, task.Done, \"Expected task to be not done, but it is done\")\n}\n\nfunc TestAddTask(t *testing.T) {\n\ttodoList := NewTodoList(\"My Todo List\")\n\ttask := NewTask(\"My Task\")\n\n\ttodoList.AddTask(1, task)\n\n\ttasks := todoList.GetTasks()\n\n\tuassert.Equal(t, 1, len(tasks))\n\tuassert.True(t, tasks[0] == task, \"Task does not match\")\n}\n\nfunc TestToggleTaskStatus(t *testing.T) {\n\ttask := NewTask(\"My Task\")\n\n\tToggleTaskStatus(task)\n\tuassert.True(t, task.Done, \"Expected task to be done, but it is not done\")\n\n\tToggleTaskStatus(task)\n\tuassert.False(t, task.Done, \"Expected task to be done, but it is not done\")\n}\n\nfunc TestRemoveTask(t *testing.T) {\n\ttodoList := NewTodoList(\"My Todo List\")\n\ttask := NewTask(\"My Task\")\n\ttodoList.AddTask(1, task)\n\n\ttodoList.RemoveTask(\"1\")\n\n\ttasks := todoList.GetTasks()\n\tuassert.Equal(t, 0, len(tasks))\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "g2YEC4TPpRQpxQJs2yZUenprvb6TmVvAOjjheQPQL4rZmwmcrUFrx4nGB5FHYRD55U0Zz8FUqJKpq6DMIJc9Cg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "ui", + "path": "gno.land/p/demo/ui", + "files": [ + { + "name": "ui.gno", + "body": "package ui\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\ntype DOM struct {\n\t// metadata\n\tPrefix string\n\tTitle string\n\tWithComments bool\n\tClasses []string\n\n\t// elements\n\tHeader Element\n\tBody Element\n\tFooter Element\n}\n\nfunc (dom DOM) String() string {\n\tclasses := strings.Join(dom.Classes, \" \")\n\n\toutput := \"\"\n\n\tif classes != \"\" {\n\t\toutput += \"\u003cmain class='\" + classes + \"'\u003e\" + \"\\n\\n\"\n\t}\n\n\tif dom.Title != \"\" {\n\t\toutput += H1(dom.Title).String(dom) + \"\\n\"\n\t}\n\n\tif header := dom.Header.String(dom); header != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- header --\u003e\"\n\t\t}\n\t\toutput += header + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /header --\u003e\"\n\t\t}\n\t}\n\n\tif body := dom.Body.String(dom); body != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- body --\u003e\"\n\t\t}\n\t\toutput += body + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /body --\u003e\"\n\t\t}\n\t}\n\n\tif footer := dom.Footer.String(dom); footer != \"\" {\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- footer --\u003e\"\n\t\t}\n\t\toutput += footer + \"\\n\"\n\t\tif dom.WithComments {\n\t\t\toutput += \"\u003c!-- /footer --\u003e\"\n\t\t}\n\t}\n\n\tif classes != \"\" {\n\t\toutput += \"\u003c/main\u003e\"\n\t}\n\n\t// TODO: cleanup double new-lines.\n\n\treturn output\n}\n\ntype Jumbotron []DomStringer\n\nfunc (j Jumbotron) String(dom DOM) string {\n\toutput := `\u003cdiv class=\"jumbotron\"\u003e` + \"\\n\\n\"\n\tfor _, elem := range j {\n\t\toutput += elem.String(dom) + \"\\n\"\n\t}\n\toutput += `\u003c/div\u003e\u003c!-- /jumbotron --\u003e` + \"\\n\"\n\treturn output\n}\n\n// XXX: rename Element to Div?\ntype Element []DomStringer\n\nfunc (e *Element) Append(elems ...DomStringer) {\n\t*e = append(*e, elems...)\n}\n\nfunc (e *Element) String(dom DOM) string {\n\toutput := \"\"\n\tfor _, elem := range *e {\n\t\toutput += elem.String(dom) + \"\\n\"\n\t}\n\treturn output\n}\n\ntype Breadcrumb []DomStringer\n\nfunc (b *Breadcrumb) Append(elems ...DomStringer) {\n\t*b = append(*b, elems...)\n}\n\nfunc (b Breadcrumb) String(dom DOM) string {\n\toutput := \"\"\n\tfor idx, entry := range b {\n\t\tif idx \u003e 0 {\n\t\t\toutput += \" / \"\n\t\t}\n\t\toutput += entry.String(dom)\n\t}\n\treturn output\n}\n\ntype Columns struct {\n\tMaxWidth int\n\tColumns []Element\n}\n\nfunc (c *Columns) Append(elems ...Element) {\n\tc.Columns = append(c.Columns, elems...)\n}\n\nfunc (c Columns) String(dom DOM) string {\n\toutput := `\u003cdiv class=\"columns-` + strconv.Itoa(c.MaxWidth) + `\"\u003e` + \"\\n\"\n\tfor _, entry := range c.Columns {\n\t\toutput += `\u003cdiv class=\"column\"\u003e` + \"\\n\\n\"\n\t\toutput += entry.String(dom)\n\t\toutput += \"\u003c/div\u003e\u003c!-- /column--\u003e\\n\"\n\t}\n\toutput += \"\u003c/div\u003e\u003c!-- /columns-\" + strconv.Itoa(c.MaxWidth) + \" --\u003e\\n\"\n\treturn output\n}\n\ntype Link struct {\n\tText string\n\tPath string\n\tURL string\n}\n\n// TODO: image\n\n// TODO: pager\n\nfunc (l Link) String(dom DOM) string {\n\turl := \"\"\n\tswitch {\n\tcase l.Path != \"\" \u0026\u0026 l.URL != \"\":\n\t\tpanic(\"a link should have a path or a URL, not both.\")\n\tcase l.Path != \"\":\n\t\tif l.Text == \"\" {\n\t\t\tl.Text = l.Path\n\t\t}\n\t\turl = dom.Prefix + l.Path\n\tcase l.URL != \"\":\n\t\tif l.Text == \"\" {\n\t\t\tl.Text = l.URL\n\t\t}\n\t\turl = l.URL\n\t}\n\n\treturn \"[\" + l.Text + \"](\" + url + \")\"\n}\n\ntype BulletList []DomStringer\n\nfunc (bl BulletList) String(dom DOM) string {\n\toutput := \"\"\n\n\tfor _, entry := range bl {\n\t\toutput += \"- \" + entry.String(dom) + \"\\n\"\n\t}\n\n\treturn output\n}\n\nfunc Text(s string) DomStringer {\n\treturn Raw{Content: s}\n}\n\ntype DomStringer interface {\n\tString(dom DOM) string\n}\n\ntype Raw struct {\n\tContent string\n}\n\nfunc (r Raw) String(_ DOM) string {\n\treturn r.Content\n}\n\ntype (\n\tH1 string\n\tH2 string\n\tH3 string\n\tH4 string\n\tH5 string\n\tH6 string\n\tBold string\n\tItalic string\n\tCode string\n\tParagraph string\n\tQuote string\n\tHR struct{}\n)\n\nfunc (text H1) String(_ DOM) string { return \"# \" + string(text) + \"\\n\" }\nfunc (text H2) String(_ DOM) string { return \"## \" + string(text) + \"\\n\" }\nfunc (text H3) String(_ DOM) string { return \"### \" + string(text) + \"\\n\" }\nfunc (text H4) String(_ DOM) string { return \"#### \" + string(text) + \"\\n\" }\nfunc (text H5) String(_ DOM) string { return \"##### \" + string(text) + \"\\n\" }\nfunc (text H6) String(_ DOM) string { return \"###### \" + string(text) + \"\\n\" }\nfunc (text Quote) String(_ DOM) string { return \"\u003e \" + string(text) + \"\\n\" }\nfunc (text Bold) String(_ DOM) string { return \"**\" + string(text) + \"**\" }\nfunc (text Italic) String(_ DOM) string { return \"_\" + string(text) + \"_\" }\nfunc (text Paragraph) String(_ DOM) string { return \"\\n\" + string(text) + \"\\n\" }\nfunc (_ HR) String(_ DOM) string { return \"\\n---\\n\" }\n\nfunc (text Code) String(_ DOM) string {\n\t// multiline\n\tif strings.Contains(string(text), \"\\n\") {\n\t\treturn \"\\n```\\n\" + string(text) + \"\\n```\\n\"\n\t}\n\n\t// single line\n\treturn \"`\" + string(text) + \"`\"\n}\n" + }, + { + "name": "ui_test.gno", + "body": "package ui\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "vJc9bMXQVBkl5FraGQ5SoFbZObuWQNQojfOUaJ12ahcb1GNuhG4J7CZ2M/FxkVtozbXMyMYoRWXlHIP2SmL0BQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "watchdog", + "path": "gno.land/p/demo/watchdog", + "files": [ + { + "name": "watchdog.gno", + "body": "package watchdog\n\nimport \"time\"\n\ntype Watchdog struct {\n\tDuration time.Duration\n\tlastUpdate time.Time\n\tlastDown time.Time\n}\n\nfunc (w *Watchdog) Alive() {\n\tnow := time.Now()\n\tif !w.IsAlive() {\n\t\tw.lastDown = now\n\t}\n\tw.lastUpdate = now\n}\n\nfunc (w Watchdog) Status() string {\n\tif w.IsAlive() {\n\t\treturn \"OK\"\n\t}\n\treturn \"KO\"\n}\n\nfunc (w Watchdog) IsAlive() bool {\n\treturn time.Since(w.lastUpdate) \u003c w.Duration\n}\n\nfunc (w Watchdog) UpSince() time.Time {\n\treturn w.lastDown\n}\n\nfunc (w Watchdog) DownSince() time.Time {\n\tif !w.IsAlive() {\n\t\treturn w.lastUpdate\n\t}\n\treturn time.Time{}\n}\n" + }, + { + "name": "watchdog_test.gno", + "body": "package watchdog\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestPackage(t *testing.T) {\n\tw := Watchdog{Duration: 5 * time.Minute}\n\tuassert.False(t, w.IsAlive())\n\tw.Alive()\n\tuassert.True(t, w.IsAlive())\n\t// XXX: add more tests when we'll be able to \"skip time\".\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "+JCWQONBZGUnCnFTNN/UvnZZKBJwEfNIbcPIEt1nA43BvHGeA/GYlmRpnjZHhS1FHzWiSb0jxRbLrhGLBrjCBA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "executor", + "path": "gno.land/p/gov/executor", + "files": [ + { + "name": "callback.gno", + "body": "package executor\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nvar errInvalidCaller = errors.New(\"invalid executor caller\")\n\n// NewCallbackExecutor creates a new callback executor with the provided callback function\nfunc NewCallbackExecutor(callback func() error, path string) *CallbackExecutor {\n\treturn \u0026CallbackExecutor{\n\t\tcallback: callback,\n\t\tdaoPkgPath: path,\n\t}\n}\n\n// CallbackExecutor is an implementation of the dao.Executor interface,\n// based on a specific callback.\n// The given callback should verify the validity of the govdao call\ntype CallbackExecutor struct {\n\tcallback func() error // the callback to be executed\n\tdaoPkgPath string // the active pkg path of the govdao\n}\n\n// Execute runs the executor's callback function.\nfunc (exec *CallbackExecutor) Execute() error {\n\t// Verify the caller is an adequate Realm\n\tcaller := std.CurrentRealm().PkgPath()\n\tif caller != exec.daoPkgPath {\n\t\treturn errInvalidCaller\n\t}\n\n\tif exec.callback != nil {\n\t\treturn exec.callback()\n\t}\n\n\treturn nil\n}\n" + }, + { + "name": "context.gno", + "body": "package executor\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/context\"\n)\n\ntype propContextKey string\n\nfunc (k propContextKey) String() string { return string(k) }\n\nconst (\n\tstatusContextKey = propContextKey(\"govdao-prop-status\")\n\tapprovedStatus = \"approved\"\n)\n\nvar errNotApproved = errors.New(\"not approved by govdao\")\n\n// CtxExecutor is an implementation of the dao.Executor interface,\n// based on the given context.\n// It utilizes the given context to assert the validity of the govdao call\ntype CtxExecutor struct {\n\tcallbackCtx func(ctx context.Context) error // the callback ctx fn, if any\n\tdaoPkgPath string // the active pkg path of the govdao\n}\n\n// NewCtxExecutor creates a new executor with the provided callback function.\nfunc NewCtxExecutor(callback func(ctx context.Context) error, path string) *CtxExecutor {\n\treturn \u0026CtxExecutor{\n\t\tcallbackCtx: callback,\n\t\tdaoPkgPath: path,\n\t}\n}\n\n// Execute runs the executor's callback function\nfunc (exec *CtxExecutor) Execute() error {\n\t// Verify the caller is an adequate Realm\n\tcaller := std.CurrentRealm().PkgPath()\n\tif caller != exec.daoPkgPath {\n\t\treturn errInvalidCaller\n\t}\n\n\t// Create the context\n\tctx := context.WithValue(\n\t\tcontext.Empty(),\n\t\tstatusContextKey,\n\t\tapprovedStatus,\n\t)\n\n\treturn exec.callbackCtx(ctx)\n}\n\n// IsApprovedByGovdaoContext asserts that the govdao approved the context\nfunc IsApprovedByGovdaoContext(ctx context.Context) bool {\n\tv := ctx.Value(statusContextKey)\n\tif v == nil {\n\t\treturn false\n\t}\n\n\tvs, ok := v.(string)\n\n\treturn ok \u0026\u0026 vs == approvedStatus\n}\n\n// AssertContextApprovedByGovDAO asserts the given context\n// was approved by GOVDAO\nfunc AssertContextApprovedByGovDAO(ctx context.Context) {\n\tif IsApprovedByGovdaoContext(ctx) {\n\t\treturn\n\t}\n\n\tpanic(errNotApproved)\n}\n" + }, + { + "name": "proposal_test.gno", + "body": "package executor\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/context\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestExecutor_Callback(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"govdao not caller\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\te := NewCallbackExecutor(cb, \"gno.land/r/gov/dao\")\n\n\t\t// Execute as not the /r/gov/dao caller\n\t\tuassert.ErrorIs(t, e.Execute(), errInvalidCaller)\n\t\tuassert.False(t, called, \"expected proposal to not execute\")\n\t})\n\n\tt.Run(\"execution successful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCallbackExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\ttesting.SetRealm(r)\n\n\t\tuassert.NoError(t, e.Execute())\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n\n\tt.Run(\"execution unsuccessful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\texpectedErr = errors.New(\"unexpected\")\n\n\t\t\tcb = func() error {\n\t\t\t\tcalled = true\n\n\t\t\t\treturn expectedErr\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCallbackExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\ttesting.SetRealm(r)\n\n\t\tuassert.ErrorIs(t, e.Execute(), expectedErr)\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n}\n\nfunc TestExecutor_Context(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"govdao not caller\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func(ctx context.Context) error {\n\t\t\t\tif !IsApprovedByGovdaoContext(ctx) {\n\t\t\t\t\tt.Fatal(\"not govdao caller\")\n\t\t\t\t}\n\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\te := NewCtxExecutor(cb, \"gno.land/r/gov/dao\")\n\n\t\t// Execute as not the /r/gov/dao caller\n\t\tuassert.ErrorIs(t, e.Execute(), errInvalidCaller)\n\t\tuassert.False(t, called, \"expected proposal to not execute\")\n\t})\n\n\tt.Run(\"execution successful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\n\t\t\tcb = func(ctx context.Context) error {\n\t\t\t\tif !IsApprovedByGovdaoContext(ctx) {\n\t\t\t\t\tt.Fatal(\"not govdao caller\")\n\t\t\t\t}\n\n\t\t\t\tcalled = true\n\n\t\t\t\treturn nil\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCtxExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\ttesting.SetRealm(r)\n\n\t\turequire.NoError(t, e.Execute())\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n\n\tt.Run(\"execution unsuccessful\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar (\n\t\t\tcalled = false\n\t\t\texpectedErr = errors.New(\"unexpected\")\n\n\t\t\tcb = func(ctx context.Context) error {\n\t\t\t\tif !IsApprovedByGovdaoContext(ctx) {\n\t\t\t\t\tt.Fatal(\"not govdao caller\")\n\t\t\t\t}\n\n\t\t\t\tcalled = true\n\n\t\t\t\treturn expectedErr\n\t\t\t}\n\t\t)\n\n\t\t// Create the executor\n\t\tdaoPkgPath := \"gno.land/r/gov/dao\"\n\t\te := NewCtxExecutor(cb, daoPkgPath)\n\n\t\t// Execute as the /r/gov/dao caller\n\t\tr := std.NewCodeRealm(daoPkgPath)\n\t\ttesting.SetRealm(r)\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\terr := e.Execute()\n\n\t\t\tuassert.ErrorIs(t, err, expectedErr)\n\t\t})\n\n\t\tuassert.True(t, called, \"expected proposal to execute\")\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "FDkgl+JmQ8/UAizTx2d4bf5vyNXtEo15SX/tOrYK7Wd8n16NpRONDQAFFVXSvPgG6hN+gJlw+xrNRQboqOnzDw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "datasource", + "path": "gno.land/p/jeronimoalbi/datasource", + "files": [ + { + "name": "datasource.gno", + "body": "// Package datasource defines generic interfaces for datasources.\n//\n// Datasources contain a set of records which can optionally be\n// taggable. Tags can optionally be used to filter records by taxonomy.\n//\n// Datasources can help in cases where the data sent during\n// communication between different realms needs to be generic\n// to avoid direct dependencies.\npackage datasource\n\nimport \"errors\"\n\n// ErrInvalidRecord indicates that a datasource contains invalid records.\nvar ErrInvalidRecord = errors.New(\"datasource records is not valid\")\n\ntype (\n\t// Fields defines an interface for read-only fields.\n\tFields interface {\n\t\t// Has checks whether a field exists.\n\t\tHas(name string) bool\n\n\t\t// Get retrieves the value associated with the given field.\n\t\tGet(name string) (value any, found bool)\n\t}\n\n\t// Record defines a datasource record.\n\tRecord interface {\n\t\t// ID returns the unique record's identifier.\n\t\tID() string\n\n\t\t// String returns a string representation of the record.\n\t\tString() string\n\n\t\t// Fields returns record fields and values.\n\t\tFields() (Fields, error)\n\t}\n\n\t// TaggableRecord defines a datasource record that supports tags.\n\t// Tags can be used to build a taxonomy to filter records by category.\n\tTaggableRecord interface {\n\t\t// Tags returns a list of tags for the record.\n\t\tTags() []string\n\t}\n\n\t// ContentRecord defines a datasource record that can return content.\n\tContentRecord interface {\n\t\t// Content returns the record content.\n\t\tContent() (string, error)\n\t}\n\n\t// Iterator defines an iterator of datasource records.\n\tIterator interface {\n\t\t// Next returns true when a new record is available.\n\t\tNext() bool\n\n\t\t// Err returns any error raised when reading records.\n\t\tErr() error\n\n\t\t// Record returns the current record.\n\t\tRecord() Record\n\t}\n\n\t// Datasource defines a generic datasource.\n\tDatasource interface {\n\t\t// Records returns a new datasource records iterator.\n\t\tRecords(Query) Iterator\n\n\t\t// Size returns the total number of records in the datasource.\n\t\t// When -1 is returned it means datasource doesn't support size.\n\t\tSize() int\n\n\t\t// Record returns a single datasource record.\n\t\tRecord(id string) (Record, error)\n\t}\n)\n\n// NewIterator returns a new record iterator for a datasource query.\nfunc NewIterator(ds Datasource, options ...QueryOption) Iterator {\n\treturn ds.Records(NewQuery(options...))\n}\n\n// QueryRecords return a slice of records for a datasource query.\nfunc QueryRecords(ds Datasource, options ...QueryOption) ([]Record, error) {\n\tvar (\n\t\trecords []Record\n\t\tquery = NewQuery(options...)\n\t\titer = ds.Records(query)\n\t)\n\n\tfor i := 0; i \u003c query.Count \u0026\u0026 iter.Next(); i++ {\n\t\tr := iter.Record()\n\t\tif r == nil {\n\t\t\treturn nil, ErrInvalidRecord\n\t\t}\n\n\t\trecords = append(records, r)\n\t}\n\n\tif err := iter.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn records, nil\n}\n" + }, + { + "name": "datasource_test.gno", + "body": "package datasource\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestNewIterator(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\trecords []Record\n\t\terr error\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\trecords: []Record{\n\t\t\t\ttestRecord{id: \"1\"},\n\t\t\t\ttestRecord{id: \"2\"},\n\t\t\t\ttestRecord{id: \"3\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"error\",\n\t\t\terr: errors.New(\"test\"),\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tds := testDatasource{\n\t\t\t\trecords: tc.records,\n\t\t\t\terr: tc.err,\n\t\t\t}\n\n\t\t\t// Act\n\t\t\titer := NewIterator(ds)\n\n\t\t\t// Assert\n\t\t\tif tc.err != nil {\n\t\t\t\tuassert.ErrorIs(t, tc.err, iter.Err())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tuassert.NoError(t, iter.Err())\n\n\t\t\tfor i := 0; iter.Next(); i++ {\n\t\t\t\tr := iter.Record()\n\t\t\t\turequire.NotEqual(t, nil, r, \"valid record\")\n\t\t\t\turequire.True(t, i \u003c len(tc.records), \"iteration count\")\n\t\t\t\tuassert.Equal(t, tc.records[i].ID(), r.ID())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestQueryRecords(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\trecords []Record\n\t\trecordCount int\n\t\toptions []QueryOption\n\t\terr error\n\t}{\n\t\t{\n\t\t\tname: \"ok\",\n\t\t\trecords: []Record{\n\t\t\t\ttestRecord{id: \"1\"},\n\t\t\t\ttestRecord{id: \"2\"},\n\t\t\t\ttestRecord{id: \"3\"},\n\t\t\t},\n\t\t\trecordCount: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"with count\",\n\t\t\toptions: []QueryOption{WithCount(2)},\n\t\t\trecords: []Record{\n\t\t\t\ttestRecord{id: \"1\"},\n\t\t\t\ttestRecord{id: \"2\"},\n\t\t\t\ttestRecord{id: \"3\"},\n\t\t\t},\n\t\t\trecordCount: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid record\",\n\t\t\trecords: []Record{\n\t\t\t\ttestRecord{id: \"1\"},\n\t\t\t\tnil,\n\t\t\t\ttestRecord{id: \"3\"},\n\t\t\t},\n\t\t\terr: ErrInvalidRecord,\n\t\t},\n\t\t{\n\t\t\tname: \"iterator error\",\n\t\t\trecords: []Record{\n\t\t\t\ttestRecord{id: \"1\"},\n\t\t\t\ttestRecord{id: \"3\"},\n\t\t\t},\n\t\t\terr: errors.New(\"test\"),\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tds := testDatasource{\n\t\t\t\trecords: tc.records,\n\t\t\t\terr: tc.err,\n\t\t\t}\n\n\t\t\t// Act\n\t\t\trecords, err := QueryRecords(ds, tc.options...)\n\n\t\t\t// Assert\n\t\t\tif tc.err != nil {\n\t\t\t\tuassert.ErrorIs(t, tc.err, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tuassert.NoError(t, err)\n\n\t\t\turequire.Equal(t, tc.recordCount, len(records), \"record count\")\n\t\t\tfor i, r := range records {\n\t\t\t\turequire.NotEqual(t, nil, r, \"valid record\")\n\t\t\t\tuassert.Equal(t, tc.records[i].ID(), r.ID())\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype testDatasource struct {\n\trecords []Record\n\terr error\n}\n\nfunc (testDatasource) Size() int { return -1 }\nfunc (testDatasource) Record(string) (Record, error) { return nil, nil }\nfunc (ds testDatasource) Records(Query) Iterator { return \u0026testIter{records: ds.records, err: ds.err} }\n\ntype testRecord struct {\n\tid string\n\tfields Fields\n\terr error\n}\n\nfunc (r testRecord) ID() string { return r.id }\nfunc (r testRecord) String() string { return \"str\" + r.id }\nfunc (r testRecord) Fields() (Fields, error) { return r.fields, r.err }\n\ntype testIter struct {\n\tindex int\n\trecords []Record\n\tcurrent Record\n\terr error\n}\n\nfunc (it testIter) Err() error { return it.err }\nfunc (it testIter) Record() Record { return it.current }\n\nfunc (it *testIter) Next() bool {\n\tcount := len(it.records)\n\tif it.err != nil || count == 0 || it.index \u003e= count {\n\t\treturn false\n\t}\n\tit.current = it.records[it.index]\n\tit.index++\n\treturn true\n}\n" + }, + { + "name": "query.gno", + "body": "package datasource\n\nimport \"gno.land/p/demo/avl\"\n\n// DefaultQueryRecords defines the default number of records returned by queries.\nconst DefaultQueryRecords = 50\n\nvar defaultQuery = Query{Count: DefaultQueryRecords}\n\ntype (\n\t// QueryOption configures datasource queries.\n\tQueryOption func(*Query)\n\n\t// Query contains datasource query options.\n\tQuery struct {\n\t\t// Offset of the first record to return during iteration.\n\t\tOffset int\n\n\t\t// Count contains the number to records that query should return.\n\t\tCount int\n\n\t\t// Tag contains a tag to use as filter for the records.\n\t\tTag string\n\n\t\t// Filters contains optional query filters by field value.\n\t\tFilters avl.Tree\n\t}\n)\n\n// WithOffset configures query to return records starting from an offset.\nfunc WithOffset(offset int) QueryOption {\n\treturn func(q *Query) {\n\t\tq.Offset = offset\n\t}\n}\n\n// WithCount configures the number of records that query returns.\nfunc WithCount(count int) QueryOption {\n\treturn func(q *Query) {\n\t\tif count \u003c 1 {\n\t\t\tcount = DefaultQueryRecords\n\t\t}\n\t\tq.Count = count\n\t}\n}\n\n// ByTag configures query to filter by tag.\nfunc ByTag(tag string) QueryOption {\n\treturn func(q *Query) {\n\t\tq.Tag = tag\n\t}\n}\n\n// WithFilter assigns a new filter argument to a query.\n// This option can be used multiple times if more than one\n// filter has to be given to the query.\nfunc WithFilter(field string, value any) QueryOption {\n\treturn func(q *Query) {\n\t\tq.Filters.Set(field, value)\n\t}\n}\n\n// NewQuery creates a new datasource query.\nfunc NewQuery(options ...QueryOption) Query {\n\tq := defaultQuery\n\tfor _, apply := range options {\n\t\tapply(\u0026q)\n\t}\n\treturn q\n}\n" + }, + { + "name": "query_test.gno", + "body": "package datasource\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestNewQuery(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\toptions []QueryOption\n\t\tsetup func() Query\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t\tsetup: func() Query {\n\t\t\t\treturn Query{Count: DefaultQueryRecords}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with offset\",\n\t\t\toptions: []QueryOption{WithOffset(100)},\n\t\t\tsetup: func() Query {\n\t\t\t\treturn Query{\n\t\t\t\t\tOffset: 100,\n\t\t\t\t\tCount: DefaultQueryRecords,\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with count\",\n\t\t\toptions: []QueryOption{WithCount(10)},\n\t\t\tsetup: func() Query {\n\t\t\t\treturn Query{Count: 10}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with invalid count\",\n\t\t\toptions: []QueryOption{WithCount(0)},\n\t\t\tsetup: func() Query {\n\t\t\t\treturn Query{Count: DefaultQueryRecords}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"by tag\",\n\t\t\toptions: []QueryOption{ByTag(\"foo\")},\n\t\t\tsetup: func() Query {\n\t\t\t\treturn Query{\n\t\t\t\t\tTag: \"foo\",\n\t\t\t\t\tCount: DefaultQueryRecords,\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with filter\",\n\t\t\toptions: []QueryOption{WithFilter(\"foo\", 42)},\n\t\t\tsetup: func() Query {\n\t\t\t\tq := Query{Count: DefaultQueryRecords}\n\t\t\t\tq.Filters.Set(\"foo\", 42)\n\t\t\t\treturn q\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with multiple filters\",\n\t\t\toptions: []QueryOption{\n\t\t\t\tWithFilter(\"foo\", 42),\n\t\t\t\tWithFilter(\"bar\", \"baz\"),\n\t\t\t},\n\t\t\tsetup: func() Query {\n\t\t\t\tq := Query{Count: DefaultQueryRecords}\n\t\t\t\tq.Filters.Set(\"foo\", 42)\n\t\t\t\tq.Filters.Set(\"bar\", \"baz\")\n\t\t\t\treturn q\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\twant := tc.setup()\n\n\t\t\t// Act\n\t\t\tq := NewQuery(tc.options...)\n\n\t\t\t// Assert\n\t\t\tuassert.Equal(t, want.Offset, q.Offset)\n\t\t\tuassert.Equal(t, want.Count, q.Count)\n\t\t\tuassert.Equal(t, want.Tag, q.Tag)\n\t\t\tuassert.Equal(t, want.Filters.Size(), q.Filters.Size())\n\n\t\t\twant.Filters.Iterate(\"\", \"\", func(k string, v any) bool {\n\t\t\t\tgot, exists := q.Filters.Get(k)\n\t\t\t\tuassert.True(t, exists)\n\t\t\t\tif exists {\n\t\t\t\t\tuassert.Equal(t, fmt.Sprint(v), fmt.Sprint(got))\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t})\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "TblWMlqKUFCgpGoQvbiausfwmx2IrWGY3+UPwgSiXpKv6ER4sf6+qA/T3RsbJI4GBmcPB4uF5b7zJCUBjRtMCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "collection", + "path": "gno.land/p/moul/collection", + "files": [ + { + "name": "collection.gno", + "body": "// Package collection provides a generic collection implementation with support for\n// multiple indexes, including unique indexes and case-insensitive indexes.\n// It is designed to be used with any type and allows efficient lookups using\n// different fields or computed values.\n//\n// Example usage:\n//\n//\t// Define a data type\n//\ttype User struct {\n//\t Name string\n//\t Email string\n//\t Age int\n//\t Username string\n//\t Tags []string\n//\t}\n//\n//\t// Create a new collection\n//\tc := collection.New()\n//\n//\t// Add indexes with different options\n//\tc.AddIndex(\"name\", func(v any) string {\n//\t return v.(*User).Name\n//\t}, UniqueIndex)\n//\n//\tc.AddIndex(\"email\", func(v any) string {\n//\t return v.(*User).Email\n//\t}, UniqueIndex|CaseInsensitiveIndex)\n//\n//\tc.AddIndex(\"age\", func(v any) string {\n//\t return strconv.Itoa(v.(*User).Age)\n//\t}, DefaultIndex) // Non-unique index\n//\n//\tc.AddIndex(\"username\", func(v any) string {\n//\t return v.(*User).Username\n//\t}, UniqueIndex|SparseIndex) // Allow empty usernames\n//\n//\t// For tags, we index all tags for the user\n//\tc.AddIndex(\"tag\", func(v any) []string {\n//\t return v.(*User).Tags\n//\t}, DefaultIndex) // Non-unique to allow multiple users with same tag\n//\n//\t// Store an object\n//\tid := c.Set(\u0026User{\n//\t Name: \"Alice\",\n//\t Email: \"alice@example.com\",\n//\t Age: 30,\n//\t Tags: []string{\"admin\", \"moderator\"}, // User can have multiple tags\n//\t})\n//\n//\t// Retrieve by any index\n//\tentry := c.GetFirst(\"email\", \"alice@example.com\")\n//\tadminUsers := c.GetAll(\"tag\", \"admin\") // Find all users with admin tag\n//\tmodUsers := c.GetAll(\"tag\", \"moderator\") // Find all users with moderator tag\n//\n// Index options can be combined using the bitwise OR operator.\n// Available options:\n// - DefaultIndex: Regular index with no special behavior\n// - UniqueIndex: Ensures values are unique within the index\n// - CaseInsensitiveIndex: Makes string comparisons case-insensitive\n// - SparseIndex: Skips indexing empty values (nil or empty string)\n//\n// Example: UniqueIndex|CaseInsensitiveIndex for a case-insensitive unique index\npackage collection\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n)\n\n// New creates a new Collection instance with an initialized ID index.\n// The ID index is a special unique index that is always present and\n// serves as the primary key for all objects in the collection.\nfunc New() *Collection {\n\tc := \u0026Collection{\n\t\tindexes: make(map[string]*Index),\n\t\tidGen: seqid.ID(0),\n\t}\n\t// Initialize _id index\n\tc.indexes[IDIndex] = \u0026Index{\n\t\toptions: UniqueIndex,\n\t\ttree: avl.NewTree(),\n\t}\n\treturn c\n}\n\n// Collection represents a collection of objects with multiple indexes\ntype Collection struct {\n\tindexes map[string]*Index\n\tidGen seqid.ID\n}\n\nconst (\n\t// IDIndex is the reserved name for the primary key index\n\tIDIndex = \"_id\"\n)\n\n// IndexOption represents configuration options for an index using bit flags\ntype IndexOption uint64\n\nconst (\n\t// DefaultIndex is a basic index with no special options\n\tDefaultIndex IndexOption = 0\n\n\t// UniqueIndex ensures no duplicate values are allowed\n\tUniqueIndex IndexOption = 1 \u003c\u003c iota\n\n\t// CaseInsensitiveIndex automatically converts string values to lowercase\n\tCaseInsensitiveIndex\n\n\t// SparseIndex only indexes non-empty values\n\tSparseIndex\n)\n\n// Index represents an index with its configuration and data.\n// The index function can return either:\n// - string: for single-value indexes\n// - []string: for multi-value indexes where one object can be indexed under multiple keys\n//\n// The backing tree stores either a single ID or []string for multiple IDs per key.\ntype Index struct {\n\tfn any\n\toptions IndexOption\n\ttree avl.ITree\n}\n\n// AddIndex adds a new index to the collection with the specified options\n//\n// Parameters:\n// - name: the unique name of the index (e.g., \"tags\")\n// - indexFn: a function that extracts either a string or []string from an object\n// - options: bit flags for index configuration (e.g., UniqueIndex)\nfunc (c *Collection) AddIndex(name string, indexFn any, options IndexOption) {\n\tif name == IDIndex {\n\t\tpanic(\"_id is a reserved index name\")\n\t}\n\tc.indexes[name] = \u0026Index{\n\t\tfn: indexFn,\n\t\toptions: options,\n\t\ttree: avl.NewTree(),\n\t}\n}\n\n// storeIndex handles how we store an ID in the index tree\nfunc (idx *Index) store(key string, idStr string) {\n\tstored, exists := idx.tree.Get(key)\n\tif !exists {\n\t\t// First entry for this key\n\t\tidx.tree.Set(key, idStr)\n\t\treturn\n\t}\n\n\t// Handle existing entries\n\tswitch existing := stored.(type) {\n\tcase string:\n\t\tif existing == idStr {\n\t\t\treturn // Already stored\n\t\t}\n\t\t// Convert to array\n\t\tidx.tree.Set(key, []string{existing, idStr})\n\tcase []string:\n\t\t// Check if ID already exists\n\t\tfor _, id := range existing {\n\t\t\tif id == idStr {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// Append new ID\n\t\tidx.tree.Set(key, append(existing, idStr))\n\t}\n}\n\n// removeIndex handles how we remove an ID from the index tree\nfunc (idx *Index) remove(key string, idStr string) {\n\tstored, exists := idx.tree.Get(key)\n\tif !exists {\n\t\treturn\n\t}\n\n\tswitch existing := stored.(type) {\n\tcase string:\n\t\tif existing == idStr {\n\t\t\tidx.tree.Remove(key)\n\t\t}\n\tcase []string:\n\t\tnewIds := make([]string, 0, len(existing))\n\t\tfor _, id := range existing {\n\t\t\tif id != idStr {\n\t\t\t\tnewIds = append(newIds, id)\n\t\t\t}\n\t\t}\n\t\tif len(newIds) == 0 {\n\t\t\tidx.tree.Remove(key)\n\t\t} else if len(newIds) == 1 {\n\t\t\tidx.tree.Set(key, newIds[0])\n\t\t} else {\n\t\t\tidx.tree.Set(key, newIds)\n\t\t}\n\t}\n}\n\n// generateKeys extracts one or more keys from an object for a given index.\nfunc generateKeys(idx *Index, obj any) ([]string, bool) {\n\tif obj == nil {\n\t\treturn nil, false\n\t}\n\n\tswitch fnTyped := idx.fn.(type) {\n\tcase func(any) string:\n\t\t// Single-value index\n\t\tkey := fnTyped(obj)\n\t\treturn []string{key}, true\n\tcase func(any) []string:\n\t\t// Multi-value index\n\t\tkeys := fnTyped(obj)\n\t\treturn keys, true\n\tdefault:\n\t\tpanic(\"invalid index function type\")\n\t}\n}\n\n// Set adds or updates an object in the collection.\n// Returns a positive ID if successful.\n// Returns 0 if:\n// - The object is nil\n// - A uniqueness constraint would be violated\n// - Index generation fails for any index\nfunc (c *Collection) Set(obj any) uint64 {\n\tif obj == nil {\n\t\treturn 0\n\t}\n\n\t// Generate new ID\n\tid := c.idGen.Next()\n\tidStr := id.String()\n\n\t// Check uniqueness constraints first\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tcontinue\n\t\t}\n\t\tkeys, ok := generateKeys(idx, obj)\n\t\tif !ok {\n\t\t\treturn 0\n\t\t}\n\n\t\tfor _, key := range keys {\n\t\t\t// Skip empty values for sparse indexes\n\t\t\tif idx.options\u0026SparseIndex != 0 \u0026\u0026 key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\tkey = strings.ToLower(key)\n\t\t\t}\n\t\t\t// Only check uniqueness for unique + single-value indexes\n\t\t\t// (UniqueIndex is ambiguous; skipping that scenario)\n\t\t\tif idx.options\u0026UniqueIndex != 0 {\n\t\t\t\tif existing, exists := idx.tree.Get(key); exists \u0026\u0026 existing != nil {\n\t\t\t\t\treturn 0\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Store in _id index first (the actual object)\n\tc.indexes[IDIndex].tree.Set(idStr, obj)\n\n\t// Store in all other indexes\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tcontinue\n\t\t}\n\t\tkeys, ok := generateKeys(idx, obj)\n\t\tif !ok {\n\t\t\t// Rollback: remove from _id index\n\t\t\tc.indexes[IDIndex].tree.Remove(idStr)\n\t\t\treturn 0\n\t\t}\n\n\t\tfor _, key := range keys {\n\t\t\tif idx.options\u0026SparseIndex != 0 \u0026\u0026 key == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\tkey = strings.ToLower(key)\n\t\t\t}\n\t\t\tidx.store(key, idStr)\n\t\t}\n\t}\n\n\treturn uint64(id)\n}\n\n// Get retrieves entries matching the given key in the specified index.\n// Returns an iterator over the matching entries.\nfunc (c *Collection) Get(indexName string, key string) EntryIterator {\n\tidx, exists := c.indexes[indexName]\n\tif !exists {\n\t\treturn EntryIterator{err: errors.New(\"index not found: \" + indexName)}\n\t}\n\n\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\tkey = strings.ToLower(key)\n\t}\n\n\tif indexName == IDIndex {\n\t\t// For ID index, validate the ID format first\n\t\t_, err := seqid.FromString(key)\n\t\tif err != nil {\n\t\t\treturn EntryIterator{err: err}\n\t\t}\n\t}\n\n\treturn EntryIterator{\n\t\tcollection: c,\n\t\tindexName: indexName,\n\t\tkey: key,\n\t}\n}\n\n// GetFirst returns the first matching entry or nil if none found\nfunc (c *Collection) GetFirst(indexName, key string) *Entry {\n\titer := c.Get(indexName, key)\n\tif iter.Next() {\n\t\treturn iter.Value()\n\t}\n\treturn nil\n}\n\n// Delete removes an object by its ID and returns true if something was deleted\nfunc (c *Collection) Delete(id uint64) bool {\n\tidStr := seqid.ID(id).String()\n\n\t// Get the object first to clean up other indexes\n\tobj, exists := c.indexes[IDIndex].tree.Get(idStr)\n\tif !exists {\n\t\treturn false\n\t}\n\n\t// Remove from all indexes\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tidx.tree.Remove(idStr)\n\t\t\tcontinue\n\t\t}\n\t\tkeys, ok := generateKeys(idx, obj)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, key := range keys {\n\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\tkey = strings.ToLower(key)\n\t\t\t}\n\t\t\tidx.remove(key, idStr)\n\t\t}\n\t}\n\treturn true\n}\n\n// Update updates an existing object and returns true if successful\n// Returns true if the update was successful.\n// Returns false if:\n// - The object is nil\n// - The ID doesn't exist\n// - A uniqueness constraint would be violated\n// - Index generation fails for any index\n//\n// If the update fails, the collection remains unchanged.\nfunc (c *Collection) Update(id uint64, obj any) bool {\n\tif obj == nil {\n\t\treturn false\n\t}\n\tidStr := seqid.ID(id).String()\n\toldObj, exists := c.indexes[IDIndex].tree.Get(idStr)\n\tif !exists {\n\t\treturn false\n\t}\n\n\t// Check unique constraints\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tcontinue\n\t\t}\n\n\t\tif idx.options\u0026UniqueIndex != 0 {\n\t\t\tnewKeys, newOk := generateKeys(idx, obj)\n\t\t\t_, oldOk := generateKeys(idx, oldObj)\n\t\t\tif !newOk || !oldOk {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tfor _, newKey := range newKeys {\n\t\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\t\tnewKey = strings.ToLower(newKey)\n\t\t\t\t}\n\n\t\t\t\tfound, _ := idx.tree.Get(newKey)\n\t\t\t\tif found != nil {\n\t\t\t\t\tif storedID, ok := found.(string); !ok || storedID != idStr {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Store old index entries for potential rollback\n\toldEntries := make(map[string][]string)\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tcontinue\n\t\t}\n\t\toldKeys, ok := generateKeys(idx, oldObj)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tvar adjusted []string\n\t\tfor _, okey := range oldKeys {\n\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\tokey = strings.ToLower(okey)\n\t\t\t}\n\t\t\t// Remove the oldObj from the index right away\n\t\t\tidx.remove(okey, idStr)\n\t\t\tadjusted = append(adjusted, okey)\n\t\t}\n\t\toldEntries[name] = adjusted\n\t}\n\n\t// Update the object in the _id index\n\tc.indexes[IDIndex].tree.Set(idStr, obj)\n\n\t// Add new index entries\n\tfor name, idx := range c.indexes {\n\t\tif name == IDIndex {\n\t\t\tcontinue\n\t\t}\n\t\tnewKeys, ok := generateKeys(idx, obj)\n\t\tif !ok {\n\t\t\t// Rollback: restore old object and old index entries\n\t\t\tc.indexes[IDIndex].tree.Set(idStr, oldObj)\n\t\t\tfor idxName, keys := range oldEntries {\n\t\t\t\tfor _, oldKey := range keys {\n\t\t\t\t\tc.indexes[idxName].store(oldKey, idStr)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\t\tfor _, nkey := range newKeys {\n\t\t\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\t\t\tnkey = strings.ToLower(nkey)\n\t\t\t}\n\t\t\tidx.store(nkey, idStr)\n\t\t}\n\t}\n\n\treturn true\n}\n\n// GetAll retrieves all entries matching the given key in the specified index.\nfunc (c *Collection) GetAll(indexName string, key string) []Entry {\n\tidx, exists := c.indexes[indexName]\n\tif !exists {\n\t\treturn nil\n\t}\n\n\tif idx.options\u0026CaseInsensitiveIndex != 0 {\n\t\tkey = strings.ToLower(key)\n\t}\n\n\tif indexName == IDIndex {\n\t\tif obj, exists := idx.tree.Get(key); exists {\n\t\t\treturn []Entry{{ID: key, Obj: obj}}\n\t\t}\n\t\treturn nil\n\t}\n\n\tidData, exists := idx.tree.Get(key)\n\tif !exists {\n\t\treturn nil\n\t}\n\n\t// Handle both single and multi-value cases based on the actual data type\n\tswitch stored := idData.(type) {\n\tcase []string:\n\t\tresult := make([]Entry, 0, len(stored))\n\t\tfor _, idStr := range stored {\n\t\t\tif obj, exists := c.indexes[IDIndex].tree.Get(idStr); exists {\n\t\t\t\tresult = append(result, Entry{ID: idStr, Obj: obj})\n\t\t\t}\n\t\t}\n\t\treturn result\n\tcase string:\n\t\tif obj, exists := c.indexes[IDIndex].tree.Get(stored); exists {\n\t\t\treturn []Entry{{ID: stored, Obj: obj}}\n\t\t}\n\t}\n\treturn nil\n}\n\n// GetIndex returns the underlying tree for an index\nfunc (c *Collection) GetIndex(name string) avl.ITree {\n\tidx, exists := c.indexes[name]\n\tif !exists {\n\t\treturn nil\n\t}\n\treturn idx.tree\n}\n" + }, + { + "name": "collection_test.gno", + "body": "package collection\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Person struct {\n\tName string\n\tAge int\n\tEmail string\n\tUsername string\n\tTags []string\n}\n\nfunc (p Person) String() string {\n\treturn ufmt.Sprintf(\"name=%s age=%d email=%s username=%s tags=%s\",\n\t\tp.Name, p.Age, p.Email, p.Username, strings.Join(p.Tags, \",\"))\n}\n\n// TestOperation represents a single operation in a test sequence\ntype TestOperation struct {\n\top string // \"set\" or \"update\"\n\tperson *Person\n\tid uint64 // for updates\n\twantID uint64\n\twantErr bool\n}\n\n// TestCase represents a complete test case with setup and operations\ntype TestCase struct {\n\tname string\n\tsetupIndex func(*Collection)\n\toperations []TestOperation\n}\n\nfunc TestBasicOperations(t *testing.T) {\n\tc := New()\n\n\t// Add indexes\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\tc.AddIndex(\"age\", func(v any) string {\n\t\treturn strconv.Itoa(v.(*Person).Age)\n\t}, DefaultIndex)\n\n\t// Test basic Set and Get\n\tp1 := \u0026Person{Name: \"Alice\", Age: 30, Email: \"alice@test.com\"}\n\tid1 := c.Set(p1)\n\tif id1 == 0 {\n\t\tt.Error(\"Failed to set first object\")\n\t}\n\n\t// Get by ID\n\titer := c.Get(IDIndex, seqid.ID(id1).String())\n\tif !iter.Next() {\n\t\tt.Error(\"Failed to get object by ID\")\n\t}\n\tentry := iter.Value()\n\tif entry.Obj.(*Person).Name != \"Alice\" {\n\t\tt.Error(\"Got wrong object\")\n\t}\n}\n\nfunc TestUniqueConstraints(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tsetup func(*Collection) uint64\n\t\twantID bool\n\t}{\n\t\t{\n\t\t\tname: \"First person\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\treturn c.Set(\u0026Person{Name: \"Alice\"})\n\t\t\t},\n\t\t\twantID: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Duplicate name\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.Set(\u0026Person{Name: \"Alice\"})\n\t\t\t\treturn c.Set(\u0026Person{Name: \"Alice\"})\n\t\t\t},\n\t\t\twantID: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Same age (non-unique index)\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.AddIndex(\"age\", func(v any) string {\n\t\t\t\t\treturn strconv.Itoa(v.(*Person).Age)\n\t\t\t\t}, DefaultIndex)\n\t\t\t\tc.Set(\u0026Person{Name: \"Alice\", Age: 30})\n\t\t\t\treturn c.Set(\u0026Person{Name: \"Bob\", Age: 30})\n\t\t\t},\n\t\t\twantID: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New()\n\t\t\tc.AddIndex(\"name\", func(v any) string {\n\t\t\t\treturn v.(*Person).Name\n\t\t\t}, UniqueIndex)\n\n\t\t\tid := tt.setup(c)\n\t\t\tif (id != 0) != tt.wantID {\n\t\t\t\tt.Errorf(\"Set() got id = %v, want non-zero: %v\", id, tt.wantID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUpdates(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\tc.AddIndex(\"username\", func(v any) string {\n\t\treturn strings.ToLower(v.(*Person).Username)\n\t}, UniqueIndex|CaseInsensitiveIndex)\n\n\t// Initial setup\n\tp1 := \u0026Person{Name: \"Alice\", Username: \"alice123\"}\n\tp2 := \u0026Person{Name: \"Bob\", Username: \"bob456\"}\n\n\tid1 := c.Set(p1)\n\tid2 := c.Set(p2)\n\n\ttests := []struct {\n\t\tname string\n\t\tid uint64\n\t\tnewPerson *Person\n\t\twantRet bool\n\t}{\n\t\t{\n\t\t\tname: \"Update to non-conflicting values\",\n\t\t\tid: id1,\n\t\t\tnewPerson: \u0026Person{Name: \"Alice2\", Username: \"alice1234\"},\n\t\t\twantRet: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Update to conflicting username\",\n\t\t\tid: id1,\n\t\t\tnewPerson: \u0026Person{Name: \"Alice2\", Username: \"bob456\"},\n\t\t\twantRet: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Update non-existent ID\",\n\t\t\tid: 99999,\n\t\t\tnewPerson: \u0026Person{Name: \"Test\", Username: \"test\"},\n\t\t\twantRet: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotID := c.Update(tt.id, tt.newPerson)\n\t\t\tif gotID != tt.wantRet {\n\t\t\t\tt.Errorf(\"Update() got = %v, want %v\", gotID, tt.wantRet)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDelete(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\n\tp1 := \u0026Person{Name: \"Alice\"}\n\tid1 := c.Set(p1)\n\n\ttests := []struct {\n\t\tname string\n\t\tid uint64\n\t\twantRet bool\n\t}{\n\t\t{\n\t\t\tname: \"Delete existing object\",\n\t\t\tid: id1,\n\t\t\twantRet: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Delete non-existent object\",\n\t\t\tid: 99999,\n\t\t\twantRet: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Delete already deleted object\",\n\t\t\tid: id1,\n\t\t\twantRet: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgotID := c.Delete(tt.id)\n\t\t\tif gotID != tt.wantRet {\n\t\t\t\tt.Errorf(\"Delete() got = %v, want %v\", gotID, tt.wantRet)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEdgeCases(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\n\ttests := []struct {\n\t\tname string\n\t\toperation func() bool\n\t\twantPanic bool\n\t}{\n\t\t{\n\t\t\tname: \"Set nil object\",\n\t\t\toperation: func() bool {\n\t\t\t\treturn c.Set(nil) != 0\n\t\t\t},\n\t\t\twantPanic: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Set wrong type\",\n\t\t\toperation: func() bool {\n\t\t\t\treturn c.Set(\"not a person\") != 0\n\t\t\t},\n\t\t\twantPanic: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Update with nil\",\n\t\t\toperation: func() bool {\n\t\t\t\tid := c.Set(\u0026Person{Name: \"Test\"})\n\t\t\t\treturn c.Update(id, nil)\n\t\t\t},\n\t\t\twantPanic: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Get with invalid index name\",\n\t\t\toperation: func() bool {\n\t\t\t\titer := c.Get(\"invalid_index\", \"key\")\n\t\t\t\tif iter.Empty() {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tentry := iter.Value()\n\t\t\t\tif entry == nil {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tid, err := seqid.FromString(entry.ID)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t},\n\t\t\twantPanic: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar got bool\n\t\t\tpanicked := false\n\n\t\t\tfunc() {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tpanicked = true\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t\tgot = tt.operation()\n\t\t\t}()\n\n\t\t\tif panicked != tt.wantPanic {\n\t\t\t\tt.Errorf(\"Operation panicked = %v, want panic = %v\", panicked, tt.wantPanic)\n\t\t\t}\n\t\t\tif !panicked \u0026\u0026 got != false {\n\t\t\t\tt.Errorf(\"Operation returned %v, want 0\", got)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIndexTypes(t *testing.T) {\n\tc := New()\n\n\t// Test different types of indexes\n\tc.AddIndex(\"composite\", func(v any) string {\n\t\tp := v.(*Person)\n\t\treturn p.Name + \":\" + strconv.Itoa(p.Age)\n\t}, UniqueIndex)\n\n\tc.AddIndex(\"case_insensitive\", func(v any) string {\n\t\treturn strings.ToLower(v.(*Person).Username)\n\t}, UniqueIndex|CaseInsensitiveIndex)\n\n\t// Test composite index\n\tp1 := \u0026Person{Name: \"Alice\", Age: 30, Username: \"Alice123\"}\n\tid1 := c.Set(p1)\n\tif id1 == 0 {\n\t\tt.Error(\"Failed to set object with composite index\")\n\t}\n\n\t// Test case-insensitive index\n\tp2 := \u0026Person{Name: \"Bob\", Age: 25, Username: \"alice123\"}\n\tid2 := c.Set(p2)\n\tif id2 != 0 {\n\t\tt.Error(\"Case-insensitive index failed to prevent duplicate\")\n\t}\n}\n\nfunc TestIndexOptions(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tsetup func(*Collection) uint64\n\t\twantID bool\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Unique case-sensitive index\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.AddIndex(\"username\", func(v any) string {\n\t\t\t\t\treturn v.(*Person).Username\n\t\t\t\t}, UniqueIndex)\n\n\t\t\t\tid1 := c.Set(\u0026Person{Username: \"Alice\"})\n\t\t\t\treturn c.Set(\u0026Person{Username: \"Alice\"}) // Should fail\n\t\t\t},\n\t\t\twantID: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Unique case-insensitive index\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.AddIndex(\"email\", func(v any) string {\n\t\t\t\t\treturn v.(*Person).Email\n\t\t\t\t}, UniqueIndex|CaseInsensitiveIndex)\n\n\t\t\t\tid1 := c.Set(\u0026Person{Email: \"test@example.com\"})\n\t\t\t\treturn c.Set(\u0026Person{Email: \"TEST@EXAMPLE.COM\"}) // Should fail\n\t\t\t},\n\t\t\twantID: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Default index\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.AddIndex(\"age\", func(v any) string {\n\t\t\t\t\treturn strconv.Itoa(v.(*Person).Age)\n\t\t\t\t}, DefaultIndex)\n\n\t\t\t\t// First person with age 30\n\t\t\t\tid1 := c.Set(\u0026Person{Age: 30})\n\t\t\t\tif id1 == 0 {\n\t\t\t\t\tt.Error(\"Failed to set first person\")\n\t\t\t\t}\n\n\t\t\t\t// Second person with same age should succeed\n\t\t\t\treturn c.Set(\u0026Person{Age: 30})\n\t\t\t},\n\t\t\twantID: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple options\",\n\t\t\tsetup: func(c *Collection) uint64 {\n\t\t\t\tc.AddIndex(\"name\", func(v any) string {\n\t\t\t\t\treturn v.(*Person).Name\n\t\t\t\t}, UniqueIndex|CaseInsensitiveIndex|SparseIndex)\n\n\t\t\t\tid1 := c.Set(\u0026Person{Name: \"Alice\"})\n\t\t\t\treturn c.Set(\u0026Person{Name: \"ALICE\"}) // Should fail\n\t\t\t},\n\t\t\twantID: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New() // Create new collection for each test\n\t\t\tid := tt.setup(c)\n\t\t\tif (id != 0) != tt.wantID {\n\t\t\t\tt.Errorf(\"got id = %v, want non-zero: %v\", id, tt.wantID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConcurrentOperations(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\n\tp1 := \u0026Person{Name: \"Alice\"}\n\tid1 := c.Set(p1)\n\titer := c.Get(\"_id\", seqid.ID(id1).String())\n\tsuccess := c.Update(id1, \u0026Person{Name: \"Alice2\"})\n\n\tif iter.Empty() || !success {\n\t\tt.Error(\"Concurrent operations failed\")\n\t}\n}\n\nfunc TestSparseIndexBehavior(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"optional_field\", func(v any) string {\n\t\treturn v.(*Person).Username\n\t}, SparseIndex)\n\n\ttests := []struct {\n\t\tname string\n\t\tperson *Person\n\t\twantID bool\n\t}{\n\t\t{\n\t\t\tname: \"Empty optional field\",\n\t\t\tperson: \u0026Person{Name: \"Alice\", Email: \"alice@test.com\"},\n\t\t\twantID: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Populated optional field\",\n\t\t\tperson: \u0026Person{Name: \"Bob\", Email: \"bob@test.com\", Username: \"bobby\"},\n\t\t\twantID: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple empty fields\",\n\t\t\tperson: \u0026Person{Name: \"Charlie\"},\n\t\t\twantID: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tid := c.Set(tt.person)\n\t\t\tif (id != 0) != tt.wantID {\n\t\t\t\tt.Errorf(\"Set() got id = %v, want non-zero: %v\", id, tt.wantID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIndexKeyGeneration(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"composite\", func(v any) string {\n\t\tp := v.(*Person)\n\t\treturn p.Name + \":\" + strconv.Itoa(p.Age)\n\t}, UniqueIndex)\n\n\ttests := []struct {\n\t\tname string\n\t\tperson *Person\n\t\twantID bool\n\t}{\n\t\t{\n\t\t\tname: \"Valid composite key\",\n\t\t\tperson: \u0026Person{Name: \"Alice\", Age: 30},\n\t\t\twantID: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Duplicate composite key\",\n\t\t\tperson: \u0026Person{Name: \"Alice\", Age: 30},\n\t\t\twantID: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Different composite key\",\n\t\t\tperson: \u0026Person{Name: \"Alice\", Age: 31},\n\t\t\twantID: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tid := c.Set(tt.person)\n\t\t\tif (id != 0) != tt.wantID {\n\t\t\t\tt.Errorf(\"Set() got id = %v, want non-zero: %v\", id, tt.wantID)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetIndex(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\n\ttests := []struct {\n\t\tname string\n\t\tindexName string\n\t\twantNil bool\n\t}{\n\t\t{\n\t\t\tname: \"Get existing index\",\n\t\t\tindexName: \"name\",\n\t\t\twantNil: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Get _id index\",\n\t\t\tindexName: IDIndex,\n\t\t\twantNil: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Get non-existent index\",\n\t\t\tindexName: \"invalid\",\n\t\t\twantNil: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\ttree := c.GetIndex(tt.indexName)\n\t\t\tif (tree == nil) != tt.wantNil {\n\t\t\t\tt.Errorf(\"GetIndex() got nil = %v, want nil = %v\", tree == nil, tt.wantNil)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAddIndexPanic(t *testing.T) {\n\tc := New()\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"Expected panic when adding _id index\")\n\t\t}\n\t}()\n\n\tc.AddIndex(IDIndex, func(v any) string {\n\t\treturn \"\"\n\t}, DefaultIndex)\n}\n\nfunc TestCaseInsensitiveOperations(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"email\", func(v any) string {\n\t\treturn v.(*Person).Email\n\t}, UniqueIndex|CaseInsensitiveIndex)\n\n\tp := \u0026Person{Email: \"Test@Example.com\"}\n\tid := c.Set(p)\n\n\ttests := []struct {\n\t\tname string\n\t\tkey string\n\t\twantObj bool\n\t\toperation string // \"get\" or \"getAll\"\n\t\twantCount int\n\t}{\n\t\t{\"Get exact match\", \"Test@Example.com\", true, \"get\", 1},\n\t\t{\"Get different case\", \"test@example.COM\", true, \"get\", 1},\n\t\t{\"Get non-existent\", \"other@example.com\", false, \"get\", 0},\n\t\t{\"GetAll exact match\", \"Test@Example.com\", true, \"getAll\", 1},\n\t\t{\"GetAll different case\", \"test@example.COM\", true, \"getAll\", 1},\n\t\t{\"GetAll non-existent\", \"other@example.com\", false, \"getAll\", 0},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tif tt.operation == \"get\" {\n\t\t\t\titer := c.Get(\"email\", tt.key)\n\t\t\t\tif iter.Empty() {\n\t\t\t\t\tif tt.wantObj {\n\t\t\t\t\t\tt.Error(\"Expected iterator to not be empty\")\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\thasValue := iter.Next()\n\t\t\t\tif hasValue != tt.wantObj {\n\t\t\t\t\tt.Errorf(\"Get() got object = %v, want object = %v\", hasValue, tt.wantObj)\n\t\t\t\t}\n\t\t\t\tif hasValue {\n\t\t\t\t\tentry := iter.Value()\n\t\t\t\t\tif entry.ID != seqid.ID(id).String() {\n\t\t\t\t\t\tt.Errorf(\"Get() got id = %v, want id = %v\", entry.ID, seqid.ID(id).String())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tentries := c.GetAll(\"email\", tt.key)\n\t\t\t\tif len(entries) != tt.wantCount {\n\t\t\t\t\tt.Errorf(\"GetAll() returned %d entries, want %d\", len(entries), tt.wantCount)\n\t\t\t\t}\n\t\t\t\tif tt.wantCount \u003e 0 {\n\t\t\t\t\tentry := entries[0]\n\t\t\t\t\tif entry.ID != seqid.ID(id).String() {\n\t\t\t\t\t\tt.Errorf(\"GetAll() returned ID %s, want %s\", entry.ID, seqid.ID(id).String())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetInvalidID(t *testing.T) {\n\tc := New()\n\titer := c.Get(IDIndex, \"not-a-valid-id\")\n\tif !iter.Empty() {\n\t\tt.Errorf(\"Get() with invalid ID format got an entry, want nil\")\n\t}\n}\n\nfunc TestGetAll(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"tags\", func(v any) []string {\n\t\treturn v.(*Person).Tags\n\t}, DefaultIndex)\n\tc.AddIndex(\"age\", func(v any) string {\n\t\treturn strconv.Itoa(v.(*Person).Age)\n\t}, DefaultIndex)\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\n\t// Create test data\n\tpeople := []*Person{\n\t\t{Name: \"Alice\", Age: 30, Tags: []string{\"dev\", \"go\"}},\n\t\t{Name: \"Bob\", Age: 30, Tags: []string{\"dev\", \"python\"}},\n\t\t{Name: \"Charlie\", Age: 25, Tags: []string{\"dev\", \"rust\"}},\n\t}\n\n\tids := make([]uint64, len(people))\n\tfor i, p := range people {\n\t\tids[i] = c.Set(p)\n\t\tif ids[i] == 0 {\n\t\t\tt.Fatalf(\"Failed to set person %s\", p.Name)\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\tindexName string\n\t\tkey string\n\t\twantCount int\n\t}{\n\t\t{\n\t\t\tname: \"Multi-value index with multiple matches\",\n\t\t\tindexName: \"tags\",\n\t\t\tkey: \"dev\",\n\t\t\twantCount: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"Multi-value index with single match\",\n\t\t\tindexName: \"tags\",\n\t\t\tkey: \"go\",\n\t\t\twantCount: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"Multi-value index with no matches\",\n\t\t\tindexName: \"tags\",\n\t\t\tkey: \"java\",\n\t\t\twantCount: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"Single-value non-unique index with multiple matches\",\n\t\t\tindexName: \"age\",\n\t\t\tkey: \"30\",\n\t\t\twantCount: 2,\n\t\t},\n\t\t{\n\t\t\tname: \"Single-value unique index\",\n\t\t\tindexName: \"name\",\n\t\t\tkey: \"Alice\",\n\t\t\twantCount: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"Non-existent index\",\n\t\t\tindexName: \"invalid\",\n\t\t\tkey: \"value\",\n\t\t\twantCount: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\titer := c.Get(tt.indexName, tt.key)\n\t\t\tcount := 0\n\t\t\tfor iter.Next() {\n\t\t\t\tentry := iter.Value()\n\t\t\t\tif entry.ID == \"\" {\n\t\t\t\t\tt.Error(\"Got entry with empty ID\")\n\t\t\t\t}\n\t\t\t\tif entry.Obj == nil {\n\t\t\t\t\tt.Error(\"Got entry with nil Obj\")\n\t\t\t\t}\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif count != tt.wantCount {\n\t\t\t\tt.Errorf(\"Got %d entries, want %d\", count, tt.wantCount)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIndexOperations(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tsetup func(*Collection) (uint64, error)\n\t\tverify func(*Collection, uint64) error\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Basic set and get\",\n\t\t\tsetup: func(c *Collection) (uint64, error) {\n\t\t\t\tc.AddIndex(\"name\", func(v any) string {\n\t\t\t\t\treturn v.(*Person).Name\n\t\t\t\t}, UniqueIndex)\n\t\t\t\treturn c.Set(\u0026Person{Name: \"Alice\", Age: 30}), nil\n\t\t\t},\n\t\t\tverify: func(c *Collection, id uint64) error {\n\t\t\t\titer := c.Get(IDIndex, seqid.ID(id).String())\n\t\t\t\tif !iter.Next() {\n\t\t\t\t\treturn errors.New(\"failed to get object by ID\")\n\t\t\t\t}\n\t\t\t\tentry := iter.Value()\n\t\t\t\tif entry.Obj.(*Person).Name != \"Alice\" {\n\t\t\t\t\treturn errors.New(\"got wrong object\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Composite index\",\n\t\t\tsetup: func(c *Collection) (uint64, error) {\n\t\t\t\tc.AddIndex(\"composite\", func(v any) string {\n\t\t\t\t\tp := v.(*Person)\n\t\t\t\t\treturn p.Name + \":\" + strconv.Itoa(p.Age)\n\t\t\t\t}, UniqueIndex)\n\t\t\t\treturn c.Set(\u0026Person{Name: \"Alice\", Age: 30}), nil\n\t\t\t},\n\t\t\tverify: func(c *Collection, id uint64) error {\n\t\t\t\titer := c.Get(\"composite\", \"Alice:30\")\n\t\t\t\tif !iter.Next() {\n\t\t\t\t\treturn errors.New(\"failed to get object by composite index\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t// Add more test cases combining unique scenarios from original tests\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New()\n\t\t\tid, err := tt.setup(c)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"setup error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil {\n\t\t\t\tif err := tt.verify(c, id); err != nil {\n\t\t\t\t\tt.Errorf(\"verification failed: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMultiValueIndexes(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"tags\", func(v any) []string {\n\t\treturn v.(*Person).Tags\n\t}, DefaultIndex)\n\n\ttests := []struct {\n\t\tname string\n\t\tsetup []*Person\n\t\tsearchTag string\n\t\twantCount int\n\t}{\n\t\t{\n\t\t\tname: \"Multiple tags, multiple matches\",\n\t\t\tsetup: []*Person{\n\t\t\t\t{Name: \"Alice\", Tags: []string{\"dev\", \"go\"}},\n\t\t\t\t{Name: \"Bob\", Tags: []string{\"dev\", \"python\"}},\n\t\t\t\t{Name: \"Charlie\", Tags: []string{\"dev\", \"rust\"}},\n\t\t\t},\n\t\t\tsearchTag: \"dev\",\n\t\t\twantCount: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"Single tag match\",\n\t\t\tsetup: []*Person{\n\t\t\t\t{Name: \"Alice\", Tags: []string{\"dev\", \"go\"}},\n\t\t\t\t{Name: \"Bob\", Tags: []string{\"dev\", \"python\"}},\n\t\t\t},\n\t\t\tsearchTag: \"go\",\n\t\t\twantCount: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"No matches\",\n\t\t\tsetup: []*Person{\n\t\t\t\t{Name: \"Alice\", Tags: []string{\"dev\", \"go\"}},\n\t\t\t\t{Name: \"Bob\", Tags: []string{\"dev\", \"python\"}},\n\t\t\t},\n\t\t\tsearchTag: \"java\",\n\t\t\twantCount: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"Empty tags\",\n\t\t\tsetup: []*Person{\n\t\t\t\t{Name: \"Alice\", Tags: []string{}},\n\t\t\t\t{Name: \"Bob\", Tags: nil},\n\t\t\t},\n\t\t\tsearchTag: \"dev\",\n\t\t\twantCount: 0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New()\n\t\t\tc.AddIndex(\"tags\", func(v any) []string {\n\t\t\t\treturn v.(*Person).Tags\n\t\t\t}, DefaultIndex)\n\n\t\t\t// Setup test data\n\t\t\tfor _, p := range tt.setup {\n\t\t\t\tif id := c.Set(p); id == 0 {\n\t\t\t\t\tt.Fatalf(\"Failed to set person %s\", p.Name)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Test Get operation\n\t\t\titer := c.Get(\"tags\", tt.searchTag)\n\t\t\tcount := 0\n\t\t\tfor iter.Next() {\n\t\t\t\tcount++\n\t\t\t}\n\t\t\tif count != tt.wantCount {\n\t\t\t\tt.Errorf(\"Get() got %d matches, want %d\", count, tt.wantCount)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetOperations(t *testing.T) {\n\tc := New()\n\tc.AddIndex(\"name\", func(v any) string {\n\t\treturn v.(*Person).Name\n\t}, UniqueIndex)\n\tc.AddIndex(\"age\", func(v any) string {\n\t\treturn strconv.Itoa(v.(*Person).Age)\n\t}, DefaultIndex)\n\n\t// Setup test data\n\ttestPeople := []*Person{\n\t\t{Name: \"Alice\", Age: 30},\n\t\t{Name: \"Bob\", Age: 30},\n\t\t{Name: \"Charlie\", Age: 25},\n\t}\n\n\tids := make([]uint64, len(testPeople))\n\tfor i, p := range testPeople {\n\t\tids[i] = c.Set(p)\n\t\tif ids[i] == 0 {\n\t\t\tt.Fatalf(\"Failed to set person %s\", p.Name)\n\t\t}\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\tindexName string\n\t\tkey string\n\t\twantCount int\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"Get by ID\",\n\t\t\tindexName: IDIndex,\n\t\t\tkey: seqid.ID(ids[0]).String(),\n\t\t\twantCount: 1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Get by unique index\",\n\t\t\tindexName: \"name\",\n\t\t\tkey: \"Alice\",\n\t\t\twantCount: 1,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Get by non-unique index\",\n\t\t\tindexName: \"age\",\n\t\t\tkey: \"30\",\n\t\t\twantCount: 2,\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname: \"Get with invalid index\",\n\t\t\tindexName: \"invalid_index\",\n\t\t\tkey: \"value\",\n\t\t\twantCount: 0,\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Get with invalid ID format\",\n\t\t\tindexName: IDIndex,\n\t\t\tkey: \"not-a-valid-id\",\n\t\t\twantCount: 0,\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\titer := c.Get(tt.indexName, tt.key)\n\t\t\tif iter.Empty() {\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\tt.Errorf(\"Get() returned empty iterator, wanted %d results\", tt.wantCount)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcount := 0\n\t\t\tfor iter.Next() {\n\t\t\t\tentry := iter.Value()\n\t\t\t\tif entry.ID == \"\" {\n\t\t\t\t\tt.Error(\"Got entry with empty ID\")\n\t\t\t\t}\n\t\t\t\tif entry.Obj == nil {\n\t\t\t\t\tt.Error(\"Got entry with nil Obj\")\n\t\t\t\t}\n\t\t\t\tcount++\n\t\t\t}\n\n\t\t\tif count != tt.wantCount {\n\t\t\t\tt.Errorf(\"Get() returned %d results, want %d\", count, tt.wantCount)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEntryString(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tentry *Entry\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"Nil entry\",\n\t\t\tentry: nil,\n\t\t\texpected: \"\u003cnil\u003e\",\n\t\t},\n\t\t{\n\t\t\tname: \"Person entry\",\n\t\t\tentry: \u0026Entry{\n\t\t\t\tID: \"123\",\n\t\t\t\tObj: \u0026Person{Name: \"Alice\", Age: 30},\n\t\t\t},\n\t\t\texpected: `Entry{ID: 123, Obj: name=Alice age=30 email= username= tags=}`,\n\t\t},\n\t\t{\n\t\t\tname: \"Entry with nil object\",\n\t\t\tentry: \u0026Entry{\n\t\t\t\tID: \"456\",\n\t\t\t\tObj: nil,\n\t\t\t},\n\t\t\texpected: `Entry{ID: 456, Obj: \u003cnil\u003e}`,\n\t\t},\n\t\t{\n\t\t\tname: \"Entry with complete person\",\n\t\t\tentry: \u0026Entry{\n\t\t\t\tID: \"789\",\n\t\t\t\tObj: \u0026Person{\n\t\t\t\t\tName: \"Bob\",\n\t\t\t\t\tAge: 25,\n\t\t\t\t\tEmail: \"bob@example.com\",\n\t\t\t\t\tUsername: \"bobby\",\n\t\t\t\t\tTags: []string{\"dev\", \"go\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: `Entry{ID: 789, Obj: name=Bob age=25 email=bob@example.com username=bobby tags=dev,go}`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := tt.entry.String()\n\t\t\tif got != tt.expected {\n\t\t\t\tt.Errorf(\"Entry.String() = %q, want %q\", got, tt.expected)\n\t\t\t}\n\t\t})\n\t}\n}\n" + }, + { + "name": "entry.gno", + "body": "package collection\n\nimport \"gno.land/p/demo/ufmt\"\n\n// Entry represents a single object in the collection with its ID\ntype Entry struct {\n\tID string\n\tObj any\n}\n\n// String returns a string representation of the Entry\nfunc (e *Entry) String() string {\n\tif e == nil {\n\t\treturn \"\u003cnil\u003e\"\n\t}\n\treturn ufmt.Sprintf(\"Entry{ID: %s, Obj: %v}\", e.ID, e.Obj)\n}\n\n// EntryIterator provides iteration over collection entries\ntype EntryIterator struct {\n\tcollection *Collection\n\tindexName string\n\tkey string\n\tcurrentID string\n\tcurrentObj any\n\terr error\n\tclosed bool\n\n\t// For multi-value cases\n\tids []string\n\tcurrentIdx int\n}\n\nfunc (ei *EntryIterator) Close() error {\n\tei.closed = true\n\tei.currentID = \"\"\n\tei.currentObj = nil\n\tei.ids = nil\n\treturn nil\n}\n\nfunc (ei *EntryIterator) Next() bool {\n\tif ei == nil || ei.closed || ei.err != nil {\n\t\treturn false\n\t}\n\n\t// Handle ID index specially\n\tif ei.indexName == IDIndex {\n\t\tif ei.currentID != \"\" { // We've already returned the single value\n\t\t\treturn false\n\t\t}\n\t\tobj, exists := ei.collection.indexes[IDIndex].tree.Get(ei.key)\n\t\tif !exists {\n\t\t\treturn false\n\t\t}\n\t\tei.currentID = ei.key\n\t\tei.currentObj = obj\n\t\treturn true\n\t}\n\n\t// Get the index\n\tidx, exists := ei.collection.indexes[ei.indexName]\n\tif !exists {\n\t\treturn false\n\t}\n\n\t// Initialize ids slice if needed\n\tif ei.ids == nil {\n\t\tidData, exists := idx.tree.Get(ei.key)\n\t\tif !exists {\n\t\t\treturn false\n\t\t}\n\n\t\tswitch stored := idData.(type) {\n\t\tcase []string:\n\t\t\tei.ids = stored\n\t\t\tei.currentIdx = -1\n\t\tcase string:\n\t\t\tei.ids = []string{stored}\n\t\t\tei.currentIdx = -1\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Move to next ID\n\tei.currentIdx++\n\tif ei.currentIdx \u003e= len(ei.ids) {\n\t\treturn false\n\t}\n\n\t// Fetch the actual object\n\tei.currentID = ei.ids[ei.currentIdx]\n\tobj, exists := ei.collection.indexes[IDIndex].tree.Get(ei.currentID)\n\tif !exists {\n\t\t// Skip invalid entries\n\t\treturn ei.Next()\n\t}\n\tei.currentObj = obj\n\treturn true\n}\n\nfunc (ei *EntryIterator) Error() error {\n\treturn ei.err\n}\n\nfunc (ei *EntryIterator) Value() *Entry {\n\tif ei == nil || ei.closed || ei.currentID == \"\" {\n\t\treturn nil\n\t}\n\treturn \u0026Entry{\n\t\tID: ei.currentID,\n\t\tObj: ei.currentObj,\n\t}\n}\n\nfunc (ei *EntryIterator) Empty() bool {\n\tif ei == nil || ei.closed || ei.err != nil {\n\t\treturn true\n\t}\n\n\t// Handle ID index specially\n\tif ei.indexName == IDIndex {\n\t\t_, exists := ei.collection.indexes[IDIndex].tree.Get(ei.key)\n\t\treturn !exists\n\t}\n\n\t// Get the index\n\tidx, exists := ei.collection.indexes[ei.indexName]\n\tif !exists {\n\t\treturn true\n\t}\n\n\t// Check if key exists in index\n\tidData, exists := idx.tree.Get(ei.key)\n\tif !exists {\n\t\treturn true\n\t}\n\n\t// Check if there are any valid IDs\n\tswitch stored := idData.(type) {\n\tcase []string:\n\t\treturn len(stored) == 0\n\tcase string:\n\t\treturn stored == \"\"\n\tdefault:\n\t\treturn true\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "XD9efha7Pms3nEeb6tVew8rbQPjIenacs4N3Uw+1CzWwumYt1qX6XBgtsJMerFp7HPzh60T5yia2dZuQ8PemBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "datastore", + "path": "gno.land/p/jeronimoalbi/datastore", + "files": [ + { + "name": "datastore.gno", + "body": "package datastore\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// ErrStorageExists indicates that a storage exists with the same name.\nvar ErrStorageExists = errors.New(\"a storage with the same name exists\")\n\n// Datastore is a store that can contain multiple named storages.\n// A storage is a collection of records.\n//\n// Example usage:\n//\n//\t// Create an empty storage to store user records\n//\tvar db Datastore\n//\tstorage := db.CreateStorage(\"users\")\n//\n//\t// Get a storage that has been created before\n//\tstorage = db.GetStorage(\"profiles\")\ntype Datastore struct {\n\tstorages avl.Tree // string(name) -\u003e *Storage\n}\n\n// CreateStorage creates a new named storage within the data store.\nfunc (ds *Datastore) CreateStorage(name string, options ...StorageOption) *Storage {\n\tif ds.storages.Has(name) {\n\t\treturn nil\n\t}\n\n\ts := NewStorage(name, options...)\n\tds.storages.Set(name, \u0026s)\n\treturn \u0026s\n}\n\n// HasStorage checks if data store contains a storage with a specific name.\nfunc (ds Datastore) HasStorage(name string) bool {\n\treturn ds.storages.Has(name)\n}\n\n// GetStorage returns a storage that has been created with a specific name.\n// It returns nil when a storage with the specified name is not found.\nfunc (ds Datastore) GetStorage(name string) *Storage {\n\tif v, found := ds.storages.Get(name); found {\n\t\treturn v.(*Storage)\n\t}\n\treturn nil\n}\n" + }, + { + "name": "datastore_test.gno", + "body": "package datastore\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestDatastoreCreateStorage(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\tstorageName string\n\t\tmustFail bool\n\t\tsetup func(*Datastore)\n\t}{\n\t\t{\n\t\t\tname: \"success\",\n\t\t\tstorageName: \"users\",\n\t\t},\n\t\t{\n\t\t\tname: \"storage exists\",\n\t\t\tstorageName: \"users\",\n\t\t\tmustFail: true,\n\t\t\tsetup: func(db *Datastore) {\n\t\t\t\tdb.CreateStorage(\"users\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar db Datastore\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026db)\n\t\t\t}\n\n\t\t\tstorage := db.CreateStorage(tc.storageName)\n\n\t\t\tif tc.mustFail {\n\t\t\t\tuassert.Equal(t, nil, storage)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NotEqual(t, nil, storage, \"storage created\")\n\t\t\tuassert.Equal(t, tc.storageName, storage.Name())\n\t\t\tuassert.True(t, db.HasStorage(tc.storageName))\n\t\t})\n\t}\n}\n\nfunc TestDatastoreHasStorage(t *testing.T) {\n\tvar (\n\t\tdb Datastore\n\t\tname = \"users\"\n\t)\n\n\tuassert.False(t, db.HasStorage(name))\n\n\tdb.CreateStorage(name)\n\tuassert.True(t, db.HasStorage(name))\n}\n\nfunc TestDatastoreGetStorage(t *testing.T) {\n\tvar (\n\t\tdb Datastore\n\t\tname = \"users\"\n\t)\n\n\tstorage := db.GetStorage(name)\n\tuassert.Equal(t, nil, storage)\n\n\tdb.CreateStorage(name)\n\n\tstorage = db.GetStorage(name)\n\turequire.NotEqual(t, nil, storage, \"storage found\")\n\tuassert.Equal(t, name, storage.Name())\n}\n" + }, + { + "name": "doc.gno", + "body": "// Package datastore provides support to store multiple collections of records.\n//\n// It supports the definition of multiple storages, where each one is a collection\n// of records. Records can have any number of user defined fields which are added\n// dynamically when values are set on a record. These fields can also be renamed\n// or removed.\n//\n// Storages have support for simple schemas that allows users to pre-define fields\n// which can optionally have a default value also defined. Default values are\n// assigned to new records on creation.\n//\n// User defined schemas can optionally be strict, which means that records from a\n// storage using the schema can only assign values to the pre-defined set of fields.\n// In which case, assigning a value to an unknown field would result on an error.\n//\n// Package also support the definition of custom record indexes. Indexes are used\n// by storages to search and iterate records.\n// The default index is the ID index but custom single and multi value indexes can\n// be defined.\n//\n// WARNING: Using this package to store your realm data must be carefully considered.\n// The fact that record fields are not strictly typed and can be renamed or removed\n// could lead to issues if not careful when coding your realm(s). So it's recommended\n// that you consider other alternatives first, like alternative patterns or solutions\n// provided by the blockchain to deal with data, types and data migration for example.\n//\n// Example usage:\n//\n//\tvar db datastore.Datastore\n//\n//\t// Define a unique case insensitive index for user emails\n//\temailIdx := datastore.NewIndex(\"email\", func(r datastore.Record) string {\n//\t return r.MustGet(\"email\").(string)\n//\t}).Unique().CaseInsensitive()\n//\n//\t// Create a new storage for user records\n//\tstorage := db.CreateStorage(\"users\", datastore.WithIndex(emailIdx))\n//\n//\t// Add a user with a single \"email\" field\n//\tuser := storage.NewRecord()\n//\tuser.Set(\"email\", \"foo@bar.org\")\n//\n//\t// Save to assing user ID and update indexes\n//\tuser.Save()\n//\n//\t// Find user by email using the custom index\n//\tuser, _ = storage.Get(emailIdx.Name(), \"foo@bar.org\")\n//\n//\t// Find user by ID\n//\tuser, _ = storage.GetByID(user.ID())\n//\n//\t// Search user's profile by email in another existing storage\n//\tstorage = db.GetStorage(\"profiles\")\n//\temail := user.MustGet(\"email\").(string)\n//\tprofile, found := storage.Get(\"user\", email)\n//\tif !found {\n//\t panic(\"profile not found\")\n//\t}\n//\n//\t// Delete the profile from the storage and update indexes\n//\tstorage.Delete(profile.ID())\n//\n// Example query usage:\n//\n//\tvar db datastore.Datastore\n//\n//\t// Create a query with a custom offset and size\n//\tstorage := db.GetStorage(\"users\")\n//\trecordset, err := storage.Query(datastore.WithOffset(100), datastore.WithSize(50))\n//\tif err != nil {\n//\t panic(err)\n//\t}\n//\n//\t// Get all query results\n//\tvar records []Record\n//\trecordset.Iterate(func(r datastore.Record) bool {\n//\t records = append(records, r)\n//\t return false\n//\t})\n//\n// Example query using a custom index usage:\n//\n//\tvar db datastore.Datastore\n//\n//\t// Create a query to get records using a custom pre-defined index\n//\tstorage := db.GetStorage(\"posts\")\n//\trecordset, err := storage.Query(datastore.UseIndex(\"tags\", \"tagname\"))\n//\tif err != nil {\n//\t panic(err)\n//\t}\n//\n//\t// Get all query results\n//\tvar records []Record\n//\trecordset.Iterate(func(r datastore.Record) bool {\n//\t records = append(records, r)\n//\t return false\n//\t})\npackage datastore\n" + }, + { + "name": "index.gno", + "body": "package datastore\n\nimport (\n\t\"gno.land/p/moul/collection\"\n)\n\n// DefaultIndexOptions defines the default options for new indexes.\nconst DefaultIndexOptions = collection.DefaultIndex | collection.SparseIndex\n\ntype (\n\t// IndexFn defines a type for single value indexing functions.\n\t// This type of function extracts a single string value from\n\t// a record that is then used to index it.\n\tIndexFn func(Record) string\n\n\t// IndexMultiValueFn defines a type for multi value indexing functions.\n\t// This type of function extracts multiple string values from a\n\t// record that are then used to index it.\n\tIndexMultiValueFn func(Record) []string\n\n\t// Index defines a type for custom user defined storage indexes.\n\t// Storages are by default indexed by the auto geneated record ID\n\t// but can additionally be indexed by other custom record fields.\n\tIndex struct {\n\t\tname string\n\t\toptions collection.IndexOption\n\t\tfn interface{}\n\t}\n)\n\n// NewIndex creates a new single value index.\n//\n// Usage example:\n//\n//\t// Index a User record by email\n//\tidx := NewIndex(\"email\", func(r Record) string {\n//\t return r.MustGet(\"email\").(string)\n//\t})\nfunc NewIndex(name string, fn IndexFn) Index {\n\treturn Index{\n\t\tname: name,\n\t\toptions: DefaultIndexOptions,\n\t\tfn: func(v interface{}) string {\n\t\t\treturn fn(v.(Record))\n\t\t},\n\t}\n}\n\n// NewMultiValueIndex creates a new multi value index.\n//\n// Usage example:\n//\n//\t// Index a Post record by tag\n//\tidx := NewMultiValueIndex(\"tag\", func(r Record) []string {\n//\t return r.MustGet(\"tags\").([]string)\n//\t})\nfunc NewMultiValueIndex(name string, fn IndexMultiValueFn) Index {\n\treturn Index{\n\t\tname: name,\n\t\toptions: DefaultIndexOptions,\n\t\tfn: func(v interface{}) []string {\n\t\t\treturn fn(v.(Record))\n\t\t},\n\t}\n}\n\n// Name returns index's name.\nfunc (idx Index) Name() string {\n\treturn idx.name\n}\n\n// Options returns current index options.\n// These options define the index behavior regarding case sensitivity and uniquenes.\nfunc (idx Index) Options() collection.IndexOption {\n\treturn idx.options\n}\n\n// Func returns the function that storage collections apply\n// to each record to get the value to use for indexing it.\nfunc (idx Index) Func() interface{} {\n\treturn idx.fn\n}\n\n// Unique returns a copy of the index that indexes record values as unique values.\n// Returned index contains previous options plus the unique one.\nfunc (idx Index) Unique() Index {\n\tif idx.options\u0026collection.UniqueIndex == 0 {\n\t\tidx.options |= collection.UniqueIndex\n\t}\n\treturn idx\n}\n\n// CaseInsensitive returns a copy of the index that indexes record values ignoring casing.\n// Returned index contains previous options plus the case insensitivity one.\nfunc (idx Index) CaseInsensitive() Index {\n\tif idx.options\u0026collection.CaseInsensitiveIndex == 0 {\n\t\tidx.options |= collection.CaseInsensitiveIndex\n\t}\n\treturn idx\n}\n" + }, + { + "name": "index_test.gno", + "body": "package datastore\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/moul/collection\"\n)\n\nfunc TestNewIndex(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\toptions collection.IndexOption\n\t\tsetup func(Index) Index\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t\toptions: DefaultIndexOptions,\n\t\t},\n\t\t{\n\t\t\tname: \"unique\",\n\t\t\toptions: DefaultIndexOptions | collection.UniqueIndex,\n\t\t\tsetup: func(idx Index) Index { return idx.Unique() },\n\t\t},\n\t\t{\n\t\t\tname: \"case insensitive\",\n\t\t\toptions: DefaultIndexOptions | collection.CaseInsensitiveIndex,\n\t\t\tsetup: func(idx Index) Index { return idx.CaseInsensitive() },\n\t\t},\n\t\t{\n\t\t\tname: \"unique case insensitive\",\n\t\t\toptions: DefaultIndexOptions | collection.CaseInsensitiveIndex | collection.UniqueIndex,\n\t\t\tsetup: func(idx Index) Index { return idx.CaseInsensitive().Unique() },\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tname := \"foo\"\n\t\t\tidx := NewIndex(name, func(Record) string { return \"\" })\n\n\t\t\tif tc.setup != nil {\n\t\t\t\tidx = tc.setup(idx)\n\t\t\t}\n\n\t\t\tuassert.Equal(t, name, idx.Name())\n\t\t\tuassert.Equal(t, uint64(tc.options), uint64(idx.Options()))\n\n\t\t\t_, ok := idx.Func().(func(interface{}) string)\n\t\t\tuassert.True(t, ok)\n\t\t})\n\t}\n}\n\nfunc TestNewMultiIndex(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\toptions collection.IndexOption\n\t\tsetup func(Index) Index\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t\toptions: DefaultIndexOptions,\n\t\t},\n\t\t{\n\t\t\tname: \"unique\",\n\t\t\toptions: DefaultIndexOptions | collection.UniqueIndex,\n\t\t\tsetup: func(idx Index) Index { return idx.Unique() },\n\t\t},\n\t\t{\n\t\t\tname: \"case insensitive\",\n\t\t\toptions: DefaultIndexOptions | collection.CaseInsensitiveIndex,\n\t\t\tsetup: func(idx Index) Index { return idx.CaseInsensitive() },\n\t\t},\n\t\t{\n\t\t\tname: \"unique case insensitive\",\n\t\t\toptions: DefaultIndexOptions | collection.CaseInsensitiveIndex | collection.UniqueIndex,\n\t\t\tsetup: func(idx Index) Index { return idx.CaseInsensitive().Unique() },\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tname := \"foo\"\n\t\t\tidx := NewMultiValueIndex(name, func(Record) []string { return nil })\n\n\t\t\tif tc.setup != nil {\n\t\t\t\tidx = tc.setup(idx)\n\t\t\t}\n\n\t\t\tuassert.Equal(t, name, idx.Name())\n\t\t\tuassert.Equal(t, uint64(tc.options), uint64(idx.Options()))\n\n\t\t\t_, ok := idx.Func().(func(interface{}) []string)\n\t\t\tuassert.True(t, ok)\n\t\t})\n\t}\n}\n" + }, + { + "name": "query.gno", + "body": "package datastore\n\nimport (\n\t\"gno.land/p/moul/collection\"\n)\n\nvar defaultQuery = Query{indexName: collection.IDIndex}\n\n// Query contains arguments for querying a storage.\ntype Query struct {\n\toffset int\n\tsize int\n\tindexName string\n\tindexKey string\n}\n\n// Offset returns the position of the first record to return.\n// The minimum offset value is 0.\nfunc (q Query) Offset() int {\n\treturn q.offset\n}\n\n// Size returns the maximum number of records a query returns.\nfunc (q Query) Size() int {\n\treturn q.size\n}\n\n// IndexName returns the name of the storage index being used for the query.\nfunc (q Query) IndexName() string {\n\treturn q.indexName\n}\n\n// IndexKey return the index key value to locate the records.\n// An empty string is returned when all indexed records match the query.\nfunc (q Query) IndexKey() string {\n\treturn q.indexKey\n}\n\n// IsEmpty checks if the query is empty.\n// Empty queries return no records.\nfunc (q Query) IsEmpty() bool {\n\treturn q.indexName == \"\"\n}\n" + }, + { + "name": "query_options.gno", + "body": "package datastore\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\nvar (\n\tErrEmptyQueryIndexName = errors.New(\"query index name is empty\")\n\tErrInvalidQueryOffset = errors.New(\"minimum allowed query offset is 0\")\n\tErrInvalidQuerySize = errors.New(\"minimum allowed query size is 1\")\n)\n\n// QueryOption configures queries.\ntype QueryOption func(*Query) error\n\n// WithOffset assigns the offset or position of the first record that query must return.\n// The minimum allowed offset is 0.\nfunc WithOffset(offset int) QueryOption {\n\treturn func(q *Query) error {\n\t\tif offset \u003c 0 {\n\t\t\treturn ErrInvalidQueryOffset\n\t\t}\n\n\t\tq.offset = offset\n\t\treturn nil\n\t}\n}\n\n// WithSize assigns the maximum number of records that query can return.\n// The minimum allowed size is 1.\nfunc WithSize(size int) QueryOption {\n\treturn func(q *Query) error {\n\t\tif size \u003c 1 {\n\t\t\treturn ErrInvalidQuerySize\n\t\t}\n\n\t\tq.size = size\n\t\treturn nil\n\t}\n}\n\n// UseIndex assigns the index that the query must use to get the records.\n// Using an index requires a key value to locate the records within the index.\nfunc UseIndex(name, key string) QueryOption {\n\treturn func(q *Query) error {\n\t\tq.indexName = strings.TrimSpace(name)\n\t\tif q.indexName == \"\" {\n\t\t\treturn ErrEmptyQueryIndexName\n\t\t}\n\n\t\tq.indexKey = key\n\t\treturn nil\n\t}\n}\n" + }, + { + "name": "record.gno", + "body": "package datastore\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/moul/collection\"\n)\n\n// ErrUndefinedField indicates that a field in not defined in a record's schema.\nvar ErrUndefinedField = errors.New(\"undefined field\")\n\ntype (\n\t// Record stores values for one or more fields.\n\tRecord interface {\n\t\tReadOnlyRecord\n\n\t\t// Set assings a value to a record field.\n\t\t// If the field doesn't exist it's created if the underlying schema allows it.\n\t\t// Storage schema can optionally be strict in which case no new fields other than\n\t\t// the ones that were previously defined are allowed.\n\t\tSet(field string, value interface{}) error\n\n\t\t// Save assigns an ID to newly created records and update storage indexes.\n\t\tSave() bool\n\t}\n\n\t// ReadOnlyRecord defines an interface for read-only records.\n\tReadOnlyRecord interface {\n\t\t// ID returns record's ID\n\t\tID() uint64\n\n\t\t// Key returns a string representation of the record's ID.\n\t\t// It's used to be able to search records within the ID index.\n\t\tKey() string\n\n\t\t// Type returns the record's type.\n\t\tType() string\n\n\t\t// Fields returns the list of the record's field names.\n\t\tFields() []string\n\n\t\t// IsEmpty checks if the record has no values.\n\t\tIsEmpty() bool\n\n\t\t// HasField checks if the record has a specific field.\n\t\tHasField(name string) bool\n\n\t\t// Get returns the value of a record's field.\n\t\tGet(field string) (value interface{}, found bool)\n\n\t\t// MustGet returns the value of a record's field or panics when the field is not found.\n\t\tMustGet(field string) interface{}\n\t}\n\n\t// RecordIterFn defines a type for record iteration functions.\n\tRecordIterFn func(Record) (stop bool)\n\n\t// Recordset defines an interface that allows iterating multiple records.\n\tRecordset interface {\n\t\t// Iterate iterates records in order.\n\t\tIterate(fn RecordIterFn) (stopped bool)\n\n\t\t// ReverseIterate iterates records in reverse order.\n\t\tReverseIterate(fn RecordIterFn) (stopped bool)\n\n\t\t// Size returns the number of records in the recordset.\n\t\tSize() int\n\t}\n)\n\ntype record struct {\n\tid uint64\n\tschema *Schema\n\tcollection *collection.Collection\n\tvalues avl.Tree // string(field index) -\u003e interface{}\n}\n\n// ID returns record's ID\nfunc (r record) ID() uint64 {\n\treturn r.id\n}\n\n// Key returns a string representation of the record's ID.\n// It's used to be able to search records within the ID index.\nfunc (r record) Key() string {\n\treturn seqid.ID(r.id).String()\n}\n\n// Type returns the record's type.\nfunc (r record) Type() string {\n\treturn r.schema.Name()\n}\n\n// Fields returns the list of the record's field names.\nfunc (r record) Fields() []string {\n\treturn r.schema.Fields()\n}\n\n// IsEmpty checks if the record has no values.\nfunc (r record) IsEmpty() bool {\n\treturn r.values.Size() == 0\n}\n\n// HasField checks if the record has a specific field.\nfunc (r record) HasField(name string) bool {\n\treturn r.schema.HasField(name)\n}\n\n// Set assings a value to a record field.\n// If the field doesn't exist it's created if the underlying schema allows it.\n// Storage schema can optionally be strict in which case no new fields other than\n// the ones that were previously defined are allowed.\nfunc (r *record) Set(field string, value interface{}) error {\n\ti := r.schema.GetFieldIndex(field)\n\tif i == -1 {\n\t\tif r.schema.IsStrict() {\n\t\t\treturn ErrUndefinedField\n\t\t}\n\n\t\ti, _ = r.schema.AddField(field, nil)\n\t}\n\n\tkey := castIntToKey(i)\n\tr.values.Set(key, value)\n\treturn nil\n}\n\n// Get returns the value of a record's field.\nfunc (r record) Get(field string) (value interface{}, found bool) {\n\ti := r.schema.GetFieldIndex(field)\n\tif i == -1 {\n\t\treturn nil, false\n\t}\n\n\tkey := castIntToKey(i)\n\treturn r.values.Get(key)\n}\n\n// MustGet returns the value of a record's field or panics when the field is not found.\nfunc (r record) MustGet(field string) interface{} {\n\tv, found := r.Get(field)\n\tif !found {\n\t\tpanic(\"field not found: \" + field)\n\t}\n\treturn v\n}\n\n// Save assigns an ID to newly created records and update storage indexes.\nfunc (r *record) Save() bool {\n\tif r.id == 0 {\n\t\tr.id = r.collection.Set(r)\n\t\treturn r.id != 0\n\t}\n\treturn r.collection.Update(r.id, r)\n}\n\ntype recordset struct {\n\tquery Query\n\trecords avl.ITree\n\tkeys []string\n\tsize int\n}\n\n// Iterate iterates records in order.\nfunc (rs recordset) Iterate(fn RecordIterFn) (stopped bool) {\n\tif rs.isUsingCustomIndex() {\n\t\tfor _, k := range rs.keys {\n\t\t\tv, found := rs.records.Get(k)\n\t\t\tif !found {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif fn(v.(Record)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t}\n\n\toffset := rs.query.Offset()\n\tcount := rs.query.Size()\n\tif count == 0 {\n\t\tcount = rs.records.Size()\n\t}\n\n\treturn rs.records.IterateByOffset(offset, count, func(_ string, v interface{}) bool {\n\t\treturn fn(v.(Record))\n\t})\n}\n\n// ReverseIterate iterates records in reverse order.\nfunc (rs recordset) ReverseIterate(fn RecordIterFn) (stopped bool) {\n\tif rs.isUsingCustomIndex() {\n\t\tfor i := len(rs.keys) - 1; i \u003e= 0; i-- {\n\t\t\tv, found := rs.records.Get(rs.keys[i])\n\t\t\tif !found {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif fn(v.(Record)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t}\n\n\toffset := rs.query.Offset()\n\tcount := rs.query.Size()\n\tif count == 0 {\n\t\tcount = rs.records.Size()\n\t}\n\n\treturn rs.records.ReverseIterateByOffset(offset, count, func(_ string, v interface{}) bool {\n\t\treturn fn(v.(Record))\n\t})\n}\n\n// Size returns the number of records in the recordset.\nfunc (rs recordset) Size() int {\n\treturn rs.size\n}\n\nfunc (rs recordset) isUsingCustomIndex() bool {\n\treturn rs.query.IndexName() != collection.IDIndex\n}\n" + }, + { + "name": "record_test.gno", + "body": "package datastore\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\t_ Record = (*record)(nil)\n\t_ Recordset = (*recordset)(nil)\n)\n\nfunc TestRecordDefaults(t *testing.T) {\n\t// Arrange\n\tstorage := NewStorage(\"foo\")\n\n\t// Act\n\tr := storage.NewRecord()\n\n\t// Assert\n\tuassert.Equal(t, uint64(0), r.ID())\n\tuassert.Equal(t, \"0000000\", r.Key())\n\tuassert.Equal(t, \"Foo\", r.Type())\n\tuassert.Equal(t, nil, r.Fields())\n\tuassert.True(t, r.IsEmpty())\n}\n\nfunc TestRecordHasField(t *testing.T) {\n\tstorage := NewStorage(\"foo\")\n\tstorage.Schema().AddField(\"foo\", nil)\n\n\tr := storage.NewRecord()\n\n\tuassert.True(t, r.HasField(\"foo\"))\n\tuassert.False(t, r.HasField(\"undefined\"))\n}\n\nfunc TestRecordSet(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\toptions []SchemaOption\n\t\tfield string\n\t\tfieldsCount int\n\t\tvalue int\n\t\terr error\n\t}{\n\t\t{\n\t\t\tname: \"first new field\",\n\t\t\tfield: \"test\",\n\t\t\tfieldsCount: 1,\n\t\t\tvalue: 42,\n\t\t},\n\t\t{\n\t\t\tname: \"new extra field\",\n\t\t\toptions: []SchemaOption{\n\t\t\t\tWithField(\"foo\"),\n\t\t\t\tWithField(\"bar\"),\n\t\t\t},\n\t\t\tfield: \"test\",\n\t\t\tfieldsCount: 3,\n\t\t\tvalue: 42,\n\t\t},\n\t\t{\n\t\t\tname: \"existing field\",\n\t\t\toptions: []SchemaOption{\n\t\t\t\tWithField(\"test\"),\n\t\t\t},\n\t\t\tfield: \"test\",\n\t\t\tfieldsCount: 1,\n\t\t\tvalue: 42,\n\t\t},\n\t\t{\n\t\t\tname: \"undefined field\",\n\t\t\toptions: []SchemaOption{Strict()},\n\t\t\tfield: \"test\",\n\t\t\tfieldsCount: 1,\n\t\t\tvalue: 42,\n\t\t\terr: ErrUndefinedField,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\ts := NewSchema(\"Foo\", tc.options...)\n\t\t\tstorage := NewStorage(\"foo\", WithSchema(s))\n\t\t\tr := storage.NewRecord()\n\n\t\t\t// Act\n\t\t\terr := r.Set(tc.field, tc.value)\n\n\t\t\t// Assert\n\t\t\tif tc.err != nil {\n\t\t\t\turequire.ErrorIs(t, err, tc.err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err)\n\t\t\tuassert.True(t, r.HasField(\"test\"))\n\t\t\tuassert.False(t, r.IsEmpty())\n\t\t\tuassert.Equal(t, tc.fieldsCount, len(r.Fields()))\n\t\t})\n\t}\n}\n\nfunc TestRecordGet(t *testing.T) {\n\tstorage := NewStorage(\"foo\")\n\tr := storage.NewRecord()\n\tr.Set(\"foo\", \"bar\")\n\tr.Set(\"test\", 42)\n\n\tv, found := r.Get(\"test\")\n\turequire.True(t, found, \"get setted value\")\n\n\tgot, ok := v.(int)\n\turequire.True(t, ok, \"setted value type\")\n\tuassert.Equal(t, 42, got)\n\n\t_, found = r.Get(\"unknown\")\n\tuassert.False(t, found)\n}\n\nfunc TestRecordSave(t *testing.T) {\n\tindex := NewIndex(\"name\", func(r Record) string {\n\t\treturn r.MustGet(\"name\").(string)\n\t}).Unique().CaseInsensitive()\n\n\tstorage := NewStorage(\"foo\", WithIndex(index))\n\tcases := []struct {\n\t\tname string\n\t\tid uint64\n\t\tfieldValue, key string\n\t\tstorageSize int\n\t\tsetup func(Storage) Record\n\t}{\n\t\t{\n\t\t\tname: \"create first record\",\n\t\t\tid: 1,\n\t\t\tkey: \"0000001\",\n\t\t\tfieldValue: \"foo\",\n\t\t\tstorageSize: 1,\n\t\t\tsetup: func(s Storage) Record { return s.NewRecord() },\n\t\t},\n\t\t{\n\t\t\tname: \"create second record\",\n\t\t\tid: 2,\n\t\t\tkey: \"0000002\",\n\t\t\tfieldValue: \"bar\",\n\t\t\tstorageSize: 2,\n\t\t\tsetup: func(s Storage) Record { return s.NewRecord() },\n\t\t},\n\t\t{\n\t\t\tname: \"update second record\",\n\t\t\tid: 2,\n\t\t\tkey: \"0000002\",\n\t\t\tfieldValue: \"baz\",\n\t\t\tstorageSize: 2,\n\t\t\tsetup: func(s Storage) Record {\n\t\t\t\tr, _ := storage.Get(index.Name(), \"bar\")\n\t\t\t\treturn r\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tr := tc.setup(storage)\n\t\t\tr.Set(\"name\", tc.fieldValue)\n\n\t\t\t_, found := storage.Get(index.Name(), tc.fieldValue)\n\t\t\turequire.False(t, found, \"record not found\")\n\t\t\turequire.True(t, r.Save(), \"save success\")\n\t\t\tuassert.Equal(t, tc.storageSize, storage.Size())\n\n\t\t\tr, found = storage.Get(index.Name(), tc.fieldValue)\n\t\t\turequire.True(t, found, \"record found\")\n\t\t\tuassert.Equal(t, tc.id, r.ID())\n\t\t\tuassert.Equal(t, tc.key, r.Key())\n\t\t\tuassert.Equal(t, tc.fieldValue, r.MustGet(\"name\"))\n\t\t})\n\t}\n}\n\nfunc TestRecordsetIterate(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\trecordIDs []uint64\n\t\tsetup func(*Storage)\n\t}{\n\t\t{\n\t\t\tname: \"single record\",\n\t\t\trecordIDs: []uint64{1},\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"two records\",\n\t\t\trecordIDs: []uint64{1, 2},\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty recordset\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tstorage := NewStorage(\"foo\")\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026storage)\n\t\t\t}\n\n\t\t\tvar (\n\t\t\t\trecords []Record\n\t\t\t\trs = storage.MustQuery()\n\t\t\t)\n\n\t\t\t// Act\n\t\t\trs.Iterate(func(r Record) bool {\n\t\t\t\trecords = append(records, r)\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\t// Assert\n\t\t\turequire.Equal(t, len(tc.recordIDs), len(records), \"results count\")\n\t\t\tfor i, r := range records {\n\t\t\t\tuassert.Equal(t, tc.recordIDs[i], r.ID())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRecordsetReverseIterate(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\trecordIDs []uint64\n\t\tsetup func(*Storage)\n\t}{\n\t\t{\n\t\t\tname: \"single record\",\n\t\t\trecordIDs: []uint64{1},\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"two records\",\n\t\t\trecordIDs: []uint64{2, 1},\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty recordser\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tstorage := NewStorage(\"foo\")\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026storage)\n\t\t\t}\n\n\t\t\tvar (\n\t\t\t\trecords []Record\n\t\t\t\trs = storage.MustQuery()\n\t\t\t)\n\n\t\t\t// Act\n\t\t\trs.ReverseIterate(func(r Record) bool {\n\t\t\t\trecords = append(records, r)\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\t// Assert\n\t\t\turequire.Equal(t, len(tc.recordIDs), len(records), \"results count\")\n\t\t\tfor i, r := range records {\n\t\t\t\tuassert.Equal(t, tc.recordIDs[i], r.ID())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRecordsetSize(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\tsize int\n\t\tsetup func(*Storage)\n\t}{\n\t\t{\n\t\t\tname: \"single record\",\n\t\t\tsize: 1,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"two records\",\n\t\t\tsize: 2,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty recordser\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tstorage := NewStorage(\"foo\")\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026storage)\n\t\t\t}\n\n\t\t\trs := storage.MustQuery()\n\n\t\t\t// Act\n\t\t\tsize := rs.Size()\n\n\t\t\t// Assert\n\t\t\tuassert.Equal(t, tc.size, size)\n\t\t})\n\t}\n}\n" + }, + { + "name": "schema.gno", + "body": "package datastore\n\nimport (\n\t\"encoding/binary\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/list\"\n)\n\n// TODO: Support versioning\n\n// Schema contains information about fields and default field values.\n// It also offers the possibility to configure it as static to indicate\n// that only configured fields should be allowed.\ntype Schema struct {\n\tname string\n\tstrict bool\n\tfields list.List // int(field index) -\u003e string(field name)\n\tdefaults avl.Tree // string(field index) -\u003e interface{}\n}\n\n// NewSchema creates a new schema.\nfunc NewSchema(name string, options ...SchemaOption) *Schema {\n\ts := \u0026Schema{name: name}\n\tfor _, apply := range options {\n\t\tapply(s)\n\t}\n\treturn s\n}\n\n// Name returns schema's name.\nfunc (s Schema) Name() string {\n\treturn s.name\n}\n\n// Fields returns the list field names that are defined in the schema.\nfunc (s Schema) Fields() []string {\n\tfields := make([]string, s.fields.Len())\n\ts.fields.ForEach(func(i int, v interface{}) bool {\n\t\tfields[i] = v.(string)\n\t\treturn false\n\t})\n\treturn fields\n}\n\n// Size returns the number of fields the schema has.\nfunc (s Schema) Size() int {\n\treturn s.fields.Len()\n}\n\n// IsStrict check if the schema is configured as a strict one.\nfunc (s Schema) IsStrict() bool {\n\treturn s.strict\n}\n\n// HasField check is a field has been defined in the schema.\nfunc (s Schema) HasField(name string) bool {\n\treturn s.GetFieldIndex(name) \u003e= 0\n}\n\n// AddField adds a new field to the schema.\n// A default field value can be specified, otherwise `defaultValue` must be nil.\nfunc (s *Schema) AddField(name string, defaultValue interface{}) (index int, added bool) {\n\tif s.HasField(name) {\n\t\treturn -1, false\n\t}\n\n\ts.fields.Append(name)\n\tindex = s.fields.Len() - 1\n\tif defaultValue != nil {\n\t\tkey := castIntToKey(index)\n\t\ts.defaults.Set(key, defaultValue)\n\t}\n\treturn index, true\n}\n\n// GetFieldIndex returns the index number of a schema field.\n//\n// Field index indicates the order the field has within the schema.\n// When defined fields are added they get an index starting from\n// field index 0.\n//\n// Fields are internally referenced by index number instead of the name\n// to be able to rename fields easily.\nfunc (s Schema) GetFieldIndex(name string) int {\n\tindex := -1\n\ts.fields.ForEach(func(i int, v interface{}) bool {\n\t\tif name != v.(string) {\n\t\t\treturn false\n\t\t}\n\n\t\tindex = i\n\t\treturn true\n\t})\n\treturn index\n}\n\n// GetFieldName returns the name of a field for a specific field index.\nfunc (s Schema) GetFieldName(index int) (name string, found bool) {\n\tv := s.fields.Get(index)\n\tif v == nil {\n\t\treturn \"\", false\n\t}\n\treturn v.(string), true\n}\n\n// GetDefault returns the default value for a field.\nfunc (s Schema) GetDefault(name string) (value interface{}, found bool) {\n\ti := s.GetFieldIndex(name)\n\tif i == -1 {\n\t\treturn nil, false\n\t}\n\treturn s.GetDefaultByIndex(i)\n}\n\n// GetDefaultByIndex returns the default value for a field by it's index.\nfunc (s Schema) GetDefaultByIndex(index int) (value interface{}, found bool) {\n\tkey := castIntToKey(index)\n\tv, found := s.defaults.Get(key)\n\tif !found {\n\t\treturn nil, false\n\t}\n\n\tif fn, ok := v.(func() interface{}); ok {\n\t\treturn fn(), true\n\t}\n\treturn v, true\n}\n\n// RenameField renames a field.\nfunc (s *Schema) RenameField(name, newName string) (renamed bool) {\n\tif s.HasField(newName) {\n\t\treturn false\n\t}\n\n\ti := s.GetFieldIndex(name)\n\tif i == -1 {\n\t\treturn false\n\t}\n\n\ts.fields.Set(i, newName)\n\treturn true\n}\n\nfunc castIntToKey(i int) string {\n\tbuf := make([]byte, 8)\n\tbinary.BigEndian.PutUint64(buf, uint64(i))\n\treturn string(buf)\n}\n" + }, + { + "name": "schema_options.gno", + "body": "package datastore\n\nimport \"strings\"\n\n// StorageOption configures schemas.\ntype SchemaOption func(*Schema)\n\n// WithField assign a new field to the schema definition.\nfunc WithField(name string) SchemaOption {\n\treturn func(s *Schema) {\n\t\tname = strings.TrimSpace(name)\n\t\tif name != \"\" {\n\t\t\ts.fields.Append(name)\n\t\t}\n\t}\n}\n\n// WithDefaultField assign a new field with a default value to the schema definition.\n// Default value is assigned to newly created records asociated to to schema.\nfunc WithDefaultField(name string, value interface{}) SchemaOption {\n\treturn func(s *Schema) {\n\t\tname = strings.TrimSpace(name)\n\t\tif name != \"\" {\n\t\t\ts.fields.Append(name)\n\n\t\t\tkey := castIntToKey(s.fields.Len() - 1)\n\t\t\ts.defaults.Set(key, value)\n\t\t}\n\t}\n}\n\n// Strict configures the schema as a strict one.\n// By default schemas should allow the creation of any user defined field,\n// making them strict limits the allowed record fields to the ones pre-defined\n// in the schema. Fields are pre-defined using `WithField`, `WithDefaultField`\n// or by calling `Schema.AddField()`.\nfunc Strict() SchemaOption {\n\treturn func(s *Schema) {\n\t\ts.strict = true\n\t}\n}\n" + }, + { + "name": "schema_test.gno", + "body": "package datastore\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestSchemaNew(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\toptions []SchemaOption\n\t\tfields []string\n\t\tstrict bool\n\t}{\n\t\t{\n\t\t\tname: \"default\",\n\t\t},\n\t\t{\n\t\t\tname: \"strict\",\n\t\t\toptions: []SchemaOption{Strict()},\n\t\t\tstrict: true,\n\t\t},\n\t\t{\n\t\t\tname: \"with fields\",\n\t\t\toptions: []SchemaOption{\n\t\t\t\tWithField(\"foo\"),\n\t\t\t\tWithField(\"bar\"),\n\t\t\t\tWithDefaultField(\"baz\", 42),\n\t\t\t},\n\t\t\tfields: []string{\"foo\", \"bar\", \"baz\"},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts := NewSchema(\"Foo\", tc.options...)\n\n\t\t\tuassert.Equal(t, \"Foo\", s.Name())\n\t\t\tuassert.Equal(t, tc.strict, s.IsStrict())\n\t\t\turequire.Equal(t, len(tc.fields), s.Size(), \"field count\")\n\n\t\t\tfor i, name := range s.Fields() {\n\t\t\t\tuassert.Equal(t, tc.fields[i], name)\n\t\t\t\tuassert.True(t, s.HasField(name))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSchemaAddField(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\toptions []SchemaOption\n\t\tfieldName string\n\t\tfieldIndex int\n\t\tfields []string\n\t\tsuccess bool\n\t}{\n\t\t{\n\t\t\tname: \"new only field\",\n\t\t\tfieldName: \"foo\",\n\t\t\tfieldIndex: 0,\n\t\t\tfields: []string{\"foo\"},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tname: \"new existing fields\",\n\t\t\toptions: []SchemaOption{\n\t\t\t\tWithField(\"foo\"),\n\t\t\t\tWithField(\"bar\"),\n\t\t\t},\n\t\t\tfieldName: \"baz\",\n\t\t\tfieldIndex: 2,\n\t\t\tfields: []string{\"foo\", \"bar\", \"baz\"},\n\t\t\tsuccess: true,\n\t\t},\n\t\t{\n\t\t\tname: \"duplicated field\",\n\t\t\toptions: []SchemaOption{WithField(\"foo\")},\n\t\t\tfieldName: \"foo\",\n\t\t\tfieldIndex: -1,\n\t\t\tfields: []string{\"foo\"},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ts := NewSchema(\"Foo\", tc.options...)\n\n\t\t\tindex, added := s.AddField(tc.fieldName, nil)\n\n\t\t\tif tc.success {\n\t\t\t\tuassert.Equal(t, tc.fieldIndex, index)\n\t\t\t\tuassert.True(t, added)\n\t\t\t} else {\n\t\t\t\tuassert.Equal(t, -1, index)\n\t\t\t\tuassert.False(t, added)\n\t\t\t}\n\n\t\t\turequire.Equal(t, len(tc.fields), s.Size(), \"field count\")\n\n\t\t\tfor i, name := range s.Fields() {\n\t\t\t\tuassert.Equal(t, tc.fields[i], name)\n\t\t\t\tuassert.True(t, s.HasField(name))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSchemaGetFieldIndex(t *testing.T) {\n\ts := NewSchema(\"Foo\")\n\ts.AddField(\"foo\", nil)\n\ts.AddField(\"bar\", nil)\n\ts.AddField(\"baz\", nil)\n\n\tuassert.Equal(t, 0, s.GetFieldIndex(\"foo\"))\n\tuassert.Equal(t, 1, s.GetFieldIndex(\"bar\"))\n\tuassert.Equal(t, 2, s.GetFieldIndex(\"baz\"))\n\n\tuassert.Equal(t, -1, s.GetFieldIndex(\"\"))\n\tuassert.Equal(t, -1, s.GetFieldIndex(\"unknown\"))\n}\n\nfunc TestSchemaGetFieldName(t *testing.T) {\n\ts := NewSchema(\"Foo\")\n\ts.AddField(\"foo\", nil)\n\ts.AddField(\"bar\", nil)\n\ts.AddField(\"baz\", nil)\n\n\tname, found := s.GetFieldName(0)\n\tuassert.Equal(t, \"foo\", name)\n\tuassert.True(t, found)\n\n\tname, found = s.GetFieldName(1)\n\tuassert.Equal(t, \"bar\", name)\n\tuassert.True(t, found)\n\n\tname, found = s.GetFieldName(2)\n\tuassert.Equal(t, \"baz\", name)\n\tuassert.True(t, found)\n\n\tname, found = s.GetFieldName(404)\n\tuassert.Equal(t, \"\", name)\n\tuassert.False(t, found)\n}\n\nfunc TestSchemaGetDefault(t *testing.T) {\n\ts := NewSchema(\"Foo\")\n\ts.AddField(\"foo\", nil)\n\ts.AddField(\"bar\", 42)\n\n\t_, found := s.GetDefault(\"foo\")\n\tuassert.False(t, found)\n\n\tv, found := s.GetDefault(\"bar\")\n\tuassert.True(t, found)\n\n\tgot, ok := v.(int)\n\turequire.True(t, ok, \"default field value\")\n\tuassert.Equal(t, 42, got)\n}\n\nfunc TestSchemaGetDefaultByIndex(t *testing.T) {\n\ts := NewSchema(\"Foo\")\n\ts.AddField(\"foo\", nil)\n\ts.AddField(\"bar\", 42)\n\n\t_, found := s.GetDefaultByIndex(0)\n\tuassert.False(t, found)\n\n\t_, found = s.GetDefaultByIndex(404)\n\tuassert.False(t, found)\n\n\tv, found := s.GetDefaultByIndex(1)\n\tuassert.True(t, found)\n\n\tgot, ok := v.(int)\n\turequire.True(t, ok, \"default field value\")\n\tuassert.Equal(t, 42, got)\n}\n\nfunc TestSchemaRenameField(t *testing.T) {\n\ts := NewSchema(\"Foo\")\n\ts.AddField(\"foo\", nil)\n\ts.AddField(\"bar\", nil)\n\n\trenamed := s.RenameField(\"foo\", \"bar\")\n\tuassert.False(t, renamed)\n\n\trenamed = s.RenameField(\"\", \"baz\")\n\tuassert.False(t, renamed)\n\n\trenamed = s.RenameField(\"foo\", \"\")\n\tuassert.True(t, renamed)\n\n\trenamed = s.RenameField(\"\", \"foo\")\n\tuassert.True(t, renamed)\n\n\trenamed = s.RenameField(\"foo\", \"foobar\")\n\tuassert.True(t, renamed)\n\n\turequire.Equal(t, 2, s.Size(), \"field count\")\n\tfields := []string{\"foobar\", \"bar\"}\n\tfor i, name := range s.Fields() {\n\t\tuassert.Equal(t, fields[i], name)\n\t\tuassert.True(t, s.HasField(name))\n\t}\n}\n" + }, + { + "name": "storage.gno", + "body": "package datastore\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/moul/collection\"\n)\n\n// NewStorage creates a new records storage.\nfunc NewStorage(name string, options ...StorageOption) Storage {\n\ts := Storage{\n\t\tname: name,\n\t\tcollection: collection.New(),\n\t\tschema: NewSchema(strings.Title(name)),\n\t}\n\n\tfor _, apply := range options {\n\t\tapply(\u0026s)\n\t}\n\treturn s\n}\n\n// Storage stores a collection of records.\n//\n// By default it searches records by record ID but it allows\n// using custom user defined indexes for other record fields.\n//\n// When a storage is created it defines a default schema that\n// keeps track of record fields. Storage can be optionally\n// created with a user defined schema in cases where the number\n// of fields has to be pre-defined or when new records must have\n// one or more fields initialized to default values.\ntype Storage struct {\n\tname string\n\tcollection *collection.Collection\n\tschema *Schema\n}\n\n// Name returns storage's name.\nfunc (s Storage) Name() string {\n\treturn s.name\n}\n\n// Collection returns the undelying collection used by the\n// storage to store all records.\nfunc (s Storage) Collection() *collection.Collection {\n\treturn s.collection\n}\n\n// Schema returns the schema being used to track record fields.\nfunc (s Storage) Schema() *Schema {\n\treturn s.schema\n}\n\n// Size returns the number of records that the storage have.\nfunc (s Storage) Size() int {\n\treturn s.collection.GetIndex(collection.IDIndex).Size()\n}\n\n// NewRecord creates a new storage record.\n//\n// If a custom schema with default field values is assigned to\n// storage it's used to assign initial default values when new\n// records are created.\n//\n// Creating a new record doesn't assign an ID to it, a new ID\n// is generated and assigned to the record when it's saved for\n// the first time.\nfunc (s Storage) NewRecord() Record {\n\tr := \u0026record{\n\t\tschema: s.schema,\n\t\tcollection: s.collection,\n\t}\n\n\t// Assign default record values if the schema defines them\n\tfor i, name := range s.schema.Fields() {\n\t\tif v, found := s.schema.GetDefaultByIndex(i); found {\n\t\t\tr.Set(name, v)\n\t\t}\n\t}\n\treturn r\n}\n\n// Query returns a recordset that matches the query parameters.\n// By default query selects records using the ID index.\n//\n// Example usage:\n//\n//\t// Get 50 records starting from the one at position 100\n//\trs, _ := storage.Query(\n//\t\tWithOffset(100),\n//\t\tWithSize(50),\n//\t)\n//\n//\t// Iterate records to create a new slice\n//\tvar records []Record\n//\trs.Iterate(func (r Record) bool {\n//\t\trecords = append(records, r)\n//\t\treturn false\n//\t})\nfunc (s Storage) Query(options ...QueryOption) (Recordset, error) {\n\t// Initialize the recordset for the query\n\trs := recordset{\n\t\tquery: defaultQuery,\n\t\trecords: s.collection.GetIndex(collection.IDIndex),\n\t}\n\n\tfor _, apply := range options {\n\t\tif err := apply(\u0026rs.query); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tindexName := rs.query.IndexName()\n\tif indexName != collection.IDIndex {\n\t\t// When using a custom index get the keys to get records from the ID index\n\t\tkeys, err := s.getIndexRecordsKeys(indexName, rs.query.IndexKey())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Adjust the number of keys to match available query options\n\t\tif offset := rs.query.Offset(); offset \u003e 0 {\n\t\t\tif offset \u003e len(keys) {\n\t\t\t\tkeys = nil\n\t\t\t} else {\n\t\t\t\tkeys = keys[offset:]\n\t\t\t}\n\t\t}\n\n\t\tif size := rs.query.Size(); size \u003e 0 \u0026\u0026 size \u003c len(keys) {\n\t\t\tkeys = keys[:size]\n\t\t}\n\n\t\trs.keys = keys\n\t\trs.size = len(keys)\n\t} else {\n\t\t// When using the default ID index init size with the total number of records\n\t\trs.size = rs.records.Size()\n\n\t\t// Adjust recordset size to match available query options\n\t\tif offset := rs.query.Offset(); offset \u003e 0 {\n\t\t\tif offset \u003e rs.size {\n\t\t\t\trs.size = 0\n\t\t\t} else {\n\t\t\t\trs.size -= offset\n\t\t\t}\n\t\t}\n\n\t\tif size := rs.query.Size(); size \u003e 0 \u0026\u0026 size \u003c rs.size {\n\t\t\trs.size = size\n\t\t}\n\t}\n\n\treturn rs, nil\n}\n\n// MustQuery returns a recordset that matches the query parameters or panics on error.\n// By default query selects records using the ID index.\n//\n// Example usage:\n//\n//\t// Get 50 records starting from the one at position 100\n//\tvar records []Record\n//\tstorage.MustQuery(\n//\t\tWithOffset(100),\n//\t\tWithSize(50),\n//\t).Iterate(func (r Record) bool {\n//\t\trecords = append(records, r)\n//\t\treturn false\n//\t})\nfunc (s Storage) MustQuery(options ...QueryOption) Recordset {\n\trs, err := s.Query(options...)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn rs\n}\n\n// Get returns the first record found for a key within a storage index.\n//\n// This is a convenience method to get a single record. A multi index will\n// always return the first record value for the specified key in this case.\n// To get multiple records create a query using a custom index and key value\n// or use the underlying storage collection.\nfunc (s Storage) Get(indexName, indexKey string) (_ Record, found bool) {\n\titer := s.collection.Get(indexName, indexKey)\n\tif iter.Next() {\n\t\treturn iter.Value().Obj.(Record), true\n\t}\n\treturn nil, false\n}\n\n// GetByID returns a record whose ID matches the specified ID.\nfunc (s Storage) GetByID(id uint64) (_ Record, found bool) {\n\titer := s.collection.Get(collection.IDIndex, seqid.ID(id).String())\n\tif iter.Next() {\n\t\treturn iter.Value().Obj.(Record), true\n\t}\n\treturn nil, false\n}\n\n// Delete deletes a record from the storage.\nfunc (s Storage) Delete(id uint64) bool {\n\treturn s.collection.Delete(id)\n}\n\nfunc (s Storage) getIndexRecordsKeys(indexName, indexKey string) ([]string, error) {\n\tidx := s.collection.GetIndex(indexName)\n\tif idx == nil {\n\t\treturn nil, errors.New(\"storage index for query not found: \" + indexName)\n\t}\n\n\tvar keys []string\n\tif v, found := idx.Get(indexKey); found {\n\t\tkeys = castIfaceToRecordKeys(v)\n\t\tif keys == nil {\n\t\t\treturn nil, errors.New(\"unexpected storage index key format\")\n\t\t}\n\t}\n\treturn keys, nil\n}\n\nfunc castIfaceToRecordKeys(v interface{}) []string {\n\tswitch k := v.(type) {\n\tcase []string:\n\t\treturn k\n\tcase string:\n\t\treturn []string{k}\n\t}\n\treturn nil\n}\n" + }, + { + "name": "storage_options.gno", + "body": "package datastore\n\n// StorageOption configures storages.\ntype StorageOption func(*Storage)\n\n// WithSchema assigns a schema to the storage.\nfunc WithSchema(s *Schema) StorageOption {\n\treturn func(st *Storage) {\n\t\tif s != nil {\n\t\t\tst.schema = s\n\t\t}\n\t}\n}\n\n// WithIndex assigns an index to the storage.\nfunc WithIndex(i Index) StorageOption {\n\treturn func(st *Storage) {\n\t\tst.collection.AddIndex(i.name, i.fn, i.options)\n\t}\n}\n" + }, + { + "name": "storage_test.gno", + "body": "package datastore\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestStorageDefaults(t *testing.T) {\n\tname := \"foo\"\n\tstorage := NewStorage(name)\n\n\tuassert.Equal(t, name, storage.Name())\n\tuassert.NotEqual(t, nil, storage.Collection())\n\tuassert.Equal(t, 0, storage.Size())\n\n\ts := storage.Schema()\n\tuassert.NotEqual(t, nil, s)\n\tuassert.Equal(t, strings.Title(name), s.Name())\n}\n\nfunc TestStorageNewRecord(t *testing.T) {\n\tfield := \"status\"\n\tdefaultValue := \"testing\"\n\ts := NewSchema(\"Foo\", WithDefaultField(field, defaultValue))\n\tstorage := NewStorage(\"foo\", WithSchema(s))\n\n\tr := storage.NewRecord()\n\turequire.NotEqual(t, nil, r, \"new record is not nil\")\n\tuassert.Equal(t, uint64(0), r.ID())\n\tuassert.Equal(t, storage.Schema().Name(), r.Type())\n\n\tv, found := r.Get(field)\n\turequire.True(t, found, \"default value found\")\n\n\tgot, ok := v.(string)\n\turequire.True(t, ok, \"default value type\")\n\tuassert.Equal(t, defaultValue, got)\n}\n\nfunc TestStorageQuery(t *testing.T) {\n\tindex := NewIndex(\"tag\", func(r Record) string {\n\t\tif v, found := r.Get(\"tag\"); found {\n\t\t\treturn v.(string)\n\t\t}\n\t\treturn \"\"\n\t})\n\n\tcases := []struct {\n\t\tname string\n\t\toptions []QueryOption\n\t\tresults []uint64\n\t\tsetup func() *Storage\n\t\terrMsg string\n\t}{\n\t\t{\n\t\t\tname: \"default query\",\n\t\t\tresults: []uint64{1, 2},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with size\",\n\t\t\tresults: []uint64{1},\n\t\t\toptions: []QueryOption{WithSize(1)},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with offset\",\n\t\t\tresults: []uint64{2},\n\t\t\toptions: []QueryOption{WithOffset(1)},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with offset overflow\",\n\t\t\toptions: []QueryOption{WithOffset(4)},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"with size and offset\",\n\t\t\tresults: []uint64{2, 3},\n\t\t\toptions: []QueryOption{WithSize(2), WithOffset(1)},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"custom index\",\n\t\t\toptions: []QueryOption{UseIndex(\"tag\", \"A\")},\n\t\t\tresults: []uint64{1, 3},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\", WithIndex(index))\n\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"A\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"A\")\n\t\t\t\tr.Save()\n\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"custom index with offset\",\n\t\t\toptions: []QueryOption{UseIndex(\"tag\", \"B\"), WithOffset(1)},\n\t\t\tresults: []uint64{3, 4},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\", WithIndex(index))\n\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"A\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"custom index with offset and size\",\n\t\t\toptions: []QueryOption{UseIndex(\"tag\", \"B\"), WithOffset(1), WithSize(1)},\n\t\t\tresults: []uint64{3},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\", WithIndex(index))\n\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"A\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"tag\", \"B\")\n\t\t\t\tr.Save()\n\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"custom index not found\",\n\t\t\toptions: []QueryOption{UseIndex(\"foo\", \"B\")},\n\t\t\tsetup: func() *Storage {\n\t\t\t\ts := NewStorage(\"foo\")\n\t\t\t\treturn \u0026s\n\t\t\t},\n\t\t\terrMsg: \"storage index for query not found: foo\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Arrange\n\t\t\tstorage := tc.setup()\n\n\t\t\t// Act\n\t\t\trs, err := storage.Query(tc.options...)\n\n\t\t\t// Assert\n\t\t\tif tc.errMsg != \"\" {\n\t\t\t\tuassert.ErrorContains(t, err, tc.errMsg, \"expect error\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"expect no error\")\n\t\t\turequire.NotEqual(t, nil, rs, \"new record is not nil\")\n\t\t\turequire.Equal(t, len(tc.results), rs.Size(), \"expect query results count to match\")\n\n\t\t\tvar i int\n\t\t\trs.Iterate(func(r Record) bool {\n\t\t\t\turequire.Equal(t, tc.results[i], r.ID(), \"expect result IDs to match\")\n\t\t\t\ti++\n\t\t\t\treturn false\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestStorageGet(t *testing.T) {\n\tindex := NewIndex(\"name\", func(r Record) string {\n\t\tif v, found := r.Get(\"name\"); found {\n\t\t\treturn v.(string)\n\t\t}\n\t\treturn \"\"\n\t})\n\n\tcases := []struct {\n\t\tname string\n\t\tkey string\n\t\trecordID uint64\n\t\tsetup func(*Storage)\n\t}{\n\t\t{\n\t\t\tname: \"single record\",\n\t\t\tkey: \"foobar\",\n\t\t\trecordID: 1,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"name\", \"foobar\")\n\t\t\t\tr.Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"two records\",\n\t\t\tkey: \"foobar\",\n\t\t\trecordID: 1,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"name\", \"foobar\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"name\", \"foobar\")\n\t\t\t\tr.Save()\n\n\t\t\t\tr = s.NewRecord()\n\t\t\t\tr.Set(\"name\", \"extra\")\n\t\t\t\tr.Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"record not found\",\n\t\t\tkey: \"unknown\",\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\tr := s.NewRecord()\n\t\t\t\tr.Set(\"name\", \"foobar\")\n\t\t\t\tr.Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty storage\",\n\t\t\tkey: \"foobar\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tstorage := NewStorage(\"foo\", WithIndex(index))\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026storage)\n\t\t\t}\n\n\t\t\tr, found := storage.Get(index.Name(), tc.key)\n\n\t\t\tif tc.recordID == 0 {\n\t\t\t\tuassert.Equal(t, nil, r, \"expect no record\")\n\t\t\t\tuassert.False(t, found, \"expect record not found\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tuassert.True(t, found, \"expect record found\")\n\t\t\turequire.NotEqual(t, nil, r, \"expect record to be found\")\n\t\t\tuassert.Equal(t, tc.recordID, r.ID(), \"expect ID to match\")\n\t\t})\n\t}\n}\n\nfunc TestStorageGetByID(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\trecordID uint64\n\t\tfound bool\n\t\tsetup func(*Storage)\n\t}{\n\t\t{\n\t\t\tname: \"single record\",\n\t\t\trecordID: 1,\n\t\t\tfound: true,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple records\",\n\t\t\trecordID: 2,\n\t\t\tfound: true,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"record not found\",\n\t\t\trecordID: 3,\n\t\t\tsetup: func(s *Storage) {\n\t\t\t\ts.NewRecord().Save()\n\t\t\t\ts.NewRecord().Save()\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty storage\",\n\t\t\trecordID: 1,\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tstorage := NewStorage(\"foo\")\n\t\t\tif tc.setup != nil {\n\t\t\t\ttc.setup(\u0026storage)\n\t\t\t}\n\n\t\t\tr, found := storage.GetByID(tc.recordID)\n\n\t\t\tif !tc.found {\n\t\t\t\tuassert.Equal(t, nil, r, \"expect no record\")\n\t\t\t\tuassert.False(t, found, \"expect record not found\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tuassert.True(t, found, \"expect record found\")\n\t\t\turequire.NotEqual(t, nil, r, \"expect record to be found\")\n\t\t\tuassert.Equal(t, tc.recordID, r.ID(), \"expect ID to match\")\n\t\t})\n\t}\n}\n\nfunc TestStorageDelete(t *testing.T) {\n\tstorage := NewStorage(\"foo\")\n\tr := storage.NewRecord()\n\tr.Save()\n\n\tdeleted := storage.Delete(r.ID())\n\tuassert.True(t, deleted)\n\n\tdeleted = storage.Delete(r.ID())\n\tuassert.False(t, deleted)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "TEVhQfeanz9Qpt4QaTZ1dhXqtR7ezIzirZ/VYiVzNK3ct0+D+ikUU4jJyoel2CrDhmxh++7KYStu+9msd/c3Bg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "pager", + "path": "gno.land/p/jeronimoalbi/pager", + "files": [ + { + "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/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "PBWxmVHTSZ/PdTPXmYvjp1yehw5IliyMJFNJ2eiEPze8qMJ5C3XiVn92ZNCtRmitfXJd/5TTsfr6rpEcOGvOBQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "pkgerr", + "path": "gno.land/p/leon/pkgerr", + "files": [ + { + "name": "err.gno", + "body": "// Package pkgerr provides a custom error wrapper that prepends the realm link to your error\n// This is useful for identifying the source package of the error.\n//\n// Usage:\n//\n// To wrap an error with realm and chain domain information, use the `New` function:\n//\n//\terr := pkgerr.New(\"my error message\") // in gno.land/r/leon/example\n//\tfmt.Println(err.Error()) // Output: \"r/leon/example: my error message\"\npackage pkgerr\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// pkgErr is a custom error type that prepends the current\n// realm link to the original error message.\ntype pkgErr struct {\n\toriginalErr error\n}\n\n// New creates a new pkgErr with the given error. The returned error will include\n// the current realm link in its message.\nfunc New(msg string) error {\n\treturn \u0026pkgErr{originalErr: errors.New(msg)}\n}\n\nfunc Wrap(err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\treturn \u0026pkgErr{originalErr: err}\n}\n\nfunc (e pkgErr) Unwrap() error {\n\treturn e.originalErr\n}\n\n// Error implements the `error` interface for pkgErr.\nfunc (e *pkgErr) Error() string {\n\treturn ufmt.Sprintf(\"%s: %s\",\n\t\tstrings.TrimPrefix(std.CurrentRealm().PkgPath(), \"gno.land/\"),\n\t\te.originalErr)\n}\n" + }, + { + "name": "err_test.gno", + "body": "package pkgerr\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n)\n\nconst pkgPath = \"gno.land/r/leon/test\"\n\nvar prefix = strings.TrimPrefix(pkgPath, \"gno.land/\")\n\nfunc TestNew(t *testing.T) {\n\terr := New(\"my error message\")\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got nil\")\n\t}\n\n\ttesting.SetRealm(std.NewCodeRealm(pkgPath))\n\n\texpected := prefix + \": my error message\"\n\n\tif err.Error() != expected {\n\t\tt.Errorf(\"Expected error message %q, got %q\", expected, err.Error())\n\t}\n}\n\nfunc TestWrap(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(pkgPath))\n\n\toriginalErr := errors.New(\"original error\")\n\twrappedErr := Wrap(originalErr)\n\tif wrappedErr == nil {\n\t\tt.Fatal(\"Expected an error, got nil\")\n\t}\n\n\texpected := prefix + \": original error\"\n\tif wrappedErr.Error() != expected {\n\t\tt.Errorf(\"Expected error message %q, got %q\", expected, wrappedErr.Error())\n\t}\n}\n\nfunc TestUnwrap(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(pkgPath))\n\toriginalErr := errors.New(\"original error\")\n\twrappedErr := Wrap(originalErr)\n\n\tunwrappedErr := wrappedErr.(*pkgErr).Unwrap()\n\tif unwrappedErr != originalErr {\n\t\tt.Errorf(\"Expected unwrapped error %v, got %v\", originalErr, unwrappedErr)\n\t}\n}\n\nfunc TestErrorMethod(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(pkgPath))\n\toriginalErr := errors.New(\"original error\")\n\tpkgErr := \u0026pkgErr{originalErr: originalErr}\n\n\texpected := prefix + \": original error\"\n\tif pkgErr.Error() != expected {\n\t\tt.Errorf(\"Expected error message %q, got %q\", expected, pkgErr.Error())\n\t}\n}\n\nfunc TestWrapNilError(t *testing.T) {\n\terr := Wrap(nil)\n\tif err != nil {\n\t\tt.Errorf(\"Expected nil error, got %v\", err)\n\t}\n}\n\nfunc TestNewWithEmptyMessage(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(pkgPath))\n\terr := New(\"\")\n\tif err == nil {\n\t\tt.Fatal(\"Expected an error, got nil\")\n\t}\n\n\texpected := prefix + \": \"\n\tif err.Error() != expected {\n\t\tt.Errorf(\"Expected error message %q, got %q\", expected, err.Error())\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "d1al+LbGTNZYypi1P7qLyv/8wdGnQqNNQnbjHLmg1TRGXFD0MqJku1Swk7qIdnNpsQOtXEu6S7FdONA2k1nFBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "md", + "path": "gno.land/p/mason/md", + "files": [ + { + "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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "jE+1lfGP5keoCYrffwgdv6DhgAqCAEvO5duz7r1u7tLKOyX3NvLJw58hScxdAGDngTWt3ybVm1F6VjVQuyh/Cw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "zobrist", + "path": "gno.land/p/morgan/chess/zobrist", + "files": [ + { + "name": "zobrist.gno", + "body": "// Package zobrist contains an implementation of the Zobrist Hashing algorithm.\n// https://www.chessprogramming.org/Zobrist_Hashing\n//\n// The hash is performed using a series of random numbers generated through\n// rands.go, in this source directory.\npackage zobrist\n\n// Board is a representation of a chess board.\n// Details on how to transform a chess algebraic position into an index\n// can be found at [Square].\ntype Board [64]Piece\n\n// Piece represents a piece on the board.\ntype Piece byte\n\n// Possible values of Piece. Within the context of Board, Piece is assumed to\n// be white, unless p\u0026PieceBlack != 0. Note PieceBlack is not a valid piece; it\n// must be bitwise OR'd to a non-empty piece.\nconst (\n\tPieceEmpty Piece = iota\n\n\tPiecePawn\n\tPieceRook\n\tPieceKnight\n\tPieceBishop\n\tPieceQueen\n\tPieceKing\n\n\tPieceBlack Piece = 8 // bit-flag\n)\n\n// InitialPosition is the zobrist hash (as implemented by this package)\n// for the initial chess position.\nconst InitialPosition uint64 = 1957848132405881468\n\n// Hash generates a Zobrist hash with the given parameter.\n//\n// castling can assume a value between [0,16),\n// epFile should be set to a value between [0,8) -- if there is no en passant\n// file, pass 255 (as a convention).\nfunc Hash(b Board, isBlack bool, castling, epFile byte) uint64 {\n\tvar hash uint64\n\tif isBlack {\n\t\thash ^= hashBlack\n\t}\n\tfor sq, p := range b {\n\t\tif p == PieceEmpty {\n\t\t\tcontinue\n\t\t}\n\t\tif p\u0026PieceBlack == 0 {\n\t\t\t// values 0-5\n\t\t\tp--\n\t\t} else {\n\t\t\t// values 6-11\n\t\t\tp -= PieceBlack | PiecePawn\n\t\t}\n\t\thash ^= hashPieces[sq*12+int(p)]\n\t}\n\thash ^= hashCastling[castling]\n\tif epFile \u003c 8 {\n\t\thash ^= hashEpFile[epFile]\n\t}\n\treturn hash\n}\n\nvar (\n\thashBlack uint64 = 0x331cf7440c179431\n\thashCastling = [...]uint64{\n\t\t0xda401e23397e78ca, 0x41dd6246f3cdd0fc, 0xe7a57999953a1784, 0x1e1fdc4db40f6c93,\n\t\t0xff76ddf8b88d7c47, 0xb25c00230f48e677, 0xfdffd279de1e888a, 0x61e89216839cbe34,\n\t\t0xc082fd59e3b8e542, 0x041d42f60a9a6719, 0xa162c6fbfe15b8c0, 0x5105c749ea4f9def,\n\t\t0x68e60d8b032b2f1c, 0xc4c95eb6f2a5600d, 0xd005b4bf6d3d18e1, 0xd18bb1e4fd30b333,\n\t}\n\thashEpFile = [...]uint64{\n\t\t0x986e8dc916fd650b, 0x97dfccc4b5827a70, 0x5888298d167f8b9b, 0x937b5022b30a532b,\n\t\t0x40228426ba8bb614, 0xb7c1c137ace2674e, 0x0c3ef7d3fac8d0be, 0x9cdf9d7b763f2e6c,\n\t}\n\t// 6 pieces * 2 colours * 64 squares = 768 random uint64's.\n\thashPieces = [...]uint64{\n\t\t0x6de9d290a487babc, 0x81095e35de2bc008, 0xfdf0e0d93a5f4c5c, 0xc63827a4cea42d4b,\n\t\t0x103c199f0290bdae, 0x61bf576e61c2d862, 0x178b9ec5427fb45c, 0xd34361ffe3a6abb7,\n\t\t0x4301de67181ccc30, 0xb70e72de5895cf5e, 0xedf154e7e16965fd, 0x543efdb0aeef09d2,\n\t\t0xb2091f463dce5477, 0x0db5bef892262850, 0x69c6bdc6ddb7c1cf, 0x3955a54fd5a60f96,\n\t\t0x74f5e115e8293d45, 0x9e9fb05af1c96e09, 0xab6685da47c28488, 0x80b82ee1a494665d,\n\t\t0x412701df2f89d3a1, 0x8ac06869db920dd2, 0xea7b99e10ba51307, 0xc13365ed0d123d8a,\n\t\t0x42cebc0bcaba8cd7, 0x314a7e49e9b51bcf, 0x5abb9198d4ad0f9d, 0x0a6f4456d77eb317,\n\t\t0x04d2ba6598ffb494, 0x89cdd7e425256b29, 0xfb5853f8b71a1496, 0x2111a3ba29f78227,\n\t\t0x1ac8ec9f94e60981, 0x9c1ea2e3e24da61f, 0x62cf491e74065750, 0x19578de8083259ac,\n\t\t0xb4a576703ccf3d02, 0xb38e458dd8196e9e, 0x42f5d714cd5947e5, 0x7b84ea130bcb7f7f,\n\t\t0xb05f2fcc47356344, 0x87c9c45a24657fe4, 0x815aff9977943588, 0x484c65c342290c44,\n\t\t0x2d326480e45a4f1a, 0x6c06c7db312dc1d2, 0xb6f01ac7c66f5a45, 0xf4c88ad4ff1176db,\n\t\t0x5f921eb462e69949, 0x990de640b9b274e1, 0x9c961b70243bf6ab, 0x51f7bfa5e4bc9575,\n\t\t0x2e89aed81f40939a, 0x78ce647c4760c4b4, 0xf12f7ebcc4f86666, 0x44bce0ebee1fc047,\n\t\t0x19613b81961e8c69, 0x1481b5c8affcba3a, 0x94d6321ba8417f26, 0xa43bf26acbb02a17,\n\t\t0xe0a66cca2572428a, 0x25fd4f9950444f18, 0x33728d2c1bdf953f, 0x8c35d0d7a31a7723,\n\t\t0x93a6641e67e92047, 0x0d27f43da10061d2, 0x560384de9e6b4b4f, 0xec1f847836357e1b,\n\t\t0xe9cd3eaf397443d3, 0x3cad122344e04c1b, 0x80626be75cccc58f, 0xaf6fcf59f731ff23,\n\t\t0x60f92853b13f2480, 0x865fb5e62163b781, 0x6019d5d1170770e9, 0x5e9528c7589f10c4,\n\t\t0x792d01b3672169c2, 0x8f812368414a03f7, 0xf8d7bd34601ad664, 0x13b52975cf7b3251,\n\t\t0xcb4fdf3b50b64338, 0x0536184766d7b464, 0x72ded1ba157bb20f, 0xf8ae92fde84a5c19,\n\t\t0xf2379c8815c2e71c, 0x86a6fda12a95af71, 0x3e18f5f431fd566d, 0x9e0b75a4eacc0b96,\n\t\t0x83ff97e0917f9db3, 0x8bd3889e98157154, 0xbc56ee3a9bcf3b4b, 0x8386739420fd2727,\n\t\t0x160dbcab1f83a9dc, 0x6e4e36335993cf57, 0xb53933016a624b7c, 0x11e7b44599a6a980,\n\t\t0x6f6c927432d695ae, 0x672ead6a7a67c3ce, 0xa313c9ce239a1a1e, 0x1d9b559b13be222a,\n\t\t0x73dfff734fbbee8b, 0x87abe0f6a14be15c, 0x42388fe5e4f904c2, 0x7705ef7492f0dc2d,\n\t\t0xc0e281c867e902b1, 0xbf29803779c71b67, 0xf468583ebde5789a, 0xe28ba371a6cac398,\n\t\t0x917f56b7b1c27398, 0xbf29669535aaa4e0, 0xe744219af67a0621, 0xd5af8e55ff58d5d5,\n\t\t0x72e2cc0e495c7094, 0x72c857412018b79a, 0x1db6efd2b47cb031, 0x9739fb93667f2bbe,\n\t\t0x7151b89b41dcb1bb, 0x24cf2e514e7f33e0, 0xfbc38081840febe1, 0x6eddef1967d0d653,\n\t\t0xf1f91203400ae92c, 0x26b1777c7babae3d, 0x3baf2f2abd8910c8, 0x5b47759ffbd37842,\n\t\t0xffe868968ec9270d, 0x89a7ac0d255b357f, 0xd9b56079f915c43d, 0x422ba52ec00bb2f9,\n\t\t0x18f66fad5c3b238c, 0x2a33af6d60146d64, 0xefdc9649adf7e956, 0x005f5181aa2d41db,\n\t\t0x1dfff9e7449938d3, 0xeabeac20a34e6545, 0x1e786123884674fd, 0x6a719ae518a98556,\n\t\t0x6020b1ea2fe10223, 0x600a04f0521dcb55, 0xa6c947a541137295, 0x80df3344d3cdfe9f,\n\t\t0xc2fdcc9cf8da02bd, 0xa3d52d7633c4b5b8, 0x7596ea450a6c4d79, 0x298299d89d59d5d1,\n\t\t0x0020b9b7c790a672, 0x7867f067a107dbbc, 0x04ae6e4821bbd355, 0x075efcf522abba1d,\n\t\t0x831e0c547977c42e, 0x252a2ccbb9cae9b9, 0xb64ba9e683a44b52, 0x1c271189f6f2fcea,\n\t\t0xde6252635b8a6c1e, 0x4294ae1f329ddd8a, 0xda93eb7bf7d9706f, 0x4382925fe1404821,\n\t\t0xf791164a1b549969, 0xd60e8934c81cb154, 0x81019c98d5254d75, 0x024f3dac35b8e853,\n\t\t0x2d5c20f2e708a079, 0x8efb9c33bf895e8a, 0x378d97d4b623cacc, 0xcb62d165606b574c,\n\t\t0x8ad6ec30e8367851, 0x9fb21d6482f9a6b8, 0xa7725c027baed0e7, 0xb3e15af756bcee91,\n\t\t0x407b67ef1c9c71bc, 0x6f981f475f94d19c, 0x86c26906e61dec18, 0xa263045310ee8935,\n\t\t0x272ac35622e61a11, 0xf6fc4c7c91691cbf, 0x910393ad76ced568, 0x10d3bcbcf6454bf8,\n\t\t0x3c05a3b1703469a0, 0x78029abf41b1bd40, 0x34b4e74ce8b474e9, 0xe3b47e81c39fefa6,\n\t\t0x91d83224fe32db32, 0xe6c96cb196fcc83e, 0xf7b94f6f7231d874, 0x9a834ff1a03f5b38,\n\t\t0x0e98878ae294edc1, 0xff2694b96b00b0dd, 0xae3bdce9265aac7c, 0xc97887009d14f08d,\n\t\t0xb7f44cdd578ec417, 0xe602ed604ef3c824, 0xe352a0ac128bcc3a, 0x79a0b291d4cd9c14,\n\t\t0x5ee4a9e2c195ff16, 0x26fa8a6d4d66e24d, 0xaeafa129bd9c62c7, 0x3da01af74b818094,\n\t\t0x38c499fb2823505b, 0x757a32af70db20da, 0xcd2b0fec6311d81c, 0xd10ed19e0da6460a,\n\t\t0x0f7b060a7707dc32, 0x7042d0c262a6a06b, 0xd0d437e753d560ed, 0xaefcc4e07a948586,\n\t\t0x9516944f8863368c, 0x60cfa52a9cfaf4d5, 0x2e22817497bd2772, 0xb7dfc139fbd79268,\n\t\t0x15ea3777b785a4df, 0xf44cb9592c31d48b, 0x5893e556af63aa5e, 0xb63609fb150e1e1f,\n\t\t0x44920c624c2c144e, 0xd9e13f6f8d504303, 0x09681482e9af2e2c, 0x072c18b4d02a88d0,\n\t\t0xd3f8886bd2e3c58c, 0x081abec6305b42c8, 0xe68c68756cba2638, 0x3f1843fc7969e258,\n\t\t0xb25d2db11a7b6757, 0x0a066242ead527d6, 0xf2a47d93d403bd29, 0xbbf5a65e74ca4d34,\n\t\t0x7ea55dabf5244210, 0x79d3ed1639e00808, 0xb2dbb1f606335295, 0xdf0b2c1256b1b3af,\n\t\t0x2bc9bf68fbf2c035, 0x7aabc308077a3400, 0x945a92d9b807784d, 0xae3a32ff2dd3ad6a,\n\t\t0x63a59bf8137d3c2b, 0xa919df7ac39f4ce3, 0x00c3688b75e3754b, 0xc589f782da55edcb,\n\t\t0xe97758166f728546, 0x3d9ebff5bea838f0, 0xe3f141938b9281dd, 0xc3c3a7a75aafbbac,\n\t\t0x1d6f368464eb8c54, 0x596ce122feada666, 0xf768ffa3509627c0, 0x5ea90c12875fe357,\n\t\t0x11307b7514087de5, 0x1eb75d679ca6ebe1, 0xfa2a85179ecccdc0, 0x73cafcc11d1a19b6,\n\t\t0x7942c51eb73934db, 0xd31925b7bebd3766, 0x4f1975c179b4b082, 0x406ec764a5b0059b,\n\t\t0x7df4dcbd21411be8, 0xaaa5c8df72d0e4f3, 0xfefa53616f4effb2, 0x82dd4177438fcc5b,\n\t\t0x8cc62697b107de0e, 0x140066b1a9e48d6a, 0xa1bc5118a5048eb3, 0x8924bdfe570facc7,\n\t\t0x55cf653d02d670b0, 0xa9130a65e2f0fa04, 0x0f6a1eecdb30d8a3, 0x255cb52477aba452,\n\t\t0xade06ddc6f835cd4, 0xd329541bdbb6b279, 0xdd921ee95ea595ea, 0xaedbba578197ffab,\n\t\t0x85b9b59808076c56, 0x2439ddd88b842c8a, 0xb11190014736ff58, 0x3223b6457cd90b6f,\n\t\t0x5b1c51474d5b443d, 0xca8b496fbed47b11, 0xe371cd43623b4e5f, 0x1e5a62f4040e9242,\n\t\t0xe5baa812538471c2, 0x3914ded0b9c68d7b, 0x211ceb64d9099bbe, 0x7fc33f702220dc78,\n\t\t0x3b5e9c5840a2b8b3, 0xa0e21e2c37d4c30a, 0x169fa2324b2addc3, 0x9b457d6ae103fa48,\n\t\t0x91f1f72f5e258912, 0xeb701bd0b95314c7, 0x7965c7b11ffbb5d7, 0x898b14b385433c77,\n\t\t0xe1ae26dbc9ec4653, 0xac37595684b326d3, 0x84bf7f441b85ef51, 0x0486ee8e1cf0efb2,\n\t\t0xa9b8acdf69f73960, 0xa34d1a08c8076a32, 0x7f9246295970518c, 0x6f656dc6fb980791,\n\t\t0xcfe89b893838d8ab, 0x51e9a6e88c6a59dd, 0x0eacf50f85a334bb, 0x82d23ccf2a49be8a,\n\t\t0xa7d385b0cf2a7d4e, 0xa3a849d79ebac654, 0xbc000cf465923371, 0xa1a3c61b7d9dde7d,\n\t\t0x08878bf2cf37eace, 0xf0ed44ef8046656b, 0xdb7c02c81afd1072, 0x3c7daea881c1c1af,\n\t\t0xba43d6f4df817382, 0x6d828358a59f5f2c, 0x75eebe28937070ad, 0x792df3f928010752,\n\t\t0x0c3be6141c90c5d9, 0x0efec51f83e272a8, 0x9a6f416b0a6df723, 0x04b2e247d3914036,\n\t\t0xcbbaf36eded44848, 0xda39769f8831bdcb, 0x47068c11232c8118, 0xf120b77c9b3e6210,\n\t\t0x52e11d727c031745, 0x2c78f899a17fb88f, 0x95ddc231c0cdd8c2, 0x1b32bca26532b25b,\n\t\t0x31af0dbbe225beec, 0x594e64fbe32f365f, 0xd4218aabd87b5569, 0xbb36aff9719c04cc,\n\t\t0xf44e23c7aa02657a, 0x0f980e87e514d690, 0x02db35ab5497bb72, 0x5adcdefaa2eae880,\n\t\t0x59b3ce68c22fac3d, 0x7dd52a14da3abc88, 0xcdf09a31d10c28cf, 0xc7ac17887fb668da,\n\t\t0xac55650afca95f7d, 0x81374af3db3e7b4b, 0x6c68c53c855bafc7, 0x1406727187441363,\n\t\t0x5ba83433a8bf732d, 0x4a4a968409ab0137, 0x304dd4ec27b37a5e, 0x5c00efb6a1a4b9ed,\n\t\t0xc06eecaa0c82859f, 0x9aa9465883ac52c3, 0x993f3d988706142a, 0x6d7395a0b1e83c09,\n\t\t0xaf8a2b5634ffe143, 0x9769ee0de8694d5e, 0xd439980424a5db0f, 0xb04f5a21742ae9e2,\n\t\t0xbf9059f0b1c4171c, 0x4ff1a289c070f22d, 0x6dd87cd9642d3ca6, 0x9febd09c132f3c88,\n\t\t0x332f6b2973b8d987, 0x14465e925981e26b, 0x5b4074fc700e8910, 0x6b5eb71612931147,\n\t\t0xa796b842757c937c, 0x4becbeca2c5a8d3b, 0xc48f1a998822fbd2, 0xd223e3864f6fa221,\n\t\t0x344cc8ffede3909e, 0x980724e16f14e4f4, 0xb836750d3f50f20b, 0x84c87a98513035a5,\n\t\t0x1dacdc18f768c936, 0xa99bb4ad168e2e38, 0x6e0708645f549d34, 0x9b8d705a0c81bae2,\n\t\t0x54a4ff6b225d98fb, 0x362cfdb47340082c, 0x2c07d8af734835bf, 0x5e04437c1e32b391,\n\t\t0x1be670cba5f4e187, 0xa71852f798fb9887, 0xa06f2d65637c765c, 0x8bbfe9dd54e33f7c,\n\t\t0xf3dd657e0650ecf7, 0x7e42c999e6430f76, 0x6073b783560802b0, 0xb69a6e688e4d8317,\n\t\t0xa0bfb135c7f32d01, 0x5e32d7581368346d, 0x68629a024f2c770c, 0x5018edb9a0dfee9c,\n\t\t0x3a3c1e0d89599a97, 0x95a78f07d2312971, 0x3b813a9323dea87c, 0xd63e9b2a16a2cfc2,\n\t\t0xebb8758e940ae51b, 0x363faaf7e372367f, 0x8f2a34f38a508850, 0xe05b4ef569b14cfc,\n\t\t0x5104b9217fe22504, 0xd349aaf78cb844bc, 0x6236abb35107b232, 0x1635a8e5231cfac6,\n\t\t0x53772874e0d274fb, 0x347c6f99ae78d163, 0x1ca9cc2fb069df18, 0x9aab3fa7c74018cf,\n\t\t0xe7a77550899599d6, 0x14d4ed6292d3f471, 0x21ead5efdaa996a0, 0xbf4fbba5d0ac53ef,\n\t\t0x3bc684e5c1b1722e, 0x2ac4ada0514a8c51, 0x22d6399046a83428, 0x3b08d444ef83104e,\n\t\t0x022c75a7c9f64723, 0xa5ed7da2590be77f, 0x8e721ad2defe24aa, 0x3e69e053e2b187a2,\n\t\t0xce60fe699dcacb24, 0x7e72705bc9af79da, 0x594e8f4f4ae42029, 0x6021f96539b44c14,\n\t\t0x6b8c89769f397818, 0x8866edf93b123c2e, 0xcb158860c0919543, 0x01ff9fe71fb36a63,\n\t\t0x90db5d765d3f5011, 0x46541703158ba9ac, 0x09225f5b655d3ba6, 0x4213c64639e1736f,\n\t\t0x68c8b644dd94355a, 0x87169c4e0431171b, 0xf36646eec0067423, 0x99e7cd8c751aa646,\n\t\t0xcfe626d3031a2171, 0x3ed71337f54757f3, 0x5ac1fd1f4b353432, 0x25dfc52821ea2394,\n\t\t0x35ad1be1cb2ad7be, 0x26139157c61fc26b, 0x35e9e273d88f99e7, 0xa0ee86a30ea7343b,\n\t\t0x886ce1e26d6001d9, 0x014403c0715f7b11, 0x3a6bd359b9f433b7, 0x5513d22bae1f21a1,\n\t\t0xe38a239677ed4277, 0x17b39ca14896c138, 0x41b98a7a2b89109f, 0x6c5b2c21ad260da1,\n\t\t0x45aec24a92505b1e, 0xa0bfe245c932c47d, 0x6318997a226c3ac2, 0x820cac6be1f42886,\n\t\t0x5760c2b35415c521, 0x83e7a063f2e6cd56, 0x76332d7fc2dcb1ff, 0xeb9ac3304d4b72ea,\n\t\t0x00c393e61141c46f, 0x4e0e603506aaef80, 0xd78b9f0a8b9042b6, 0xdb6b1ce781374a54,\n\t\t0x463f921e426d10e0, 0xd3928841c7d7c481, 0x82b84916883bd6a0, 0x339a8eb1581931db,\n\t\t0x004e077b5dc400da, 0xfbe0bb55d0595e84, 0xdebac9094a7416c5, 0xbbe4aeee8f16ea37,\n\t\t0x903143492d3a0958, 0xdc10c884e32309b9, 0x5758329e210f4bfe, 0x9fa79b8124950e9c,\n\t\t0x1dea376717a93e38, 0xd5ecd2f169f0c9a9, 0x12bc43a1112d451f, 0x972c868fd1c0ab91,\n\t\t0x5385dcbe56133869, 0xfdbaa4cb4f597f9e, 0x6e7acd0f42030036, 0xd89476e12d8cbe1f,\n\t\t0x5d567b8c0902e5d7, 0xf5b6430d3246c66b, 0x1ff82d8cc5bf68f2, 0x43939e47df3c2997,\n\t\t0xc20a589e2eccf7ad, 0x55100d45c1468ce9, 0xca1a2bbbfa7f99a0, 0xd97425cce2b3ca74,\n\t\t0xba2c73d6b645b815, 0x5c5b487b827e5c4a, 0x707cd09ac43a88b9, 0x453346a6582fa103,\n\t\t0xf781a361fe52e6d9, 0xfa302baeb75815ca, 0xfae30f25cfc3f559, 0x94a6d9edc13d4d14,\n\t\t0x1ff9243cd4b19e60, 0xd979205825360367, 0x066e3de27c6cf6fb, 0xd4736aabe8eacefe,\n\t\t0xdb64a75b5794885f, 0xc8b376b48351c9dd, 0x2bf1f8e779c49d15, 0x6fc413b9230771e6,\n\t\t0x824bfbefc0392561, 0x0ed0b8ad97a79ce3, 0xa763ee97f261840e, 0xd756fa2efc2781a1,\n\t\t0x61460c93e9a965be, 0x4d9d0b6847a7306d, 0xe9695b4b911b10f6, 0xa27997d0823ddddd,\n\t\t0xf3c563dd03448651, 0xab837e3084a2d65c, 0x5083294e0a4d749b, 0x1218544274486331,\n\t\t0xdc0dc147e39c57b7, 0x67145247fa3b66b2, 0x409e9f1d134bf695, 0x174ec11ccb150efe,\n\t\t0x026cca48bc69b7ca, 0xf7fb076aef504b5a, 0xfa8fdf601acf9ac0, 0xad03549894853639,\n\t\t0xaf315b2f8d954b06, 0x0f28d793bae103a7, 0x46a140f98a954e03, 0x21c1997400961c3c,\n\t\t0x292bf7bd55409eb7, 0xbd0b7669a52edffe, 0x350c5a32dddc5fdf, 0xa4dcf45331142e57,\n\t\t0x9e87a71388559236, 0xd35f7f41c3c1ed37, 0x660b4cb35e186a63, 0x8fc3ca5285b75d19,\n\t\t0x0970820ef13ba1c6, 0xea8d7ee8c0f14148, 0x0ab7ddf3b3d3758b, 0x9d8a0befc4bdb9ba,\n\t\t0xdb917c2a2a832bb7, 0xb2dceb528077e210, 0x4730786f8c9acd54, 0x8903ad39dc1b9b0f,\n\t\t0x6ec2884ffbca1aa1, 0xcb9e6a0660c3e3a2, 0x3162d7fec3b970af, 0xb284839e161a4286,\n\t\t0x982a1a79a1d7766f, 0x2b400e2a1d9714ec, 0xe892501cb2706459, 0x6fd7fb1bff1d3fcb,\n\t\t0x46392fa39c367d6a, 0x9e018a08716c4dc5, 0x167c4c329a9bfa2e, 0x649d2b29dfedb145,\n\t\t0x137591ec13d426ff, 0x9e3363980fe7da47, 0x04c9f9da52a0fae2, 0xd815b33f27fd7410,\n\t\t0x8a96624c4f789f6d, 0x18343837448f64dc, 0xbbd3ab977386ee3b, 0xa2b4ffb4b8f58918,\n\t\t0x1c13fc6f1956a2e8, 0xac7988d677b2d63d, 0xd03ecaf6c2e15145, 0x552528467c783247,\n\t\t0xa136ea87edcfe2ff, 0xc2b017fad6722a8a, 0x496b1d4b56893bd9, 0x8d1b173b6b59ce8a,\n\t\t0x4beb409746a3b1c9, 0x7f42465934340659, 0xd1c5cc9eef153ff2, 0x2d874e915df47096,\n\t\t0xc6262045a8619834, 0x04729a19deab5aa6, 0xc981a87fc69259ee, 0x89a239d61aa270ee,\n\t\t0x83a6ff9710437b1e, 0x44abfd3de24fdbf2, 0x8f3adfda0e3650ef, 0x9d8f73b40a578c2d,\n\t\t0x9a1c451324a30c3c, 0x4b57fe4caf865edf, 0xf5424003e4a17dd6, 0xc5b1561aadb03ab3,\n\t\t0x5b15e2791b0dd4ff, 0xc3f4da34adffddb3, 0x71c22fa8624965b4, 0xf2dc6b254c28af72,\n\t\t0x985e8e4a6b96ac4b, 0x224a9984be53e804, 0xa5c1b977dbe38b7b, 0xf5904fac298383db,\n\t\t0xe046ed14aa4bb4a4, 0x065f2b6273c95e85, 0x25bee6fdd8823f01, 0xc401c8f47f8063ce,\n\t\t0xc457975bb198da7a, 0x5b1b68f651d9f275, 0x27f5ba16132250e2, 0xaf21fd989c053981,\n\t\t0x7f2227281262014d, 0x4acff17cd88624c0, 0x04f1d174b80e66ec, 0x5ab3ec7a8e8c5f45,\n\t\t0x36df34fa8d802366, 0x2dc45158fb0505b9, 0x09c81949f6981e7a, 0x1b40e30915de08e4,\n\t\t0x5de1eae5e97ce5b9, 0xcbd089d1d17fedf7, 0xc61337c3959e5c2d, 0xee10d945d75eba04,\n\t\t0xf842d40598067f10, 0x4aa524d182736f5f, 0xff8dfbef7cf7ed73, 0x1a97605236a60f0b,\n\t\t0x1d627812ff7f0e46, 0xf95f3005af31daeb, 0x16dd691afa094b27, 0xa341a1f55ebdaf16,\n\t\t0xae4427ce089c81ee, 0x2a1d03bbf12ae877, 0x6219e32ebec52155, 0x104b5fccc184df6a,\n\t\t0x38fe4cd53cb1b4f5, 0x4b0f4c6a630ae5f9, 0x7e3736c21fcf82e7, 0xa3c535da32890136,\n\t\t0x957584eefed5c5b6, 0x8c5285471dda564b, 0x4c16c99cf64c3737, 0xc91fabd40b0ba31f,\n\t\t0x68982161074fce8c, 0x3c4e204aba55c7cf, 0x562d7364029f465f, 0x603446fcd29438f5,\n\t\t0x21c6244966890e45, 0x798ff5753194e48e, 0x3047800e51ea3bac, 0xc88043df5d5304a5,\n\t\t0xf0131c1f3a1a6606, 0x68f3be12e32bdb4f, 0x8a32c291670af7a0, 0x6c7639e075126b95,\n\t\t0xc2dc5d872ffdb1d1, 0xd7ef8519a343443f, 0xae36b135b2c6c3ab, 0xc2ba69004ad234ca,\n\t\t0x29ead2a1a6aa4a38, 0x361262755e7e706f, 0x5a691aaf4616d7d4, 0xdc721a4d5412977e,\n\t\t0x3c3b63d739fa7357, 0xe305255d4e333ece, 0x1aa76bbb5ba55887, 0x876f314814e54688,\n\t\t0x145df714c9a7afb5, 0x2f2bc11a57760c1e, 0xd7d2ad3305b9d44c, 0x566c06f97a90736c,\n\t\t0x9a3aaf529e2fd129, 0x29652b7ffebb6213, 0x15c6c74f62425d83, 0x4dc913fc82a43e91,\n\t\t0x7a01f560f603b8dc, 0xafa2291ecf10106d, 0xdc7611daba496735, 0xf81302aab11f65bc,\n\t\t0x743b9b1937af1167, 0xb7174f8520995ae4, 0xca881e5ea17f8705, 0xf605904ebbeda8ea,\n\t\t0x438ad40d293ad385, 0x0bb7d5de136f3d1c, 0xaae0348d3ffc5ff5, 0xf91e7e51f404856a,\n\t\t0x31fbd722068a6fc1, 0x864891d15bb4a89d, 0x6d126f0a4dfa6f1c, 0xd712b3b470fb5aaa,\n\t\t0x210679c2e7f2c4e5, 0x95e4a2f927478150, 0x7dd767a69d4b6e5f, 0x872e096f6218826e,\n\t\t0x377a47cffc920bad, 0xd25c95981469b866, 0xa88505a39da468a5, 0x37be0e4e0c788ab3,\n\t\t0x00eab8b3319105d9, 0x69b5e0512d2ee447, 0x03cc0ab9a4afbd84, 0xd8c48e75c57d27ef,\n\t\t0x50b9fbb4b2a7e28b, 0xcb6adb8d1ff67876, 0x73a1001086e53111, 0xf5ab519c8013349e,\n\t\t0x3cad9c51cbd789aa, 0x6054d55d2dfde2c6, 0x080ed1165d00c782, 0xa5c61ab7d8ad913d,\n\t\t0xf793f44ab7b08399, 0xea808a254cb32744, 0x725543e54ab02d17, 0x1a9e9b7af53bfd57,\n\t\t0x17e4f93f31e1946f, 0xc9c5ddeae73ecd62, 0x97d23bd21fd433cf, 0xa3b275491370d1d6,\n\t\t0xb11f894611aa3032, 0x88812884130146c5, 0x5e1d165feed365f2, 0x74eb3d8a3aa050eb,\n\t\t0xbdb53fef87ce2b77, 0xb56b7c6b8e252272, 0x1578c4dafc9cdd1a, 0xbcc3ebcf5e0e426e,\n\t\t0x050dde0254c70bf1, 0x85570c34369e7dfc, 0x79e9f82cab6ca84c, 0xbc81b8b89e3ebff5,\n\t\t0xc30e1620853f58a0, 0xc93e6d17fd29de5c, 0x22a81e8b93023d85, 0xb050be8554dcae34,\n\t\t0xa21c4e0248fafe46, 0x691a89370cf92c8f, 0x328f846846aaa897, 0xf36e7fd2b6e00d55,\n\t\t0x74eca9441886f398, 0x9b77954cfbc5d37d, 0xa601cc3d8dbd9417, 0x0d227beab2f2188d,\n\t\t0x6332124a8037360e, 0x475b581c1ae4617f, 0xe59ef4433484c860, 0x4ea43c61313e18dc,\n\t\t0x52880f1db6a06c9f, 0x3eda91ae52b504a7, 0x0902fa9c936f035d, 0xdf5807133df966ef,\n\t\t0x8f072fd1b4c332d1, 0x17d50c8eda5f369c, 0xc147ebe8f1c74e79, 0x9c5a22cb3b907ead,\n\t\t0x1d45a8b4b979eef7, 0x50c0493f531caf0b, 0x9b0503132930ad42, 0x1d93c9c4729d622f,\n\t\t0x62035e3181c9deb5, 0x8dfa8a80e9771bee, 0xae35a09d89a02aa2, 0x8692654edf0e67de,\n\t\t0xac06586213c259f1, 0x2a206d5282ec17c0, 0xa460b439cfc938a8, 0x04a0e405ba56c8ae,\n\t\t0x662b08dac8a62507, 0x469b055fe85471a6, 0x155250849d3aa846, 0xa126917e719bd859,\n\t\t0x7424f5be7c4447b0, 0x992b63c06e98a715, 0x3a182aacddbfa805, 0xda0c271e2fc2c6d6,\n\t}\n)\n" + }, + { + "name": "zobrist_test.gno", + "body": "package zobrist\n\nimport (\n\t\"testing\"\n)\n\n// piece character to internal piece\nvar p = [256]Piece{\n\t'P': PiecePawn,\n\t'R': PieceRook,\n\t'N': PieceKnight,\n\t'B': PieceBishop,\n\t'Q': PieceQueen,\n\t'K': PieceKing,\n\n\t'p': PieceBlack | PiecePawn,\n\t'r': PieceBlack | PieceRook,\n\t'n': PieceBlack | PieceKnight,\n\t'b': PieceBlack | PieceBishop,\n\t'q': PieceBlack | PieceQueen,\n\t'k': PieceBlack | PieceKing,\n}\n\n// NewBoard returns a Board normally set up at the initial position for standard\n// chess.\nfunc NewBoard() Board {\n\treturn Board{\n\t\t// row 1\n\t\tp['R'], p['N'], p['B'], p['Q'],\n\t\tp['K'], p['B'], p['N'], p['R'],\n\t\t// row 2\n\t\tp['P'], p['P'], p['P'], p['P'],\n\t\tp['P'], p['P'], p['P'], p['P'],\n\n\t\t// rows 3, 4, 5, 6\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\n\t\t// row 7\n\t\tp['p'], p['p'], p['p'], p['p'],\n\t\tp['p'], p['p'], p['p'], p['p'],\n\t\t// row 8\n\t\tp['r'], p['n'], p['b'], p['q'],\n\t\tp['k'], p['b'], p['n'], p['r'],\n\t}\n}\n\nfunc TestInitialPosition(t *testing.T) {\n\th := Hash(NewBoard(), false, 0, 255)\n\tif h != InitialPosition {\n\t\tt.Fatalf(\"InitialPosition is invalid: set to %d, should be %d\", InitialPosition, h)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "sdK0tYJya9HXyS6rSZNKBHPNMmYOyrBlouWZcmm3rknGC5kE+AGkROrjIKO0An9xilx17huRmyvkvhK5xMydDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "chess", + "path": "gno.land/p/morgan/chess", + "files": [ + { + "name": "engine.gno", + "body": "// Package chess implements a simple chess engine, designed to implement all\n// of the official FIDE rules with the intention of validating moves for a\n// \"chess server\" realm (gno.land/r/demo/chess).\n//\n// To implement the rules, the FIDE \"Laws of Chess\" are used as a reference:\n// https://www.fide.com/FIDE/handbook/LawsOfChess.pdf\n//\n// This package was designed with a focus on clarity and on using this code as\n// a didactic tool. Any contributions to the code should respect this.\npackage chess\n\nimport (\n\t\"errors\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/morgan/chess/zobrist\"\n)\n\n// PositionFlags. The lower 4 bits indicate an en passant column; the upper\n// 4 indicate castling rights.\ntype PositionFlags byte\n\nconst (\n\tEnPassant PositionFlags = 1 \u003c\u003c (iota + 3)\n\tNoCastleWQ\n\tNoCastleWK\n\tNoCastleBQ\n\tNoCastleBK\n\n\tMaskEnPassant = 7 // low 4 bits\n)\n\n// CastlingRights returns FEN castling rights.\n// https://www.chessprogramming.org/Forsyth-Edwards_Notation#Castling_ability\nfunc (p PositionFlags) CastlingRights() string {\n\ts := \"\"\n\tif p\u0026NoCastleWK == 0 {\n\t\ts += \"K\"\n\t}\n\tif p\u0026NoCastleWQ == 0 {\n\t\ts += \"Q\"\n\t}\n\tif p\u0026NoCastleBK == 0 {\n\t\ts += \"k\"\n\t}\n\tif p\u0026NoCastleBQ == 0 {\n\t\ts += \"q\"\n\t}\n\tif s == \"\" {\n\t\treturn \"-\"\n\t}\n\treturn s\n}\n\n// Position contains the information about a chessboard, and surrounding\n// context: the previous moves, the castling rights and \"en passant\" column.\n//\n// NOTE: the position of a piece is encoded in a [Square].\ntype Position struct {\n\tB Board\n\tMoves []Move\n\tFlags PositionFlags\n\n\t// Halfmoves since the last pawn move or capture.\n\t// https://www.chessprogramming.org/Halfmove_Clock\n\tHalfMoveClock uint16\n\t// Used to calculate repeating positions (3- 5-fold repetition).\n\t// Zobrist hashing: https://www.chessprogramming.org/Zobrist_Hashing\n\t// Reset together with halfmove clock.\n\tHashes []uint64\n}\n\n// NewPosition returns a new Position, set up with the initial board position.\nfunc NewPosition() Position {\n\treturn Position{\n\t\tB: NewBoard(),\n\t\tMoves: make([]Move, 0, 80), // typical chess game is ~40 moves, 80 half-moves\n\t\tHashes: []uint64{zobrist.InitialPosition},\n\t}\n}\n\n// Color of the \"next\" move after p.Moves. (White for even len(p.Moves),\n// Black otherwise)\nfunc (p Position) Color() Color { return Color(len(p.Moves)\u00261 == 1) }\n\n// uintSlice attaches the methods of sort.Interface to []uint64, sorting in increasing order.\ntype uintSlice []uint64\n\nfunc (x uintSlice) Len() int { return len(x) }\nfunc (x uintSlice) Less(i, j int) bool { return x[i] \u003c x[j] }\nfunc (x uintSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }\n\n// maxHashCount counts the maximum number of repeating hashes in p.Hashes.\nfunc (p Position) maxHashCount() int {\n\tif len(p.Hashes) == 0 {\n\t\treturn 0\n\t}\n\tsort.Sort(uintSlice(p.Hashes))\n\tvar last uint64\n\tvar cur, max int\n\tfor _, v := range p.Hashes {\n\t\tif last != v {\n\t\t\tlast = v\n\t\t\tif cur \u003e= max {\n\t\t\t\tmax = cur\n\t\t\t}\n\t\t\tcur = 0\n\t\t}\n\t\tcur++\n\t}\n\tif cur \u003e= max {\n\t\tmax = cur\n\t}\n\treturn max\n}\n\nfunc sign(n int8) int8 {\n\tswitch {\n\tcase n \u003e 0:\n\t\treturn 1\n\tcase n \u003c 0:\n\t\treturn -1\n\tdefault:\n\t\treturn 0\n\t}\n}\n\nfunc abs(n int8) int8 {\n\treturn n * sign(n)\n}\n\n// EncodeFEN encodes p into FEN.\n// https://www.chessprogramming.org/Forsyth-Edwards_Notation\nfunc (p Position) EncodeFEN() string {\n\tvar s string\n\temptyCount := 0\n\t// FEN has different ordering from us, as [0] is a black rook while for us\n\t// is a white rook. So we need to invert the order of rows.\n\tfor i := 56; i \u003e= 0; i++ {\n\t\tv := p.B[i]\n\t\tif v == PieceEmpty {\n\t\t\temptyCount++\n\t\t\tif i%8 == 7 {\n\t\t\t\ts += strconv.Itoa(emptyCount) + \"/\"\n\t\t\t\temptyCount = 0\n\t\t\t\ti -= 16\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif emptyCount \u003e 0 {\n\t\t\ts += strconv.Itoa(emptyCount)\n\t\t\temptyCount = 0\n\t\t}\n\t\ts += v.String()\n\t\tif i%8 == 7 {\n\t\t\ts += \"/\"\n\t\t\ti -= 16\n\t\t}\n\t}\n\t// remove trailing slash\n\ts = s[:len(s)-1]\n\n\tstrs := []string{\n\t\ts, // 0: piece placement\n\t\t\"w\", // 1: side to move\n\t\tp.Flags.CastlingRights(), // 2: castling ability\n\t\t\"-\", // 3: e.p. target square\n\n\t\tstrconv.Itoa(int(p.HalfMoveClock)), // 4: halfmove clock\n\t\tstrconv.Itoa(len(p.Moves)/2 + 1), // 5: fullmove counter\n\t}\n\n\tvar epFile byte\n\tif p.Flags\u0026EnPassant \u003e 0 {\n\t\tepFile = 'a' + byte(p.Flags\u0026MaskEnPassant)\n\t}\n\n\tif p.Color() == Black {\n\t\tstrs[1] = \"b\"\n\t\tif epFile != 0 {\n\t\t\tstrs[3] = string(epFile) + \"3\"\n\t\t}\n\t} else if epFile != 0 {\n\t\tstrs[3] = string(epFile) + \"6\"\n\t}\n\n\treturn strings.Join(strs, \" \")\n}\n\n// ValidateMove checks whether the given move is legal in Chess.\n//\n// Caller must guarantee m.To and m.From to be valid (\u003c64).\nfunc (p Position) ValidateMove(m Move) (newP Position, valid bool) {\n\tif p.B[m.To].StripColor() == PieceKing {\n\t\treturn\n\t}\n\n\treturn p.validateMove(m)\n}\n\n// validateMove allows for m to be a \"king-capture\" move, which is illegal in\n// chess, but it is useful for InCheck.\n// This is commonly known in chess programming as a \"pseudo-legal\" move.\nfunc (oldp Position) validateMove(m Move) (newPos Position, ok bool) {\n\tp := oldp\n\n\tpiece := p.B[m.From]\n\n\t// piece moved must be of player's color\n\tcolor := p.Color()\n\tif piece == PieceEmpty || piece.Color() != color ||\n\t\t// additionally, check piece has actually moved\n\t\tm.From == m.To {\n\t\treturn\n\t}\n\t// destination must not be occupied by piece of same color\n\tif to := p.B[m.To]; to != PieceEmpty \u0026\u0026 to.Color() == color {\n\t\treturn\n\t}\n\n\t// one of the two necessarily != 0 (consequence of m.From != m.To).\n\tdelta := m.From.Sub(m.To)\n\tdr, dc := delta[0], delta[1]\n\n\t// Keep old castling rights; remove en passant info.\n\tnewFlags := p.Flags \u0026^ (MaskEnPassant | EnPassant)\n\t// Marked as true for succesful promotions.\n\tvar promoted bool\n\n\tisDiag := func() bool {\n\t\t// move diagonally (|dr| == |dc|)\n\t\tif abs(dr) != abs(dc) {\n\t\t\treturn false\n\t\t}\n\t\tsignr, signc := sign(dr), sign(dc)\n\t\t// squares crossed must be empty\n\t\tfor i := int8(1); i \u003c abs(dr); i++ {\n\t\t\tif p.B[m.From.Move(i*signr, i*signc)] != PieceEmpty {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\tisHorizVert := func() bool {\n\t\t// only one of dr, dc must be 0 (horiz/vert movement)\n\t\tif dr != 0 \u0026\u0026 dc != 0 {\n\t\t\treturn false\n\t\t}\n\t\t// squares crossed must be empty\n\t\tfor i := int8(1); i \u003c abs(dr); i++ {\n\t\t\tif p.B[m.From.Move(i*sign(dr), 0)] != PieceEmpty {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\tfor i := int8(1); i \u003c abs(dc); i++ {\n\t\t\tif p.B[m.From.Move(0, i*sign(dc))] != PieceEmpty {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\tswitch piece.StripColor() {\n\tcase PieceRook:\n\t\tif !isHorizVert() {\n\t\t\treturn\n\t\t}\n\t\t// if rook has moved from a starting position, this disables castling\n\t\t// on the side of the rook. flag accordingly in the move.\n\t\tvar fg Square\n\t\tif color == Black {\n\t\t\tfg = 7 \u003c\u003c 3\n\t\t}\n\t\tswitch m.From {\n\t\tcase fg: // a-col rook (either side)\n\t\t\tif color == White {\n\t\t\t\tnewFlags |= NoCastleWQ\n\t\t\t} else {\n\t\t\t\tnewFlags |= NoCastleBQ\n\t\t\t}\n\t\tcase fg | 7: // h-col rook (either side)\n\t\t\tif color == White {\n\t\t\t\tnewFlags |= NoCastleWK\n\t\t\t} else {\n\t\t\t\tnewFlags |= NoCastleBK\n\t\t\t}\n\t\t}\n\n\tcase PieceKnight:\n\t\t// move L-shaped\n\t\t// rationale: if you only have positive integers, the only way you can\n\t\t// obtain x * y == 2 is if x,y are either 1,2 or 2,1.\n\t\tif abs(dc*dr) != 2 {\n\t\t\treturn\n\t\t}\n\n\tcase PieceBishop:\n\t\tif !isDiag() {\n\t\t\treturn\n\t\t}\n\n\tcase PieceQueen:\n\t\tif !isHorizVert() \u0026\u0026 !isDiag() {\n\t\t\treturn\n\t\t}\n\n\tcase PieceKing:\n\t\t// castling\n\t\tif abs(dc) == 2 \u0026\u0026 dr == 0 {\n\t\t\t// determine if castle is a valid form of castling for the given color\n\t\t\tctype := m.isCastle(color)\n\t\t\tif ctype == 0 {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif false ||\n\t\t\t\t// check that there are no previous moves which disable castling\n\t\t\t\tp.castlingDisabled(color, ctype) ||\n\t\t\t\t// check that we have the exact board set ups we need\n\t\t\t\t// + make sure that the original and crossed squares are not in check\n\t\t\t\t!p.checkCastlingSetup(ctype) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// perform rook move here\n\t\t\tp.B = p.B.castleRookMove(color, ctype)\n\t\t\t// add NoCastle flags to prevent any further castling\n\t\t\tif color == White {\n\t\t\t\tnewFlags |= NoCastleWQ | NoCastleWK\n\t\t\t} else {\n\t\t\t\tnewFlags |= NoCastleBQ | NoCastleBK\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\t// move 1sq in all directions\n\t\tif dc \u003c -1 || dc \u003e 1 || dr \u003c -1 || dr \u003e 1 {\n\t\t\treturn\n\t\t}\n\t\t// king has moved: disable castling.\n\t\tif color == White {\n\t\t\tnewFlags |= NoCastleWQ | NoCastleWK\n\t\t} else {\n\t\t\tnewFlags |= NoCastleBQ | NoCastleBK\n\t\t}\n\n\tcase PiecePawn:\n\t\t// determine direction depending on color\n\t\tdir := int8(1)\n\t\tif color == Black {\n\t\t\tdir = -1\n\t\t}\n\n\t\tswitch {\n\t\tcase dc == 0 \u0026\u0026 dr == dir: // 1sq up\n\t\t\t// destination must be empty (no captures allowed)\n\t\t\tif p.B[m.To] != PieceEmpty {\n\t\t\t\treturn\n\t\t\t}\n\t\tcase dc == 0 \u0026\u0026 dr == dir*2: // 2sq up (only from starting row)\n\t\t\twantRow := Square(1)\n\t\t\tif color == Black {\n\t\t\t\twantRow = 6\n\t\t\t}\n\t\t\t// check starting row, and that two squares are empty\n\t\t\tif (m.From\u003e\u003e3) != wantRow ||\n\t\t\t\tp.B[m.From.Move(int8(dir), 0)] != PieceEmpty ||\n\t\t\t\tp.B[m.To] != PieceEmpty {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t_, col := m.To.Split()\n\t\t\tnewFlags |= EnPassant | PositionFlags(col)\n\t\tcase abs(dc) == 1 \u0026\u0026 dr == dir: // capture on diag\n\t\t\t// must be a capture\n\t\t\tif p.B[m.To] == PieceEmpty {\n\t\t\t\tif sq := p.checkEnPassant(color, m.To); sq != SquareInvalid {\n\t\t\t\t\t// remove other pawn\n\t\t\t\t\tp.B[sq] = PieceEmpty\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// p.B[m.To] is necessarily an opponent piece; we check \u0026 return\n\t\t\t// p.B[m.To].Color == color at the beginning of the fn.\n\t\tdefault: // not a recognized move\n\t\t\treturn\n\t\t}\n\n\t\trow := m.To \u003e\u003e 3\n\t\tif (color == White \u0026\u0026 row == 7) ||\n\t\t\t(color == Black \u0026\u0026 row == 0) {\n\t\t\tswitch m.Promotion {\n\t\t\tcase 0:\n\t\t\t\t// m.To is a king? then this is a pseudo-move check.\n\t\t\t\t// assume queen in that case.\n\t\t\t\tif p.B[m.To].StripColor() != PieceKing {\n\t\t\t\t\t// no promotion given, invalid\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tm.Promotion = PieceQueen\n\t\t\tcase PieceQueen, PieceBishop, PieceKnight, PieceRook:\n\t\t\tdefault:\n\t\t\t\treturn\n\t\t\t}\n\t\t\tpromoted = true\n\t\t\tp.B[m.From] = m.Promotion | color.Piece()\n\t\t}\n\t}\n\n\t// reject moves with promotion if there's nothing to promote\n\tif m.Promotion != 0 \u0026\u0026 !promoted {\n\t\treturn\n\t}\n\n\tif p.B[m.To].StripColor() == PieceKing {\n\t\t// King captures don't check for our own king in check;\n\t\t// these are only \"theoretical\" moves.\n\t\treturn Position{}, true\n\t}\n\n\t// perform board mutation\n\tcapture := p.B[m.To] != PieceEmpty\n\tp.B[m.From], p.B[m.To] = PieceEmpty, p.B[m.From]\n\tp.Flags = newFlags\n\tp.Moves = append([]Move{}, p.Moves...)\n\tp.Moves = append(p.Moves, m)\n\n\t// halfmove clock + hashes logic\n\tif piece.StripColor() == PiecePawn || capture {\n\t\t// reset both\n\t\tp.HalfMoveClock = 0\n\t\tp.Hashes = nil\n\t} else {\n\t\tp.HalfMoveClock++\n\t}\n\tep := byte(255)\n\tif p.Flags\u0026EnPassant != 0 {\n\t\tep = byte(p.Flags \u0026 MaskEnPassant)\n\t}\n\t// Not ideal, but avoids GenMoves potentially polluting \"real moves\" hashes.\n\tp.Hashes = append([]uint64{}, p.Hashes...)\n\tp.Hashes = append(\n\t\tp.Hashes,\n\t\t// color is inverted, because we consider the present move as already\n\t\t// done (hence, it is the other player to move).\n\t\tzobrist.Hash(toZobristBoard(p.B), bool(!color), byte(p.Flags)\u003e\u003e4, ep),\n\t)\n\n\t// is our king in check, as a result of the current move?\n\tif p.B.InCheck(color) {\n\t\treturn\n\t}\n\treturn p, true\n}\n\nfunc toZobristBoard(src Board) zobrist.Board {\n\tvar zb zobrist.Board\n\tfor pos, piece := range src {\n\t\tzb[pos] = zobrist.Piece(piece)\n\t}\n\treturn zb\n}\n\n// used by InCheck to simulate a move by black player.\nvar blackPrevMoves = make([]Move, 1)\n\n// InCheck checks whether the king with the given color is in check.\n// If such king does not exist on the board, InCheck returns false.\n//\n// A king is in check if the move from a piece of the other color\n// towards the king is valid, ignoring any checks on the other color's king.\n//\n// NOTE: the last remark is important:\n// https://lichess.org/analysis/4k3/8/4b3/8/8/8/K3R3/8_w_-_-_0_1?color=white\n// -- this is still a check for white, even if _technically_ black couldn't\n// move the bishop (as that would check its own king)\nfunc (b Board) InCheck(color Color) bool {\n\tpWant := PieceKing | color.Piece()\n\tkingp := b.findPiece(pWant)\n\tif kingp == SquareInvalid {\n\t\treturn false\n\t}\n\n\tpos := Position{B: b}\n\tif color == White {\n\t\t// color == White -\u003e simulate a move by black player -\u003e pos.Moves odd\n\t\tpos.Moves = blackPrevMoves\n\t}\n\n\tfor sq, piece := range b {\n\t\tif piece == PieceEmpty || piece.Color() == color {\n\t\t\tcontinue\n\t\t}\n\t\t_, ok := pos.validateMove(Move{\n\t\t\tFrom: Square(sq),\n\t\t\tTo: kingp,\n\t\t\t// validateMove (unexp) understands that moves to capture a king are\n\t\t\t// pseudo moves, so it doesn't check for checking on its own king,\n\t\t\t// or promotion.\n\t\t})\n\t\tif ok {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Board is a representation of a chess board.\n// Details on how to transform a chess algebraic position into an index\n// can be found at [Square].\ntype Board [64]Piece\n\n// NewBoard returns a Board normally set up at the initial position for standard\n// chess.\nfunc NewBoard() Board {\n\treturn Board{\n\t\t// row 1\n\t\tp['R'], p['N'], p['B'], p['Q'],\n\t\tp['K'], p['B'], p['N'], p['R'],\n\t\t// row 2\n\t\tp['P'], p['P'], p['P'], p['P'],\n\t\tp['P'], p['P'], p['P'], p['P'],\n\n\t\t// rows 3, 4, 5, 6\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\t\t0, 0, 0, 0, 0, 0, 0, 0,\n\n\t\t// row 7\n\t\tp['p'], p['p'], p['p'], p['p'],\n\t\tp['p'], p['p'], p['p'], p['p'],\n\t\t// row 8\n\t\tp['r'], p['n'], p['b'], p['q'],\n\t\tp['k'], p['b'], p['n'], p['r'],\n\t}\n}\n\nfunc (b Board) findPiece(pWant Piece) Square {\n\tfor sq, p := range b {\n\t\tif p == pWant {\n\t\t\treturn Square(sq)\n\t\t}\n\t}\n\treturn SquareInvalid\n}\n\nfunc (p Position) checkCastlingSetup(typ byte) bool {\n\t// set up correct row and piece flags according to color\n\tc := p.Color()\n\tb := p.B\n\tvar fg Square\n\tvar pfg Piece\n\tif c == Black {\n\t\tfg, pfg = 7\u003c\u003c3, PieceBlack\n\t}\n\n\t// cross are the squares that the king starts from,\n\t// crosses and \"lands\". they are recorded as they must all be\n\t// not in check by any opponent piece.\n\tvar cross [3]Square\n\n\tif typ == 'K' {\n\t\tif !(b[fg|4] == pfg|PieceKing \u0026\u0026\n\t\t\tb[fg|5] == PieceEmpty \u0026\u0026\n\t\t\tb[fg|6] == PieceEmpty \u0026\u0026\n\t\t\tb[fg|7] == pfg|PieceRook) {\n\t\t\treturn false\n\t\t}\n\t\tcross = [3]Square{fg | 4, fg | 5, fg | 6}\n\t} else {\n\t\tif !(b[fg|4] == pfg|PieceKing \u0026\u0026\n\t\t\tb[fg|3] == PieceEmpty \u0026\u0026\n\t\t\tb[fg|2] == PieceEmpty \u0026\u0026\n\t\t\tb[fg|1] == PieceEmpty \u0026\u0026\n\t\t\tb[fg|0] == pfg|PieceRook) {\n\t\t\treturn false\n\t\t}\n\t\tcross = [3]Square{fg | 4, fg | 3, fg | 2}\n\t}\n\n\ttestb := p.B\n\tfor _, sq := range cross {\n\t\ttestb[sq] = pfg | PieceKing\n\t\tif testb.InCheck(c) {\n\t\t\treturn false\n\t\t}\n\t\ttestb[sq] = PieceEmpty\n\t}\n\n\treturn true\n}\n\nfunc (b Board) castleRookMove(c Color, typ byte) Board {\n\tvar fg Square\n\tvar pfg Piece\n\tif c == Black {\n\t\tfg, pfg = 7\u003c\u003c3, PieceBlack\n\t}\n\n\tif typ == 'K' {\n\t\tb[fg|7], b[fg|5] = PieceEmpty, PieceRook|pfg\n\t} else {\n\t\tb[fg|0], b[fg|3] = PieceEmpty, PieceRook|pfg\n\t}\n\treturn b\n}\n\nfunc (p Position) castlingDisabled(color Color, kind byte) bool {\n\tif kind != 'K' \u0026\u0026 kind != 'Q' {\n\t\treturn false\n\t}\n\n\t// Determine what flag we're looking for.\n\tvar want PositionFlags\n\tswitch {\n\tcase color == White \u0026\u0026 kind == 'K':\n\t\twant = NoCastleWK\n\tcase color == White \u0026\u0026 kind == 'Q':\n\t\twant = NoCastleWQ\n\tcase color == Black \u0026\u0026 kind == 'K':\n\t\twant = NoCastleBK\n\tcase color == Black \u0026\u0026 kind == 'Q':\n\t\twant = NoCastleBQ\n\t}\n\n\treturn p.Flags\u0026want != 0\n}\n\nfunc (p Position) checkEnPassant(c Color, sq Square) Square {\n\trow, col := sq.Split()\n\tif p.Flags\u0026EnPassant == 0 ||\n\t\t(c == White \u0026\u0026 row != 5) ||\n\t\t(c == Black \u0026\u0026 row != 2) {\n\t\treturn SquareInvalid\n\t}\n\twantCol := byte(p.Flags \u0026 MaskEnPassant)\n\n\tif col != wantCol {\n\t\treturn SquareInvalid\n\t}\n\n\tif c == White {\n\t\treturn Square(4\u003c\u003c3 | col)\n\t}\n\treturn Square(3\u003c\u003c3 | col)\n}\n\n// InsufficientMaterial tests for insufficient material on the board, in which\n// case it returns true.\n//\n// See this reference for the rules used:\n// https://www.chess.com/article/view/how-chess-games-can-end-8-ways-explained#insufficient-material\n//\n// - king vs king\n// - king+N/B vs king\n// - king+NN vs king\n// - king+N/B vs king+N/B\nfunc (bd Board) InsufficientMaterial() bool {\n\t// strategy:\n\t// store the pieces which could count for an insufficient material\n\t// scenario in w, b.\n\t// if we encounter any pawn, queen, or rook, material is sufficient.\n\t// if we encounter 2 bishops, material is sufficient.\n\t// afterwards, we verify that w and b are one of the possible insuf material\n\t// scenarios.\n\n\tconst (\n\t\timN byte = 1 \u003c\u003c iota // knight\n\t\timN2 // second knight\n\t\timB // bishop\n\t)\n\tvar w, b byte\n\tfor _, p := range bd {\n\t\tstrip := p.StripColor()\n\t\tswitch strip {\n\t\tcase PieceQueen, PiecePawn, PieceRook:\n\t\t\treturn false\n\t\tcase PieceKing, PieceEmpty:\n\t\t\tcontinue\n\t\t}\n\t\t// strip is one of PieceBishop PieceKnight\n\t\tt := \u0026w\n\t\tif p.Color() == Black {\n\t\t\tt = \u0026b\n\t\t}\n\t\tif strip == PieceBishop {\n\t\t\tif *t\u0026imB != 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t*t |= imB\n\t\t} else {\n\t\t\tif *t\u0026imN2 != 0 {\n\t\t\t\t// third knight (ie. from pawn promotion)\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif *t\u0026imN != 0 {\n\t\t\t\t*t |= imN2\n\t\t\t} else {\n\t\t\t\t*t |= imN\n\t\t\t}\n\t\t}\n\t}\n\t// make it so that w is bigger than b\n\tif b \u003e w {\n\t\tw, b = b, w\n\t}\n\tswitch [2]byte{w, b} {\n\tcase [2]byte{0, 0}, // king vs king\n\t\t[2]byte{imN, 0}, // king+N/B vs king\n\t\t[2]byte{imB, 0},\n\t\t[2]byte{imN | imN2, 0}, // king+NN vs king\n\t\t[2]byte{imN, imN}, // king+N/B vs king+N/B\n\t\t[2]byte{imB, imB},\n\t\t[2]byte{imB, imN}: // imB \u003e imN, so [2]byte{imN, imB} cannot happen\n\t\treturn true\n\t}\n\treturn false\n}\n\n// GenMoves implements a rudimentary move generator.\n// This is not used beyond aiding in determing stalemate and doing perft tests.\n// Each generated move is passed to cb.\n// If cb returns an error, it is returned without processing further moves.\nfunc (p Position) GenMoves(cb func(Position, Move) error) error {\n\tcolor := p.Color()\n\tfor sq, piece := range p.B {\n\t\tif piece == PieceEmpty || piece.Color() != color {\n\t\t\tcontinue\n\t\t}\n\n\t\tfrom := Square(sq)\n\n\t\tpstrip := piece.StripColor()\n\t\t// If the piece is a pawn, and they are on the second last row, we know\n\t\t// that whatever move they do (advance, or take diagonally) they're going\n\t\t// to promote.\n\t\tprom := pstrip == PiecePawn \u0026\u0026\n\t\t\t((color == White \u0026\u0026 from\u003e\u003e3 == 6) ||\n\t\t\t\t(color == Black \u0026\u0026 from\u003e\u003e3 == 1))\n\n\t\t// delta generator needs to know if p is black\n\t\tif pstrip == PiecePawn \u0026\u0026 color == Black {\n\t\t\tpstrip |= Black.Piece()\n\t\t}\n\n\t\tvar err error\n\t\tdeltaGenerator(pstrip, func(delta Delta) byte {\n\t\t\t// create move; if the resulting square is oob, continue\n\t\t\tm := Move{\n\t\t\t\tFrom: from,\n\t\t\t\tTo: from.Apply(delta),\n\t\t\t}\n\t\t\tif m.To == SquareInvalid ||\n\t\t\t\t(p.B[m.To] != PieceEmpty \u0026\u0026 p.B[m.To].Color() == color) {\n\t\t\t\treturn deltaGenStopLinear\n\t\t\t}\n\n\t\t\t// handle promotion case\n\t\t\tif prom {\n\t\t\t\tm.Promotion = PieceQueen\n\t\t\t}\n\n\t\t\t// if it's a valid move, call cb on it\n\t\t\tnewp, ok := p.ValidateMove(m)\n\t\t\tif !ok {\n\t\t\t\treturn deltaGenOK\n\t\t\t}\n\t\t\tif err = cb(newp, m); err != nil {\n\t\t\t\treturn deltaGenStop\n\t\t\t}\n\n\t\t\t// if we've promoted, handle the cases where we've promoted to a non-queen.\n\t\t\tif !prom {\n\t\t\t\treturn deltaGenOK\n\t\t\t}\n\n\t\t\tfor _, promPiece := range [...]Piece{PieceRook, PieceKnight, PieceBishop} {\n\t\t\t\tnewp.B[m.To] = promPiece | color.Piece()\n\t\t\t\tm.Promotion = promPiece\n\t\t\t\tif err = cb(newp, m); err != nil {\n\t\t\t\t\treturn deltaGenStop\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn deltaGenOK\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nconst (\n\t// carry on normally\n\tdeltaGenOK = iota\n\t// if the generator is doing a linear attack (ie. rook, bishop, queen),\n\t// then stop that (there is a piece of same colour in the way.)\n\tdeltaGenStopLinear\n\t// abort generation asap.\n\tdeltaGenStop\n)\n\n/*func init() {\n\tfor i := PiecePawn; i \u003c= PieceKing; i++ {\n\t\tprintln(\"generator \", i.String())\n\t\tdeltaGenerator(i, func(d Delta) byte {\n\t\t\tprintln(\" \", d[0], d[1])\n\t\t\treturn deltaGenOK\n\t\t})\n\t}\n}*/\n\n// deltaGenerator generates the possible ways in which p can move.\n// the callback may return one of the three deltaGen* values.\nfunc deltaGenerator(p Piece, cb func(d Delta) byte) {\n\tdoLinear := func(d Delta) bool {\n\t\tfor i := int8(1); i \u003c= 7; i++ {\n\t\t\tswitch cb(d.Mul(i)) {\n\t\t\tcase deltaGenStopLinear:\n\t\t\t\treturn false\n\t\t\tcase deltaGenStop:\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\trotate := func(d Delta, lin bool) bool {\n\t\tfor i := 0; i \u003c 4; i++ {\n\t\t\tif lin {\n\t\t\t\tif doLinear(d) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif cb(d) == deltaGenStop {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\td = d.Rot()\n\t\t}\n\t\treturn false\n\t}\n\n\t// In the following, we use logical OR's to do conditional evaluation\n\t// (if the first item returns true, the second won't be evaluated)\n\tswitch p {\n\tcase PiecePawn, PiecePawn | PieceBlack:\n\t\tdir := int8(1)\n\t\tif p.Color() == Black {\n\t\t\tdir = -1\n\t\t}\n\t\t// try moving 1sq forward; if we get StopLinear, don't try to do 2sq.\n\t\tfw := cb(Delta{dir, 0})\n\t\tif fw == deltaGenStop {\n\t\t\treturn\n\t\t}\n\t\tif fw != deltaGenStopLinear {\n\t\t\tif cb(Delta{dir * 2, 0}) == deltaGenStop {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t_ = cb(Delta{dir, 1}) == deltaGenStop ||\n\t\t\tcb(Delta{dir, -1}) == deltaGenStop\n\n\tcase PieceRook:\n\t\trotate(Delta{0, 1}, true)\n\tcase PieceBishop:\n\t\trotate(Delta{1, 1}, true)\n\tcase PieceKnight:\n\t\t_ = rotate(Delta{1, 2}, false) ||\n\t\t\trotate(Delta{2, 1}, false)\n\tcase PieceQueen:\n\t\t_ = rotate(Delta{0, 1}, true) ||\n\t\t\trotate(Delta{1, 1}, true)\n\tcase PieceKing:\n\t\t_ = rotate(Delta{0, 1}, false) ||\n\t\t\trotate(Delta{1, 1}, false) ||\n\t\t\tcb(Delta{0, 2}) == deltaGenStop ||\n\t\t\tcb(Delta{0, -2}) == deltaGenStop\n\t}\n}\n\n// Outcome is returned by IsFinished, and indicates if the game has finished,\n// and additionally if either of the player can claim draw by the 50-move rule\n// or by 3-fold repetition.\n//\n// Either one of the sequential outcomes will be set (values 1-5), indicating\n// that the game is terminated, or the low 3 bits will be 0, and\n// the Can* flags may be set.\ntype Outcome byte\n\nconst (\n\tNotFinished Outcome = iota\n\tCheckmate\n\tStalemate\n\tDrawn75Move\n\tDrawn5Fold\n\n\tCan50Move Outcome = 8\n\tCan3Fold Outcome = 16\n\tCanInsufficient Outcome = 32\n)\n\nvar errFound = errors.New(\"found\")\n\n// IsFinished determines if the game at the given position can be considered\n// \"finished\".\n// See [Outcome] for the possible results.\nfunc (p Position) IsFinished() Outcome {\n\terr := p.GenMoves(func(Position, Move) error {\n\t\treturn errFound\n\t})\n\t// If there is any legal move, this is not any kind of mate.\n\tif err != nil {\n\t\tmhc := p.maxHashCount()\n\t\tswitch {\n\t\tcase mhc \u003e= 5:\n\t\t\treturn Drawn5Fold\n\t\tcase p.HalfMoveClock \u003e= 150:\n\t\t\treturn Drawn75Move\n\t\t}\n\t\tvar o Outcome\n\t\tif mhc \u003e= 3 {\n\t\t\to |= Can3Fold\n\t\t}\n\t\tif p.HalfMoveClock \u003e= 100 {\n\t\t\to |= Can50Move\n\t\t}\n\t\tif p.B.InsufficientMaterial() {\n\t\t\to |= CanInsufficient\n\t\t}\n\t\treturn o\n\t}\n\n\t// No legal moves. Is the king in check?\n\tif p.B.InCheck(p.Color()) {\n\t\treturn Checkmate\n\t}\n\treturn Stalemate\n}\n\n// Color determines a player's color -- either white or black.\ntype Color bool\n\nconst (\n\tWhite Color = false\n\tBlack Color = true\n)\n\n// Piece returns the color as a piece to be OR'd into a Piece;\n// ie. 0 on White, and [PieceBlack] on black.\nfunc (c Color) Piece() Piece {\n\tif c == White {\n\t\treturn 0\n\t}\n\treturn PieceBlack\n}\n\n// Piece represents a piece on the board.\ntype Piece byte\n\n// PieceFromChar returns the piece corresponding to the given character.\n// White pieces are uppercase, and black pieces are lowercase.\n// If a piece is invalid, PieceInvalid is returned.\nfunc PieceFromChar(b byte) Piece {\n\treturn p[b]\n}\n\n// piece character to internal piece\nvar p = [256]Piece{\n\t'P': PiecePawn,\n\t'R': PieceRook,\n\t'N': PieceKnight,\n\t'B': PieceBishop,\n\t'Q': PieceQueen,\n\t'K': PieceKing,\n\n\t'p': PieceBlack | PiecePawn,\n\t'r': PieceBlack | PieceRook,\n\t'n': PieceBlack | PieceKnight,\n\t'b': PieceBlack | PieceBishop,\n\t'q': PieceBlack | PieceQueen,\n\t'k': PieceBlack | PieceKing,\n}\n\nvar pstring = [PieceBlack | PieceKing + 1]byte{\n\tPiecePawn: 'P',\n\tPieceRook: 'R',\n\tPieceKnight: 'N',\n\tPieceBishop: 'B',\n\tPieceQueen: 'Q',\n\tPieceKing: 'K',\n\tPieceBlack | PiecePawn: 'p',\n\tPieceBlack | PieceRook: 'r',\n\tPieceBlack | PieceKnight: 'n',\n\tPieceBlack | PieceBishop: 'b',\n\tPieceBlack | PieceQueen: 'q',\n\tPieceBlack | PieceKing: 'k',\n}\n\nfunc (p Piece) String() string {\n\tif int(p) \u003e= len(pstring) {\n\t\treturn \"\"\n\t}\n\tv := pstring[p]\n\tif v == 0 {\n\t\treturn \"\"\n\t}\n\treturn string(v)\n}\n\n// Possible values of Piece. Within the context of Board, Piece is assumed to\n// be white, unless p\u0026PieceBlack != 0. Note PieceBlack is not a valid piece; it\n// must be bitwise OR'd to a non-empty piece.\nconst (\n\tPieceEmpty Piece = iota\n\n\tPiecePawn\n\tPieceRook\n\tPieceKnight\n\tPieceBishop\n\tPieceQueen\n\tPieceKing\n\n\tPieceBlack Piece = 8 // bit-flag\n)\n\n// Color returns the color of the piece.\nfunc (p Piece) Color() Color { return Color(p\u0026PieceBlack != 0) }\n\n// Piece returns the given Piece without color information.\nfunc (p Piece) StripColor() Piece { return p \u0026^ PieceBlack }\n\n// Switch switches the color of the given piece.\nfunc (p Piece) Switch() Piece {\n\tif p.Color() == Black {\n\t\treturn p \u0026^ PieceBlack\n\t}\n\treturn p | PieceBlack\n}\n\n// Delta represents a 2d vector for indicating a movement from one square\n// to another. The first value indicates the change in column, the second the\n// change in rows.\ntype Delta [2]int8\n\n// Valid ensures the two values of delta are valid.\nfunc (d Delta) Valid() bool {\n\treturn d[0] \u003e= -7 \u0026\u0026 d[0] \u003c= 7 \u0026\u0026\n\t\td[1] \u003e= -7 \u0026\u0026 d[1] \u003c= 7 \u0026\u0026\n\t\t!(d[0] == 0 \u0026\u0026 d[1] == 0)\n}\n\n// Rot applies a 90 degree anti-clockwise rotation to d.\nfunc (d Delta) Rot() Delta {\n\t// Rationale: this is just matrix-vector multiplication.\n\t// 90 deg rotation is just the matrix {0, -1; 1, 0}.\n\treturn Delta{d[1], -d[0]}\n}\n\n// Mul multiplies both values by n, otherwise known as scalar product.\nfunc (d Delta) Mul(n int8) Delta {\n\treturn Delta{d[0] * n, d[1] * n}\n}\n\n// Square encodes piece position information, in chess the \"square\" the piece is on.\n// Indexing 0 as the LSB, bits 0-3 indicate the column and bits 4-6 indicate\n// the row. For instance, square 44 (decimal) is:\n//\n//\t44 = 0b00 101 100 = d5\n//\t ^row ^col\n//\n// (note: in algebraic notation, this is swapped: the letter represents the\n// column, and the number represents the row).\ntype Square byte\n\n// SquareInvalid is returned by some Square-related methods to indicate\n// invalid parameters.\nconst SquareInvalid Square = 255\n\n// String returns p in algebraic notation.\nfunc (q Square) String() string {\n\tif q \u003e= 64 {\n\t\treturn \"\u003cinvalid\u003e\"\n\t}\n\treturn string(q\u00267+'a') + string(q\u003e\u003e3+'1')\n}\n\n// SquareFromString returns Square, reading the human-readable algebraic\n// notation in s. s must be 2 bytes long, with the first byte a letter included\n// between ['a'; 'h'], and the second a number included between ['1';'8'].\n// If s is invalid, SquareInvalid is returned.\nfunc SquareFromString(s string) Square {\n\tif len(s) != 2 {\n\t\treturn SquareInvalid\n\t}\n\tcol, row := s[0]-'a', s[1]-'1'\n\t// because s[0] is a byte, if s[0] \u003c 'a' then the above will underflow and\n\t// row will be \u003e= 8 (same for col).\n\tif row \u003e= 8 || col \u003e= 8 {\n\t\treturn SquareInvalid\n\t}\n\treturn Square(row\u003c\u003c3 | col)\n}\n\n// Move changes the square of q, moving it vertically according to dr\n// (delta row) and horizontally according to dc (delta column).\n// If the resulting square is not on the board, then SquareInvalid is returned.\nfunc (q Square) Move(dr, dc int8) Square {\n\tif q == SquareInvalid || !(Delta{dr, dc}).Valid() {\n\t\treturn SquareInvalid\n\t}\n\n\trow, col := int8(q\u003e\u003e3), int8(q\u00267)\n\trow += dr\n\tcol += dc\n\n\tnr, nc := Square(row), Square(col)\n\tif nr \u003e= 8 || nc \u003e= 8 {\n\t\treturn SquareInvalid\n\t}\n\treturn nr\u003c\u003c3 | nc\n}\n\n// Apply applies the given delta to the square.\n// It is shorthand for q.Move(d[0], d[1]).\nfunc (q Square) Apply(d Delta) Square { return q.Move(d[0], d[1]) }\n\n// Split splits Square into its components.\n// This function does not check if p is invalid.\nfunc (q Square) Split() (row, col byte) {\n\treturn byte(q \u003e\u003e 3), byte(q \u0026 7)\n}\n\n// SplitI works like [Square.Split], but returns int8's instead\n// of bytes.\nfunc (q Square) SplitI() (row, col int8) {\n\treturn int8(q \u003e\u003e 3), int8(q \u0026 7)\n}\n\n// Sub calculates the difference between the two squares.\n// q is the originating square, s is the ending square. The difference in\n// rows and columns from q to s is returned; for instance, d1.Sub(a4) yields\n// Delta{3, -3}.\nfunc (q Square) Sub(s Square) Delta {\n\tfr, fc := q.SplitI()\n\ttr, tc := s.SplitI()\n\treturn Delta{tr - fr, tc - fc}\n}\n\n// Move represents a chess game move.\ntype Move struct {\n\tFrom, To Square\n\tPromotion Piece\n}\n\n// String returns a string representation of Move.\n// It is in the form of \"Long Algebraic Notation\".\n// https://backscattering.de/chess/uci/#move-lan\nfunc (m Move) String() string {\n\tp := \"\"\n\tif m.Promotion != 0 {\n\t\tp = string(m.Promotion.String()[0] + ('a' - 'A'))\n\t}\n\treturn m.From.String() + m.To.String() + p\n}\n\nvar (\n\tcastleWhiteQ = Move{From: SquareFromString(\"e1\"), To: SquareFromString(\"c1\")}\n\tcastleWhiteK = Move{From: SquareFromString(\"e1\"), To: SquareFromString(\"g1\")}\n\tcastleBlackQ = Move{From: SquareFromString(\"e8\"), To: SquareFromString(\"c8\")}\n\tcastleBlackK = Move{From: SquareFromString(\"e8\"), To: SquareFromString(\"g8\")}\n)\n\n// returns 0, 'K' or 'Q'.\nfunc (m Move) isCastle(c Color) (kind byte) {\n\tif c == White {\n\t\tswitch m {\n\t\tcase castleWhiteQ:\n\t\t\treturn 'Q'\n\t\tcase castleWhiteK:\n\t\t\treturn 'K'\n\t\t}\n\t} else {\n\t\tswitch m {\n\t\tcase castleBlackQ:\n\t\t\treturn 'Q'\n\t\tcase castleBlackK:\n\t\t\treturn 'K'\n\t\t}\n\t}\n\treturn 0\n}\n" + }, + { + "name": "engine_test.gno", + "body": "package chess\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc movePromo(from, to string, promo Piece) Move {\n\tm := move(from, to)\n\tm.Promotion = promo\n\treturn m\n}\n\nfunc move(from, to string) Move {\n\treturn Move{\n\t\tFrom: SquareFromString(strings.ToLower(from)),\n\t\tTo: SquareFromString(strings.ToLower(to)),\n\t}\n}\n\nfunc TestCheckmate(t *testing.T) {\n\tfp := unsafeFEN(\"rn1qkbnr/pbpp1ppp/1p6/4p3/2B1P3/5Q2/PPPP1PPP/RNB1K1NR w KQkq - 0 1\")\n\tm := move(\"f3\", \"f7\")\n\tnewp, ok := fp.ValidateMove(m)\n\tif !ok {\n\t\tt.Fatal(\"ValidateMove returned false\")\n\t}\n\tmr := newp.IsFinished()\n\tif mr != Checkmate {\n\t\tt.Fatalf(\"expected Checkmate (%d), got %d\", Checkmate, mr)\n\t}\n}\n\nfunc TestCheckmateFromFEN(t *testing.T) {\n\tfp := unsafeFEN(\"rn1qkbnr/pbpp1Qpp/1p6/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 1\")\n\tmr := fp.IsFinished()\n\tif mr != Checkmate {\n\t\tt.Fatalf(\"expected Checkmate (%d), got %d\", Checkmate, mr)\n\t}\n}\n\nfunc TestStalemate(t *testing.T) {\n\tfp := unsafeFEN(\"k1K5/8/8/8/8/8/8/1Q6 w - - 0 1\")\n\tm := move(\"b1\", \"b6\")\n\tnewp, ok := fp.ValidateMove(m)\n\tif !ok {\n\t\tt.Fatal(\"ValidateMove rejected move\")\n\t}\n\tmr := newp.IsFinished()\n\tif mr != Stalemate {\n\t\tt.Fatalf(\"expected Stalemate (%d), got %d\", Stalemate, mr)\n\t}\n}\n\n// position shouldn't result in check/stalemate\n// because pawn can move http://en.lichess.org/Pc6mJDZN#138\nfunc TestNotMate(t *testing.T) {\n\tfp := unsafeFEN(\"8/3P4/8/8/8/7k/7p/7K w - - 2 70\")\n\tm := movePromo(\"d7\", \"d8\", PieceQueen)\n\tnewp, ok := fp.ValidateMove(m)\n\tif !ok {\n\t\tt.Fatal(\"ValidateMove returned false\")\n\t}\n\tmr := newp.IsFinished()\n\tif mr != NotFinished {\n\t\tt.Fatalf(\"expected NotFinished (%d), got %d\", NotFinished, mr)\n\t}\n}\n\nfunc TestXFoldRepetition(t *testing.T) {\n\tp := NewPosition()\n\tloop := [...]Move{\n\t\tmove(\"g1\", \"f3\"),\n\t\tmove(\"g8\", \"f6\"),\n\t\tmove(\"f3\", \"g1\"),\n\t\tmove(\"f6\", \"g8\"),\n\t}\n\tvar valid bool\n\tfor i := 0; i \u003c 5; i++ {\n\t\tfor j, m := range loop {\n\t\t\tp, valid = p.ValidateMove(m)\n\t\t\tif !valid {\n\t\t\t\tt.Fatalf(\"move %s not considered valid\", m.String())\n\t\t\t}\n\t\t\tfini := p.IsFinished()\n\t\t\tswitch {\n\t\t\tcase (i == 3 \u0026\u0026 j == 3) || i == 4:\n\t\t\t\t// after the fourth full iteration, it should be marked as \"drawn\" for 5-fold.\n\t\t\t\tif fini != Drawn5Fold {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, Drawn5Fold, fini)\n\t\t\t\t}\n\t\t\tcase (i == 1 \u0026\u0026 j == 3) || i \u003e= 2:\n\t\t\t\t// After the second full iteration, IsFinished should mark this as \"can 3 fold\"\n\t\t\t\tif fini != Can3Fold {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, Can3Fold, fini)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tif fini != NotFinished {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, NotFinished, fini)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc assertMoves(p Position, moves ...Move) Position {\n\tvar valid bool\n\tfor _, move := range moves {\n\t\tp, valid = p.ValidateMove(move)\n\t\tif !valid {\n\t\t\tpanic(\"invalid move\")\n\t\t}\n\t}\n\treturn p\n}\n\nfunc TestXFoldRepetition2(t *testing.T) {\n\t// Like TestXFoldRepetition, but starts after the initial position.\n\n\tp := assertMoves(\n\t\tNewPosition(),\n\t\tmove(\"f2\", \"f4\"),\n\t\tmove(\"c7\", \"c5\"),\n\t)\n\n\tloop := [...]Move{\n\t\tmove(\"g1\", \"f3\"),\n\t\tmove(\"g8\", \"f6\"),\n\t\tmove(\"f3\", \"g1\"),\n\t\tmove(\"f6\", \"g8\"),\n\t}\n\tvar valid bool\n\tfor i := 0; i \u003c 5; i++ {\n\t\tfor j, m := range loop {\n\t\t\tp, valid = p.ValidateMove(m)\n\t\t\tif !valid {\n\t\t\t\tt.Fatalf(\"move %s not considered valid\", m.String())\n\t\t\t}\n\t\t\tfini := p.IsFinished()\n\t\t\tswitch {\n\t\t\tcase i == 4:\n\t\t\t\tif fini != Drawn5Fold {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, Drawn5Fold, fini)\n\t\t\t\t}\n\t\t\tcase i \u003e= 2:\n\t\t\t\tif fini != Can3Fold {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, Can3Fold, fini)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tif fini != NotFinished {\n\t\t\t\t\tt.Errorf(\"i: %d j: %d; expect %d got %d\", i, j, NotFinished, fini)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestXMoveRule(t *testing.T) {\n\tp := NewPosition()\n\n\tp.HalfMoveClock = 99\n\tnewp := assertMoves(p, move(\"g1\", \"f3\"))\n\tfini := newp.IsFinished()\n\tif fini != Can50Move {\n\t\tt.Errorf(\"want %d got %d\", Can50Move, fini)\n\t}\n\n\tp.HalfMoveClock = 149\n\tnewp = assertMoves(p, move(\"g1\", \"f3\"))\n\tfini = newp.IsFinished()\n\tif fini != Drawn75Move {\n\t\tt.Errorf(\"want %d got %d\", Drawn75Move, fini)\n\t}\n}\n\nfunc TestInsufficientMaterial(t *testing.T) {\n\tfens := []string{\n\t\t\"8/2k5/8/8/8/3K4/8/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/8/3K1N2/8/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/8/3K1B2/8/8 w - - 1 1\",\n\t\t\"8/2k5/2b5/8/8/3K1B2/8/8 w - - 1 1\",\n\t\t\"8/2k1n1n1/8/8/8/3K4/8/8 w - - 1 1\",\n\t\t\"8/2k1b3/8/8/8/3K1B2/8/8 w - - 1 1\",\n\t}\n\tfor _, f := range fens {\n\t\tpos := unsafeFEN(f)\n\t\to := pos.IsFinished()\n\t\tif o != CanInsufficient {\n\t\t\tt.Errorf(\"fen %q: want %d got %d\", f, CanInsufficient, o)\n\t\t}\n\t}\n}\n\nfunc TestSufficientMaterial(t *testing.T) {\n\tfens := []string{\n\t\t\"8/2k5/8/8/8/3K1B2/4N3/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/8/3KBB2/8/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/4P3/3K4/8/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/8/3KQ3/8/8 w - - 1 1\",\n\t\t\"8/2k5/8/8/8/3KR3/8/8 w - - 1 1\",\n\t}\n\tfor _, f := range fens {\n\t\tpos := unsafeFEN(f)\n\t\to := pos.IsFinished()\n\t\tif o != NotFinished {\n\t\t\tt.Errorf(\"fen %q: want %d got %d\", f, NotFinished, o)\n\t\t}\n\t}\n}\n\ntype moveTest struct {\n\tpos unsafeFENRes\n\tm Move\n\tpostPos unsafeFENRes\n}\n\nvar (\n\tinvalidMoves = []moveTest{\n\t\t// out of turn moves\n\t\t{m: move(\"E7\", \"E5\"), pos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\")},\n\t\t{m: move(\"E2\", \"E4\"), pos: unsafeFEN(\"rnbqkbnr/1ppppppp/p7/8/8/8/PPPPPPPP/RNBQKBNR b KQkq - 0 1\")},\n\t\t// pawn moves\n\t\t{m: move(\"E2\", \"D3\"), pos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\")},\n\t\t{m: move(\"E2\", \"F3\"), pos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\")},\n\t\t{m: move(\"E2\", \"E5\"), pos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\")},\n\t\t{m: move(\"A2\", \"A1\"), pos: unsafeFEN(\"8/8/8/8/8/8/p7/8 b - - 0 1\")},\n\t\t{m: move(\"E6\", \"E5\"), pos: unsafeFEN(`2b1r3/2k2p1B/p2np3/4B3/8/5N2/PP1K1PPP/3R4 b - - 2 1`)},\n\t\t{m: move(\"H7\", \"H5\"), pos: unsafeFEN(`2bqkbnr/rpppp2p/2n2p2/p5pB/5P2/4P3/PPPP2PP/RNBQK1NR b KQk - 4 6`)},\n\t\t// knight moves\n\t\t{m: move(\"E4\", \"F2\"), pos: unsafeFEN(\"8/8/8/3pp3/4N3/8/5B2/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"F3\"), pos: unsafeFEN(\"8/8/8/3pp3/4N3/8/5B2/8 w - - 0 1\")},\n\t\t// bishop moves\n\t\t{m: move(\"E4\", \"C6\"), pos: unsafeFEN(\"8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"E5\"), pos: unsafeFEN(\"8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"E4\"), pos: unsafeFEN(\"8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"F3\"), pos: unsafeFEN(\"8/8/8/3pp3/4B3/5N2/8/8 w - - 0 1\")},\n\t\t// rook moves\n\t\t{m: move(\"B2\", \"B1\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"C3\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"B8\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"G7\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1R6/1B6 w - - 0 1\")},\n\t\t// queen moves\n\t\t{m: move(\"B2\", \"B1\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"C4\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"B8\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1\")},\n\t\t{m: move(\"B2\", \"G7\"), pos: unsafeFEN(\"8/1p5b/4N3/4p3/8/8/1Q6/1B6 w - - 0 1\")},\n\t\t// king moves\n\t\t{m: move(\"E4\", \"F3\"), pos: unsafeFEN(\"5r2/8/8/8/4K3/8/8/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"F4\"), pos: unsafeFEN(\"5r2/8/8/8/4K3/8/8/8 w - - 0 1\")},\n\t\t{m: move(\"E4\", \"F5\"), pos: unsafeFEN(\"5r2/8/8/8/4K3/8/8/8 w - - 0 1\")},\n\t\t// castleing\n\t\t{m: move(\"E1\", \"B1\"), pos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1\")},\n\t\t{m: move(\"E8\", \"B8\"), pos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 0 1\")},\n\t\t{m: move(\"E1\", \"C1\"), pos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R2QK2R w KQkq - 0 1\")},\n\t\t{m: move(\"E1\", \"C1\"), pos: unsafeFEN(\"2r1k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1\")},\n\t\t{m: move(\"E1\", \"C1\"), pos: unsafeFEN(\"3rk2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1\")},\n\t\t{m: move(\"E1\", \"G1\"), pos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R w Qkq - 0 1\")},\n\t\t{m: move(\"E1\", \"C1\"), pos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R w Kkq - 0 1\")},\n\t\t// invalid promotion for non-pawn move\n\t\t{m: movePromo(\"B8\", \"D7\", PieceQueen), pos: unsafeFEN(\"rn1qkb1r/pp3ppp/2p1pn2/3p4/2PP4/2NQPN2/PP3PPP/R1B1K2R b KQkq - 0 7\")},\n\t\t// en passant on doubled pawn file http://en.lichess.org/TnRtrHxf#24\n\t\t{m: move(\"E3\", \"F6\"), pos: unsafeFEN(\"r1b2rk1/pp2b1pp/1qn1p3/3pPp2/1P1P4/P2BPN2/6PP/RN1Q1RK1 w - f6 0 13\")},\n\t\t// can't move piece out of pin (even if checking enemy king) http://en.lichess.org/JCRBhXH7#62\n\t\t{m: move(\"E1\", \"E7\"), pos: unsafeFEN(\"4R3/1r1k2pp/p1p5/1pP5/8/8/1PP3PP/2K1Rr2 w - - 5 32\")},\n\t\t// invalid one up pawn capture\n\t\t{m: move(\"E6\", \"E5\"), pos: unsafeFEN(`2b1r3/2k2p1B/p2np3/4B3/8/5N2/PP1K1PPP/3R4 b - - 2 1`)},\n\t\t// invalid two up pawn capture\n\t\t{m: move(\"H7\", \"H5\"), pos: unsafeFEN(`2bqkbnr/rpppp2p/2n2p2/p5pB/5P2/4P3/PPPP2PP/RNBQK1NR b KQk - 4 6`)},\n\t\t// invalid pawn move d5e4\n\t\t{m: move(\"D5\", \"E4\"), pos: unsafeFEN(`rnbqkbnr/pp2pppp/8/2pp4/3P4/4PN2/PPP2PPP/RNBQKB1R b KQkq - 0 3`)},\n\t}\n\n\tpositionUpdates = []moveTest{\n\t\t{\n\t\t\tm: move(\"E2\", \"E4\"),\n\t\t\tpos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\"),\n\t\t\tpostPos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1\"),\n\t\t},\n\t\t{\n\t\t\tm: move(\"E1\", \"G1\"),\n\t\t\tpos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1\"),\n\t\t\tpostPos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R4RK1 b kq - 1 1\"),\n\t\t},\n\t\t{\n\t\t\tm: move(\"A4\", \"B3\"),\n\t\t\tpos: unsafeFEN(\"2r3k1/1q1nbppp/r3p3/3pP3/pPpP4/P1Q2N2/2RN1PPP/2R4K b - b3 0 23\"),\n\t\t\tpostPos: unsafeFEN(\"2r3k1/1q1nbppp/r3p3/3pP3/11pP4/PpQ2N2/2RN1PPP/2R4K w - - 0 24\"),\n\t\t},\n\t\t{\n\t\t\tm: move(\"E1\", \"G1\"),\n\t\t\tpos: unsafeFEN(\"r2qk2r/pp1n1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P4PPP/R1B1K2R w KQkq - 1 9\"),\n\t\t\tpostPos: unsafeFEN(\"r2qk2r/pp1n1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P4PPP/R1B2RK1 b kq - 2 9\"),\n\t\t},\n\t\t// half move clock - knight move to f3 from starting position\n\t\t{\n\t\t\tm: move(\"G1\", \"F3\"),\n\t\t\tpos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\"),\n\t\t\tpostPos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/5N2/PPPPPPPP/RNBQKB1R b KQkq - 1 1\"),\n\t\t},\n\t\t// half move clock - king side castle\n\t\t{\n\t\t\tm: move(\"E1\", \"G1\"),\n\t\t\tpos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1\"),\n\t\t\tpostPos: unsafeFEN(\"r3k2r/8/8/8/8/8/8/R4RK1 b kq - 1 1\"),\n\t\t},\n\t\t// half move clock - queen side castle\n\t\t{\n\t\t\tm: move(\"E1\", \"C1\"),\n\t\t\tpos: unsafeFEN(\"r3k2r/ppqn1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P2B1PPP/R3K2R w KQkq - 3 10\"),\n\t\t\tpostPos: unsafeFEN(\"r3k2r/ppqn1ppp/2pbpn2/3p4/2PP4/1PNQPN2/P2B1PPP/2KR3R b kq - 4 10\"),\n\t\t},\n\t\t// half move clock - pawn push\n\t\t{\n\t\t\tm: move(\"E2\", \"E4\"),\n\t\t\tpos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\"),\n\t\t\tpostPos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1\"),\n\t\t},\n\t\t// half move clock - pawn capture\n\t\t{\n\t\t\tm: move(\"E4\", \"D5\"),\n\t\t\tpos: unsafeFEN(\"r1bqkbnr/ppp1pppp/2n5/3p4/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3\"),\n\t\t\tpostPos: unsafeFEN(\"r1bqkbnr/ppp1pppp/2n5/3P4/8/5N2/PPPP1PPP/RNBQKB1R b KQkq - 0 3\"),\n\t\t},\n\t\t// half move clock - en passant\n\t\t{\n\t\t\tm: move(\"E5\", \"F6\"),\n\t\t\tpos: unsafeFEN(\"r1bqkbnr/ppp1p1pp/2n5/3pPp2/8/5N2/PPPP1PPP/RNBQKB1R w KQkq f6 0 4\"),\n\t\t\tpostPos: unsafeFEN(\"r1bqkbnr/ppp1p1pp/2n2P2/3p4/8/5N2/PPPP1PPP/RNBQKB1R b KQkq - 0 4\"),\n\t\t},\n\t\t// half move clock - piece captured by knight\n\t\t{\n\t\t\tm: move(\"C6\", \"D4\"),\n\t\t\tpos: unsafeFEN(\"r1bqkbnr/ppp1p1pp/2n5/3pPp2/3N4/8/PPPP1PPP/RNBQKB1R b KQkq - 1 4\"),\n\t\t\tpostPos: unsafeFEN(\"r1bqkbnr/ppp1p1pp/8/3pPp2/3n4/8/PPPP1PPP/RNBQKB1R w KQkq - 0 5\"),\n\t\t},\n\t}\n)\n\nfunc TestInvalidMoves(t *testing.T) {\n\tfor _, mt := range invalidMoves {\n\t\t_, ok := mt.pos.ValidateMove(mt.m)\n\t\tif ok {\n\t\t\tt.Errorf(\"fen %q: unexpected valid move (%s)\", mt.pos.orig, mt.m.String())\n\t\t}\n\t}\n}\n\nfunc TestPositionUpdates(t *testing.T) {\n\tfor _, mt := range positionUpdates {\n\t\tnp, ok := mt.pos.ValidateMove(mt.m)\n\t\tif !ok {\n\t\t\tt.Errorf(\"fen %q: rejected valid move (%s)\", mt.pos.orig, mt.m.String())\n\t\t\tcontinue\n\t\t}\n\t\tif np.B != mt.postPos.B {\n\t\t\tt.Errorf(\"%q: boards don't match\", mt.pos.orig)\n\t\t}\n\t\tif np.HalfMoveClock != mt.postPos.HalfMoveClock {\n\t\t\tt.Errorf(\"%q: hmc doesn't match; want %d got %d\", mt.pos.orig, mt.postPos.HalfMoveClock, np.HalfMoveClock)\n\t\t}\n\t\tif np.HalfMoveClock == 0 \u0026\u0026 len(np.Hashes) != 1 {\n\t\t\tt.Errorf(\"%q: hashes not reset\", mt.pos.orig)\n\t\t}\n\t\tif np.Flags != mt.postPos.Flags {\n\t\t\tt.Errorf(\"%q: flags don't match; want %d got %d\", mt.pos.orig, mt.postPos.Flags, np.Flags)\n\t\t}\n\t}\n}\n\nfunc TestPerft(t *testing.T) {\n\tmoves := make([]Move, 0, 10)\n\tfor n, res := range perfResults {\n\t\tt.Run(fmt.Sprintf(\"n%d\", n), func(t *testing.T) {\n\t\t\tif testing.Short() {\n\t\t\t\tt.Skip(\"skipping perft in short tests\")\n\t\t\t}\n\t\t\tres.pos.Moves = append(moves[:0], res.pos.Moves...)\n\t\t\tcounts := make([]int, len(res.nodesPerDepth))\n\t\t\tCountMoves(res.pos.Position, len(res.nodesPerDepth), counts)\n\t\t\tt.Logf(\"counts: %v\", counts)\n\t\t\tif !intsMatch(counts, res.nodesPerDepth) {\n\t\t\t\tt.Errorf(\"counts don't match: got %v want %v\", counts, res.nodesPerDepth)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc intsMatch(xx, yy []int) bool {\n\tif len(xx) != len(yy) {\n\t\treturn false\n\t}\n\tfor i := range xx {\n\t\tif xx[i] != yy[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nconst perftDebug = false\n\nfunc CountMoves(p Position, depth int, counts []int) {\n\ttotal := 0\n\tl := len(counts) - depth\n\tp.GenMoves(func(newp Position, m Move) error {\n\t\tcounts[l]++\n\t\tif depth \u003e 1 {\n\t\t\tcountMoves(newp, depth-1, counts)\n\t\t}\n\t\tdelta := counts[len(counts)-1] - total\n\t\tif perftDebug {\n\t\t\tfmt.Printf(\"%s%s: %d\\n\", m.From.String(), m.To.String(), delta)\n\t\t}\n\t\ttotal += delta\n\t\treturn nil\n\t})\n}\n\nfunc countMoves(p Position, depth int, counts []int) {\n\tl := len(counts) - depth\n\tp.GenMoves(func(newp Position, m Move) error {\n\t\tcounts[l]++\n\t\tif depth \u003e 1 {\n\t\t\tcountMoves(newp, depth-1, counts)\n\t\t}\n\t\treturn nil\n\t})\n}\n\ntype perfTest struct {\n\tpos unsafeFENRes\n\tnodesPerDepth []int\n}\n\n/* https://www.chessprogramming.org/Perft_Results */\nvar perfResults = []perfTest{\n\t{pos: unsafeFEN(\"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\"), nodesPerDepth: []int{\n\t\t20, 400, 8902, // 197281,\n\t\t// 4865609, 119060324, 3195901860, 84998978956, 2439530234167, 69352859712417\n\t}},\n\t{pos: unsafeFEN(\"r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1\"), nodesPerDepth: []int{\n\t\t48, 2039, 97862,\n\t\t// 4085603, 193690690\n\t}},\n\t{pos: unsafeFEN(\"8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1\"), nodesPerDepth: []int{\n\t\t14, 191, 2812, 43238, // 674624,\n\t\t// 11030083, 178633661\n\t}},\n\t{pos: unsafeFEN(\"r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1\"), nodesPerDepth: []int{\n\t\t6, 264, 9467, // 422333,\n\t\t// 15833292, 706045033\n\t}},\n\t{pos: unsafeFEN(\"r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1\"), nodesPerDepth: []int{\n\t\t6, 264, 9467, // 422333,\n\t\t// 15833292, 706045033\n\t}},\n\t{pos: unsafeFEN(\"rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8\"), nodesPerDepth: []int{\n\t\t44, 1486, 62379,\n\t\t// 2103487, 89941194\n\t}},\n\t{pos: unsafeFEN(\"r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10\"), nodesPerDepth: []int{\n\t\t46, 2079, // 89890,\n\t\t// 3894594, 164075551, 6923051137, 287188994746, 11923589843526, 490154852788714\n\t}},\n}\n\n// ---\n// testing utility functions\n\n// FEN decoding: see https://www.chessprogramming.org/Forsyth-Edwards_Notation\n// copied mostly from notnil/chess and adapted to our own system.\n\ntype unsafeFENRes struct {\n\tPosition\n\torig string\n}\n\nfunc unsafeFEN(fen string) unsafeFENRes {\n\tp, e := decodeFEN(fen)\n\tif e != nil {\n\t\tpanic(e)\n\t}\n\treturn unsafeFENRes{p, fen}\n}\n\n// Decodes FEN into Board and previous moves.\nfunc decodeFEN(fen string) (p Position, err error) {\n\tfen = strings.TrimSpace(fen)\n\tparts := strings.Split(fen, \" \")\n\tif len(parts) != 6 {\n\t\terr = fmt.Errorf(\"chess: fen invalid notation %s must have 6 sections\", fen)\n\t\treturn\n\t}\n\n\tp = NewPosition()\n\n\t// fen board\n\tvar ok bool\n\tp.B, ok = fenBoard(parts[0])\n\tif !ok {\n\t\terr = fmt.Errorf(\"chess: invalid fen board %s\", parts[0])\n\t\treturn\n\t}\n\n\t// do castling rights first (more convenient to set prev)\n\tif parts[2] != \"KQkq\" {\n\t\tp.Flags = castleRightsToPositionFlags(parts[2])\n\t}\n\n\t// color to play\n\tcolor := Color(parts[1] == \"b\")\n\tif color == Black {\n\t\t// add fake move to make len(prev) odd\n\t\tp.Moves = append(p.Moves, Move{})\n\t}\n\n\t// en passant\n\tif parts[3] != \"-\" {\n\t\tf, e := parseEnPassant(parts[3])\n\t\tif e != nil {\n\t\t\terr = e\n\t\t\treturn\n\t\t}\n\t\tp.Flags |= f\n\t}\n\n\thalfMove, _ := strconv.Atoi(parts[4])\n\tp.HalfMoveClock = uint16(halfMove)\n\n\t// parts[5]: full move counter, probably never implementing\n\n\treturn\n}\n\n// generates board from fen format: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR\nfunc fenBoard(boardStr string) (Board, bool) {\n\trankStrs := strings.Split(boardStr, \"/\")\n\tif len(rankStrs) != 8 {\n\t\treturn Board{}, false\n\t}\n\tvar b Board\n\tfor idx, pieces := range rankStrs {\n\t\trank := (7 - Square(idx)) \u003c\u003c 3\n\t\tfile := Square(0)\n\t\tfor _, ch := range pieces {\n\t\t\tif ch \u003e= '1' \u0026\u0026 ch \u003c= '8' {\n\t\t\t\tdelta := byte(ch) - '0'\n\t\t\t\tfile += Square(delta)\n\t\t\t\tif file \u003e 8 {\n\t\t\t\t\treturn b, false\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpiece := p[byte(ch)]\n\t\t\tif piece == PieceEmpty || file \u003e= 8 {\n\t\t\t\treturn b, false\n\t\t\t}\n\t\t\tb[rank|file] = piece\n\t\t\tfile++\n\t\t}\n\t\tif file != 8 {\n\t\t\treturn b, false\n\t\t}\n\t}\n\treturn b, true\n}\n\nfunc castleRightsToPositionFlags(cr string) (pf PositionFlags) {\n\tpf = NoCastleWQ | NoCastleWK | NoCastleBQ | NoCastleBK\n\tif cr == \"-\" {\n\t\treturn\n\t}\n\tfor _, ch := range cr {\n\t\tswitch ch {\n\t\tcase 'K':\n\t\t\tpf \u0026^= NoCastleWK\n\t\tcase 'Q':\n\t\t\tpf \u0026^= NoCastleWQ\n\t\tcase 'k':\n\t\t\tpf \u0026^= NoCastleBK\n\t\tcase 'q':\n\t\t\tpf \u0026^= NoCastleBQ\n\t\t}\n\t}\n\treturn\n}\n\nfunc parseEnPassant(strpos string) (PositionFlags, error) {\n\teppos := SquareFromString(strpos)\n\tif eppos == SquareInvalid {\n\t\treturn 0, fmt.Errorf(\"invalid pos: %s\", eppos)\n\t}\n\trow, col := eppos.Split()\n\tif row != 5 \u0026\u0026 row != 2 {\n\t\treturn 0, fmt.Errorf(\"invalid en passant pos: %s\", eppos)\n\t}\n\treturn EnPassant | PositionFlags(col), nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "DPq8wbin5ZBAW1ptG4bP9F1zr/pd7luwW0vP5P2q0dooscG+t1eQOcq6zLvjwLamUEzC2qlvBGay3AXfecf4DA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "glicko2", + "path": "gno.land/p/morgan/chess/glicko2", + "files": [ + { + "name": "glicko2.gno", + "body": "package glicko2\n\nimport (\n\t\"math\"\n\t\"std\"\n)\n\n// http://www.glicko.net/glicko/glicko2.pdf\n\nconst (\n\t// Step 1.\n\t// Determine a rating and RD for each player at the onset of the rating period. The\n\t// system constant, τ , which constrains the change in volatility over time, needs to be\n\t// set prior to application of the system. Reasonable choices are between 0.3 and 1.2,\n\t// though the system should be tested to decide which value results in greatest predictive\n\t// accuracy. Smaller values of τ prevent the volatility measures from changing by large\n\t// amounts, which in turn prevent enormous changes in ratings based on very improbable\n\t// results. If the application of Glicko-2 is expected to involve extremely improbable\n\t// collections of game outcomes, then τ should be set to a small value, even as small as,\n\t// say, τ = 0.2.\n\tGlickoTau = 0.5\n\n\tGlickoInitialRating = 1500\n\tGlickoInitialRD = 350\n\tGlickoInitialVolatility = 0.06\n\n\tglickoScaleFactor = 173.7178\n)\n\ntype PlayerRating struct {\n\tID std.Address\n\n\tRating float64\n\tRatingDeviation float64\n\tRatingVolatility float64\n\n\t// working values, these are referred to as μ, φ and σ in the paper.\n\twr, wrd, wrv float64\n}\n\nfunc NewPlayerRating(addr std.Address) *PlayerRating {\n\treturn \u0026PlayerRating{\n\t\tID: addr,\n\t\tRating: GlickoInitialRating,\n\t\tRatingDeviation: GlickoInitialRD,\n\t\tRatingVolatility: GlickoInitialVolatility,\n\t}\n}\n\n// RatingScore is the outcome of a game between two players.\ntype RatingScore struct {\n\tWhite, Black std.Address\n\tScore float64 // 0 = black win, 0.5 = draw, 1 = white win\n}\n\nfunc getRatingScore(scores []RatingScore, player, opponent std.Address) float64 {\n\tfor _, score := range scores {\n\t\tif score.White == player \u0026\u0026 score.Black == opponent {\n\t\t\treturn score.Score\n\t\t}\n\t\tif score.Black == player \u0026\u0026 score.White == opponent {\n\t\t\treturn 1 - score.Score\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc UpdateRatings(source []*PlayerRating, scores []RatingScore) {\n\t// step 2: assign working wr/wrd/wrv\n\tfor _, player := range source {\n\t\tplayer.wr = (player.Rating - GlickoInitialRating) / glickoScaleFactor\n\t\tplayer.wrd = player.RatingDeviation / glickoScaleFactor\n\t\tplayer.wrv = player.RatingVolatility\n\t}\n\n\tfor _, player := range source {\n\t\t// step 3: compute the quantity v. This is the estimated variance of the team’s/player’s\n\t\t// rating based only on game outcomes\n\t\t// step 4: compute the quantity ∆, the estimated improvement in rating\n\t\tv, delta, competed := glickoVDelta(player, source, scores)\n\n\t\tnewRDPart := math.Sqrt(player.wrd*player.wrd + player.wrv*player.wrv)\n\t\tif !competed {\n\t\t\t// fast path for players who have not competed:\n\t\t\t// update only rating deviation\n\t\t\tplayer.RatingDeviation = newRDPart\n\t\t\tplayer.wr, player.wrd, player.wrv = 0, 0, 0\n\t\t\tcontinue\n\t\t}\n\n\t\t// step 5: determine the new value, σ′, of the volatility.\n\t\tplayer.wrv = glickoVolatility(player, delta, v)\n\t\t// step 6 and 7\n\t\tplayer.wrd = 1 / math.Sqrt(1/v+1/(newRDPart*newRDPart))\n\t\tplayer.wr = player.wr + player.wrd*player.wrd*(delta/v)\n\n\t\t// step 8\n\t\tplayer.Rating = player.wr*glickoScaleFactor + 1500\n\t\tplayer.RatingDeviation = player.wrd * glickoScaleFactor\n\t\tplayer.RatingVolatility = player.wrv\n\t\tplayer.wrv, player.wrd, player.wr = 0, 0, 0\n\t}\n}\n\nfunc glickoVDelta(player *PlayerRating, source []*PlayerRating, scores []RatingScore) (v, delta float64, competed bool) {\n\tvar deltaPartial float64\n\tfor _, opponent := range source {\n\t\tif opponent.ID == player.ID {\n\t\t\tcontinue\n\t\t}\n\t\tscore := getRatingScore(scores, player.ID, opponent.ID)\n\t\tif score == -1 { // not found\n\t\t\tcontinue\n\t\t}\n\t\tcompeted = true\n\n\t\t// Step 3\n\t\tgTheta := 1 / math.Sqrt(1+(3*opponent.wrd*opponent.wrd)/(math.Pi*math.Pi))\n\t\teMu := 1 / (1 + math.Exp(-gTheta*(player.wr-opponent.wr)))\n\t\tv += gTheta * gTheta * eMu * (1 - eMu)\n\n\t\t// step 4\n\t\tdeltaPartial += gTheta * (score - eMu)\n\t}\n\tv = 1 / v\n\treturn v, v * deltaPartial, competed\n}\n\nconst glickoVolatilityEps = 0.000001\n\n// Step 5\nfunc glickoVolatility(player *PlayerRating, delta, v float64) float64 {\n\taInit := math.Log(player.wrv * player.wrv)\n\tA := aInit\n\tvar B float64\n\trdp2 := player.wrd * player.wrd\n\tif delta*delta \u003e rdp2+v {\n\t\tB = math.Log(delta*delta - rdp2 - v)\n\t} else {\n\t\tk := float64(1)\n\t\tfor ; glickoVolatilityF(A-k*GlickoTau, aInit, rdp2, delta, v) \u003c 0; k++ {\n\t\t}\n\t\tB = A - k*GlickoTau\n\t}\n\tfA, fB := glickoVolatilityF(A, aInit, rdp2, delta, v), glickoVolatilityF(B, aInit, rdp2, delta, v)\n\tfor math.Abs(B-A) \u003e glickoVolatilityEps {\n\t\tC := A + ((A-B)*fA)/(fB-fA)\n\t\tfC := glickoVolatilityF(C, aInit, rdp2, delta, v)\n\t\tif fC*fB \u003c= 0 {\n\t\t\tA, fA = B, fB\n\t\t} else {\n\t\t\tfA = fA / 2\n\t\t}\n\t\tB, fB = C, fC\n\t}\n\treturn math.Exp(A / 2)\n}\n\n// rdp2: player.rd, power 2\nfunc glickoVolatilityF(x, a, rdp2, delta, v float64) float64 {\n\texpX := math.Exp(x)\n\treturn (expX*(delta*delta-rdp2-v-expX))/(2*pow2(rdp2+v+expX)) -\n\t\t(x-a)/(GlickoTau*GlickoTau)\n}\n\nfunc pow2(f float64) float64 { return f * f }\n" + }, + { + "name": "glicko2_test.gno", + "body": "package glicko2\n\nimport (\n\t\"testing\"\n)\n\nfunc TestExampleCalculations(t *testing.T) {\n\t// These are from the example in prof. Glickman's paper.\n\t// At the end, t.Log should print for the first player updated values\n\t// for Rating, RatingDeviation and RatingVolatility matching those in the\n\t// examples.\n\tratings := []*PlayerRating{\n\t\t{ID: \"1\", Rating: 1500, RatingDeviation: 200, RatingVolatility: 0.06},\n\t\t{ID: \"2\", Rating: 1400, RatingDeviation: 30, RatingVolatility: 0.06},\n\t\t{ID: \"3\", Rating: 1550, RatingDeviation: 100, RatingVolatility: 0.06},\n\t\t{ID: \"4\", Rating: 1700, RatingDeviation: 300, RatingVolatility: 0.06},\n\t}\n\tscores := []RatingScore{\n\t\t{White: \"1\", Black: \"2\", Score: 1},\n\t\t{White: \"1\", Black: \"3\", Score: 0},\n\t\t{White: \"1\", Black: \"4\", Score: 0},\n\t}\n\tUpdateRatings(ratings, scores)\n\tr := ratings[0]\n\tt.Logf(\"%.4f (± %.4f, volatility: %.4f); working values: %.2f / %.2f / %.2f\\n\",\n\t\tr.Rating, r.RatingDeviation, r.RatingVolatility,\n\t\tr.wr, r.wrd, r.wrv,\n\t)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "LhCKa/yL4h1B+KDXY0doIe8iDa8Gizp+qkw2CwmOo0pc1B+XLbvkM4q3TjjmZLCgHd58s9un3+s9GsKKTV5iBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "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 := std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n//\t addr2 := std.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 std.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 (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\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 std.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 std.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 std.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 std.Address) bool) {\n\ts.tree.IterateByOffset(offset, count, func(key string, _ any) bool {\n\t\treturn cb(std.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 std.Address) bool) {\n\ts.tree.ReverseIterateByOffset(offset, count, func(key string, _ any) bool {\n\t\treturn cb(std.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\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestSet(t *testing.T) {\n\taddr1 := std.Address(\"addr1\")\n\taddr2 := std.Address(\"addr2\")\n\taddr3 := std.Address(\"addr3\")\n\n\ttests := []struct {\n\t\tname string\n\t\tactions func(s *Set)\n\t\tsize int\n\t\thas map[std.Address]bool\n\t\taddrs []std.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[std.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[std.Address]bool{\n\t\t\t\taddr1: true,\n\t\t\t\taddr2: false,\n\t\t\t},\n\t\t\taddrs: []std.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[std.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: []std.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[std.Address]bool{\n\t\t\t\taddr1: false,\n\t\t\t\taddr2: true,\n\t\t\t},\n\t\t\taddrs: []std.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[std.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 := []std.Address{}\n\t\t\t\tset.IterateByOffset(0, 10, func(addr std.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 []std.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: []std.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: []std.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: []std.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 std.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 std.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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "L1Kvyy5P0IAub2ULbZyKHitpdeLwOCX/1qJt+JyAVhwRkiIKFPiBcDpEuBeUWbNQ9X9U0beuNRMPnVbrMSjsDw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "once", + "path": "gno.land/p/moul/once", + "files": [ + { + "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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "T+VMsiIRJvAJyPHWUY4uY3jd6d0RQJ7sX2TVFVbJuF1G72HUo6C5I5JS/CZ5SzT354MZ764PBGTw2FYZB/GXBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "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 auth = authz.New()\n//\n//\t// Create functions that require authorization\n//\tfunc UpdateConfig(newValue string) error {\n//\t return auth.Do(\"update_config\", func() error {\n//\t config = newValue\n//\t return nil\n//\t })\n//\t}\n//\n// See example_test.gno for more usage examples.\npackage authz\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/rotree\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/p/moul/once\"\n)\n\n// Authorizer is the main wrapper object that handles authority management\ntype Authorizer struct {\n\tcurrent Authority\n}\n\n// Authority represents an entity that can authorize privileged actions\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(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// New creates a new Authorizer with the current realm's address as authority\nfunc New() *Authorizer {\n\treturn \u0026Authorizer{\n\t\tcurrent: NewMemberAuthority(std.PreviousRealm().Address()),\n\t}\n}\n\n// NewWithAuthority creates a new Authorizer with a specific authority\nfunc NewWithAuthority(authority Authority) *Authorizer {\n\treturn \u0026Authorizer{\n\t\tcurrent: authority,\n\t}\n}\n\n// Current returns the current authority implementation\nfunc (a *Authorizer) Current() Authority {\n\treturn a.current\n}\n\n// Transfer changes the current authority after validation\nfunc (a *Authorizer) Transfer(newAuthority Authority) error {\n\t// Ask current authority to validate the transfer\n\treturn a.current.Authorize(\"transfer_authority\", func() error {\n\t\ta.current = newAuthority\n\t\treturn nil\n\t})\n}\n\n// Do executes a privileged action through the current authority\nfunc (a *Authorizer) Do(title string, action PrivilegedAction, args ...any) error {\n\treturn a.current.Authorize(title, action, args...)\n}\n\n// String returns a string representation of the current authority\nfunc (a *Authorizer) String() string {\n\treturn a.current.String()\n}\n\n// MemberAuthority is the default implementation using addrset for member management\ntype MemberAuthority struct {\n\tmembers addrset.Set\n}\n\nfunc NewMemberAuthority(members ...std.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(title string, action PrivilegedAction, args ...any) error {\n\tcaller := std.PreviousRealm().Address()\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\treturn ufmt.Sprintf(\"member_authority[size=%d]\", a.members.Size())\n}\n\n// AddMember adds a new member to the authority\nfunc (a *MemberAuthority) AddMember(addr std.Address) error {\n\treturn a.Authorize(\"add_member\", func() error {\n\t\ta.members.Add(addr)\n\t\treturn nil\n\t})\n}\n\n// RemoveMember removes a member from the authority\nfunc (a *MemberAuthority) RemoveMember(addr std.Address) error {\n\treturn a.Authorize(\"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 std.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 std.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: std.DerivePkgAddr(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 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: std.DerivePkgAddr(path),\n\t\tcontractHandler: handler,\n\t\tproposer: proposer,\n\t}\n}\n\nfunc (a *ContractAuthority) Authorize(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\tcaller := std.PreviousRealm().Address()\n\t\tif caller != 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(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 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(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(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\"errors\"\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestNew(t *testing.T) {\n\ttestAddr := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(testAddr)\n\n\tauth := New()\n\n\t// Check that the current authority is a MemberAuthority\n\tmemberAuth, ok := auth.Current().(*MemberAuthority)\n\tuassert.True(t, ok, \"expected MemberAuthority\")\n\n\t// Check that the caller is a member\n\tuassert.True(t, memberAuth.Has(testAddr), \"caller should be a member\")\n\n\t// Check string representation\n\texpected := ufmt.Sprintf(\"member_authority[size=%d]\", 1)\n\tuassert.Equal(t, expected, auth.String())\n}\n\nfunc TestNewWithAuthority(t *testing.T) {\n\ttestAddr := testutils.TestAddress(\"alice\")\n\tmemberAuth := NewMemberAuthority(testAddr)\n\n\tauth := NewWithAuthority(memberAuth)\n\n\t// Check that the current authority is the one we provided\n\tuassert.True(t, auth.Current() == memberAuth, \"expected provided authority\")\n}\n\nfunc TestAuthorizerAuthorize(t *testing.T) {\n\ttestAddr := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(testAddr)\n\n\tauth := New()\n\n\t// Test successful action with args\n\texecuted := false\n\targs := []any{\"test_arg\", 123}\n\terr := auth.Do(\"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.SetOriginCaller(testutils.TestAddress(\"bob\"))\n\n\texecuted = false\n\terr = auth.Do(\"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.SetOriginCaller(testAddr)\n\texpectedErr := errors.New(\"test error\")\n\n\terr = auth.Do(\"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\ttestAddr := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(testAddr)\n\n\tauth := New()\n\n\t// Test transfer to new member authority\n\tnewAddr := testutils.TestAddress(\"bob\")\n\tnewAuth := NewMemberAuthority(newAddr)\n\n\terr := auth.Transfer(newAuth)\n\tuassert.True(t, err == nil, \"expected no error\")\n\tuassert.True(t, auth.Current() == newAuth, \"expected new authority\")\n\n\t// Test unauthorized transfer\n\ttesting.SetOriginCaller(testutils.TestAddress(\"carol\"))\n\n\terr = auth.Transfer(NewMemberAuthority(testAddr))\n\tuassert.True(t, err != nil, \"expected error\")\n\n\t// Test transfer to contract authority\n\ttesting.SetOriginCaller(newAddr)\n\n\tcontractAuth := NewContractAuthority(\"gno.land/r/test\", func(title string, action PrivilegedAction) error {\n\t\treturn action()\n\t})\n\n\terr = auth.Transfer(contractAuth)\n\tuassert.True(t, err == nil, \"expected no error\")\n\tuassert.True(t, auth.Current() == contractAuth, \"expected contract authority\")\n}\n\nfunc TestAuthorizerTransferChain(t *testing.T) {\n\ttestAddr := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(testAddr)\n\n\t// Create a chain of transfers\n\tauth := New()\n\n\t// First transfer to a new member authority\n\tnewAddr := testutils.TestAddress(\"bob\")\n\tmemberAuth := NewMemberAuthority(newAddr)\n\n\terr := auth.Transfer(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\n\ttesting.SetOriginCaller(newAddr)\n\terr = auth.Transfer(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\n\ttesting.SetOriginCaller(std.DerivePkgAddr(\"gno.land/r/test\"))\n\terr = auth.Transfer(autoAuth)\n\tuassert.True(t, err == nil, \"unexpected error in final transfer\")\n\tuassert.True(t, auth.Current() == autoAuth, \"expected auto-accept authority\")\n}\n\nfunc TestAuthorizerWithDroppedAuthority(t *testing.T) {\n\ttestAddr := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(testAddr)\n\n\tauth := New()\n\n\t// Transfer to dropped authority\n\terr := auth.Transfer(NewDroppedAuthority())\n\tuassert.True(t, err == nil, \"expected no error\")\n\n\t// Try to execute action\n\terr = auth.Do(\"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(NewMemberAuthority(testAddr))\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\ttesting.SetOriginCaller(std.DerivePkgAddr(\"gno.land/r/test\"))\n\n\ttestArgs := []any{\"proposal_id\", 42, \"metadata\", map[string]string{\"key\": \"value\"}}\n\terr := contractAuth.Authorize(\"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\ttesting.SetOriginCaller(std.DerivePkgAddr(\"gno.land/r/test\"))\n\n\ttestArgs := []any{\"proposal_id\", 42, \"metadata\", map[string]string{\"key\": \"value\"}}\n\n\terr := contractAuth.Authorize(\"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(\"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\ttestAddr := testutils.TestAddress(\"alice\")\n\tmemberAuth := NewMemberAuthority(testAddr)\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.SetOriginCaller(std.DerivePkgAddr(\"gno.land/r/test\"))\n\t\treturn action()\n\t}, memberAuth)\n\n\t// Test authorized member\n\ttesting.SetOriginCaller(testAddr)\n\ttestArgs := []any{\"proposal_metadata\", \"test value\"}\n\terr := contractAuth.Authorize(\"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\ttesting.SetOriginCaller(testutils.TestAddress(\"bob\"))\n\terr = contractAuth.Authorize(\"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\tauth := NewAutoAcceptAuthority()\n\n\t// Test that any action is authorized\n\texecuted := false\n\terr := auth.Authorize(\"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\ttesting.SetOriginCaller(testutils.TestAddress(\"random\"))\n\texecuted = false\n\terr = auth.Authorize(\"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\n\t// Test that any action is authorized with args\n\texecuted := false\n\ttestArgs := []any{\"arg1\", 42, \"arg3\"}\n\terr := auth.Authorize(\"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 []std.Address{alice, bob} {\n\t\ttesting.SetOriginCaller(member)\n\t\terr := auth.Authorize(\"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\ttesting.SetOriginCaller(carol)\n\terr := auth.Authorize(\"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[std.Address]bool)\n\ttree.Iterate(\"\", \"\", func(key string, _ any) bool {\n\t\tfound[std.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 := New()\n\n\t// Current should never be nil after initialization\n\tuassert.True(t, auth.Current() != nil, \"current authority should not be nil\")\n\n\t// Current should not be nil after transfer\n\terr := auth.Transfer(NewAutoAcceptAuthority())\n\tuassert.True(t, err == nil, \"transfer should succeed\")\n\tuassert.True(t, auth.Current() != nil, \"current authority should not be nil after transfer\")\n}\n\nfunc TestAuthorizerString(t *testing.T) {\n\tauth := New()\n\n\t// Test initial string representation\n\tstr := auth.String()\n\tuassert.True(t, str != \"\", \"string representation should not be empty\")\n\n\t// Test string after transfer\n\tautoAuth := NewAutoAcceptAuthority()\n\terr := auth.Transfer(autoAuth)\n\tuassert.True(t, err == nil, \"transfer should succeed\")\n\n\tnewStr := auth.String()\n\tuassert.True(t, newStr != \"\", \"string representation should not be empty\")\n\tuassert.True(t, newStr != str, \"string should change after transfer\")\n}\n\nfunc TestContractAuthorityValidation(t *testing.T) {\n\t// Test empty path\n\tauth := NewContractAuthority(\"\", nil)\n\ttesting.SetOriginCaller(std.DerivePkgAddr(\"\"))\n\terr := auth.Authorize(\"test\", func() error {\n\t\treturn nil\n\t})\n\tuassert.True(t, err != nil, \"empty path authority should fail to authorize\")\n\n\t// Test nil handler\n\tauth = NewContractAuthority(\"gno.land/r/test\", nil)\n\ttesting.SetOriginCaller(std.DerivePkgAddr(\"gno.land/r/test\"))\n\terr = auth.Authorize(\"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\ttesting.SetOriginCaller(std.DerivePkgAddr(\"gno.land/r/test\"))\n\terr = contractAuth.Authorize(\"test\", func() error {\n\t\treturn nil\n\t})\n\tuassert.True(t, err == nil, \"valid contract authority should authorize successfully\")\n}\n" + }, + { + "name": "example_test.gno", + "body": "package authz\n\nimport (\n\t\"std\"\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 := New()\n\n\t// Use the authority to perform a privileged action\n\tauth.Do(\"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\tauth := New()\n\n\t// Add a new member to the authority\n\tmemberAuth := auth.Current().(*MemberAuthority)\n\tmemberAuth.AddMember(std.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.Do(\"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\tstd.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"), // admin1\n\t\tstd.Address(\"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.Do(\"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\tauth := New()\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(daoAuthority)\n}\n\n// Mock handler for examples\nfunc mockDAOHandler(title string, action PrivilegedAction) error {\n\treturn action()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "AGA82vwB2HI1dHxiWPBQRFFKdqqVnEsBfSPhYLEdDa7Maojpoanp7tyCMbcMguhmdLlDZspwVpXDnAI8p/n+CA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "cow", + "path": "gno.land/p/moul/cow", + "files": [ + { + "name": "node.gno", + "body": "package cow\n\n//----------------------------------------\n// Node\n\n// Node represents a node in an AVL tree.\ntype Node struct {\n\tkey string // key is the unique identifier for the node.\n\tvalue any // value is the data stored in the node.\n\theight int8 // height is the height of the node in the tree.\n\tsize int // size is the number of nodes in the subtree rooted at this node.\n\tleftNode *Node // leftNode is the left child of the node.\n\trightNode *Node // rightNode is the right child of the node.\n}\n\n// NewNode creates a new node with the given key and value.\nfunc NewNode(key string, value any) *Node {\n\treturn \u0026Node{\n\t\tkey: key,\n\t\tvalue: value,\n\t\theight: 0,\n\t\tsize: 1,\n\t}\n}\n\n// Size returns the size of the subtree rooted at the node.\nfunc (node *Node) Size() int {\n\tif node == nil {\n\t\treturn 0\n\t}\n\treturn node.size\n}\n\n// IsLeaf checks if the node is a leaf node (has no children).\nfunc (node *Node) IsLeaf() bool {\n\treturn node.height == 0\n}\n\n// Key returns the key of the node.\nfunc (node *Node) Key() string {\n\treturn node.key\n}\n\n// Value returns the value of the node.\nfunc (node *Node) Value() any {\n\treturn node.value\n}\n\n// _copy creates a copy of the node (excluding value).\nfunc (node *Node) _copy() *Node {\n\tif node.height == 0 {\n\t\tpanic(\"Why are you copying a value node?\")\n\t}\n\treturn \u0026Node{\n\t\tkey: node.key,\n\t\theight: node.height,\n\t\tsize: node.size,\n\t\tleftNode: node.leftNode,\n\t\trightNode: node.rightNode,\n\t}\n}\n\n// Has checks if a node with the given key exists in the subtree rooted at the node.\nfunc (node *Node) Has(key string) (has bool) {\n\tif node == nil {\n\t\treturn false\n\t}\n\tif node.key == key {\n\t\treturn true\n\t}\n\tif node.height == 0 {\n\t\treturn false\n\t}\n\tif key \u003c node.key {\n\t\treturn node.getLeftNode().Has(key)\n\t}\n\treturn node.getRightNode().Has(key)\n}\n\n// Get searches for a node with the given key in the subtree rooted at the node\n// and returns its index, value, and whether it exists.\nfunc (node *Node) Get(key string) (index int, value any, exists bool) {\n\tif node == nil {\n\t\treturn 0, nil, false\n\t}\n\n\tif node.height == 0 {\n\t\tif node.key == key {\n\t\t\treturn 0, node.value, true\n\t\t}\n\t\tif node.key \u003c key {\n\t\t\treturn 1, nil, false\n\t\t}\n\t\treturn 0, nil, false\n\t}\n\n\tif key \u003c node.key {\n\t\treturn node.getLeftNode().Get(key)\n\t}\n\n\trightNode := node.getRightNode()\n\tindex, value, exists = rightNode.Get(key)\n\tindex += node.size - rightNode.size\n\treturn index, value, exists\n}\n\n// GetByIndex retrieves the key-value pair of the node at the given index\n// in the subtree rooted at the node.\nfunc (node *Node) GetByIndex(index int) (key string, value any) {\n\tif node.height == 0 {\n\t\tif index == 0 {\n\t\t\treturn node.key, node.value\n\t\t}\n\t\tpanic(\"GetByIndex asked for invalid index\")\n\t}\n\t// TODO: could improve this by storing the sizes\n\tleftNode := node.getLeftNode()\n\tif index \u003c leftNode.size {\n\t\treturn leftNode.GetByIndex(index)\n\t}\n\treturn node.getRightNode().GetByIndex(index - leftNode.size)\n}\n\n// Set inserts a new node with the given key-value pair into the subtree rooted at the node,\n// and returns the new root of the subtree and whether an existing node was updated.\n//\n// XXX consider a better way to do this... perhaps split Node from Node.\nfunc (node *Node) Set(key string, value any) (newSelf *Node, updated bool) {\n\tif node == nil {\n\t\treturn NewNode(key, value), false\n\t}\n\n\t// Always create a new node for leaf nodes\n\tif node.height == 0 {\n\t\treturn node.setLeaf(key, value)\n\t}\n\n\t// Copy the node before modifying\n\tnewNode := node._copy()\n\tif key \u003c node.key {\n\t\tnewNode.leftNode, updated = node.getLeftNode().Set(key, value)\n\t} else {\n\t\tnewNode.rightNode, updated = node.getRightNode().Set(key, value)\n\t}\n\n\tif !updated {\n\t\tnewNode.calcHeightAndSize()\n\t\treturn newNode.balance(), updated\n\t}\n\n\treturn newNode, updated\n}\n\n// setLeaf inserts a new leaf node with the given key-value pair into the subtree rooted at the node,\n// and returns the new root of the subtree and whether an existing node was updated.\nfunc (node *Node) setLeaf(key string, value any) (newSelf *Node, updated bool) {\n\tif key == node.key {\n\t\treturn NewNode(key, value), true\n\t}\n\n\tif key \u003c node.key {\n\t\treturn \u0026Node{\n\t\t\tkey: node.key,\n\t\t\theight: 1,\n\t\t\tsize: 2,\n\t\t\tleftNode: NewNode(key, value),\n\t\t\trightNode: node,\n\t\t}, false\n\t}\n\n\treturn \u0026Node{\n\t\tkey: key,\n\t\theight: 1,\n\t\tsize: 2,\n\t\tleftNode: node,\n\t\trightNode: NewNode(key, value),\n\t}, false\n}\n\n// Remove deletes the node with the given key from the subtree rooted at the node.\n// returns the new root of the subtree, the new leftmost leaf key (if changed),\n// the removed value and the removal was successful.\nfunc (node *Node) Remove(key string) (\n\tnewNode *Node, newKey string, value any, removed bool,\n) {\n\tif node == nil {\n\t\treturn nil, \"\", nil, false\n\t}\n\tif node.height == 0 {\n\t\tif key == node.key {\n\t\t\treturn nil, \"\", node.value, true\n\t\t}\n\t\treturn node, \"\", nil, false\n\t}\n\tif key \u003c node.key {\n\t\tvar newLeftNode *Node\n\t\tnewLeftNode, newKey, value, removed = node.getLeftNode().Remove(key)\n\t\tif !removed {\n\t\t\treturn node, \"\", value, false\n\t\t}\n\t\tif newLeftNode == nil { // left node held value, was removed\n\t\t\treturn node.rightNode, node.key, value, true\n\t\t}\n\t\tnode = node._copy()\n\t\tnode.leftNode = newLeftNode\n\t\tnode.calcHeightAndSize()\n\t\tnode = node.balance()\n\t\treturn node, newKey, value, true\n\t}\n\n\tvar newRightNode *Node\n\tnewRightNode, newKey, value, removed = node.getRightNode().Remove(key)\n\tif !removed {\n\t\treturn node, \"\", value, false\n\t}\n\tif newRightNode == nil { // right node held value, was removed\n\t\treturn node.leftNode, \"\", value, true\n\t}\n\tnode = node._copy()\n\tnode.rightNode = newRightNode\n\tif newKey != \"\" {\n\t\tnode.key = newKey\n\t}\n\tnode.calcHeightAndSize()\n\tnode = node.balance()\n\treturn node, \"\", value, true\n}\n\n// getLeftNode returns the left child of the node.\nfunc (node *Node) getLeftNode() *Node {\n\treturn node.leftNode\n}\n\n// getRightNode returns the right child of the node.\nfunc (node *Node) getRightNode() *Node {\n\treturn node.rightNode\n}\n\n// rotateRight performs a right rotation on the node and returns the new root.\n// NOTE: overwrites node\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) rotateRight() *Node {\n\tnode = node._copy()\n\tl := node.getLeftNode()\n\t_l := l._copy()\n\n\t_lrCached := _l.rightNode\n\t_l.rightNode = node\n\tnode.leftNode = _lrCached\n\n\tnode.calcHeightAndSize()\n\t_l.calcHeightAndSize()\n\n\treturn _l\n}\n\n// rotateLeft performs a left rotation on the node and returns the new root.\n// NOTE: overwrites node\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) rotateLeft() *Node {\n\tnode = node._copy()\n\tr := node.getRightNode()\n\t_r := r._copy()\n\n\t_rlCached := _r.leftNode\n\t_r.leftNode = node\n\tnode.rightNode = _rlCached\n\n\tnode.calcHeightAndSize()\n\t_r.calcHeightAndSize()\n\n\treturn _r\n}\n\n// calcHeightAndSize updates the height and size of the node based on its children.\n// NOTE: mutates height and size\nfunc (node *Node) calcHeightAndSize() {\n\tnode.height = maxInt8(node.getLeftNode().height, node.getRightNode().height) + 1\n\tnode.size = node.getLeftNode().size + node.getRightNode().size\n}\n\n// calcBalance calculates the balance factor of the node.\nfunc (node *Node) calcBalance() int {\n\treturn int(node.getLeftNode().height) - int(node.getRightNode().height)\n}\n\n// balance balances the subtree rooted at the node and returns the new root.\n// NOTE: assumes that node can be modified\n// TODO: optimize balance \u0026 rotate\nfunc (node *Node) balance() (newSelf *Node) {\n\tbalance := node.calcBalance()\n\tif balance \u003e= -1 {\n\t\treturn node\n\t}\n\tif balance \u003e 1 {\n\t\tif node.getLeftNode().calcBalance() \u003e= 0 {\n\t\t\t// Left Left Case\n\t\t\treturn node.rotateRight()\n\t\t}\n\t\t// Left Right Case\n\t\tleft := node.getLeftNode()\n\t\tnode.leftNode = left.rotateLeft()\n\t\treturn node.rotateRight()\n\t}\n\n\tif node.getRightNode().calcBalance() \u003c= 0 {\n\t\t// Right Right Case\n\t\treturn node.rotateLeft()\n\t}\n\n\t// Right Left Case\n\tright := node.getRightNode()\n\tnode.rightNode = right.rotateRight()\n\treturn node.rotateLeft()\n}\n\n// Shortcut for TraverseInRange.\nfunc (node *Node) Iterate(start, end string, cb func(*Node) bool) bool {\n\treturn node.TraverseInRange(start, end, true, true, cb)\n}\n\n// Shortcut for TraverseInRange.\nfunc (node *Node) ReverseIterate(start, end string, cb func(*Node) bool) bool {\n\treturn node.TraverseInRange(start, end, false, true, cb)\n}\n\n// TraverseInRange traverses all nodes, including inner nodes.\n// Start is inclusive and end is exclusive when ascending,\n// Start and end are inclusive when descending.\n// Empty start and empty end denote no start and no end.\n// If leavesOnly is true, only visit leaf nodes.\n// NOTE: To simulate an exclusive reverse traversal,\n// just append 0x00 to start.\nfunc (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\tif node == nil {\n\t\treturn false\n\t}\n\tafterStart := (start == \"\" || start \u003c node.key)\n\tstartOrAfter := (start == \"\" || start \u003c= node.key)\n\tbeforeEnd := false\n\tif ascending {\n\t\tbeforeEnd = (end == \"\" || node.key \u003c end)\n\t} else {\n\t\tbeforeEnd = (end == \"\" || node.key \u003c= end)\n\t}\n\n\t// Run callback per inner/leaf node.\n\tstop := false\n\tif (!node.IsLeaf() \u0026\u0026 !leavesOnly) ||\n\t\t(node.IsLeaf() \u0026\u0026 startOrAfter \u0026\u0026 beforeEnd) {\n\t\tstop = cb(node)\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t}\n\tif node.IsLeaf() {\n\t\treturn stop\n\t}\n\n\tif ascending {\n\t\t// check lower nodes, then higher\n\t\tif afterStart {\n\t\t\tstop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t\tif beforeEnd {\n\t\t\tstop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t} else {\n\t\t// check the higher nodes first\n\t\tif beforeEnd {\n\t\t\tstop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t\tif stop {\n\t\t\treturn stop\n\t\t}\n\t\tif afterStart {\n\t\t\tstop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb)\n\t\t}\n\t}\n\n\treturn stop\n}\n\n// TraverseByOffset traverses all nodes, including inner nodes.\n// A limit of math.MaxInt means no limit.\nfunc (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\tif node == nil {\n\t\treturn false\n\t}\n\n\t// fast paths. these happen only if TraverseByOffset is called directly on a leaf.\n\tif limit \u003c= 0 || offset \u003e= node.size {\n\t\treturn false\n\t}\n\tif node.IsLeaf() {\n\t\tif offset \u003e 0 {\n\t\t\treturn false\n\t\t}\n\t\treturn cb(node)\n\t}\n\n\t// go to the actual recursive function.\n\treturn node.traverseByOffset(offset, limit, descending, leavesOnly, cb)\n}\n\n// TraverseByOffset traverses the subtree rooted at the node by offset and limit,\n// in either ascending or descending order, and applies the callback function to each traversed node.\n// If leavesOnly is true, only leaf nodes are visited.\nfunc (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool {\n\t// caller guarantees: offset \u003c node.size; limit \u003e 0.\n\tif !leavesOnly {\n\t\tif cb(node) {\n\t\t\treturn true\n\t\t}\n\t}\n\tfirst, second := node.getLeftNode(), node.getRightNode()\n\tif descending {\n\t\tfirst, second = second, first\n\t}\n\tif first.IsLeaf() {\n\t\t// either run or skip, based on offset\n\t\tif offset \u003e 0 {\n\t\t\toffset--\n\t\t} else {\n\t\t\tcb(first)\n\t\t\tlimit--\n\t\t\tif limit \u003c= 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// possible cases:\n\t\t// 1 the offset given skips the first node entirely\n\t\t// 2 the offset skips none or part of the first node, but the limit requires some of the second node.\n\t\t// 3 the offset skips none or part of the first node, and the limit stops our search on the first node.\n\t\tif offset \u003e= first.size {\n\t\t\toffset -= first.size // 1\n\t\t} else {\n\t\t\tif first.traverseByOffset(offset, limit, descending, leavesOnly, cb) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// number of leaves which could actually be called from inside\n\t\t\tdelta := first.size - offset\n\t\t\toffset = 0\n\t\t\tif delta \u003e= limit {\n\t\t\t\treturn true // 3\n\t\t\t}\n\t\t\tlimit -= delta // 2\n\t\t}\n\t}\n\n\t// because of the caller guarantees and the way we handle the first node,\n\t// at this point we know that limit \u003e 0 and there must be some values in\n\t// this second node that we include.\n\n\t// =\u003e if the second node is a leaf, it has to be included.\n\tif second.IsLeaf() {\n\t\treturn cb(second)\n\t}\n\t// =\u003e if it is not a leaf, it will still be enough to recursively call this\n\t// function with the updated offset and limit\n\treturn second.traverseByOffset(offset, limit, descending, leavesOnly, cb)\n}\n\n// Only used in testing...\nfunc (node *Node) lmd() *Node {\n\tif node.height == 0 {\n\t\treturn node\n\t}\n\treturn node.getLeftNode().lmd()\n}\n\n// Only used in testing...\nfunc (node *Node) rmd() *Node {\n\tif node.height == 0 {\n\t\treturn node\n\t}\n\treturn node.getRightNode().rmd()\n}\n\nfunc maxInt8(a, b int8) int8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\n// Equal compares two nodes for structural equality.\n// WARNING: This is an expensive operation that recursively traverses the entire tree structure.\n// It should only be used in tests or when absolutely necessary.\nfunc (node *Node) Equal(other *Node) bool {\n\t// Handle nil cases\n\tif node == nil || other == nil {\n\t\treturn node == other\n\t}\n\n\t// Compare node properties\n\tif node.key != other.key ||\n\t\tnode.value != other.value ||\n\t\tnode.height != other.height ||\n\t\tnode.size != other.size {\n\t\treturn false\n\t}\n\n\t// Compare children\n\tleftEqual := (node.leftNode == nil \u0026\u0026 other.leftNode == nil) ||\n\t\t(node.leftNode != nil \u0026\u0026 other.leftNode != nil \u0026\u0026 node.leftNode.Equal(other.leftNode))\n\tif !leftEqual {\n\t\treturn false\n\t}\n\n\trightEqual := (node.rightNode == nil \u0026\u0026 other.rightNode == nil) ||\n\t\t(node.rightNode != nil \u0026\u0026 other.rightNode != nil \u0026\u0026 node.rightNode.Equal(other.rightNode))\n\treturn rightEqual\n}\n" + }, + { + "name": "node_test.gno", + "body": "package cow\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestTraverseByOffset(t *testing.T) {\n\tconst testStrings = `Alfa\nAlfred\nAlpha\nAlphabet\nBeta\nBeth\nBook\nBrowser`\n\ttt := []struct {\n\t\tname string\n\t\tdesc bool\n\t}{\n\t\t{\"ascending\", false},\n\t\t{\"descending\", true},\n\t}\n\n\tfor _, tt := range tt {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tsl := strings.Split(testStrings, \"\\n\")\n\n\t\t\t// sort a first time in the order opposite to how we'll be traversing\n\t\t\t// the tree, to ensure that we are not just iterating through with\n\t\t\t// insertion order.\n\t\t\tsort.Strings(sl)\n\t\t\tif !tt.desc {\n\t\t\t\treverseSlice(sl)\n\t\t\t}\n\n\t\t\tr := NewNode(sl[0], nil)\n\t\t\tfor _, v := range sl[1:] {\n\t\t\t\tr, _ = r.Set(v, nil)\n\t\t\t}\n\n\t\t\t// then sort sl in the order we'll be traversing it, so that we can\n\t\t\t// compare the result with sl.\n\t\t\treverseSlice(sl)\n\n\t\t\tvar result []string\n\t\t\tfor i := 0; i \u003c len(sl); i++ {\n\t\t\t\tr.TraverseByOffset(i, 1, tt.desc, true, func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif !slicesEqual(sl, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", sl, result)\n\t\t\t}\n\n\t\t\tfor l := 2; l \u003c= len(sl); l++ {\n\t\t\t\t// \"slices\"\n\t\t\t\tfor i := 0; i \u003c= len(sl); i++ {\n\t\t\t\t\tmax := i + l\n\t\t\t\t\tif max \u003e len(sl) {\n\t\t\t\t\t\tmax = len(sl)\n\t\t\t\t\t}\n\t\t\t\t\texp := sl[i:max]\n\t\t\t\t\tactual := []string{}\n\n\t\t\t\t\tr.TraverseByOffset(i, l, tt.desc, true, func(tr *Node) bool {\n\t\t\t\t\t\tactual = append(actual, tr.Key())\n\t\t\t\t\t\treturn false\n\t\t\t\t\t})\n\t\t\t\t\tif !slicesEqual(exp, actual) {\n\t\t\t\t\t\tt.Errorf(\"want %v got %v\", exp, actual)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHas(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\thasKey string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t\"has key in non-empty tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"B\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in non-empty tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"has key in single-node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t\"A\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in single-node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t\"B\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"does not have key in empty tree\",\n\t\t\t[]string{},\n\t\t\t\"A\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tresult := tree.Has(tt.hasKey)\n\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGet(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\tgetKey string\n\t\texpectIdx int\n\t\texpectVal any\n\t\texpectExists bool\n\t}{\n\t\t{\n\t\t\t\"get existing key\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"B\",\n\t\t\t1,\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"get non-existent key (smaller)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"@\",\n\t\t\t0,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get non-existent key (larger)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\t5,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get from empty tree\",\n\t\t\t[]string{},\n\t\t\t\"A\",\n\t\t\t0,\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tidx, val, exists := tree.Get(tt.getKey)\n\n\t\t\tif idx != tt.expectIdx {\n\t\t\t\tt.Errorf(\"Expected index %d, got %d\", tt.expectIdx, idx)\n\t\t\t}\n\n\t\t\tif val != tt.expectVal {\n\t\t\t\tt.Errorf(\"Expected value %v, got %v\", tt.expectVal, val)\n\t\t\t}\n\n\t\t\tif exists != tt.expectExists {\n\t\t\t\tt.Errorf(\"Expected exists %t, got %t\", tt.expectExists, exists)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetByIndex(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\tidx int\n\t\texpectKey string\n\t\texpectVal any\n\t\texpectPanic bool\n\t}{\n\t\t{\n\t\t\t\"get by valid index\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t2,\n\t\t\t\"C\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by valid index (smallest)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t0,\n\t\t\t\"A\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by valid index (largest)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t4,\n\t\t\t\"E\",\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"get by invalid index (negative)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t-1,\n\t\t\t\"\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"get by invalid index (out of range)\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t5,\n\t\t\t\"\",\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tif tt.expectPanic {\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r == nil {\n\t\t\t\t\t\tt.Errorf(\"Expected a panic but didn't get one\")\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\tkey, val := tree.GetByIndex(tt.idx)\n\n\t\t\tif !tt.expectPanic {\n\t\t\t\tif key != tt.expectKey {\n\t\t\t\t\tt.Errorf(\"Expected key %s, got %s\", tt.expectKey, key)\n\t\t\t\t}\n\n\t\t\t\tif val != tt.expectVal {\n\t\t\t\t\tt.Errorf(\"Expected value %v, got %v\", tt.expectVal, val)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRemove(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\tremoveKey string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"remove leaf node\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"D\"},\n\t\t\t\"B\",\n\t\t\t[]string{\"A\", \"C\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"remove node with one child\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"D\"},\n\t\t\t\"A\",\n\t\t\t[]string{\"B\", \"C\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"remove node with two children\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"C\",\n\t\t\t[]string{\"A\", \"B\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"remove root node\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"C\",\n\t\t\t[]string{\"A\", \"B\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"remove non-existent key\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t\"F\",\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree, _, _, _ = tree.Remove(tt.removeKey)\n\n\t\t\tresult := make([]string, 0)\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTraverse(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"empty tree\",\n\t\t\t[]string{},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"single node tree\",\n\t\t\t[]string{\"A\"},\n\t\t\t[]string{\"A\"},\n\t\t},\n\t\t{\n\t\t\t\"small tree\",\n\t\t\t[]string{\"C\", \"A\", \"B\", \"E\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"large tree\",\n\t\t\t[]string{\"H\", \"D\", \"L\", \"B\", \"F\", \"J\", \"N\", \"A\", \"C\", \"E\", \"G\", \"I\", \"K\", \"M\", \"O\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\", \"I\", \"J\", \"K\", \"L\", \"M\", \"N\", \"O\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\tt.Run(\"iterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"ReverseIterate\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\ttree.ReverseIterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, len(tt.expected))\n\t\t\t\tcopy(expected, tt.expected)\n\t\t\t\tfor i, j := 0, len(expected)-1; i \u003c j; i, j = i+1, j-1 {\n\t\t\t\t\texpected[i], expected[j] = expected[j], expected[i]\n\t\t\t\t}\n\t\t\t\tif !slicesEqual(expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", expected, result)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tt.Run(\"TraverseInRange\", func(t *testing.T) {\n\t\t\t\tvar result []string\n\t\t\t\tstart, end := \"C\", \"M\"\n\t\t\t\ttree.TraverseInRange(start, end, true, true, func(n *Node) bool {\n\t\t\t\t\tresult = append(result, n.Key())\n\t\t\t\t\treturn false\n\t\t\t\t})\n\t\t\t\texpected := make([]string, 0)\n\t\t\t\tfor _, key := range tt.expected {\n\t\t\t\t\tif key \u003e= start \u0026\u0026 key \u003c end {\n\t\t\t\t\t\texpected = append(expected, key)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif !slicesEqual(expected, result) {\n\t\t\t\t\tt.Errorf(\"want %v got %v\", expected, result)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\nfunc TestRotateWhenHeightDiffers(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"right rotation when left subtree is higher\",\n\t\t\t[]string{\"E\", \"C\", \"A\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"left rotation when right subtree is higher\",\n\t\t\t[]string{\"A\", \"C\", \"E\", \"D\", \"F\"},\n\t\t\t[]string{\"A\", \"C\", \"D\", \"E\", \"F\"},\n\t\t},\n\t\t{\n\t\t\t\"left-right rotation\",\n\t\t\t[]string{\"E\", \"A\", \"C\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t\t{\n\t\t\t\"right-left rotation\",\n\t\t\t[]string{\"A\", \"E\", \"C\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"E\", \"D\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\t// perform rotation or balance\n\t\t\ttree = tree.balance()\n\n\t\t\t// check tree structure\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRotateAndBalance(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"right rotation\",\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left rotation\",\n\t\t\t[]string{\"E\", \"D\", \"C\", \"B\", \"A\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"left-right rotation\",\n\t\t\t[]string{\"C\", \"A\", \"E\", \"B\", \"D\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t\t{\n\t\t\t\"right-left rotation\",\n\t\t\t[]string{\"C\", \"E\", \"A\", \"D\", \"B\"},\n\t\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tvar tree *Node\n\t\t\tfor _, key := range tt.input {\n\t\t\t\ttree, _ = tree.Set(key, nil)\n\t\t\t}\n\n\t\t\ttree = tree.balance()\n\n\t\t\tvar result []string\n\t\t\ttree.Iterate(\"\", \"\", func(n *Node) bool {\n\t\t\t\tresult = append(result, n.Key())\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\tif !slicesEqual(tt.expected, result) {\n\t\t\t\tt.Errorf(\"want %v got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc slicesEqual(w1, w2 []string) bool {\n\tif len(w1) != len(w2) {\n\t\treturn false\n\t}\n\tfor i := 0; i \u003c len(w1); i++ {\n\t\tif w1[0] != w2[0] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc maxint8(a, b int8) int8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc reverseSlice(ss []string) {\n\tfor i := 0; i \u003c len(ss)/2; i++ {\n\t\tj := len(ss) - 1 - i\n\t\tss[i], ss[j] = ss[j], ss[i]\n\t}\n}\n\nfunc TestNodeStructuralSharing(t *testing.T) {\n\tt.Run(\"unmodified paths remain shared\", func(t *testing.T) {\n\t\troot := NewNode(\"B\", 2)\n\t\troot, _ = root.Set(\"A\", 1)\n\t\troot, _ = root.Set(\"C\", 3)\n\n\t\toriginalRight := root.rightNode\n\t\tnewRoot, _ := root.Set(\"A\", 10)\n\n\t\tif newRoot.rightNode != originalRight {\n\t\t\tt.Error(\"Unmodified right subtree should remain shared\")\n\t\t}\n\t})\n\n\tt.Run(\"multiple modifications reuse shared structure\", func(t *testing.T) {\n\t\t// Create initial tree\n\t\troot := NewNode(\"B\", 2)\n\t\troot, _ = root.Set(\"A\", 1)\n\t\troot, _ = root.Set(\"C\", 3)\n\n\t\t// Store original nodes\n\t\toriginalRight := root.rightNode\n\n\t\t// First modification\n\t\tmod1, _ := root.Set(\"A\", 10)\n\n\t\t// Second modification\n\t\tmod2, _ := mod1.Set(\"C\", 30)\n\n\t\t// Check sharing in first modification\n\t\tif mod1.rightNode != originalRight {\n\t\t\tt.Error(\"First modification should share unmodified right subtree\")\n\t\t}\n\n\t\t// Check that second modification creates new right node\n\t\tif mod2.rightNode == originalRight {\n\t\t\tt.Error(\"Second modification should create new right node\")\n\t\t}\n\t})\n}\n\nfunc TestNodeCopyOnWrite(t *testing.T) {\n\tt.Run(\"copy preserves structure\", func(t *testing.T) {\n\t\troot := NewNode(\"B\", 2)\n\t\troot, _ = root.Set(\"A\", 1)\n\t\troot, _ = root.Set(\"C\", 3)\n\n\t\t// Only copy non-leaf nodes\n\t\tif !root.IsLeaf() {\n\t\t\tcopied := root._copy()\n\t\t\tif copied == root {\n\t\t\t\tt.Error(\"Copy should create new instance\")\n\t\t\t}\n\n\t\t\t// Create temporary trees to use Equal method\n\t\t\toriginal := \u0026Tree{node: root}\n\t\t\tcopiedTree := \u0026Tree{node: copied}\n\t\t\tif !original.Equal(copiedTree) {\n\t\t\t\tt.Error(\"Copied node should preserve structure\")\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"removal copy pattern\", func(t *testing.T) {\n\t\t// Create a more complex tree to test removal\n\t\troot := NewNode(\"B\", 2)\n\t\troot, _ = root.Set(\"A\", 1)\n\t\troot, _ = root.Set(\"C\", 3)\n\t\troot, _ = root.Set(\"D\", 4) // Add this to ensure proper tree structure\n\n\t\t// Store references to original nodes\n\t\toriginalRight := root.rightNode\n\t\toriginalRightRight := originalRight.rightNode\n\n\t\t// Remove \"A\" which should only affect the left subtree\n\t\tnewRoot, _, _, _ := root.Remove(\"A\")\n\n\t\t// Verify right subtree remains unchanged and shared\n\t\tif newRoot.rightNode != originalRight {\n\t\t\tt.Error(\"Right subtree should remain shared during removal of left node\")\n\t\t}\n\n\t\t// Also verify deeper nodes remain shared\n\t\tif newRoot.rightNode.rightNode != originalRightRight {\n\t\t\tt.Error(\"Deep right subtree should remain shared during removal\")\n\t\t}\n\n\t\t// Verify original tree is unchanged\n\t\tif _, _, exists := root.Get(\"A\"); !exists {\n\t\t\tt.Error(\"Original tree should remain unchanged\")\n\t\t}\n\t})\n\n\tt.Run(\"copy leaf node panic\", func(t *testing.T) {\n\t\tleaf := NewNode(\"A\", 1)\n\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\tt.Error(\"Expected panic when copying leaf node\")\n\t\t\t}\n\t\t}()\n\n\t\t// This should panic with our specific message\n\t\tleaf._copy()\n\t})\n}\n\nfunc TestNodeEqual(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tnode1 func() *Node\n\t\tnode2 func() *Node\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"nil nodes\",\n\t\t\tnode1: func() *Node { return nil },\n\t\t\tnode2: func() *Node { return nil },\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"one nil node\",\n\t\t\tnode1: func() *Node { return NewNode(\"A\", 1) },\n\t\t\tnode2: func() *Node { return nil },\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"single leaf nodes equal\",\n\t\t\tnode1: func() *Node { return NewNode(\"A\", 1) },\n\t\t\tnode2: func() *Node { return NewNode(\"A\", 1) },\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"single leaf nodes different key\",\n\t\t\tnode1: func() *Node { return NewNode(\"A\", 1) },\n\t\t\tnode2: func() *Node { return NewNode(\"B\", 1) },\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"single leaf nodes different value\",\n\t\t\tnode1: func() *Node { return NewNode(\"A\", 1) },\n\t\t\tnode2: func() *Node { return NewNode(\"A\", 2) },\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"complex trees equal\",\n\t\t\tnode1: func() *Node {\n\t\t\t\tn, _ := NewNode(\"B\", 2).Set(\"A\", 1)\n\t\t\t\tn, _ = n.Set(\"C\", 3)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\tnode2: func() *Node {\n\t\t\t\tn, _ := NewNode(\"B\", 2).Set(\"A\", 1)\n\t\t\t\tn, _ = n.Set(\"C\", 3)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"complex trees different structure\",\n\t\t\tnode1: func() *Node {\n\t\t\t\t// Create a tree with structure:\n\t\t\t\t// B\n\t\t\t\t// / \\\n\t\t\t\t// A D\n\t\t\t\tn := NewNode(\"B\", 2)\n\t\t\t\tn, _ = n.Set(\"A\", 1)\n\t\t\t\tn, _ = n.Set(\"D\", 4)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\tnode2: func() *Node {\n\t\t\t\t// Create a tree with structure:\n\t\t\t\t// C\n\t\t\t\t// / \\\n\t\t\t\t// A D\n\t\t\t\tn := NewNode(\"C\", 3)\n\t\t\t\tn, _ = n.Set(\"A\", 1)\n\t\t\t\tn, _ = n.Set(\"D\", 4)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\texpected: false, // These trees should be different\n\t\t},\n\t\t{\n\t\t\tname: \"complex trees same structure despite different insertion order\",\n\t\t\tnode1: func() *Node {\n\t\t\t\tn, _ := NewNode(\"B\", 2).Set(\"A\", 1)\n\t\t\t\tn, _ = n.Set(\"C\", 3)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\tnode2: func() *Node {\n\t\t\t\tn, _ := NewNode(\"A\", 1).Set(\"B\", 2)\n\t\t\t\tn, _ = n.Set(\"C\", 3)\n\t\t\t\treturn n\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"truly different structures\",\n\t\t\tnode1: func() *Node {\n\t\t\t\tn, _ := NewNode(\"B\", 2).Set(\"A\", 1)\n\t\t\t\treturn n // Tree with just two nodes\n\t\t\t},\n\t\t\tnode2: func() *Node {\n\t\t\t\tn, _ := NewNode(\"B\", 2).Set(\"C\", 3)\n\t\t\t\treturn n // Different two-node tree\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tnode1 := tt.node1()\n\t\t\tnode2 := tt.node2()\n\t\t\tresult := node1.Equal(node2)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Expected Equal to return %v for %s\", tt.expected, tt.name)\n\t\t\t\tprintln(\"\\nComparison failed:\")\n\t\t\t\tprintln(\"Tree 1:\")\n\t\t\t\tprintTree(node1, 0)\n\t\t\t\tprintln(\"Tree 2:\")\n\t\t\t\tprintTree(node2, 0)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helper function to print tree structure\nfunc printTree(node *Node, level int) {\n\tif node == nil {\n\t\treturn\n\t}\n\tindent := strings.Repeat(\" \", level)\n\tprintln(fmt.Sprintf(\"%sKey: %s, Value: %v, Height: %d, Size: %d\",\n\t\tindent, node.key, node.value, node.height, node.size))\n\tprintTree(node.leftNode, level+1)\n\tprintTree(node.rightNode, level+1)\n}\n" + }, + { + "name": "tree.gno", + "body": "// Package cow provides a Copy-on-Write (CoW) AVL tree implementation.\n// This is a fork of gno.land/p/demo/avl that adds CoW functionality\n// while maintaining the original AVL tree interface and properties.\n//\n// Copy-on-Write creates a copy of a data structure only when it is modified,\n// while still presenting the appearance of a full copy. When a tree is cloned,\n// it initially shares all its nodes with the original tree. Only when a\n// modification is made to either the original or the clone are new nodes created,\n// and only along the path from the root to the modified node.\n//\n// Key features:\n// - O(1) cloning operation\n// - Minimal memory usage through structural sharing\n// - Full AVL tree functionality (self-balancing, ordered operations)\n// - Thread-safe for concurrent reads of shared structures\n//\n// While the CoW mechanism handles structural copying automatically, users need\n// to consider how to handle the values stored in the tree:\n//\n// 1. Simple Values (int, string, etc.):\n// - These are copied by value automatically\n// - No additional handling needed\n//\n// 2. Complex Values (structs, pointers):\n// - Only the reference is copied by default\n// - Users must implement their own deep copy mechanism if needed\n//\n// Example:\n//\n//\t// Create original tree\n//\toriginal := cow.NewTree()\n//\toriginal.Set(\"key1\", \"value1\")\n//\n//\t// Create a clone - O(1) operation\n//\tclone := original.Clone()\n//\n//\t// Modify clone - only affected nodes are copied\n//\tclone.Set(\"key1\", \"modified\")\n//\n//\t// Original remains unchanged\n//\tval, _ := original.Get(\"key1\") // Returns \"value1\"\npackage cow\n\ntype IterCbFn func(key string, value any) bool\n\n//----------------------------------------\n// Tree\n\n// The zero struct can be used as an empty tree.\ntype Tree struct {\n\tnode *Node\n}\n\n// NewTree creates a new empty AVL tree.\nfunc NewTree() *Tree {\n\treturn \u0026Tree{\n\t\tnode: nil,\n\t}\n}\n\n// Size returns the number of key-value pair in the tree.\nfunc (tree *Tree) Size() int {\n\treturn tree.node.Size()\n}\n\n// Has checks whether a key exists in the tree.\n// It returns true if the key exists, otherwise false.\nfunc (tree *Tree) Has(key string) (has bool) {\n\treturn tree.node.Has(key)\n}\n\n// Get retrieves the value associated with the given key.\n// It returns the value 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// Equal returns true if the two trees contain the same key-value pairs.\n// WARNING: This is an expensive operation that recursively traverses the entire tree structure.\n// It should only be used in tests or when absolutely necessary.\nfunc (tree *Tree) Equal(other *Tree) bool {\n\tif tree == nil || other == nil {\n\t\treturn tree == other\n\t}\n\treturn tree.node.Equal(other.node)\n}\n\n// Clone creates a shallow copy of the tree\nfunc (tree *Tree) Clone() *Tree {\n\tif tree == nil {\n\t\treturn nil\n\t}\n\treturn \u0026Tree{\n\t\tnode: tree.node,\n\t}\n}\n" + }, + { + "name": "tree_test.gno", + "body": "package cow\n\nimport (\n\t\"testing\"\n)\n\nfunc TestNewTree(t *testing.T) {\n\ttree := NewTree()\n\tif tree.node != nil {\n\t\tt.Error(\"Expected tree.node to be nil\")\n\t}\n}\n\nfunc TestTreeSize(t *testing.T) {\n\ttree := NewTree()\n\tif tree.Size() != 0 {\n\t\tt.Error(\"Expected empty tree size to be 0\")\n\t}\n\n\ttree.Set(\"key1\", \"value1\")\n\ttree.Set(\"key2\", \"value2\")\n\tif tree.Size() != 2 {\n\t\tt.Error(\"Expected tree size to be 2\")\n\t}\n}\n\nfunc TestTreeHas(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tif !tree.Has(\"key1\") {\n\t\tt.Error(\"Expected tree to have key1\")\n\t}\n\n\tif tree.Has(\"key2\") {\n\t\tt.Error(\"Expected tree to not have key2\")\n\t}\n}\n\nfunc TestTreeGet(t *testing.T) {\n\ttree := NewTree()\n\ttree.Set(\"key1\", \"value1\")\n\n\tvalue, 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\n// Verify that Tree implements avl.ITree\n// var _ avl.ITree = (*Tree)(nil) // TODO: fix gnovm bug: ./examples/gno.land/p/moul/cow: test pkg: panic: gno.land/p/moul/cow/tree_test.gno:166:5: name avl not defined in fileset with files [node.gno tree.gno node_test.gno tree_test.gno]:\n\nfunc TestCopyOnWrite(t *testing.T) {\n\t// Create original tree\n\toriginal := NewTree()\n\toriginal.Set(\"A\", 1)\n\toriginal.Set(\"B\", 2)\n\toriginal.Set(\"C\", 3)\n\n\t// Create a clone\n\tclone := original.Clone()\n\n\t// Modify clone\n\tclone.Set(\"B\", 20)\n\tclone.Set(\"D\", 4)\n\n\t// Verify original is unchanged\n\tif val, _ := original.Get(\"B\"); val != 2 {\n\t\tt.Errorf(\"Original tree was modified: expected B=2, got B=%v\", val)\n\t}\n\tif original.Has(\"D\") {\n\t\tt.Error(\"Original tree was modified: found key D\")\n\t}\n\n\t// Verify clone has new values\n\tif val, _ := clone.Get(\"B\"); val != 20 {\n\t\tt.Errorf(\"Clone not updated: expected B=20, got B=%v\", val)\n\t}\n\tif val, _ := clone.Get(\"D\"); val != 4 {\n\t\tt.Errorf(\"Clone not updated: expected D=4, got D=%v\", val)\n\t}\n}\n\nfunc TestCopyOnWriteEdgeCases(t *testing.T) {\n\tt.Run(\"nil tree clone\", func(t *testing.T) {\n\t\tvar original *Tree\n\t\tclone := original.Clone()\n\t\tif clone != nil {\n\t\t\tt.Error(\"Expected nil clone from nil tree\")\n\t\t}\n\t})\n\n\tt.Run(\"empty tree clone\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\tclone := original.Clone()\n\n\t\t// Modify clone\n\t\tclone.Set(\"A\", 1)\n\n\t\tif original.Size() != 0 {\n\t\t\tt.Error(\"Original empty tree was modified\")\n\t\t}\n\t\tif clone.Size() != 1 {\n\t\t\tt.Error(\"Clone was not modified\")\n\t\t}\n\t})\n\n\tt.Run(\"multiple clones\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\toriginal.Set(\"A\", 1)\n\t\toriginal.Set(\"B\", 2)\n\n\t\t// Create multiple clones\n\t\tclone1 := original.Clone()\n\t\tclone2 := original.Clone()\n\t\tclone3 := clone1.Clone()\n\n\t\t// Modify each clone differently\n\t\tclone1.Set(\"A\", 10)\n\t\tclone2.Set(\"B\", 20)\n\t\tclone3.Set(\"C\", 30)\n\n\t\t// Check original remains unchanged\n\t\tif val, _ := original.Get(\"A\"); val != 1 {\n\t\t\tt.Errorf(\"Original modified: expected A=1, got A=%v\", val)\n\t\t}\n\t\tif val, _ := original.Get(\"B\"); val != 2 {\n\t\t\tt.Errorf(\"Original modified: expected B=2, got B=%v\", val)\n\t\t}\n\n\t\t// Verify each clone has correct values\n\t\tif val, _ := clone1.Get(\"A\"); val != 10 {\n\t\t\tt.Errorf(\"Clone1 incorrect: expected A=10, got A=%v\", val)\n\t\t}\n\t\tif val, _ := clone2.Get(\"B\"); val != 20 {\n\t\t\tt.Errorf(\"Clone2 incorrect: expected B=20, got B=%v\", val)\n\t\t}\n\t\tif val, _ := clone3.Get(\"C\"); val != 30 {\n\t\t\tt.Errorf(\"Clone3 incorrect: expected C=30, got C=%v\", val)\n\t\t}\n\t})\n\n\tt.Run(\"clone after removal\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\toriginal.Set(\"A\", 1)\n\t\toriginal.Set(\"B\", 2)\n\t\toriginal.Set(\"C\", 3)\n\n\t\t// Remove a node and then clone\n\t\toriginal.Remove(\"B\")\n\t\tclone := original.Clone()\n\n\t\t// Modify clone\n\t\tclone.Set(\"B\", 20)\n\n\t\t// Verify original state\n\t\tif original.Has(\"B\") {\n\t\t\tt.Error(\"Original tree should not have key B\")\n\t\t}\n\n\t\t// Verify clone state\n\t\tif val, _ := clone.Get(\"B\"); val != 20 {\n\t\t\tt.Errorf(\"Clone incorrect: expected B=20, got B=%v\", val)\n\t\t}\n\t})\n\n\tt.Run(\"concurrent modifications\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\toriginal.Set(\"A\", 1)\n\t\toriginal.Set(\"B\", 2)\n\n\t\tclone1 := original.Clone()\n\t\tclone2 := original.Clone()\n\n\t\t// Modify same key in different clones\n\t\tclone1.Set(\"B\", 20)\n\t\tclone2.Set(\"B\", 30)\n\n\t\t// Each clone should have its own value\n\t\tif val, _ := clone1.Get(\"B\"); val != 20 {\n\t\t\tt.Errorf(\"Clone1 incorrect: expected B=20, got B=%v\", val)\n\t\t}\n\t\tif val, _ := clone2.Get(\"B\"); val != 30 {\n\t\t\tt.Errorf(\"Clone2 incorrect: expected B=30, got B=%v\", val)\n\t\t}\n\t})\n\n\tt.Run(\"deep tree modifications\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\t// Create a deeper tree\n\t\tkeys := []string{\"M\", \"F\", \"T\", \"B\", \"H\", \"P\", \"Z\"}\n\t\tfor _, k := range keys {\n\t\t\toriginal.Set(k, k)\n\t\t}\n\n\t\tclone := original.Clone()\n\n\t\t// Modify a deep node\n\t\tclone.Set(\"H\", \"modified\")\n\n\t\t// Check original remains unchanged\n\t\tif val, _ := original.Get(\"H\"); val != \"H\" {\n\t\t\tt.Errorf(\"Original modified: expected H='H', got H=%v\", val)\n\t\t}\n\n\t\t// Verify clone modification\n\t\tif val, _ := clone.Get(\"H\"); val != \"modified\" {\n\t\t\tt.Errorf(\"Clone incorrect: expected H='modified', got H=%v\", val)\n\t\t}\n\t})\n\n\tt.Run(\"rebalancing test\", func(t *testing.T) {\n\t\toriginal := NewTree()\n\t\t// Insert nodes that will cause rotations\n\t\tkeys := []string{\"A\", \"B\", \"C\", \"D\", \"E\"}\n\t\tfor _, k := range keys {\n\t\t\toriginal.Set(k, k)\n\t\t}\n\n\t\tclone := original.Clone()\n\n\t\t// Add more nodes to clone to trigger rebalancing\n\t\tclone.Set(\"F\", \"F\")\n\t\tclone.Set(\"G\", \"G\")\n\n\t\t// Verify original structure remains unchanged\n\t\toriginalKeys := collectKeys(original)\n\t\texpectedOriginal := []string{\"A\", \"B\", \"C\", \"D\", \"E\"}\n\t\tif !slicesEqual(originalKeys, expectedOriginal) {\n\t\t\tt.Errorf(\"Original tree structure changed: got %v, want %v\", originalKeys, expectedOriginal)\n\t\t}\n\n\t\t// Verify clone has all keys\n\t\tcloneKeys := collectKeys(clone)\n\t\texpectedClone := []string{\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\"}\n\t\tif !slicesEqual(cloneKeys, expectedClone) {\n\t\t\tt.Errorf(\"Clone tree structure incorrect: got %v, want %v\", cloneKeys, expectedClone)\n\t\t}\n\t})\n\n\tt.Run(\"value mutation test\", func(t *testing.T) {\n\t\ttype MutableValue struct {\n\t\t\tData string\n\t\t}\n\n\t\toriginal := NewTree()\n\t\tmutable := \u0026MutableValue{Data: \"original\"}\n\t\toriginal.Set(\"key\", mutable)\n\n\t\tclone := original.Clone()\n\n\t\t// Modify the mutable value\n\t\tmutable.Data = \"modified\"\n\n\t\t// Both original and clone should see the modification\n\t\t// because we're not deep copying values\n\t\torigVal, _ := original.Get(\"key\")\n\t\tcloneVal, _ := clone.Get(\"key\")\n\n\t\tif origVal.(*MutableValue).Data != \"modified\" {\n\t\t\tt.Error(\"Original value not modified as expected\")\n\t\t}\n\t\tif cloneVal.(*MutableValue).Data != \"modified\" {\n\t\t\tt.Error(\"Clone value not modified as expected\")\n\t\t}\n\t})\n}\n\n// Helper function to collect all keys in order\nfunc collectKeys(tree *Tree) []string {\n\tvar keys []string\n\ttree.Iterate(\"\", \"\", func(key string, _ any) bool {\n\t\tkeys = append(keys, key)\n\t\treturn false\n\t})\n\treturn keys\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "BGZGTRdk4ZX1U0R2fgsLvN0qLsvZS7XghGoDXl1aFWz5hKF/T2FV6duUL1wP2AHwePU9js0Wa73TRytKLiMgCA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "realmpath", + "path": "gno.land/p/moul/realmpath", + "files": [ + { + "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\"net/url\"\n\t\"std\"\n\t\"strings\"\n)\n\nvar chainDomain = std.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 = std.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\"net/url\"\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n\t\"gno.land/p/moul/realmpath\"\n)\n\nfunc TestExample(t *testing.T) {\n\tcd := std.ChainDomain()\n\ttesting.SetRealm(std.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 := std.ChainDomain()\n\ttesting.SetRealm(std.NewCodeRealm(cd + \"/r/lorem/ipsum\"))\n\n\ttests := []struct {\n\t\trawPath string\n\t\trealm 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: 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 // 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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "rHiViPTOjchuzNv1PNnsL2gvVuK3rF5r9GWzFGGXnh73t/wd0Gh/l0qcv1eIkr9bja8c3SgFmtL3FFaLjzCMBA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "debug", + "path": "gno.land/p/moul/debug", + "files": [ + { + "name": "debug.gno", + "body": "// Package debug provides utilities for logging and displaying debug information\n// within Gno realms. It supports conditional rendering of logs and metadata,\n// toggleable via query parameters.\n//\n// Key Features:\n// - Log collection and display using Markdown formatting.\n// - Metadata display for realm path, address, and height.\n// - Collapsible debug section for cleaner presentation.\n// - Query-based debug toggle using `?debug=1`.\npackage debug\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/moul/realmpath\"\n)\n\n// Debug encapsulates debug information, including logs and metadata.\ntype Debug struct {\n\tLogs []string\n\tHideMetadata bool\n}\n\n// Log appends a new line of debug information to the Logs slice.\nfunc (d *Debug) Log(line string) {\n\td.Logs = append(d.Logs, line)\n}\n\n// Render generates the debug content as a collapsible Markdown section.\n// It conditionally renders logs and metadata if enabled via the `?debug=1` query parameter.\nfunc (d Debug) Render(path string) string {\n\tif realmpath.Parse(path).Query.Get(\"debug\") != \"1\" {\n\t\treturn \"\"\n\t}\n\n\tvar content string\n\n\tif d.Logs != nil {\n\t\tcontent += md.H3(\"Logs\")\n\t\tcontent += md.BulletList(d.Logs)\n\t}\n\n\tif !d.HideMetadata {\n\t\tcontent += md.H3(\"Metadata\")\n\t\ttable := mdtable.Table{\n\t\t\tHeaders: []string{\"Key\", \"Value\"},\n\t\t}\n\t\ttable.Append([]string{\"`std.CurrentRealm().PkgPath()`\", string(std.CurrentRealm().PkgPath())})\n\t\ttable.Append([]string{\"`std.CurrentRealm().Address()`\", string(std.CurrentRealm().Address())})\n\t\ttable.Append([]string{\"`std.PreviousRealm().PkgPath()`\", string(std.PreviousRealm().PkgPath())})\n\t\ttable.Append([]string{\"`std.PreviousRealm().Address()`\", string(std.PreviousRealm().Address())})\n\t\ttable.Append([]string{\"`std.ChainHeight()`\", ufmt.Sprintf(\"%d\", std.ChainHeight())})\n\t\ttable.Append([]string{\"`time.Now().Format(time.RFC3339)`\", time.Now().Format(time.RFC3339)})\n\t\tcontent += table.String()\n\t}\n\n\tif content == \"\" {\n\t\treturn \"\"\n\t}\n\n\treturn md.CollapsibleSection(\"debug\", content)\n}\n\n// Render displays metadata about the current realm but does not display logs.\n// This function uses a default Debug struct with metadata enabled and no logs.\nfunc Render(path string) string {\n\treturn Debug{}.Render(path)\n}\n\n// IsEnabled checks if the `?debug=1` query parameter is set in the given path.\n// Returns true if debugging is enabled, otherwise false.\nfunc IsEnabled(path string) bool {\n\treq := realmpath.Parse(path)\n\treturn req.Query.Get(\"debug\") == \"1\"\n}\n\n// ToggleURL modifies the given path's query string to toggle the `?debug=1` parameter.\n// If debugging is currently enabled, it removes the parameter.\n// If debugging is disabled, it adds the parameter.\nfunc ToggleURL(path string) string {\n\treq := realmpath.Parse(path)\n\tif IsEnabled(path) {\n\t\treq.Query.Del(\"debug\")\n\t} else {\n\t\treq.Query.Add(\"debug\", \"1\")\n\t}\n\treturn req.String()\n}\n" + }, + { + "name": "z1_filetest.gno", + "body": "package main\n\nimport \"gno.land/p/moul/debug\"\n\nfunc main() {\n\tprintln(\"---\")\n\tprintln(debug.Render(\"\"))\n\tprintln(\"---\")\n\tprintln(debug.Render(\"?debug=1\"))\n\tprintln(\"---\")\n}\n\n// Output:\n// ---\n//\n// ---\n// \u003cdetails\u003e\u003csummary\u003edebug\u003c/summary\u003e\n//\n// ### Metadata\n// | Key | Value |\n// | --- | --- |\n// | `std.CurrentRealm().PkgPath()` | |\n// | `std.CurrentRealm().Address()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm |\n// | `std.PreviousRealm().PkgPath()` | |\n// | `std.PreviousRealm().Address()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm |\n// | `std.ChainHeight()` | 123 |\n// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z |\n//\n// \u003c/details\u003e\n//\n// ---\n" + }, + { + "name": "z2_filetest.gno", + "body": "package main\n\nimport \"gno.land/p/moul/debug\"\n\nfunc main() {\n\tvar d debug.Debug\n\td.Log(\"hello world!\")\n\td.Log(\"foobar\")\n\tprintln(\"---\")\n\tprintln(d.Render(\"\"))\n\tprintln(\"---\")\n\tprintln(d.Render(\"?debug=1\"))\n\tprintln(\"---\")\n}\n\n// Output:\n// ---\n//\n// ---\n// \u003cdetails\u003e\u003csummary\u003edebug\u003c/summary\u003e\n//\n// ### Logs\n// - hello world!\n// - foobar\n// ### Metadata\n// | Key | Value |\n// | --- | --- |\n// | `std.CurrentRealm().PkgPath()` | |\n// | `std.CurrentRealm().Address()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm |\n// | `std.PreviousRealm().PkgPath()` | |\n// | `std.PreviousRealm().Address()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm |\n// | `std.ChainHeight()` | 123 |\n// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z |\n//\n// \u003c/details\u003e\n//\n// ---\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "wO0fkXWzZN7xq+8ZvankopYYehJqZNTgcJTpeom8Bk6ywANY11H3d7Kndy1p1sby7IK0uHXu7cpPmT34MhU3Ag==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "dynreplacer", + "path": "gno.land/p/moul/dynreplacer", + "files": [ + { + "name": "dynreplacer.gno", + "body": "// Package dynreplacer provides a simple template engine for handling dynamic\n// content replacement. It is similar to strings.Replacer but with lazy\n// execution of replacements, making it more optimization-friendly in several\n// cases. While strings.Replacer requires all replacement values to be computed\n// upfront, dynreplacer only executes the callback functions for placeholders\n// that actually exist in the template, avoiding unnecessary computations.\n//\n// The package ensures efficient, non-recursive replacement of placeholders in a\n// single pass. This lazy evaluation approach is particularly beneficial when:\n// - Some replacement values are expensive to compute\n// - Not all placeholders are guaranteed to be present in the template\n// - Templates are reused with different content\n//\n// Example usage:\n//\n//\tr := dynreplacer.New(\n//\t dynreplacer.Pair{\":name:\", func() string { return \"World\" }},\n//\t dynreplacer.Pair{\":greeting:\", func() string { return \"Hello\" }},\n//\t)\n//\tresult := r.Replace(\"Hello :name:!\") // Returns \"Hello World!\"\n//\n// The replacer caches computed values, so subsequent calls with the same\n// placeholder will reuse the cached value instead of executing the callback\n// again:\n//\n//\tr := dynreplacer.New()\n//\tr.RegisterCallback(\":expensive:\", func() string { return \"computed\" })\n//\tr.Replace(\"Value1: :expensive:\") // Computes the value\n//\tr.Replace(\"Value2: :expensive:\") // Uses cached value\n//\tr.ClearCache() // Force re-computation on next use\npackage dynreplacer\n\nimport (\n\t\"strings\"\n)\n\n// Replacer manages dynamic placeholders, their associated functions, and cached\n// values.\ntype Replacer struct {\n\tcallbacks map[string]func() string\n\tcachedValues map[string]string\n}\n\n// Pair represents a placeholder and its callback function\ntype Pair struct {\n\tPlaceholder string\n\tCallback func() string\n}\n\n// New creates a new Replacer instance with optional initial replacements.\n// It accepts pairs where each pair consists of a placeholder string and\n// its corresponding callback function.\n//\n// Example:\n//\n//\tNew(\n//\t Pair{\":name:\", func() string { return \"World\" }},\n//\t Pair{\":greeting:\", func() string { return \"Hello\" }},\n//\t)\nfunc New(pairs ...Pair) *Replacer {\n\tr := \u0026Replacer{\n\t\tcallbacks: make(map[string]func() string),\n\t\tcachedValues: make(map[string]string),\n\t}\n\n\tfor _, pair := range pairs {\n\t\tr.RegisterCallback(pair.Placeholder, pair.Callback)\n\t}\n\n\treturn r\n}\n\n// RegisterCallback associates a placeholder with a function to generate its\n// content.\nfunc (r *Replacer) RegisterCallback(placeholder string, callback func() string) {\n\tr.callbacks[placeholder] = callback\n}\n\n// Replace processes the given layout, replacing placeholders with cached or\n// newly computed values.\nfunc (r *Replacer) Replace(layout string) string {\n\treplacements := []string{}\n\n\t// Check for placeholders and compute/retrieve values\n\thasReplacements := false\n\tfor placeholder, callback := range r.callbacks {\n\t\tif strings.Contains(layout, placeholder) {\n\t\t\tvalue, exists := r.cachedValues[placeholder]\n\t\t\tif !exists {\n\t\t\t\tvalue = callback()\n\t\t\t\tr.cachedValues[placeholder] = value\n\t\t\t}\n\t\t\treplacements = append(replacements, placeholder, value)\n\t\t\thasReplacements = true\n\t\t}\n\t}\n\n\t// If no replacements were found, return the original layout\n\tif !hasReplacements {\n\t\treturn layout\n\t}\n\n\t// Create a strings.Replacer with all computed replacements\n\treplacer := strings.NewReplacer(replacements...)\n\treturn replacer.Replace(layout)\n}\n\n// ClearCache clears all cached values, forcing re-computation on next Replace.\nfunc (r *Replacer) ClearCache() {\n\tr.cachedValues = make(map[string]string)\n}\n" + }, + { + "name": "dynreplacer_test.gno", + "body": "package dynreplacer\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestNew(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tpairs []Pair\n\t}{\n\t\t{\n\t\t\tname: \"empty constructor\",\n\t\t\tpairs: []Pair{},\n\t\t},\n\t\t{\n\t\t\tname: \"single pair\",\n\t\t\tpairs: []Pair{\n\t\t\t\t{\":name:\", func() string { return \"World\" }},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple pairs\",\n\t\t\tpairs: []Pair{\n\t\t\t\t{\":greeting:\", func() string { return \"Hello\" }},\n\t\t\t\t{\":name:\", func() string { return \"World\" }},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := New(tt.pairs...)\n\t\t\tuassert.True(t, r.callbacks != nil, \"callbacks map should be initialized\")\n\t\t\tuassert.True(t, r.cachedValues != nil, \"cachedValues map should be initialized\")\n\n\t\t\t// Verify all callbacks were registered\n\t\t\tfor _, pair := range tt.pairs {\n\t\t\t\t_, exists := r.callbacks[pair.Placeholder]\n\t\t\t\tuassert.True(t, exists, \"callback should be registered for \"+pair.Placeholder)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReplace(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tlayout string\n\t\tsetup func(*Replacer)\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"empty layout\",\n\t\t\tlayout: \"\",\n\t\t\tsetup: func(r *Replacer) {},\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"single replacement\",\n\t\t\tlayout: \"Hello :name:!\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":name:\", func() string { return \"World\" })\n\t\t\t},\n\t\t\texpected: \"Hello World!\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple replacements\",\n\t\t\tlayout: \":greeting: :name:!\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":greeting:\", func() string { return \"Hello\" })\n\t\t\t\tr.RegisterCallback(\":name:\", func() string { return \"World\" })\n\t\t\t},\n\t\t\texpected: \"Hello World!\",\n\t\t},\n\t\t{\n\t\t\tname: \"no recursive replacement\",\n\t\t\tlayout: \":outer:\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":outer:\", func() string { return \":inner:\" })\n\t\t\t\tr.RegisterCallback(\":inner:\", func() string { return \"content\" })\n\t\t\t},\n\t\t\texpected: \":inner:\",\n\t\t},\n\t\t{\n\t\t\tname: \"unused callbacks\",\n\t\t\tlayout: \"Hello :name:!\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":name:\", func() string { return \"World\" })\n\t\t\t\tr.RegisterCallback(\":unused:\", func() string { return \"Never Called\" })\n\t\t\t},\n\t\t\texpected: \"Hello World!\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := New()\n\t\t\ttt.setup(r)\n\t\t\tresult := r.Replace(tt.layout)\n\t\t\tuassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestCaching(t *testing.T) {\n\tr := New()\n\tcallCount := 0\n\tr.RegisterCallback(\":expensive:\", func() string {\n\t\tcallCount++\n\t\treturn \"computed\"\n\t})\n\n\tlayout := \"Value: :expensive:\"\n\n\t// First call should compute\n\tresult1 := r.Replace(layout)\n\tuassert.Equal(t, \"Value: computed\", result1)\n\tuassert.Equal(t, 1, callCount)\n\n\t// Second call should use cache\n\tresult2 := r.Replace(layout)\n\tuassert.Equal(t, \"Value: computed\", result2)\n\tuassert.Equal(t, 1, callCount)\n\n\t// After clearing cache, should recompute\n\tr.ClearCache()\n\tresult3 := r.Replace(layout)\n\tuassert.Equal(t, \"Value: computed\", result3)\n\tuassert.Equal(t, 2, callCount)\n}\n\nfunc TestComplexExample(t *testing.T) {\n\tlayout := `\n\t\t# Welcome to gno.land\n\n\t\t## Blog\n\t\t:latest-blogposts:\n\n\t\t## Events\n\t\t:next-events:\n\n\t\t## Awesome Gno\n\t\t:awesome-gno:\n\t`\n\n\tr := New(\n\t\tPair{\":latest-blogposts:\", func() string { return \"Latest blog posts content here\" }},\n\t\tPair{\":next-events:\", func() string { return \"Upcoming events listed here\" }},\n\t\tPair{\":awesome-gno:\", func() string { return \":latest-blogposts: (This should NOT be replaced again)\" }},\n\t)\n\n\tresult := r.Replace(layout)\n\n\t// Check that original placeholders are replaced\n\tuassert.True(t, !strings.Contains(result, \":latest-blogposts:\\n\"), \"':latest-blogposts:' placeholder should be replaced\")\n\tuassert.True(t, !strings.Contains(result, \":next-events:\\n\"), \"':next-events:' placeholder should be replaced\")\n\tuassert.True(t, !strings.Contains(result, \":awesome-gno:\\n\"), \"':awesome-gno:' placeholder should be replaced\")\n\n\t// Check that the replacement content is present\n\tuassert.True(t, strings.Contains(result, \"Latest blog posts content here\"), \"Blog posts content should be present\")\n\tuassert.True(t, strings.Contains(result, \"Upcoming events listed here\"), \"Events content should be present\")\n\tuassert.True(t, strings.Contains(result, \":latest-blogposts: (This should NOT be replaced again)\"),\n\t\t\"Nested placeholder should not be replaced\")\n}\n\nfunc TestEdgeCases(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tlayout string\n\t\tsetup func(*Replacer)\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"empty string placeholder\",\n\t\t\tlayout: \"Hello :\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\"\", func() string { return \"World\" })\n\t\t\t},\n\t\t\texpected: \"WorldHWorldeWorldlWorldlWorldoWorld World:World\",\n\t\t},\n\t\t{\n\t\t\tname: \"overlapping placeholders\",\n\t\t\tlayout: \"Hello :name::greeting:\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":name:\", func() string { return \"World\" })\n\t\t\t\tr.RegisterCallback(\":greeting:\", func() string { return \"Hi\" })\n\t\t\t\tr.RegisterCallback(\":name::greeting:\", func() string { return \"Should not match\" })\n\t\t\t},\n\t\t\texpected: \"Hello WorldHi\",\n\t\t},\n\t\t{\n\t\t\tname: \"replacement order\",\n\t\t\tlayout: \":a::b::c:\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":c:\", func() string { return \"3\" })\n\t\t\t\tr.RegisterCallback(\":b:\", func() string { return \"2\" })\n\t\t\t\tr.RegisterCallback(\":a:\", func() string { return \"1\" })\n\t\t\t},\n\t\t\texpected: \"123\",\n\t\t},\n\t\t{\n\t\t\tname: \"special characters in placeholders\",\n\t\t\tlayout: \"Hello :$name#123:!\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tr.RegisterCallback(\":$name#123:\", func() string { return \"World\" })\n\t\t\t},\n\t\t\texpected: \"Hello World!\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple occurrences of same placeholder\",\n\t\t\tlayout: \":name: and :name: again\",\n\t\t\tsetup: func(r *Replacer) {\n\t\t\t\tcallCount := 0\n\t\t\t\tr.RegisterCallback(\":name:\", func() string {\n\t\t\t\t\tcallCount++\n\t\t\t\t\treturn \"World\"\n\t\t\t\t})\n\t\t\t},\n\t\t\texpected: \"World and World again\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tr := New()\n\t\t\ttt.setup(r)\n\t\t\tresult := r.Replace(tt.layout)\n\t\t\tuassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "2noj9ZXrAp6hQ4tSvGlcYzrWcGpwUL4tcRKZzIquqW34zmDNGa78l+xYOCTq+OdwQ9JPwaVUYmiXZDeu7+LcAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "fifo", + "path": "gno.land/p/moul/fifo", + "files": [ + { + "name": "fifo.gno", + "body": "// Package fifo implements a fixed-size FIFO (First-In-First-Out) list data structure\n// using a singly-linked list. The implementation prioritizes storage efficiency by minimizing\n// storage operations - each add/remove operation only updates 1-2 pointers, regardless of\n// list size.\n//\n// Key features:\n// - Fixed-size with automatic removal of oldest entries when full\n// - Support for both prepend (add at start) and append (add at end) operations\n// - Constant storage usage through automatic pruning\n// - O(1) append operations and latest element access\n// - Iterator support for sequential access\n// - Dynamic size adjustment via SetMaxSize\n//\n// This implementation is optimized for frequent updates, as insertions and deletions only\n// require updating 1-2 pointers. However, random access operations are O(n) as they require\n// traversing the list. For use cases where writes are rare, a slice-based\n// implementation might be more suitable.\n//\n// The linked list structure is equally efficient for storing both small values (like pointers)\n// and larger data structures, as each node maintains a single next-pointer regardless of the\n// stored value's size.\n//\n// Example usage:\n//\n//\tlist := fifo.New(3) // Create a new list with max size 3\n//\tlist.Append(\"a\") // List: [a]\n//\tlist.Append(\"b\") // List: [a b]\n//\tlist.Append(\"c\") // List: [a b c]\n//\tlist.Append(\"d\") // List: [b c d] (oldest element \"a\" was removed)\n//\tlatest := list.Latest() // Returns \"d\"\n//\tall := list.Entries() // Returns [\"b\", \"c\", \"d\"]\npackage fifo\n\n// node represents a single element in the linked list\ntype node struct {\n\tvalue any\n\tnext *node\n}\n\n// List represents a fixed-size FIFO list\ntype List struct {\n\thead *node\n\ttail *node\n\tsize int\n\tmaxSize int\n}\n\n// New creates a new FIFO list with the specified maximum size\nfunc New(maxSize int) *List {\n\treturn \u0026List{\n\t\tmaxSize: maxSize,\n\t}\n}\n\n// Prepend adds a new entry at the start of the list. If the list exceeds maxSize,\n// the last entry is automatically removed.\nfunc (l *List) Prepend(entry any) {\n\tif l.maxSize == 0 {\n\t\treturn\n\t}\n\n\tnewNode := \u0026node{value: entry}\n\n\tif l.head == nil {\n\t\tl.head = newNode\n\t\tl.tail = newNode\n\t\tl.size = 1\n\t\treturn\n\t}\n\n\tnewNode.next = l.head\n\tl.head = newNode\n\n\tif l.size \u003c l.maxSize {\n\t\tl.size++\n\t\treturn\n\t}\n\n\t// Remove last element by traversing to second-to-last\n\tif l.size == 1 {\n\t\t// Special case: if size is 1, just update both pointers\n\t\tl.head = newNode\n\t\tl.tail = newNode\n\t\tnewNode.next = nil\n\t\treturn\n\n\t}\n\n\t// Find second-to-last node\n\tcurrent := l.head\n\tfor current.next != l.tail {\n\t\tcurrent = current.next\n\t}\n\tcurrent.next = nil\n\tl.tail = current\n\n}\n\n// Append adds a new entry at the end of the list. If the list exceeds maxSize,\n// the first entry is automatically removed.\nfunc (l *List) Append(entry any) {\n\tif l.maxSize == 0 {\n\t\treturn\n\t}\n\n\tnewNode := \u0026node{value: entry}\n\n\tif l.head == nil {\n\t\tl.head = newNode\n\t\tl.tail = newNode\n\t\tl.size = 1\n\t\treturn\n\t}\n\n\tl.tail.next = newNode\n\tl.tail = newNode\n\n\tif l.size \u003c l.maxSize {\n\t\tl.size++\n\t} else {\n\t\tl.head = l.head.next\n\t}\n}\n\n// Get returns the entry at the specified index.\n// Index 0 is the oldest entry, Size()-1 is the newest.\nfunc (l *List) Get(index int) any {\n\tif index \u003c 0 || index \u003e= l.size {\n\t\treturn nil\n\t}\n\n\tcurrent := l.head\n\tfor i := 0; i \u003c index; i++ {\n\t\tcurrent = current.next\n\t}\n\treturn current.value\n}\n\n// Size returns the current number of entries in the list\nfunc (l *List) Size() int {\n\treturn l.size\n}\n\n// MaxSize returns the maximum size configured for this list\nfunc (l *List) MaxSize() int {\n\treturn l.maxSize\n}\n\n// Entries returns all current entries as a slice\nfunc (l *List) Entries() []any {\n\tentries := make([]any, l.size)\n\tcurrent := l.head\n\tfor i := 0; i \u003c l.size; i++ {\n\t\tentries[i] = current.value\n\t\tcurrent = current.next\n\t}\n\treturn entries\n}\n\n// Iterator returns a function that can be used to iterate over the entries\n// from oldest to newest. Returns nil when there are no more entries.\nfunc (l *List) Iterator() func() any {\n\tcurrent := l.head\n\treturn func() any {\n\t\tif current == nil {\n\t\t\treturn nil\n\t\t}\n\t\tvalue := current.value\n\t\tcurrent = current.next\n\t\treturn value\n\t}\n}\n\n// Latest returns the most recent entry.\n// Returns nil if the list is empty.\nfunc (l *List) Latest() any {\n\tif l.tail == nil {\n\t\treturn nil\n\t}\n\treturn l.tail.value\n}\n\n// SetMaxSize updates the maximum size of the list.\n// If the new maxSize is smaller than the current size,\n// the oldest entries are removed to fit the new size.\nfunc (l *List) SetMaxSize(maxSize int) {\n\tif maxSize \u003c 0 {\n\t\tmaxSize = 0\n\t}\n\n\t// If new maxSize is smaller than current size,\n\t// remove oldest entries until we fit\n\tif maxSize \u003c l.size {\n\t\t// Special case: if new maxSize is 0, clear the list\n\t\tif maxSize == 0 {\n\t\t\tl.head = nil\n\t\t\tl.tail = nil\n\t\t\tl.size = 0\n\t\t} else {\n\t\t\t// Keep the newest entries by moving head forward\n\t\t\tdiff := l.size - maxSize\n\t\t\tfor i := 0; i \u003c diff; i++ {\n\t\t\t\tl.head = l.head.next\n\t\t\t}\n\t\t\tl.size = maxSize\n\t\t}\n\t}\n\n\tl.maxSize = maxSize\n}\n\n// Delete removes the element at the specified index.\n// Returns true if an element was removed, false if the index was invalid.\nfunc (l *List) Delete(index int) bool {\n\tif index \u003c 0 || index \u003e= l.size {\n\t\treturn false\n\t}\n\n\t// Special case: deleting the only element\n\tif l.size == 1 {\n\t\tl.head = nil\n\t\tl.tail = nil\n\t\tl.size = 0\n\t\treturn true\n\t}\n\n\t// Special case: deleting first element\n\tif index == 0 {\n\t\tl.head = l.head.next\n\t\tl.size--\n\t\treturn true\n\t}\n\n\t// Find the node before the one to delete\n\tcurrent := l.head\n\tfor i := 0; i \u003c index-1; i++ {\n\t\tcurrent = current.next\n\t}\n\n\t// Special case: deleting last element\n\tif index == l.size-1 {\n\t\tl.tail = current\n\t\tcurrent.next = nil\n\t} else {\n\t\tcurrent.next = current.next.next\n\t}\n\n\tl.size--\n\treturn true\n}\n" + }, + { + "name": "fifo_test.gno", + "body": "package fifo\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestNew(t *testing.T) {\n\tl := New(5)\n\tuassert.Equal(t, 5, l.MaxSize())\n\tuassert.Equal(t, 0, l.Size())\n}\n\nfunc TestAppend(t *testing.T) {\n\tl := New(3)\n\n\t// Test adding within capacity\n\tl.Append(1)\n\tl.Append(2)\n\tuassert.Equal(t, 2, l.Size())\n\tuassert.Equal(t, 1, l.Get(0))\n\tuassert.Equal(t, 2, l.Get(1))\n\n\t// Test overflow behavior\n\tl.Append(3)\n\tl.Append(4)\n\tuassert.Equal(t, 3, l.Size())\n\tuassert.Equal(t, 2, l.Get(0))\n\tuassert.Equal(t, 3, l.Get(1))\n\tuassert.Equal(t, 4, l.Get(2))\n}\n\nfunc TestPrepend(t *testing.T) {\n\tl := New(3)\n\n\t// Test adding within capacity\n\tl.Prepend(1)\n\tl.Prepend(2)\n\tuassert.Equal(t, 2, l.Size())\n\tuassert.Equal(t, 2, l.Get(0))\n\tuassert.Equal(t, 1, l.Get(1))\n\n\t// Test overflow behavior\n\tl.Prepend(3)\n\tl.Prepend(4)\n\tuassert.Equal(t, 3, l.Size())\n\tuassert.Equal(t, 4, l.Get(0))\n\tuassert.Equal(t, 3, l.Get(1))\n\tuassert.Equal(t, 2, l.Get(2))\n}\n\nfunc TestGet(t *testing.T) {\n\tl := New(3)\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\n\t// Test valid indices\n\tuassert.Equal(t, 1, l.Get(0))\n\tuassert.Equal(t, 2, l.Get(1))\n\tuassert.Equal(t, 3, l.Get(2))\n\n\t// Test invalid indices\n\tuassert.True(t, l.Get(-1) == nil)\n\tuassert.True(t, l.Get(3) == nil)\n}\n\nfunc TestEntries(t *testing.T) {\n\tl := New(3)\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\n\tentries := l.Entries()\n\tuassert.Equal(t, 3, len(entries))\n\tuassert.Equal(t, 1, entries[0])\n\tuassert.Equal(t, 2, entries[1])\n\tuassert.Equal(t, 3, entries[2])\n}\n\nfunc TestLatest(t *testing.T) {\n\tl := New(5)\n\n\t// Test empty list\n\tuassert.True(t, l.Latest() == nil)\n\n\t// Test single entry\n\tl.Append(1)\n\tuassert.Equal(t, 1, l.Latest())\n\n\t// Test multiple entries\n\tl.Append(2)\n\tl.Append(3)\n\tuassert.Equal(t, 3, l.Latest())\n\n\t// Test after overflow\n\tl.Append(4)\n\tl.Append(5)\n\tl.Append(6)\n\tuassert.Equal(t, 6, l.Latest())\n}\n\nfunc TestIterator(t *testing.T) {\n\tl := New(3)\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\n\titer := l.Iterator()\n\tuassert.Equal(t, 1, iter())\n\tuassert.Equal(t, 2, iter())\n\tuassert.Equal(t, 3, iter())\n\tuassert.True(t, iter() == nil)\n}\n\nfunc TestMixedOperations(t *testing.T) {\n\tl := New(3)\n\n\t// Mix of append and prepend operations\n\tl.Append(1) // [1]\n\tl.Prepend(2) // [2,1]\n\tl.Append(3) // [2,1,3]\n\tl.Prepend(4) // [4,2,1]\n\n\tentries := l.Entries()\n\tuassert.Equal(t, 3, len(entries))\n\tuassert.Equal(t, 4, entries[0])\n\tuassert.Equal(t, 2, entries[1])\n\tuassert.Equal(t, 1, entries[2])\n}\n\nfunc TestEmptyList(t *testing.T) {\n\tl := New(3)\n\n\t// Test operations on empty list\n\tuassert.Equal(t, 0, l.Size())\n\tuassert.True(t, l.Get(0) == nil)\n\tuassert.Equal(t, 0, len(l.Entries()))\n\tuassert.True(t, l.Latest() == nil)\n\n\titer := l.Iterator()\n\tuassert.True(t, iter() == nil)\n}\n\nfunc TestEdgeCases(t *testing.T) {\n\t// Test zero-size list\n\tl := New(0)\n\tuassert.Equal(t, 0, l.MaxSize())\n\tl.Append(1) // Should be no-op\n\tuassert.Equal(t, 0, l.Size())\n\n\t// Test single-element list\n\tl = New(1)\n\tl.Append(1)\n\tl.Append(2) // Should replace 1\n\tuassert.Equal(t, 1, l.Size())\n\tuassert.Equal(t, 2, l.Latest())\n\n\t// Test rapid append/prepend alternation\n\tl = New(3)\n\tl.Append(1) // [1]\n\tl.Prepend(2) // [2,1]\n\tl.Append(3) // [2,1,3]\n\tl.Prepend(4) // [4,2,1]\n\tl.Append(5) // [2,1,5]\n\tuassert.Equal(t, 3, l.Size())\n\tentries := l.Entries()\n\tuassert.Equal(t, 2, entries[0])\n\tuassert.Equal(t, 1, entries[1])\n\tuassert.Equal(t, 5, entries[2])\n\n\t// Test nil values\n\tl = New(2)\n\tl.Append(nil)\n\tl.Prepend(nil)\n\tuassert.Equal(t, 2, l.Size())\n\tuassert.True(t, l.Get(0) == nil)\n\tuassert.True(t, l.Get(1) == nil)\n\n\t// Test index bounds\n\tl = New(3)\n\tl.Append(1)\n\tuassert.True(t, l.Get(-1) == nil)\n\tuassert.True(t, l.Get(1) == nil)\n\n\t// Test iterator exhaustion\n\tl = New(2)\n\tl.Append(1)\n\tl.Append(2)\n\titer := l.Iterator()\n\tuassert.Equal(t, 1, iter())\n\tuassert.Equal(t, 2, iter())\n\tuassert.True(t, iter() == nil)\n\tuassert.True(t, iter() == nil)\n\n\t// Test prepend on full list\n\tl = New(2)\n\tl.Append(1)\n\tl.Append(2) // [1,2]\n\tl.Prepend(3) // [3,1]\n\tuassert.Equal(t, 2, l.Size())\n\tentries = l.Entries()\n\tuassert.Equal(t, 3, entries[0])\n\tuassert.Equal(t, 1, entries[1])\n}\n\nfunc TestSetMaxSize(t *testing.T) {\n\tl := New(5)\n\n\t// Fill the list\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\tl.Append(4)\n\tl.Append(5)\n\n\t// Test increasing maxSize\n\tl.SetMaxSize(7)\n\tuassert.Equal(t, 7, l.MaxSize())\n\tuassert.Equal(t, 5, l.Size())\n\n\t// Test reducing maxSize\n\tl.SetMaxSize(3)\n\tuassert.Equal(t, 3, l.Size())\n\tentries := l.Entries()\n\tuassert.Equal(t, 3, entries[0])\n\tuassert.Equal(t, 4, entries[1])\n\tuassert.Equal(t, 5, entries[2])\n\n\t// Test setting to zero\n\tl.SetMaxSize(0)\n\tuassert.Equal(t, 0, l.Size())\n\tuassert.True(t, l.head == nil)\n\tuassert.True(t, l.tail == nil)\n\n\t// Test negative maxSize\n\tl.SetMaxSize(-1)\n\tuassert.Equal(t, 0, l.MaxSize())\n\n\t// Test setting back to positive\n\tl.SetMaxSize(2)\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\tuassert.Equal(t, 2, l.Size())\n\tentries = l.Entries()\n\tuassert.Equal(t, 2, entries[0])\n\tuassert.Equal(t, 3, entries[1])\n}\n\nfunc TestDelete(t *testing.T) {\n\tl := New(5)\n\n\t// Test delete on empty list\n\tuassert.False(t, l.Delete(0))\n\tuassert.False(t, l.Delete(-1))\n\n\t// Fill list\n\tl.Append(1)\n\tl.Append(2)\n\tl.Append(3)\n\tl.Append(4)\n\n\t// Test invalid indices\n\tuassert.False(t, l.Delete(-1))\n\tuassert.False(t, l.Delete(4))\n\n\t// Test deleting from middle\n\tuassert.True(t, l.Delete(1))\n\tuassert.Equal(t, 3, l.Size())\n\tentries := l.Entries()\n\tuassert.Equal(t, 1, entries[0])\n\tuassert.Equal(t, 3, entries[1])\n\tuassert.Equal(t, 4, entries[2])\n\n\t// Test deleting from head\n\tuassert.True(t, l.Delete(0))\n\tuassert.Equal(t, 2, l.Size())\n\tentries = l.Entries()\n\tuassert.Equal(t, 3, entries[0])\n\tuassert.Equal(t, 4, entries[1])\n\n\t// Test deleting from tail\n\tuassert.True(t, l.Delete(1))\n\tuassert.Equal(t, 1, l.Size())\n\tuassert.Equal(t, 3, l.Latest())\n\n\t// Test deleting last element\n\tuassert.True(t, l.Delete(0))\n\tuassert.Equal(t, 0, l.Size())\n\tuassert.True(t, l.head == nil)\n\tuassert.True(t, l.tail == nil)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "0Lj6WAUyyM2IN1m5VWtdYu7T7ujtgO+a4rj8qFfhhDTDkWS2f2vYZVzHSrU+eRpUHdwu2rrPpUMrj0hTdYwNBQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "fp", + "path": "gno.land/p/moul/fp", + "files": [ + { + "name": "fp.gno", + "body": "// Package fp provides functional programming utilities for Gno, enabling\n// transformations, filtering, and other operations on slices of any.\n//\n// Example of chaining operations:\n//\n//\tnumbers := []any{1, 2, 3, 4, 5, 6}\n//\n//\t// Define predicates, mappers and reducers\n//\tisEven := func(v any) bool { return v.(int)%2 == 0 }\n//\tdouble := func(v any) any { return v.(int) * 2 }\n//\tsum := func(a, b any) any { return a.(int) + b.(int) }\n//\n//\t// Chain operations: filter even numbers, double them, then sum\n//\tevenNums := Filter(numbers, isEven) // [2, 4, 6]\n//\tdoubled := Map(evenNums, double) // [4, 8, 12]\n//\tresult := Reduce(doubled, sum, 0) // 24\n//\n//\t// Alternative: group by even/odd, then get even numbers\n//\tbyMod2 := func(v any) any { return v.(int) % 2 }\n//\tgrouped := GroupBy(numbers, byMod2) // {0: [2,4,6], 1: [1,3,5]}\n//\tevens := grouped[0] // [2,4,6]\npackage fp\n\n// Mapper is a function type that maps an element to another element.\ntype Mapper func(any) any\n\n// Predicate is a function type that evaluates a condition on an element.\ntype Predicate func(any) bool\n\n// Reducer is a function type that reduces two elements to a single value.\ntype Reducer func(any, any) any\n\n// Filter filters elements from the slice that satisfy the given predicate.\n//\n// Example:\n//\n//\tnumbers := []any{-1, 0, 1, 2}\n//\tisPositive := func(v any) bool { return v.(int) \u003e 0 }\n//\tresult := Filter(numbers, isPositive) // [1, 2]\nfunc Filter(values []any, fn Predicate) []any {\n\tresult := []any{}\n\tfor _, v := range values {\n\t\tif fn(v) {\n\t\t\tresult = append(result, v)\n\t\t}\n\t}\n\treturn result\n}\n\n// Map applies a function to each element in the slice.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3}\n//\ttoString := func(v any) any { return fmt.Sprintf(\"%d\", v) }\n//\tresult := Map(numbers, toString) // [\"1\", \"2\", \"3\"]\nfunc Map(values []any, fn Mapper) []any {\n\tresult := make([]any, len(values))\n\tfor i, v := range values {\n\t\tresult[i] = fn(v)\n\t}\n\treturn result\n}\n\n// Reduce reduces a slice to a single value by applying a function.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3, 4}\n//\tsum := func(a, b any) any { return a.(int) + b.(int) }\n//\tresult := Reduce(numbers, sum, 0) // 10\nfunc Reduce(values []any, fn Reducer, initial any) any {\n\tacc := initial\n\tfor _, v := range values {\n\t\tacc = fn(acc, v)\n\t}\n\treturn acc\n}\n\n// FlatMap maps each element to a collection and flattens the results.\n//\n// Example:\n//\n//\twords := []any{\"hello\", \"world\"}\n//\tsplit := func(v any) any {\n//\t chars := []any{}\n//\t for _, c := range v.(string) {\n//\t chars = append(chars, string(c))\n//\t }\n//\t return chars\n//\t}\n//\tresult := FlatMap(words, split) // [\"h\",\"e\",\"l\",\"l\",\"o\",\"w\",\"o\",\"r\",\"l\",\"d\"]\nfunc FlatMap(values []any, fn Mapper) []any {\n\tresult := []any{}\n\tfor _, v := range values {\n\t\tinner := fn(v).([]any)\n\t\tresult = append(result, inner...)\n\t}\n\treturn result\n}\n\n// All returns true if all elements satisfy the predicate.\n//\n// Example:\n//\n//\tnumbers := []any{2, 4, 6, 8}\n//\tisEven := func(v any) bool { return v.(int)%2 == 0 }\n//\tresult := All(numbers, isEven) // true\nfunc All(values []any, fn Predicate) bool {\n\tfor _, v := range values {\n\t\tif !fn(v) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// Any returns true if at least one element satisfies the predicate.\n//\n// Example:\n//\n//\tnumbers := []any{1, 3, 4, 7}\n//\tisEven := func(v any) bool { return v.(int)%2 == 0 }\n//\tresult := Any(numbers, isEven) // true (4 is even)\nfunc Any(values []any, fn Predicate) bool {\n\tfor _, v := range values {\n\t\tif fn(v) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// None returns true if no elements satisfy the predicate.\n//\n// Example:\n//\n//\tnumbers := []any{1, 3, 5, 7}\n//\tisEven := func(v any) bool { return v.(int)%2 == 0 }\n//\tresult := None(numbers, isEven) // true (no even numbers)\nfunc None(values []any, fn Predicate) bool {\n\tfor _, v := range values {\n\t\tif fn(v) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// Chunk splits a slice into chunks of the given size.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3, 4, 5}\n//\tresult := Chunk(numbers, 2) // [[1,2], [3,4], [5]]\nfunc Chunk(values []any, size int) [][]any {\n\tif size \u003c= 0 {\n\t\treturn nil\n\t}\n\tvar chunks [][]any\n\tfor i := 0; i \u003c len(values); i += size {\n\t\tend := i + size\n\t\tif end \u003e len(values) {\n\t\t\tend = len(values)\n\t\t}\n\t\tchunks = append(chunks, values[i:end])\n\t}\n\treturn chunks\n}\n\n// Find returns the first element that satisfies the predicate and a boolean indicating if an element was found.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3, 4}\n//\tisEven := func(v any) bool { return v.(int)%2 == 0 }\n//\tresult, found := Find(numbers, isEven) // 2, true\nfunc Find(values []any, fn Predicate) (any, bool) {\n\tfor _, v := range values {\n\t\tif fn(v) {\n\t\t\treturn v, true\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// Reverse reverses the order of elements in a slice.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3}\n//\tresult := Reverse(numbers) // [3, 2, 1]\nfunc Reverse(values []any) []any {\n\tresult := make([]any, len(values))\n\tfor i, v := range values {\n\t\tresult[len(values)-1-i] = v\n\t}\n\treturn result\n}\n\n// Zip combines two slices into a slice of pairs. If the slices have different lengths,\n// extra elements from the longer slice are ignored.\n//\n// Example:\n//\n//\ta := []any{1, 2, 3}\n//\tb := []any{\"a\", \"b\", \"c\"}\n//\tresult := Zip(a, b) // [[1,\"a\"], [2,\"b\"], [3,\"c\"]]\nfunc Zip(a, b []any) [][2]any {\n\tlength := min(len(a), len(b))\n\tresult := make([][2]any, length)\n\tfor i := 0; i \u003c length; i++ {\n\t\tresult[i] = [2]any{a[i], b[i]}\n\t}\n\treturn result\n}\n\n// Unzip splits a slice of pairs into two separate slices.\n//\n// Example:\n//\n//\tpairs := [][2]any{{1,\"a\"}, {2,\"b\"}, {3,\"c\"}}\n//\tnumbers, letters := Unzip(pairs) // [1,2,3], [\"a\",\"b\",\"c\"]\nfunc Unzip(pairs [][2]any) ([]any, []any) {\n\ta := make([]any, len(pairs))\n\tb := make([]any, len(pairs))\n\tfor i, pair := range pairs {\n\t\ta[i] = pair[0]\n\t\tb[i] = pair[1]\n\t}\n\treturn a, b\n}\n\n// GroupBy groups elements based on a key returned by a Mapper.\n//\n// Example:\n//\n//\tnumbers := []any{1, 2, 3, 4, 5, 6}\n//\tbyMod3 := func(v any) any { return v.(int) % 3 }\n//\tresult := GroupBy(numbers, byMod3) // {0: [3,6], 1: [1,4], 2: [2,5]}\nfunc GroupBy(values []any, fn Mapper) map[any][]any {\n\tresult := make(map[any][]any)\n\tfor _, v := range values {\n\t\tkey := fn(v)\n\t\tresult[key] = append(result[key], v)\n\t}\n\treturn result\n}\n\n// Flatten flattens a slice of slices into a single slice.\n//\n// Example:\n//\n//\tnested := [][]any{{1,2}, {3,4}, {5}}\n//\tresult := Flatten(nested) // [1,2,3,4,5]\nfunc Flatten(values [][]any) []any {\n\tresult := []any{}\n\tfor _, v := range values {\n\t\tresult = append(result, v...)\n\t}\n\treturn result\n}\n\n// Helper functions\nfunc min(a, b int) int {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n" + }, + { + "name": "fp_test.gno", + "body": "package fp\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestMap(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []any\n\t\tfn func(any) any\n\t\texpected []any\n\t}{\n\t\t{\n\t\t\tname: \"multiply numbers by 2\",\n\t\t\tinput: []any{1, 2, 3},\n\t\t\tfn: func(v any) any { return v.(int) * 2 },\n\t\t\texpected: []any{2, 4, 6},\n\t\t},\n\t\t{\n\t\t\tname: \"empty slice\",\n\t\t\tinput: []any{},\n\t\t\tfn: func(v any) any { return v.(int) * 2 },\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname: \"convert numbers to strings\",\n\t\t\tinput: []any{1, 2, 3},\n\t\t\tfn: func(v any) any { return fmt.Sprintf(\"%d\", v.(int)) },\n\t\t\texpected: []any{\"1\", \"2\", \"3\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Map(tt.input, tt.fn)\n\t\t\tif !equalSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Map failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilter(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []any\n\t\tfn func(any) bool\n\t\texpected []any\n\t}{\n\t\t{\n\t\t\tname: \"filter even numbers\",\n\t\t\tinput: []any{1, 2, 3, 4},\n\t\t\tfn: func(v any) bool { return v.(int)%2 == 0 },\n\t\t\texpected: []any{2, 4},\n\t\t},\n\t\t{\n\t\t\tname: \"empty slice\",\n\t\t\tinput: []any{},\n\t\t\tfn: func(v any) bool { return v.(int)%2 == 0 },\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname: \"no matches\",\n\t\t\tinput: []any{1, 3, 5},\n\t\t\tfn: func(v any) bool { return v.(int)%2 == 0 },\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname: \"all matches\",\n\t\t\tinput: []any{2, 4, 6},\n\t\t\tfn: func(v any) bool { return v.(int)%2 == 0 },\n\t\t\texpected: []any{2, 4, 6},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Filter(tt.input, tt.fn)\n\t\t\tif !equalSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Filter failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReduce(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []any\n\t\tfn func(any, any) any\n\t\tinitial any\n\t\texpected any\n\t}{\n\t\t{\n\t\t\tname: \"sum numbers\",\n\t\t\tinput: []any{1, 2, 3},\n\t\t\tfn: func(a, b any) any { return a.(int) + b.(int) },\n\t\t\tinitial: 0,\n\t\t\texpected: 6,\n\t\t},\n\t\t{\n\t\t\tname: \"empty slice\",\n\t\t\tinput: []any{},\n\t\t\tfn: func(a, b any) any { return a.(int) + b.(int) },\n\t\t\tinitial: 0,\n\t\t\texpected: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"concatenate strings\",\n\t\t\tinput: []any{\"a\", \"b\", \"c\"},\n\t\t\tfn: func(a, b any) any { return a.(string) + b.(string) },\n\t\t\tinitial: \"\",\n\t\t\texpected: \"abc\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Reduce(tt.input, tt.fn, tt.initial)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"Reduce failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFlatMap(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []any\n\t\tfn func(any) any\n\t\texpected []any\n\t}{\n\t\t{\n\t\t\tname: \"split words into chars\",\n\t\t\tinput: []any{\"go\", \"fn\"},\n\t\t\tfn: func(word any) any {\n\t\t\t\tchars := []any{}\n\t\t\t\tfor _, c := range word.(string) {\n\t\t\t\t\tchars = append(chars, string(c))\n\t\t\t\t}\n\t\t\t\treturn chars\n\t\t\t},\n\t\t\texpected: []any{\"g\", \"o\", \"f\", \"n\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty string handling\",\n\t\t\tinput: []any{\"\", \"a\", \"\"},\n\t\t\tfn: func(word any) any {\n\t\t\t\tchars := []any{}\n\t\t\t\tfor _, c := range word.(string) {\n\t\t\t\t\tchars = append(chars, string(c))\n\t\t\t\t}\n\t\t\t\treturn chars\n\t\t\t},\n\t\t\texpected: []any{\"a\"},\n\t\t},\n\t\t{\n\t\t\tname: \"nil handling\",\n\t\t\tinput: []any{nil, \"a\", nil},\n\t\t\tfn: func(word any) any {\n\t\t\t\tif word == nil {\n\t\t\t\t\treturn []any{}\n\t\t\t\t}\n\t\t\t\treturn []any{word}\n\t\t\t},\n\t\t\texpected: []any{\"a\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty slice result\",\n\t\t\tinput: []any{\"\", \"\", \"\"},\n\t\t\tfn: func(word any) any {\n\t\t\t\treturn []any{}\n\t\t\t},\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname: \"nested array flattening\",\n\t\t\tinput: []any{1, 2, 3},\n\t\t\tfn: func(n any) any {\n\t\t\t\treturn []any{n, n}\n\t\t\t},\n\t\t\texpected: []any{1, 1, 2, 2, 3, 3},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := FlatMap(tt.input, tt.fn)\n\t\t\tif !equalSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"FlatMap failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAllAnyNone(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []any\n\t\tfn func(any) bool\n\t\texpectedAll bool\n\t\texpectedAny bool\n\t\texpectedNone bool\n\t}{\n\t\t{\n\t\t\tname: \"all even numbers\",\n\t\t\tinput: []any{2, 4, 6, 8},\n\t\t\tfn: func(x any) bool { return x.(int)%2 == 0 },\n\t\t\texpectedAll: true,\n\t\t\texpectedAny: true,\n\t\t\texpectedNone: false,\n\t\t},\n\t\t{\n\t\t\tname: \"no even numbers\",\n\t\t\tinput: []any{1, 3, 5, 7},\n\t\t\tfn: func(x any) bool { return x.(int)%2 == 0 },\n\t\t\texpectedAll: false,\n\t\t\texpectedAny: false,\n\t\t\texpectedNone: true,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed even/odd numbers\",\n\t\t\tinput: []any{1, 2, 3, 4},\n\t\t\tfn: func(x any) bool { return x.(int)%2 == 0 },\n\t\t\texpectedAll: false,\n\t\t\texpectedAny: true,\n\t\t\texpectedNone: false,\n\t\t},\n\t\t{\n\t\t\tname: \"empty slice\",\n\t\t\tinput: []any{},\n\t\t\tfn: func(x any) bool { return x.(int)%2 == 0 },\n\t\t\texpectedAll: true, // vacuously true\n\t\t\texpectedAny: false, // vacuously false\n\t\t\texpectedNone: true, // vacuously true\n\t\t},\n\t\t{\n\t\t\tname: \"nil predicate handling\",\n\t\t\tinput: []any{nil, nil, nil},\n\t\t\tfn: func(x any) bool { return x == nil },\n\t\t\texpectedAll: true,\n\t\t\texpectedAny: true,\n\t\t\texpectedNone: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresultAll := All(tt.input, tt.fn)\n\t\t\tif resultAll != tt.expectedAll {\n\t\t\t\tt.Errorf(\"All failed, expected %v, got %v\", tt.expectedAll, resultAll)\n\t\t\t}\n\n\t\t\tresultAny := Any(tt.input, tt.fn)\n\t\t\tif resultAny != tt.expectedAny {\n\t\t\t\tt.Errorf(\"Any failed, expected %v, got %v\", tt.expectedAny, resultAny)\n\t\t\t}\n\n\t\t\tresultNone := None(tt.input, tt.fn)\n\t\t\tif resultNone != tt.expectedNone {\n\t\t\t\tt.Errorf(\"None failed, expected %v, got %v\", tt.expectedNone, resultNone)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestChunk(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []any\n\t\tsize int\n\t\texpected [][]any\n\t}{\n\t\t{\n\t\t\tname: \"normal chunks\",\n\t\t\tinput: []any{1, 2, 3, 4, 5},\n\t\t\tsize: 2,\n\t\t\texpected: [][]any{{1, 2}, {3, 4}, {5}},\n\t\t},\n\t\t{\n\t\t\tname: \"empty slice\",\n\t\t\tinput: []any{},\n\t\t\tsize: 2,\n\t\t\texpected: [][]any{},\n\t\t},\n\t\t{\n\t\t\tname: \"chunk size equals length\",\n\t\t\tinput: []any{1, 2, 3},\n\t\t\tsize: 3,\n\t\t\texpected: [][]any{{1, 2, 3}},\n\t\t},\n\t\t{\n\t\t\tname: \"chunk size larger than length\",\n\t\t\tinput: []any{1, 2},\n\t\t\tsize: 3,\n\t\t\texpected: [][]any{{1, 2}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Chunk(tt.input, tt.size)\n\t\t\tif !equalNestedSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Chunk failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFind(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []any\n\t\tfn func(any) bool\n\t\texpected any\n\t\tshouldFound bool\n\t}{\n\t\t{\n\t\t\tname: \"find first number greater than 2\",\n\t\t\tinput: []any{1, 2, 3, 4},\n\t\t\tfn: func(v any) bool { return v.(int) \u003e 2 },\n\t\t\texpected: 3,\n\t\t\tshouldFound: true,\n\t\t},\n\t\t{\n\t\t\tname: \"empty slice\",\n\t\t\tinput: []any{},\n\t\t\tfn: func(v any) bool { return v.(int) \u003e 2 },\n\t\t\texpected: nil,\n\t\t\tshouldFound: false,\n\t\t},\n\t\t{\n\t\t\tname: \"no match\",\n\t\t\tinput: []any{1, 2},\n\t\t\tfn: func(v any) bool { return v.(int) \u003e 10 },\n\t\t\texpected: nil,\n\t\t\tshouldFound: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult, found := Find(tt.input, tt.fn)\n\t\t\tif found != tt.shouldFound {\n\t\t\t\tt.Errorf(\"Find failed, expected found=%v, got found=%v\", tt.shouldFound, found)\n\t\t\t}\n\t\t\tif found \u0026\u0026 result != tt.expected {\n\t\t\t\tt.Errorf(\"Find failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReverse(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []any\n\t\texpected []any\n\t}{\n\t\t{\n\t\t\tname: \"normal sequence\",\n\t\t\tinput: []any{1, 2, 3, 4},\n\t\t\texpected: []any{4, 3, 2, 1},\n\t\t},\n\t\t{\n\t\t\tname: \"empty slice\",\n\t\t\tinput: []any{},\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname: \"single element\",\n\t\t\tinput: []any{1},\n\t\t\texpected: []any{1},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed types\",\n\t\t\tinput: []any{1, \"a\", true, 2.5},\n\t\t\texpected: []any{2.5, true, \"a\", 1},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Reverse(tt.input)\n\t\t\tif !equalSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Reverse failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestZipUnzip(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\ta []any\n\t\tb []any\n\t\texpectedZip [][2]any\n\t\texpectedA []any\n\t\texpectedB []any\n\t}{\n\t\t{\n\t\t\tname: \"normal case\",\n\t\t\ta: []any{1, 2, 3},\n\t\t\tb: []any{\"a\", \"b\", \"c\"},\n\t\t\texpectedZip: [][2]any{{1, \"a\"}, {2, \"b\"}, {3, \"c\"}},\n\t\t\texpectedA: []any{1, 2, 3},\n\t\t\texpectedB: []any{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty slices\",\n\t\t\ta: []any{},\n\t\t\tb: []any{},\n\t\t\texpectedZip: [][2]any{},\n\t\t\texpectedA: []any{},\n\t\t\texpectedB: []any{},\n\t\t},\n\t\t{\n\t\t\tname: \"different lengths - a shorter\",\n\t\t\ta: []any{1, 2},\n\t\t\tb: []any{\"a\", \"b\", \"c\"},\n\t\t\texpectedZip: [][2]any{{1, \"a\"}, {2, \"b\"}},\n\t\t\texpectedA: []any{1, 2},\n\t\t\texpectedB: []any{\"a\", \"b\"},\n\t\t},\n\t\t{\n\t\t\tname: \"different lengths - b shorter\",\n\t\t\ta: []any{1, 2, 3},\n\t\t\tb: []any{\"a\"},\n\t\t\texpectedZip: [][2]any{{1, \"a\"}},\n\t\t\texpectedA: []any{1},\n\t\t\texpectedB: []any{\"a\"},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed types\",\n\t\t\ta: []any{1, true, \"x\"},\n\t\t\tb: []any{2.5, false, \"y\"},\n\t\t\texpectedZip: [][2]any{{1, 2.5}, {true, false}, {\"x\", \"y\"}},\n\t\t\texpectedA: []any{1, true, \"x\"},\n\t\t\texpectedB: []any{2.5, false, \"y\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tzipped := Zip(tt.a, tt.b)\n\t\t\tif len(zipped) != len(tt.expectedZip) {\n\t\t\t\tt.Errorf(\"Zip failed, expected length %v, got %v\", len(tt.expectedZip), len(zipped))\n\t\t\t}\n\t\t\tfor i, pair := range zipped {\n\t\t\t\tif pair[0] != tt.expectedZip[i][0] || pair[1] != tt.expectedZip[i][1] {\n\t\t\t\t\tt.Errorf(\"Zip failed at index %d, expected %v, got %v\", i, tt.expectedZip[i], pair)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tunzippedA, unzippedB := Unzip(zipped)\n\t\t\tif !equalSlices(unzippedA, tt.expectedA) {\n\t\t\t\tt.Errorf(\"Unzip failed for slice A, expected %v, got %v\", tt.expectedA, unzippedA)\n\t\t\t}\n\t\t\tif !equalSlices(unzippedB, tt.expectedB) {\n\t\t\t\tt.Errorf(\"Unzip failed for slice B, expected %v, got %v\", tt.expectedB, unzippedB)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGroupBy(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput []any\n\t\tfn func(any) any\n\t\texpected map[any][]any\n\t}{\n\t\t{\n\t\t\tname: \"group by even/odd\",\n\t\t\tinput: []any{1, 2, 3, 4, 5, 6},\n\t\t\tfn: func(v any) any { return v.(int) % 2 },\n\t\t\texpected: map[any][]any{\n\t\t\t\t0: {2, 4, 6},\n\t\t\t\t1: {1, 3, 5},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty slice\",\n\t\t\tinput: []any{},\n\t\t\tfn: func(v any) any { return v.(int) % 2 },\n\t\t\texpected: map[any][]any{},\n\t\t},\n\t\t{\n\t\t\tname: \"single group\",\n\t\t\tinput: []any{2, 4, 6},\n\t\t\tfn: func(v any) any { return v.(int) % 2 },\n\t\t\texpected: map[any][]any{\n\t\t\t\t0: {2, 4, 6},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"group by type\",\n\t\t\tinput: []any{1, \"a\", 2, \"b\", true},\n\t\t\tfn: func(v any) any {\n\t\t\t\tswitch v.(type) {\n\t\t\t\tcase int:\n\t\t\t\t\treturn \"int\"\n\t\t\t\tcase string:\n\t\t\t\t\treturn \"string\"\n\t\t\t\tdefault:\n\t\t\t\t\treturn \"other\"\n\t\t\t\t}\n\t\t\t},\n\t\t\texpected: map[any][]any{\n\t\t\t\t\"int\": {1, 2},\n\t\t\t\t\"string\": {\"a\", \"b\"},\n\t\t\t\t\"other\": {true},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := GroupBy(tt.input, tt.fn)\n\t\t\tif len(result) != len(tt.expected) {\n\t\t\t\tt.Errorf(\"GroupBy failed, expected %d groups, got %d\", len(tt.expected), len(result))\n\t\t\t}\n\t\t\tfor k, v := range tt.expected {\n\t\t\t\tif !equalSlices(result[k], v) {\n\t\t\t\t\tt.Errorf(\"GroupBy failed for key %v, expected %v, got %v\", k, v, result[k])\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFlatten(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tinput [][]any\n\t\texpected []any\n\t}{\n\t\t{\n\t\t\tname: \"normal nested slices\",\n\t\t\tinput: [][]any{{1, 2}, {3, 4}, {5}},\n\t\t\texpected: []any{1, 2, 3, 4, 5},\n\t\t},\n\t\t{\n\t\t\tname: \"empty outer slice\",\n\t\t\tinput: [][]any{},\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname: \"empty inner slices\",\n\t\t\tinput: [][]any{{}, {}, {}},\n\t\t\texpected: []any{},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed types\",\n\t\t\tinput: [][]any{{1, \"a\"}, {true, 2.5}, {nil}},\n\t\t\texpected: []any{1, \"a\", true, 2.5, nil},\n\t\t},\n\t\t{\n\t\t\tname: \"single element slices\",\n\t\t\tinput: [][]any{{1}, {2}, {3}},\n\t\t\texpected: []any{1, 2, 3},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := Flatten(tt.input)\n\t\t\tif !equalSlices(result, tt.expected) {\n\t\t\t\tt.Errorf(\"Flatten failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestContains(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tslice []any\n\t\titem any\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"contains integer\",\n\t\t\tslice: []any{1, 2, 3},\n\t\t\titem: 2,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"does not contain integer\",\n\t\t\tslice: []any{1, 2, 3},\n\t\t\titem: 4,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"contains string\",\n\t\t\tslice: []any{\"a\", \"b\", \"c\"},\n\t\t\titem: \"b\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"empty slice\",\n\t\t\tslice: []any{},\n\t\t\titem: 1,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"contains nil\",\n\t\t\tslice: []any{1, nil, 3},\n\t\t\titem: nil,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed types\",\n\t\t\tslice: []any{1, \"a\", true},\n\t\t\titem: true,\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tresult := contains(tt.slice, tt.item)\n\t\t\tif result != tt.expected {\n\t\t\t\tt.Errorf(\"contains failed, expected %v, got %v\", tt.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helper function for testing\nfunc contains(slice []any, item any) bool {\n\tfor _, v := range slice {\n\t\tif v == item {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Helper functions for comparing slices\nfunc equalSlices(a, b []any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc equalNestedSlices(a, b [][]any) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif !equalSlices(a[i], b[i]) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "nlCJzr/lGFfJV37P0YnpUL0axb1tKi6sdqy2UqdyMgEMyLNNaO2PK8THlqKde6dLv7P02vsLpDvSR4dpivsdBQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "txlink", + "path": "gno.land/p/moul/txlink", + "files": [ + { + "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 primary function, Call, is designed to produce markdown links for\n// transaction functions in the current \"relative realm\". By specifying a custom\n// Realm, you can generate links that either use the current realm path or a\n// fully qualified path for another realm.\n//\n// This package is a streamlined alternative to helplink, providing similar\n// functionality for transaction links without the full feature set of helplink.\npackage txlink\n\nimport (\n\t\"net/url\"\n\t\"std\"\n\t\"strings\"\n)\n\nvar chainDomain = std.ChainDomain()\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// Realm represents a specific realm for generating tx links.\ntype Realm string\n\n// prefix returns the URL prefix for the realm.\nfunc (r Realm) prefix() string {\n\t// relative\n\tif r == \"\" {\n\t\tcurPath := std.CurrentRealm().PkgPath()\n\t\treturn strings.TrimPrefix(curPath, chainDomain)\n\t}\n\n\t// local realm -\u003e /realm\n\trealm := string(r)\n\tif strings.HasPrefix(realm, chainDomain) {\n\t\treturn strings.TrimPrefix(realm, 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\tvalues.Add(\"error\", \"odd number of arguments\")\n\t} else {\n\t\t// Add key-value pairs to values\n\t\tfor i := 0; i \u003c len(args); i += 2 {\n\t\t\tkey := args[i]\n\t\t\tvalue := args[i+1]\n\t\t\tvalues.Add(key, value)\n\t\t}\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\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestCall(t *testing.T) {\n\tcd := std.ChainDomain()\n\n\ttests := []struct {\n\t\tfn string\n\t\targs []string\n\t\twant string\n\t\trealm Realm\n\t}{\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"$help\u0026func=testFunc\u0026key=value\", \"\"},\n\t\t{\"noArgsFunc\", []string{}, \"$help\u0026func=noArgsFunc\", \"\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"$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\"}, \"$help\u0026func=test\u0026key=hello+world\", \"\"},\n\t\t{\"test\", []string{\"key\", \"a\u0026b=c\"}, \"$help\u0026func=test\u0026key=a%26b%3Dc\", \"\"},\n\t\t{\"test\", []string{\"key\", \"\"}, \"$help\u0026func=test\u0026key=\", \"\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\ttitle := string(tt.realm) + \"_\" + tt.fn\n\t\tt.Run(title, func(t *testing.T) {\n\t\t\tgot := tt.realm.Call(tt.fn, tt.args...)\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "jD8vh3hoDawC0UbUvGyNxtX4Gvrf8x2GJ+SlZg+XCK9xlfJ+6uBCaEhd1ftfA6vZcnGErF2LteqS+So5NrZZAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "helplink", + "path": "gno.land/p/moul/helplink", + "files": [ + { + "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\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/moul/txlink\"\n)\n\nvar chainDomain = std.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\trealm := string(r)\n\tif strings.HasPrefix(realm, chainDomain) {\n\t\treturn strings.TrimPrefix(realm, chainDomain)\n\t}\n\n\t// remote realm -\u003e https://remote.land/realm\n\treturn \"https://\" + string(r)\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\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestFunc(t *testing.T) {\n\tcd := std.ChainDomain()\n\ttests := []struct {\n\t\ttitle string\n\t\tfn string\n\t\targs []string\n\t\twant string\n\t\trealm Realm\n\t}{\n\t\t{\"Example\", \"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"[Example]($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]($help\u0026func=testFunc\u0026key=value)\", \"\"},\n\t\t{\"No Args\", \"noArgsFunc\", []string{}, \"[No Args]($help\u0026func=noArgsFunc)\", \"\"},\n\t\t{\"Odd Args\", \"oddArgsFunc\", []string{\"key\"}, \"[Odd Args]($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\tgot := tt.realm.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 := std.ChainDomain()\n\ttests := []struct {\n\t\tfn string\n\t\targs []string\n\t\twant string\n\t\trealm Realm\n\t}{\n\t\t{\"foo\", []string{\"bar\", \"1\", \"baz\", \"2\"}, \"$help\u0026func=foo\u0026bar=1\u0026baz=2\", \"\"},\n\t\t{\"testFunc\", []string{\"key\", \"value\"}, \"$help\u0026func=testFunc\u0026key=value\", \"\"},\n\t\t{\"noArgsFunc\", []string{}, \"$help\u0026func=noArgsFunc\", \"\"},\n\t\t{\"oddArgsFunc\", []string{\"key\"}, \"$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\tgot := tt.realm.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 := std.ChainDomain()\n\ttests := []struct {\n\t\trealm 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), func(t *testing.T) {\n\t\t\tgot := tt.realm.Home()\n\t\t\turequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "vAOjCh8uN4PggdG4OvNyHdzSVAxLfXnqc4DzHR3XNdw7lZhWFE3HiOaz3ftShwVSuqXS7VNDsJj9yH31xHI7AA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "memo", + "path": "gno.land/p/moul/memo", + "files": [ + { + "name": "memo.gno", + "body": "// Package memo provides a simple memoization utility to cache function results.\n//\n// The package offers a Memoizer type that can cache function results based on keys,\n// with optional validation of cached values. This is useful for expensive computations\n// that need to be cached and potentially invalidated based on custom conditions.\n//\n// /!\\ Important Warning for Gno Usage:\n// In Gno, storage updates only persist during transactions. This means:\n// - Cache entries created during queries will NOT persist\n// - Creating cache entries during queries will actually decrease performance\n// as it wastes resources trying to save data that won't be saved\n//\n// Best Practices:\n// - Use this pattern in transaction-driven contexts rather than query/render scenarios\n// - Consider controlled cache updates, e.g., by specific accounts (like oracles)\n// - Ideal for cases where cache updates happen every N blocks or on specific events\n// - Carefully evaluate if caching will actually improve performance in your use case\n//\n// Basic usage example:\n//\n//\tm := memo.New()\n//\n//\t// Cache expensive computation\n//\tresult := m.Memoize(\"key\", func() any {\n//\t // expensive operation\n//\t return \"computed-value\"\n//\t})\n//\n//\t// Subsequent calls with same key return cached result\n//\tresult = m.Memoize(\"key\", func() any {\n//\t // function won't be called, cached value is returned\n//\t return \"computed-value\"\n//\t})\n//\n// Example with validation:\n//\n//\ttype TimestampedValue struct {\n//\t Value string\n//\t Timestamp time.Time\n//\t}\n//\n//\tm := memo.New()\n//\n//\t// Cache value with timestamp\n//\tresult := m.MemoizeWithValidator(\n//\t \"key\",\n//\t func() any {\n//\t return TimestampedValue{\n//\t Value: \"data\",\n//\t Timestamp: time.Now(),\n//\t }\n//\t },\n//\t func(cached any) bool {\n//\t // Validate that the cached value is not older than 1 hour\n//\t if tv, ok := cached.(TimestampedValue); ok {\n//\t return time.Since(tv.Timestamp) \u003c time.Hour\n//\t }\n//\t return false\n//\t },\n//\t)\npackage memo\n\nimport (\n\t\"gno.land/p/demo/btree\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Record implements the btree.Record interface for our cache entries\ntype cacheEntry struct {\n\tkey any\n\tvalue any\n}\n\n// Less implements btree.Record interface\nfunc (e cacheEntry) Less(than btree.Record) bool {\n\t// Convert the other record to cacheEntry\n\tother := than.(cacheEntry)\n\t// Compare string representations of keys for consistent ordering\n\treturn ufmt.Sprintf(\"%v\", e.key) \u003c ufmt.Sprintf(\"%v\", other.key)\n}\n\n// Memoizer is a structure to handle memoization of function results.\ntype Memoizer struct {\n\tcache *btree.BTree\n}\n\n// New creates a new Memoizer instance.\nfunc New() *Memoizer {\n\treturn \u0026Memoizer{\n\t\tcache: btree.New(),\n\t}\n}\n\n// Memoize ensures the result of the given function is cached for the specified key.\nfunc (m *Memoizer) Memoize(key any, fn func() any) any {\n\tentry := cacheEntry{key: key}\n\tif found := m.cache.Get(entry); found != nil {\n\t\treturn found.(cacheEntry).value\n\t}\n\n\tvalue := fn()\n\tm.cache.Insert(cacheEntry{key: key, value: value})\n\treturn value\n}\n\n// MemoizeWithValidator ensures the result is cached and valid according to the validator function.\nfunc (m *Memoizer) MemoizeWithValidator(key any, fn func() any, isValid func(any) bool) any {\n\tentry := cacheEntry{key: key}\n\tif found := m.cache.Get(entry); found != nil {\n\t\tcachedEntry := found.(cacheEntry)\n\t\tif isValid(cachedEntry.value) {\n\t\t\treturn cachedEntry.value\n\t\t}\n\t}\n\n\tvalue := fn()\n\tm.cache.Insert(cacheEntry{key: key, value: value})\n\treturn value\n}\n\n// Invalidate removes the cached value for the specified key.\nfunc (m *Memoizer) Invalidate(key any) {\n\tm.cache.Delete(cacheEntry{key: key})\n}\n\n// Clear clears all cached values.\nfunc (m *Memoizer) Clear() {\n\tm.cache.Clear(true)\n}\n\n// Size returns the number of items currently in the cache.\nfunc (m *Memoizer) Size() int {\n\treturn m.cache.Len()\n}\n" + }, + { + "name": "memo_test.gno", + "body": "package memo\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\ntype timestampedValue struct {\n\tvalue any\n\ttimestamp time.Time\n}\n\n// complexKey is used to test struct keys\ntype complexKey struct {\n\tID int\n\tName string\n}\n\nfunc TestMemoize(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tkey any\n\t\tvalue any\n\t\tcallCount *int\n\t}{\n\t\t{\n\t\t\tname: \"string key and value\",\n\t\t\tkey: \"test-key\",\n\t\t\tvalue: \"test-value\",\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t{\n\t\t\tname: \"int key and value\",\n\t\t\tkey: 42,\n\t\t\tvalue: 123,\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t{\n\t\t\tname: \"mixed types\",\n\t\t\tkey: \"number\",\n\t\t\tvalue: 42,\n\t\t\tcallCount: new(int),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := New()\n\t\t\tif m.Size() != 0 {\n\t\t\t\tt.Errorf(\"Initial size = %d, want 0\", m.Size())\n\t\t\t}\n\n\t\t\tfn := func() any {\n\t\t\t\t*tt.callCount++\n\t\t\t\treturn tt.value\n\t\t\t}\n\n\t\t\t// First call should compute\n\t\t\tresult := m.Memoize(tt.key, fn)\n\t\t\tif result != tt.value {\n\t\t\t\tt.Errorf(\"Memoize() = %v, want %v\", result, tt.value)\n\t\t\t}\n\t\t\tif *tt.callCount != 1 {\n\t\t\t\tt.Errorf(\"Function called %d times, want 1\", *tt.callCount)\n\t\t\t}\n\t\t\tif m.Size() != 1 {\n\t\t\t\tt.Errorf(\"Size after first call = %d, want 1\", m.Size())\n\t\t\t}\n\n\t\t\t// Second call should use cache\n\t\t\tresult = m.Memoize(tt.key, fn)\n\t\t\tif result != tt.value {\n\t\t\t\tt.Errorf(\"Memoize() second call = %v, want %v\", result, tt.value)\n\t\t\t}\n\t\t\tif *tt.callCount != 1 {\n\t\t\t\tt.Errorf(\"Function called %d times, want 1\", *tt.callCount)\n\t\t\t}\n\t\t\tif m.Size() != 1 {\n\t\t\t\tt.Errorf(\"Size after second call = %d, want 1\", m.Size())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMemoizeWithValidator(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tkey any\n\t\tvalue any\n\t\tvalidDuration time.Duration\n\t\twaitDuration time.Duration\n\t\texpectedCalls int\n\t\tshouldRecompute bool\n\t}{\n\t\t{\n\t\t\tname: \"valid cache\",\n\t\t\tkey: \"key1\",\n\t\t\tvalue: \"value1\",\n\t\t\tvalidDuration: time.Hour,\n\t\t\twaitDuration: time.Millisecond,\n\t\t\texpectedCalls: 1,\n\t\t\tshouldRecompute: false,\n\t\t},\n\t\t{\n\t\t\tname: \"expired cache\",\n\t\t\tkey: \"key2\",\n\t\t\tvalue: \"value2\",\n\t\t\tvalidDuration: time.Millisecond,\n\t\t\twaitDuration: time.Millisecond * 2,\n\t\t\texpectedCalls: 2,\n\t\t\tshouldRecompute: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := New()\n\t\t\tcallCount := 0\n\n\t\t\tfn := func() any {\n\t\t\t\tcallCount++\n\t\t\t\treturn timestampedValue{\n\t\t\t\t\tvalue: tt.value,\n\t\t\t\t\ttimestamp: time.Now(),\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tisValid := func(cached any) bool {\n\t\t\t\tif tv, ok := cached.(timestampedValue); ok {\n\t\t\t\t\treturn time.Since(tv.timestamp) \u003c tt.validDuration\n\t\t\t\t}\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// First call\n\t\t\tresult := m.MemoizeWithValidator(tt.key, fn, isValid)\n\t\t\tif tv, ok := result.(timestampedValue); !ok || tv.value != tt.value {\n\t\t\t\tt.Errorf(\"MemoizeWithValidator() = %v, want value %v\", result, tt.value)\n\t\t\t}\n\n\t\t\t// Wait\n\t\t\ttesting.SkipHeights(10)\n\n\t\t\t// Second call\n\t\t\tresult = m.MemoizeWithValidator(tt.key, fn, isValid)\n\t\t\tif tv, ok := result.(timestampedValue); !ok || tv.value != tt.value {\n\t\t\t\tt.Errorf(\"MemoizeWithValidator() second call = %v, want value %v\", result, tt.value)\n\t\t\t}\n\n\t\t\tif callCount != tt.expectedCalls {\n\t\t\t\tt.Errorf(\"Function called %d times, want %d\", callCount, tt.expectedCalls)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInvalidate(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tkey any\n\t\tvalue any\n\t\tcallCount *int\n\t}{\n\t\t{\n\t\t\tname: \"invalidate existing key\",\n\t\t\tkey: \"test-key\",\n\t\t\tvalue: \"test-value\",\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t{\n\t\t\tname: \"invalidate non-existing key\",\n\t\t\tkey: \"missing-key\",\n\t\t\tvalue: \"test-value\",\n\t\t\tcallCount: new(int),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := New()\n\t\t\tfn := func() any {\n\t\t\t\t*tt.callCount++\n\t\t\t\treturn tt.value\n\t\t\t}\n\n\t\t\t// First call\n\t\t\tm.Memoize(tt.key, fn)\n\t\t\tif m.Size() != 1 {\n\t\t\t\tt.Errorf(\"Size after first call = %d, want 1\", m.Size())\n\t\t\t}\n\n\t\t\t// Invalidate\n\t\t\tm.Invalidate(tt.key)\n\t\t\tif m.Size() != 0 {\n\t\t\t\tt.Errorf(\"Size after invalidate = %d, want 0\", m.Size())\n\t\t\t}\n\n\t\t\t// Call again should recompute\n\t\t\tresult := m.Memoize(tt.key, fn)\n\t\t\tif result != tt.value {\n\t\t\t\tt.Errorf(\"Memoize() after invalidate = %v, want %v\", result, tt.value)\n\t\t\t}\n\t\t\tif *tt.callCount != 2 {\n\t\t\t\tt.Errorf(\"Function called %d times, want 2\", *tt.callCount)\n\t\t\t}\n\t\t\tif m.Size() != 1 {\n\t\t\t\tt.Errorf(\"Size after recompute = %d, want 1\", m.Size())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClear(t *testing.T) {\n\tm := New()\n\tcallCount := 0\n\n\tfn := func() any {\n\t\tcallCount++\n\t\treturn \"value\"\n\t}\n\n\t// Cache some values\n\tm.Memoize(\"key1\", fn)\n\tm.Memoize(\"key2\", fn)\n\n\tif callCount != 2 {\n\t\tt.Errorf(\"Initial calls = %d, want 2\", callCount)\n\t}\n\tif m.Size() != 2 {\n\t\tt.Errorf(\"Size after initial calls = %d, want 2\", m.Size())\n\t}\n\n\t// Clear cache\n\tm.Clear()\n\tif m.Size() != 0 {\n\t\tt.Errorf(\"Size after clear = %d, want 0\", m.Size())\n\t}\n\n\t// Recompute values\n\tm.Memoize(\"key1\", fn)\n\tm.Memoize(\"key2\", fn)\n\n\tif callCount != 4 {\n\t\tt.Errorf(\"Calls after clear = %d, want 4\", callCount)\n\t}\n\tif m.Size() != 2 {\n\t\tt.Errorf(\"Size after recompute = %d, want 2\", m.Size())\n\t}\n}\n\nfunc TestSize(t *testing.T) {\n\tm := New()\n\n\tif m.Size() != 0 {\n\t\tt.Errorf(\"Initial size = %d, want 0\", m.Size())\n\t}\n\n\tcallCount := 0\n\tfn := func() any {\n\t\tcallCount++\n\t\treturn \"value\"\n\t}\n\n\t// Add items\n\tm.Memoize(\"key1\", fn)\n\tif m.Size() != 1 {\n\t\tt.Errorf(\"Size after first insert = %d, want 1\", m.Size())\n\t}\n\n\tm.Memoize(\"key2\", fn)\n\tif m.Size() != 2 {\n\t\tt.Errorf(\"Size after second insert = %d, want 2\", m.Size())\n\t}\n\n\t// Duplicate key should not increase size\n\tm.Memoize(\"key1\", fn)\n\tif m.Size() != 2 {\n\t\tt.Errorf(\"Size after duplicate insert = %d, want 2\", m.Size())\n\t}\n\n\t// Remove item\n\tm.Invalidate(\"key1\")\n\tif m.Size() != 1 {\n\t\tt.Errorf(\"Size after invalidate = %d, want 1\", m.Size())\n\t}\n\n\t// Clear all\n\tm.Clear()\n\tif m.Size() != 0 {\n\t\tt.Errorf(\"Size after clear = %d, want 0\", m.Size())\n\t}\n}\n\nfunc TestMemoizeWithDifferentKeyTypes(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tkeys []any // Now an array of keys\n\t\tvalues []string // Corresponding values\n\t\tcallCount *int\n\t}{\n\t\t{\n\t\t\tname: \"integer keys\",\n\t\t\tkeys: []any{42, 43},\n\t\t\tvalues: []string{\"value-for-42\", \"value-for-43\"},\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t{\n\t\t\tname: \"float keys\",\n\t\t\tkeys: []any{3.14, 2.718},\n\t\t\tvalues: []string{\"value-for-pi\", \"value-for-e\"},\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t{\n\t\t\tname: \"bool keys\",\n\t\t\tkeys: []any{true, false},\n\t\t\tvalues: []string{\"value-for-true\", \"value-for-false\"},\n\t\t\tcallCount: new(int),\n\t\t},\n\t\t/*\n\t\t\t{\n\t\t\t\tname: \"struct keys\",\n\t\t\t\tkeys: []any{\n\t\t\t\t\tcomplexKey{ID: 1, Name: \"test1\"},\n\t\t\t\t\tcomplexKey{ID: 2, Name: \"test2\"},\n\t\t\t\t},\n\t\t\t\tvalues: []string{\"value-for-struct1\", \"value-for-struct2\"},\n\t\t\t\tcallCount: new(int),\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"nil and empty interface keys\",\n\t\t\t\tkeys: []any{nil, any(nil)},\n\t\t\t\tvalues: []string{\"value-for-nil\", \"value-for-empty-interface\"},\n\t\t\t\tcallCount: new(int),\n\t\t\t},\n\t\t*/\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tm := New()\n\n\t\t\t// Test both keys\n\t\t\tfor i, key := range tt.keys {\n\t\t\t\tvalue := tt.values[i]\n\t\t\t\tfn := func() any {\n\t\t\t\t\t*tt.callCount++\n\t\t\t\t\treturn value\n\t\t\t\t}\n\n\t\t\t\t// First call should compute\n\t\t\t\tresult := m.Memoize(key, fn)\n\t\t\t\tif result != value {\n\t\t\t\t\tt.Errorf(\"Memoize() for key %v = %v, want %v\", key, result, value)\n\t\t\t\t}\n\t\t\t\tif *tt.callCount != i+1 {\n\t\t\t\t\tt.Errorf(\"Function called %d times, want %d\", *tt.callCount, i+1)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify size includes both entries\n\t\t\tif m.Size() != 2 {\n\t\t\t\tt.Errorf(\"Size after both inserts = %d, want 2\", m.Size())\n\t\t\t}\n\n\t\t\t// Second call for each key should use cache\n\t\t\tfor i, key := range tt.keys {\n\t\t\t\tinitialCount := *tt.callCount\n\t\t\t\tresult := m.Memoize(key, func() any {\n\t\t\t\t\t*tt.callCount++\n\t\t\t\t\treturn \"should-not-be-called\"\n\t\t\t\t})\n\n\t\t\t\tif result != tt.values[i] {\n\t\t\t\t\tt.Errorf(\"Memoize() second call for key %v = %v, want %v\", key, result, tt.values[i])\n\t\t\t\t}\n\t\t\t\tif *tt.callCount != initialCount {\n\t\t\t\t\tt.Errorf(\"Cache miss for key %v\", key)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Test invalidate for each key\n\t\t\tfor i, key := range tt.keys {\n\t\t\t\tm.Invalidate(key)\n\t\t\t\tif m.Size() != 1-i {\n\t\t\t\t\tt.Errorf(\"Size after invalidate %d = %d, want %d\", i+1, m.Size(), 1-i)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMultipleKeyTypes(t *testing.T) {\n\tm := New()\n\tcallCount := 0\n\n\t// Insert different key types simultaneously (two of each type)\n\tkeys := []any{\n\t\t42, 43, // ints\n\t\t\"string-key1\", \"string-key2\", // strings\n\t\t3.14, 2.718, // floats\n\t\ttrue, false, // bools\n\t}\n\n\tfor i, key := range keys {\n\t\tvalue := i\n\t\tm.Memoize(key, func() any {\n\t\t\tcallCount++\n\t\t\treturn value\n\t\t})\n\t}\n\n\t// Verify size includes all entries\n\tif m.Size() != len(keys) {\n\t\tt.Errorf(\"Size = %d, want %d\", m.Size(), len(keys))\n\t}\n\n\t// Verify all values are cached correctly\n\tfor i, key := range keys {\n\t\tinitialCount := callCount\n\t\tresult := m.Memoize(key, func() any {\n\t\t\tcallCount++\n\t\t\treturn -1 // Should never be returned if cache works\n\t\t})\n\n\t\tif result != i {\n\t\t\tt.Errorf(\"Memoize(%v) = %v, want %v\", key, result, i)\n\t\t}\n\t\tif callCount != initialCount {\n\t\t\tt.Errorf(\"Cache miss for key %v\", key)\n\t\t}\n\t}\n\n\t// Test invalidation of pairs\n\tfor i := 0; i \u003c len(keys); i += 2 {\n\t\tm.Invalidate(keys[i])\n\t\tm.Invalidate(keys[i+1])\n\t\texpectedSize := len(keys) - (i + 2)\n\t\tif m.Size() != expectedSize {\n\t\t\tt.Errorf(\"Size after invalidating pair %d = %d, want %d\", i/2, m.Size(), expectedSize)\n\t\t}\n\t}\n\n\t// Clear and verify\n\tm.Clear()\n\tif m.Size() != 0 {\n\t\tt.Errorf(\"Size after clear = %d, want 0\", m.Size())\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "XjCZ/fVUWo5OsNdI/ytOYxpJK4x0HiqYYdsQqfEOgr+b358qCYPXYsedRMnFLW3+kJvC3guyonKuxnCrk756Ag==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "printfdebugging", + "path": "gno.land/p/demo/printfdebugging", + "files": [ + { + "name": "color.gno", + "body": "package printfdebugging\n\n// consts copied from https://github.com/fatih/color/blob/main/color.go\n\n// Attribute defines a single SGR Code\ntype Attribute int\n\nconst Escape = \"\\x1b\"\n\n// Base attributes\nconst (\n\tReset Attribute = iota\n\tBold\n\tFaint\n\tItalic\n\tUnderline\n\tBlinkSlow\n\tBlinkRapid\n\tReverseVideo\n\tConcealed\n\tCrossedOut\n)\n\nconst (\n\tResetBold Attribute = iota + 22\n\tResetItalic\n\tResetUnderline\n\tResetBlinking\n\t_\n\tResetReversed\n\tResetConcealed\n\tResetCrossedOut\n)\n\n// Foreground text colors\nconst (\n\tFgBlack Attribute = iota + 30\n\tFgRed\n\tFgGreen\n\tFgYellow\n\tFgBlue\n\tFgMagenta\n\tFgCyan\n\tFgWhite\n)\n\n// Foreground Hi-Intensity text colors\nconst (\n\tFgHiBlack Attribute = iota + 90\n\tFgHiRed\n\tFgHiGreen\n\tFgHiYellow\n\tFgHiBlue\n\tFgHiMagenta\n\tFgHiCyan\n\tFgHiWhite\n)\n\n// Background text colors\nconst (\n\tBgBlack Attribute = iota + 40\n\tBgRed\n\tBgGreen\n\tBgYellow\n\tBgBlue\n\tBgMagenta\n\tBgCyan\n\tBgWhite\n)\n\n// Background Hi-Intensity text colors\nconst (\n\tBgHiBlack Attribute = iota + 100\n\tBgHiRed\n\tBgHiGreen\n\tBgHiYellow\n\tBgHiBlue\n\tBgHiMagenta\n\tBgHiCyan\n\tBgHiWhite\n)\n" + }, + { + "name": "printfdebugging.gno", + "body": "// this package is a joke... or not.\npackage printfdebugging\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc BigRedLine(args ...string) {\n\tprintln(ufmt.Sprintf(\"%s[%dm####################################%s[%dm %s\",\n\t\tEscape, int(BgRed), Escape, int(Reset),\n\t\tstrings.Join(args, \" \"),\n\t))\n}\n\nfunc Success() {\n\tprintln(\" \\033[31mS\\033[33mU\\033[32mC\\033[36mC\\033[34mE\\033[35mS\\033[31mS\\033[0m \")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "6YHx5DlwVuhjicGAInco6cQ5r6lAa3Z44gkmyhQcuodyxuSjL4b49+U3hthznO1d3RTjZ84MEo6cTpuiArqgCA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "typeutil", + "path": "gno.land/p/moul/typeutil", + "files": [ + { + "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\"std\"\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, std.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 *std.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 std.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 []std.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 []std.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// - std.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 *std.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 std.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 []std.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 []std.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\"std\"\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 := std.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\", []std.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\", []std.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\", (*std.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 := std.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\", std.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\", (*std.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 := std.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\", std.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\", (*std.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: []std.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].(std.Address)\n\t\tba, ok2 := b[i].(std.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: []std.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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "RaRYi6mfF0BxvZVAGonedF8HMhPDh9ifIOhFxG1QY2MJ3xDLjtQOfXNRodNyFevcIBQevhsaLo7Ke9/8nRwODQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "ulist", + "path": "gno.land/p/moul/ulist", + "files": [ + { + "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/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/typeutil\"\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "5sGOeawVsss0TRSUMrBrPjY+oSZSWfAhowK67xtyRi2duvLjtBjUYp8WSX+xvBtxHi94vDKeJbgn3Zb63JWkCQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "lplist", + "path": "gno.land/p/moul/ulist/lplist", + "files": [ + { + "name": "lplist.gno", + "body": "// Package lplist provides a layered proxy implementation for lists that allows transparent\n// migration of data between different schema versions.\n//\n// LayeredProxyList wraps an existing list (source) with a new list (target) and optionally\n// applies migrations to source data when it's accessed. This enables schema evolution without\n// requiring upfront migration of all data, making it ideal for large datasets or when\n// preserving original data is important.\n//\n// Key features:\n// - Lazy migration: Data is only transformed when accessed, not stored in migrated form\n// - Append-only source: Source data is treated as immutable to preserve original data\n// - Chaining: Multiple LayeredProxyLists can be stacked for multi-step migrations\n//\n// Example usage:\n//\n//\t// Define data types for different schema versions\n//\ttype UserV1 struct {\n//\t Name string\n//\t Age int\n//\t}\n//\n//\ttype UserV2 struct {\n//\t FullName string\n//\t Age int\n//\t Active bool\n//\t}\n//\n//\t// Create source list with old schema\n//\tsourceList := ulist.New()\n//\tsourceList.Append(\n//\t UserV1{Name: \"Alice\", Age: 30},\n//\t UserV1{Name: \"Bob\", Age: 25},\n//\t)\n//\n//\t// Define migration function from V1 to V2\n//\tmigrateUserV1ToV2 := func(v any) any {\n//\t user := v.(UserV1)\n//\t return UserV2{\n//\t FullName: user.Name, // Name field renamed to FullName\n//\t Age: user.Age,\n//\t Active: true, // New field with default value\n//\t }\n//\t}\n//\n//\t// Create layered proxy with migration\n//\tproxy := NewLayeredProxyList(sourceList, migrateUserV1ToV2)\n//\n//\t// Add new data directly in V2 format\n//\tproxy.Append(UserV2{FullName: \"Charlie\", Age: 40, Active: false})\n//\n//\t// All access through proxy returns data in V2 format\n//\tfor i := 0; i \u003c proxy.Size(); i++ {\n//\t user := proxy.Get(i).(UserV2)\n//\t fmt.Printf(\"User: %s, Age: %d, Active: %t\\n\", user.FullName, user.Age, user.Active)\n//\t}\npackage lplist\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/moul/ulist\"\n)\n\n// MigratorFn is a function type that lazily converts values from source to target\ntype MigratorFn func(any) any\n\n// LayeredProxyList represents a wrapper around an existing List that handles migration\ntype LayeredProxyList struct {\n\tsource ulist.IList\n\ttarget *ulist.List\n\tmigrator MigratorFn\n\tsourceHeight int // Store initial source size to optimize lookups\n}\n\n// NewLayeredProxyList creates a new LayeredProxyList instance that wraps an existing List\nfunc NewLayeredProxyList(source ulist.IList, migrator MigratorFn) *LayeredProxyList {\n\tsourceHeight := source.TotalSize()\n\ttarget := ulist.New()\n\treturn \u0026LayeredProxyList{\n\t\tsource: source,\n\t\ttarget: target,\n\t\tmigrator: migrator,\n\t\tsourceHeight: sourceHeight,\n\t}\n}\n\n// Get retrieves the value at the specified index\n// Uses sourceHeight to efficiently route requests\nfunc (l *LayeredProxyList) Get(index int) any {\n\tif index \u003c l.sourceHeight {\n\t\t// Direct access to source for indices below sourceHeight\n\t\tval := l.source.Get(index)\n\t\tif val == nil {\n\t\t\treturn nil\n\t\t}\n\t\t// Only apply migrator if it exists\n\t\tif l.migrator != nil {\n\t\t\treturn l.migrator(val)\n\t\t}\n\t\treturn val\n\t}\n\t// For indices \u003e= sourceHeight, adjust index to be relative to target list starting at 0\n\ttargetIndex := index - l.sourceHeight\n\treturn l.target.Get(targetIndex)\n}\n\n// Append adds one or more values to the target list\nfunc (l *LayeredProxyList) Append(values ...any) {\n\tl.target.Append(values...)\n}\n\n// Delete marks elements as deleted in the appropriate list\nfunc (l *LayeredProxyList) Delete(indices ...int) error {\n\tfor _, index := range indices {\n\t\tif index \u003c l.sourceHeight {\n\t\t\terr := l.source.Delete(index)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, index := range indices {\n\t\ttargetIndex := index - l.sourceHeight\n\t\terr := l.target.Delete(targetIndex)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Size returns the total number of active elements\nfunc (l *LayeredProxyList) Size() int {\n\treturn l.source.Size() + l.target.Size()\n}\n\n// TotalSize returns the total number of elements in the list\nfunc (l *LayeredProxyList) TotalSize() int {\n\treturn l.sourceHeight + l.target.TotalSize()\n}\n\n// MustDelete deletes elements, panicking on error\nfunc (l *LayeredProxyList) MustDelete(indices ...int) {\n\tif err := l.Delete(indices...); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// MustGet retrieves a value, panicking if not found\nfunc (l *LayeredProxyList) MustGet(index int) any {\n\tval := l.Get(index)\n\tif val == nil {\n\t\tpanic(ulist.ErrDeleted)\n\t}\n\treturn val\n}\n\n// GetRange returns elements between start and end indices\nfunc (l *LayeredProxyList) GetRange(start, end int) []ulist.Entry {\n\tvar entries []ulist.Entry\n\tl.Iterator(start, end, func(index int, value any) bool {\n\t\tentries = append(entries, ulist.Entry{Index: index, Value: value})\n\t\treturn false\n\t})\n\treturn entries\n}\n\n// GetRangeByOffset returns elements starting from offset\nfunc (l *LayeredProxyList) GetRangeByOffset(offset int, count int) []ulist.Entry {\n\tvar entries []ulist.Entry\n\tl.IteratorByOffset(offset, count, func(index int, value any) bool {\n\t\tentries = append(entries, ulist.Entry{Index: index, Value: value})\n\t\treturn false\n\t})\n\treturn entries\n}\n\n// Iterator performs iteration between start and end indices\nfunc (l *LayeredProxyList) Iterator(start, end int, cb ulist.IterCbFn) bool {\n\t// For empty list or invalid range\n\tif start \u003c 0 \u0026\u0026 end \u003c 0 {\n\t\treturn false\n\t}\n\n\t// Normalize indices\n\tif start \u003c 0 {\n\t\tstart = 0\n\t}\n\tif end \u003c 0 {\n\t\tend = 0\n\t}\n\n\ttotalSize := l.TotalSize()\n\tif end \u003e= totalSize {\n\t\tend = totalSize - 1\n\t}\n\tif start \u003e= totalSize {\n\t\tstart = totalSize - 1\n\t}\n\n\t// Handle reverse iteration\n\tif start \u003e end {\n\t\tfor i := start; i \u003e= end; i-- {\n\t\t\tval := l.Get(i)\n\t\t\tif val != nil {\n\t\t\t\tif cb(i, val) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\t// Handle forward iteration\n\tfor i := start; i \u003c= end; i++ {\n\t\tval := l.Get(i)\n\t\tif val != nil {\n\t\t\tif cb(i, val) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\n// IteratorByOffset performs iteration starting from offset\nfunc (l *LayeredProxyList) IteratorByOffset(offset int, count int, cb ulist.IterCbFn) bool {\n\tif count == 0 {\n\t\treturn false\n\t}\n\n\t// Normalize offset\n\tif offset \u003c 0 {\n\t\toffset = 0\n\t}\n\ttotalSize := l.TotalSize()\n\tif offset \u003e= totalSize {\n\t\toffset = totalSize - 1\n\t}\n\n\t// Determine end based on count direction\n\tvar end int\n\tif count \u003e 0 {\n\t\tend = totalSize - 1\n\t} else {\n\t\tend = 0\n\t}\n\n\twrapperReturned := false\n\n\t// Calculate absolute value manually instead of using abs function\n\tremaining := count\n\tif remaining \u003c 0 {\n\t\tremaining = -remaining\n\t}\n\n\twrapper := func(index int, value any) bool {\n\t\tif remaining \u003c= 0 {\n\t\t\twrapperReturned = true\n\t\t\treturn true\n\t\t}\n\t\tremaining--\n\t\treturn cb(index, value)\n\t}\n\n\tret := l.Iterator(offset, end, wrapper)\n\tif wrapperReturned {\n\t\treturn false\n\t}\n\treturn ret\n}\n\n// Set updates the value at the specified index\nfunc (l *LayeredProxyList) Set(index int, value any) error {\n\tif index \u003c l.sourceHeight {\n\t\t// Cannot modify source list directly\n\t\treturn errors.New(\"cannot modify source list directly\")\n\t}\n\n\t// Adjust index to be relative to target list starting at 0\n\ttargetIndex := index - l.sourceHeight\n\treturn l.target.Set(targetIndex, value)\n}\n\n// MustSet updates the value at the specified index, panicking on error\nfunc (l *LayeredProxyList) MustSet(index int, value any) {\n\tif err := l.Set(index, value); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// GetByOffset returns elements starting from offset with count determining direction\nfunc (l *LayeredProxyList) GetByOffset(offset int, count int) []ulist.Entry {\n\tvar entries []ulist.Entry\n\tl.IteratorByOffset(offset, count, func(index int, value any) bool {\n\t\tentries = append(entries, ulist.Entry{Index: index, Value: value})\n\t\treturn false\n\t})\n\treturn entries\n}\n\n// Verify that LayeredProxyList implements IList\nvar _ ulist.IList = (*LayeredProxyList)(nil)\n" + }, + { + "name": "lplist_test.gno", + "body": "package lplist\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/moul/ulist\"\n)\n\n// TestLayeredProxyListBasicOperations tests the basic operations of LayeredProxyList\nfunc TestLayeredProxyListBasicOperations(t *testing.T) {\n\t// Create source list with initial data\n\tsource := ulist.New()\n\tsource.Append(1, 2, 3)\n\n\t// Create proxy list with a simple multiplier migrator\n\tmigrator := func(v any) any {\n\t\treturn v.(int) * 2\n\t}\n\tproxy := NewLayeredProxyList(source, migrator)\n\n\t// Test initial state\n\tif got := proxy.Size(); got != 3 {\n\t\tt.Errorf(\"initial Size() = %v, want %v\", got, 3)\n\t}\n\tif got := proxy.TotalSize(); got != 3 {\n\t\tt.Errorf(\"initial TotalSize() = %v, want %v\", got, 3)\n\t}\n\n\t// Test Get with migration\n\ttests := []struct {\n\t\tindex int\n\t\twant any\n\t}{\n\t\t{0, 2}, // 1 * 2\n\t\t{1, 4}, // 2 * 2\n\t\t{2, 6}, // 3 * 2\n\t}\n\n\tfor _, tt := range tests {\n\t\tif got := proxy.Get(tt.index); got != tt.want {\n\t\t\tt.Errorf(\"Get(%v) = %v, want %v\", tt.index, got, tt.want)\n\t\t}\n\t}\n\n\t// Test Append to target\n\tproxy.Append(7, 8)\n\tif got := proxy.Size(); got != 5 {\n\t\tt.Errorf(\"Size() after append = %v, want %v\", got, 5)\n\t}\n\n\t// Test Get from target (no migration)\n\tif got := proxy.Get(3); got != 7 {\n\t\tt.Errorf(\"Get(3) = %v, want %v\", got, 7)\n\t}\n}\n\n// TestLayeredProxyListDelete tests delete operations\nfunc TestLayeredProxyListDelete(t *testing.T) {\n\tsource := ulist.New()\n\tsource.Append(1, 2, 3)\n\tproxy := NewLayeredProxyList(source, nil)\n\tproxy.Append(4, 5)\n\n\t// Test deleting from source (should fail)\n\tif err := proxy.Delete(1); err == nil {\n\t\tt.Error(\"Delete from source should return error\")\n\t}\n\n\t// Test deleting from target (should succeed)\n\tif err := proxy.Delete(3); err != nil {\n\t\tt.Errorf(\"Delete from target failed: %s\", err.Error())\n\t}\n\n\t// After deletion, the value might be undefined rather than nil\n\t// Check that it's not equal to the original value\n\tif got := proxy.Get(3); got == 5 {\n\t\tt.Errorf(\"Get(3) after delete = %v, want it to be deleted\", got)\n\t}\n}\n\n// TestLayeredProxyListIteration tests iteration methods\nfunc TestLayeredProxyListIteration(t *testing.T) {\n\tsource := ulist.New()\n\tsource.Append(1, 2, 3)\n\tproxy := NewLayeredProxyList(source, nil)\n\tproxy.Append(4, 5)\n\n\t// Test GetRange\n\tentries := proxy.GetRange(0, 4)\n\tif len(entries) != 5 {\n\t\tt.Errorf(\"GetRange returned %v entries, want 5\", len(entries))\n\t}\n\n\t// Test reverse iteration\n\tentries = proxy.GetRange(4, 0)\n\tif len(entries) != 5 {\n\t\tt.Errorf(\"Reverse GetRange returned %v entries, want 5\", len(entries))\n\t}\n\n\t// Test IteratorByOffset with positive count\n\tvar values []any\n\tproxy.IteratorByOffset(1, 3, func(index int, value any) bool {\n\t\tvalues = append(values, value)\n\t\treturn false\n\t})\n\tif len(values) != 3 {\n\t\tt.Errorf(\"IteratorByOffset returned %v values, want 3\", len(values))\n\t}\n}\n\n// TestLayeredProxyListMustOperations tests must operations\nfunc TestLayeredProxyListMustOperations(t *testing.T) {\n\tsource := ulist.New()\n\tsource.Append(1, 2)\n\tproxy := NewLayeredProxyList(source, nil)\n\n\t// Test MustGet success\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tt.Errorf(\"MustGet panicked unexpectedly: %v\", r)\n\t\t}\n\t}()\n\tif got := proxy.MustGet(1); got != 2 {\n\t\tt.Errorf(\"MustGet(1) = %v, want 2\", got)\n\t}\n\n\t// Test MustGet panic\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Error(\"MustGet should have panicked\")\n\t\t}\n\t}()\n\tproxy.MustGet(99) // Should panic\n}\n\n// TestLayeredProxyListWithNilMigrator tests behavior without a migrator\nfunc TestLayeredProxyListWithNilMigrator(t *testing.T) {\n\tsource := ulist.New()\n\tsource.Append(1, 2)\n\tproxy := NewLayeredProxyList(source, nil)\n\n\tif got := proxy.Get(0); got != 1 {\n\t\tt.Errorf(\"Get(0) with nil migrator = %v, want 1\", got)\n\t}\n}\n\n// TestLayeredProxyListEmpty tests operations on empty lists\nfunc TestLayeredProxyListEmpty(t *testing.T) {\n\tsource := ulist.New()\n\tproxy := NewLayeredProxyList(source, nil)\n\n\tif got := proxy.Size(); got != 0 {\n\t\tt.Errorf(\"Size() of empty list = %v, want 0\", got)\n\t}\n\n\tif got := proxy.Get(0); got != nil {\n\t\tt.Errorf(\"Get(0) of empty list = %v, want nil\", got)\n\t}\n\n\tentries := proxy.GetRange(0, 10)\n\tif len(entries) != 0 {\n\t\tt.Errorf(\"GetRange on empty list returned %v entries, want 0\", len(entries))\n\t}\n}\n\n// TestLayeredProxyListChaining tests chaining of layered proxies with struct migrations\nfunc TestLayeredProxyListChaining(t *testing.T) {\n\t// Define struct types for different versions\n\ttype v1 struct {\n\t\tnamev1 string\n\t}\n\ttype v2 struct {\n\t\tnamev2 string\n\t}\n\ttype v3 struct {\n\t\tnamev3 string\n\t}\n\n\t// Create source list with v1 objects\n\tsource := ulist.New()\n\tsource.Append(v1{namev1: \"object1\"}, v1{namev1: \"object2\"})\n\n\t// Migration function from v1 to v2\n\tv1Tov2 := func(v any) any {\n\t\tobj := v.(v1)\n\t\treturn v2{namev2: obj.namev1 + \"_v2\"}\n\t}\n\n\t// Create first proxy with v1-\u003ev2 migration\n\tproxyV2 := NewLayeredProxyList(source, v1Tov2)\n\tproxyV2.Append(v2{namev2: \"direct_v2\"})\n\n\t// Migration function from v2 to v3\n\tv2Tov3 := func(v any) any {\n\t\tobj := v.(v2)\n\t\treturn v3{namev3: obj.namev2 + \"_v3\"}\n\t}\n\n\t// Create second proxy with v2-\u003ev3 migration, using the first proxy as source\n\tproxyV3 := NewLayeredProxyList(proxyV2, v2Tov3)\n\tproxyV3.Append(v3{namev3: \"direct_v3\"})\n\n\t// Verify sizes\n\tif got := proxyV3.Size(); got != 4 {\n\t\tt.Errorf(\"proxyV3.Size() = %v, want 4\", got)\n\t}\n\n\t// Test that all objects are correctly migrated when accessed through proxyV3\n\texpected := []struct {\n\t\tindex int\n\t\tname string\n\t}{\n\t\t{0, \"object1_v2_v3\"}, // v1 -\u003e v2 -\u003e v3\n\t\t{1, \"object2_v2_v3\"}, // v1 -\u003e v2 -\u003e v3\n\t\t{2, \"direct_v2_v3\"}, // v2 -\u003e v3\n\t\t{3, \"direct_v3\"}, // v3 (no migration)\n\t}\n\n\tfor _, tt := range expected {\n\t\tobj := proxyV3.Get(tt.index).(v3)\n\t\tif obj.namev3 != tt.name {\n\t\t\tt.Errorf(\"proxyV3.Get(%d).namev3 = %v, want %v\", tt.index, obj.namev3, tt.name)\n\t\t}\n\t}\n\n\t// Verify getting items from middle layer (proxyV2)\n\tmiddleExpected := []struct {\n\t\tindex int\n\t\tname string\n\t}{\n\t\t{0, \"object1_v2\"}, // v1 -\u003e v2\n\t\t{1, \"object2_v2\"}, // v1 -\u003e v2\n\t\t{2, \"direct_v2\"}, // v2 (no migration)\n\t}\n\n\tfor _, tt := range middleExpected {\n\t\tobj := proxyV2.Get(tt.index).(v2)\n\t\tif obj.namev2 != tt.name {\n\t\t\tt.Errorf(\"proxyV2.Get(%d).namev2 = %v, want %v\", tt.index, obj.namev2, tt.name)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "HuFhwd0XbyeIRPcNc7/Vnt7DleBzhw9s19odzkUA18bLi7ZNVhK/494KvbWANEa0PaTtL4aDXMG3ppMIUzsLBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "web25", + "path": "gno.land/p/moul/web25", + "files": [ + { + "name": "web25.gno", + "body": "// Pacakge web25 provides an opinionated way to register an external web2\n// frontend to provide a \"better\" web2.5 experience.\npackage web25\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/moul/realmpath\"\n)\n\ntype Config struct {\n\tCID string\n\tURL string\n\tText string\n}\n\nfunc (c *Config) SetRemoteFrontendByURL(url string) {\n\tc.CID = \"\"\n\tc.URL = url\n}\n\nfunc (c *Config) SetRemoteFrontendByCID(cid string) {\n\tc.CID = cid\n\tc.URL = \"\"\n}\n\nfunc (c Config) GetLink() string {\n\tif c.CID != \"\" {\n\t\treturn \"https://ipfs.io/ipfs/\" + c.CID\n\t}\n\treturn c.URL\n}\n\nconst DefaultText = \"Click [here]({link}) to visit the full rendering experience.\\n\"\n\n// Render displays a frontend link at the top of your realm's Render function in\n// a concistent way to help gno visitors to have a consistent experience.\n//\n// if query is not nil, then it will check if it's not disable by ?no-web25, so\n// that you can call the render function from an external point of view.\nfunc (c Config) Render(path string) string {\n\tif realmpath.Parse(path).Query.Get(\"no-web25\") == \"1\" {\n\t\treturn \"\"\n\t}\n\ttext := c.Text\n\tif text == \"\" {\n\t\ttext = DefaultText\n\t}\n\ttext = strings.ReplaceAll(text, \"{link}\", c.GetLink())\n\treturn text\n}\n" + }, + { + "name": "web25_test.gno", + "body": "package web25\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "+ikypCuol3bPC/zTjeXgK4iqMYfV17kPci4b8xtaTf7cQunokHgWMGGYA0lqA8/KvbVQW5kmw8CP5nV9vF7MAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "xmath", + "path": "gno.land/p/moul/xmath", + "files": [ + { + "name": "xmath.gen.gno", + "body": "// Code generated by generator.go; DO NOT EDIT.\npackage xmath\n\n// Int8 helpers\nfunc MaxInt8(a, b int8) int8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinInt8(a, b int8) int8 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampInt8(value, min, max int8) int8 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsInt8(x int8) int8 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignInt8(x int8) int8 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Int16 helpers\nfunc MaxInt16(a, b int16) int16 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinInt16(a, b int16) int16 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampInt16(value, min, max int16) int16 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsInt16(x int16) int16 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignInt16(x int16) int16 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Int32 helpers\nfunc MaxInt32(a, b int32) int32 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinInt32(a, b int32) int32 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampInt32(value, min, max int32) int32 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsInt32(x int32) int32 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignInt32(x int32) int32 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Int64 helpers\nfunc MaxInt64(a, b int64) int64 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinInt64(a, b int64) int64 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampInt64(value, min, max int64) int64 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsInt64(x int64) int64 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignInt64(x int64) int64 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Int helpers\nfunc MaxInt(a, b int) int {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinInt(a, b int) int {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampInt(value, min, max int) int {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsInt(x int) int {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignInt(x int) int {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Uint8 helpers\nfunc MaxUint8(a, b uint8) uint8 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinUint8(a, b uint8) uint8 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampUint8(value, min, max uint8) uint8 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\n// Uint16 helpers\nfunc MaxUint16(a, b uint16) uint16 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinUint16(a, b uint16) uint16 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampUint16(value, min, max uint16) uint16 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\n// Uint32 helpers\nfunc MaxUint32(a, b uint32) uint32 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinUint32(a, b uint32) uint32 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampUint32(value, min, max uint32) uint32 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\n// Uint64 helpers\nfunc MaxUint64(a, b uint64) uint64 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinUint64(a, b uint64) uint64 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampUint64(value, min, max uint64) uint64 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\n// Uint helpers\nfunc MaxUint(a, b uint) uint {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinUint(a, b uint) uint {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampUint(value, min, max uint) uint {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\n// Float32 helpers\nfunc MaxFloat32(a, b float32) float32 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinFloat32(a, b float32) float32 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampFloat32(value, min, max float32) float32 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsFloat32(x float32) float32 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignFloat32(x float32) float32 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// Float64 helpers\nfunc MaxFloat64(a, b float64) float64 {\n\tif a \u003e b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc MinFloat64(a, b float64) float64 {\n\tif a \u003c b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc ClampFloat64(value, min, max float64) float64 {\n\tif value \u003c min {\n\t\treturn min\n\t}\n\tif value \u003e max {\n\t\treturn max\n\t}\n\treturn value\n}\n\nfunc AbsFloat64(x float64) float64 {\n\tif x \u003c 0 {\n\t\treturn -x\n\t}\n\treturn x\n}\n\nfunc SignFloat64(x float64) float64 {\n\tif x \u003c 0 {\n\t\treturn -1\n\t}\n\tif x \u003e 0 {\n\t\treturn 1\n\t}\n\treturn 0\n}\n" + }, + { + "name": "xmath.gen_test.gno", + "body": "package xmath\n\nimport \"testing\"\n\nfunc TestInt8Helpers(t *testing.T) {\n\t// Test MaxInt8\n\tif MaxInt8(1, 2) != 2 {\n\t\tt.Error(\"MaxInt8(1, 2) should be 2\")\n\t}\n\tif MaxInt8(-1, -2) != -1 {\n\t\tt.Error(\"MaxInt8(-1, -2) should be -1\")\n\t}\n\n\t// Test MinInt8\n\tif MinInt8(1, 2) != 1 {\n\t\tt.Error(\"MinInt8(1, 2) should be 1\")\n\t}\n\tif MinInt8(-1, -2) != -2 {\n\t\tt.Error(\"MinInt8(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampInt8\n\tif ClampInt8(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampInt8(5, 1, 3) should be 3\")\n\t}\n\tif ClampInt8(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampInt8(0, 1, 3) should be 1\")\n\t}\n\tif ClampInt8(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampInt8(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsInt8\n\tif AbsInt8(-5) != 5 {\n\t\tt.Error(\"AbsInt8(-5) should be 5\")\n\t}\n\tif AbsInt8(5) != 5 {\n\t\tt.Error(\"AbsInt8(5) should be 5\")\n\t}\n\n\t// Test SignInt8\n\tif SignInt8(-5) != -1 {\n\t\tt.Error(\"SignInt8(-5) should be -1\")\n\t}\n\tif SignInt8(5) != 1 {\n\t\tt.Error(\"SignInt8(5) should be 1\")\n\t}\n\tif SignInt8(0) != 0 {\n\t\tt.Error(\"SignInt8(0) should be 0\")\n\t}\n\n}\n\nfunc TestInt16Helpers(t *testing.T) {\n\t// Test MaxInt16\n\tif MaxInt16(1, 2) != 2 {\n\t\tt.Error(\"MaxInt16(1, 2) should be 2\")\n\t}\n\tif MaxInt16(-1, -2) != -1 {\n\t\tt.Error(\"MaxInt16(-1, -2) should be -1\")\n\t}\n\n\t// Test MinInt16\n\tif MinInt16(1, 2) != 1 {\n\t\tt.Error(\"MinInt16(1, 2) should be 1\")\n\t}\n\tif MinInt16(-1, -2) != -2 {\n\t\tt.Error(\"MinInt16(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampInt16\n\tif ClampInt16(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampInt16(5, 1, 3) should be 3\")\n\t}\n\tif ClampInt16(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampInt16(0, 1, 3) should be 1\")\n\t}\n\tif ClampInt16(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampInt16(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsInt16\n\tif AbsInt16(-5) != 5 {\n\t\tt.Error(\"AbsInt16(-5) should be 5\")\n\t}\n\tif AbsInt16(5) != 5 {\n\t\tt.Error(\"AbsInt16(5) should be 5\")\n\t}\n\n\t// Test SignInt16\n\tif SignInt16(-5) != -1 {\n\t\tt.Error(\"SignInt16(-5) should be -1\")\n\t}\n\tif SignInt16(5) != 1 {\n\t\tt.Error(\"SignInt16(5) should be 1\")\n\t}\n\tif SignInt16(0) != 0 {\n\t\tt.Error(\"SignInt16(0) should be 0\")\n\t}\n\n}\n\nfunc TestInt32Helpers(t *testing.T) {\n\t// Test MaxInt32\n\tif MaxInt32(1, 2) != 2 {\n\t\tt.Error(\"MaxInt32(1, 2) should be 2\")\n\t}\n\tif MaxInt32(-1, -2) != -1 {\n\t\tt.Error(\"MaxInt32(-1, -2) should be -1\")\n\t}\n\n\t// Test MinInt32\n\tif MinInt32(1, 2) != 1 {\n\t\tt.Error(\"MinInt32(1, 2) should be 1\")\n\t}\n\tif MinInt32(-1, -2) != -2 {\n\t\tt.Error(\"MinInt32(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampInt32\n\tif ClampInt32(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampInt32(5, 1, 3) should be 3\")\n\t}\n\tif ClampInt32(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampInt32(0, 1, 3) should be 1\")\n\t}\n\tif ClampInt32(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampInt32(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsInt32\n\tif AbsInt32(-5) != 5 {\n\t\tt.Error(\"AbsInt32(-5) should be 5\")\n\t}\n\tif AbsInt32(5) != 5 {\n\t\tt.Error(\"AbsInt32(5) should be 5\")\n\t}\n\n\t// Test SignInt32\n\tif SignInt32(-5) != -1 {\n\t\tt.Error(\"SignInt32(-5) should be -1\")\n\t}\n\tif SignInt32(5) != 1 {\n\t\tt.Error(\"SignInt32(5) should be 1\")\n\t}\n\tif SignInt32(0) != 0 {\n\t\tt.Error(\"SignInt32(0) should be 0\")\n\t}\n\n}\n\nfunc TestInt64Helpers(t *testing.T) {\n\t// Test MaxInt64\n\tif MaxInt64(1, 2) != 2 {\n\t\tt.Error(\"MaxInt64(1, 2) should be 2\")\n\t}\n\tif MaxInt64(-1, -2) != -1 {\n\t\tt.Error(\"MaxInt64(-1, -2) should be -1\")\n\t}\n\n\t// Test MinInt64\n\tif MinInt64(1, 2) != 1 {\n\t\tt.Error(\"MinInt64(1, 2) should be 1\")\n\t}\n\tif MinInt64(-1, -2) != -2 {\n\t\tt.Error(\"MinInt64(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampInt64\n\tif ClampInt64(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampInt64(5, 1, 3) should be 3\")\n\t}\n\tif ClampInt64(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampInt64(0, 1, 3) should be 1\")\n\t}\n\tif ClampInt64(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampInt64(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsInt64\n\tif AbsInt64(-5) != 5 {\n\t\tt.Error(\"AbsInt64(-5) should be 5\")\n\t}\n\tif AbsInt64(5) != 5 {\n\t\tt.Error(\"AbsInt64(5) should be 5\")\n\t}\n\n\t// Test SignInt64\n\tif SignInt64(-5) != -1 {\n\t\tt.Error(\"SignInt64(-5) should be -1\")\n\t}\n\tif SignInt64(5) != 1 {\n\t\tt.Error(\"SignInt64(5) should be 1\")\n\t}\n\tif SignInt64(0) != 0 {\n\t\tt.Error(\"SignInt64(0) should be 0\")\n\t}\n\n}\n\nfunc TestIntHelpers(t *testing.T) {\n\t// Test MaxInt\n\tif MaxInt(1, 2) != 2 {\n\t\tt.Error(\"MaxInt(1, 2) should be 2\")\n\t}\n\tif MaxInt(-1, -2) != -1 {\n\t\tt.Error(\"MaxInt(-1, -2) should be -1\")\n\t}\n\n\t// Test MinInt\n\tif MinInt(1, 2) != 1 {\n\t\tt.Error(\"MinInt(1, 2) should be 1\")\n\t}\n\tif MinInt(-1, -2) != -2 {\n\t\tt.Error(\"MinInt(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampInt\n\tif ClampInt(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampInt(5, 1, 3) should be 3\")\n\t}\n\tif ClampInt(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampInt(0, 1, 3) should be 1\")\n\t}\n\tif ClampInt(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampInt(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsInt\n\tif AbsInt(-5) != 5 {\n\t\tt.Error(\"AbsInt(-5) should be 5\")\n\t}\n\tif AbsInt(5) != 5 {\n\t\tt.Error(\"AbsInt(5) should be 5\")\n\t}\n\n\t// Test SignInt\n\tif SignInt(-5) != -1 {\n\t\tt.Error(\"SignInt(-5) should be -1\")\n\t}\n\tif SignInt(5) != 1 {\n\t\tt.Error(\"SignInt(5) should be 1\")\n\t}\n\tif SignInt(0) != 0 {\n\t\tt.Error(\"SignInt(0) should be 0\")\n\t}\n\n}\n\nfunc TestUint8Helpers(t *testing.T) {\n\t// Test MaxUint8\n\tif MaxUint8(1, 2) != 2 {\n\t\tt.Error(\"MaxUint8(1, 2) should be 2\")\n\t}\n\n\t// Test MinUint8\n\tif MinUint8(1, 2) != 1 {\n\t\tt.Error(\"MinUint8(1, 2) should be 1\")\n\t}\n\n\t// Test ClampUint8\n\tif ClampUint8(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampUint8(5, 1, 3) should be 3\")\n\t}\n\tif ClampUint8(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampUint8(0, 1, 3) should be 1\")\n\t}\n\tif ClampUint8(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampUint8(2, 1, 3) should be 2\")\n\t}\n\n}\n\nfunc TestUint16Helpers(t *testing.T) {\n\t// Test MaxUint16\n\tif MaxUint16(1, 2) != 2 {\n\t\tt.Error(\"MaxUint16(1, 2) should be 2\")\n\t}\n\n\t// Test MinUint16\n\tif MinUint16(1, 2) != 1 {\n\t\tt.Error(\"MinUint16(1, 2) should be 1\")\n\t}\n\n\t// Test ClampUint16\n\tif ClampUint16(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampUint16(5, 1, 3) should be 3\")\n\t}\n\tif ClampUint16(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampUint16(0, 1, 3) should be 1\")\n\t}\n\tif ClampUint16(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampUint16(2, 1, 3) should be 2\")\n\t}\n\n}\n\nfunc TestUint32Helpers(t *testing.T) {\n\t// Test MaxUint32\n\tif MaxUint32(1, 2) != 2 {\n\t\tt.Error(\"MaxUint32(1, 2) should be 2\")\n\t}\n\n\t// Test MinUint32\n\tif MinUint32(1, 2) != 1 {\n\t\tt.Error(\"MinUint32(1, 2) should be 1\")\n\t}\n\n\t// Test ClampUint32\n\tif ClampUint32(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampUint32(5, 1, 3) should be 3\")\n\t}\n\tif ClampUint32(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampUint32(0, 1, 3) should be 1\")\n\t}\n\tif ClampUint32(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampUint32(2, 1, 3) should be 2\")\n\t}\n\n}\n\nfunc TestUint64Helpers(t *testing.T) {\n\t// Test MaxUint64\n\tif MaxUint64(1, 2) != 2 {\n\t\tt.Error(\"MaxUint64(1, 2) should be 2\")\n\t}\n\n\t// Test MinUint64\n\tif MinUint64(1, 2) != 1 {\n\t\tt.Error(\"MinUint64(1, 2) should be 1\")\n\t}\n\n\t// Test ClampUint64\n\tif ClampUint64(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampUint64(5, 1, 3) should be 3\")\n\t}\n\tif ClampUint64(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampUint64(0, 1, 3) should be 1\")\n\t}\n\tif ClampUint64(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampUint64(2, 1, 3) should be 2\")\n\t}\n\n}\n\nfunc TestUintHelpers(t *testing.T) {\n\t// Test MaxUint\n\tif MaxUint(1, 2) != 2 {\n\t\tt.Error(\"MaxUint(1, 2) should be 2\")\n\t}\n\n\t// Test MinUint\n\tif MinUint(1, 2) != 1 {\n\t\tt.Error(\"MinUint(1, 2) should be 1\")\n\t}\n\n\t// Test ClampUint\n\tif ClampUint(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampUint(5, 1, 3) should be 3\")\n\t}\n\tif ClampUint(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampUint(0, 1, 3) should be 1\")\n\t}\n\tif ClampUint(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampUint(2, 1, 3) should be 2\")\n\t}\n\n}\n\nfunc TestFloat32Helpers(t *testing.T) {\n\t// Test MaxFloat32\n\tif MaxFloat32(1, 2) != 2 {\n\t\tt.Error(\"MaxFloat32(1, 2) should be 2\")\n\t}\n\tif MaxFloat32(-1, -2) != -1 {\n\t\tt.Error(\"MaxFloat32(-1, -2) should be -1\")\n\t}\n\n\t// Test MinFloat32\n\tif MinFloat32(1, 2) != 1 {\n\t\tt.Error(\"MinFloat32(1, 2) should be 1\")\n\t}\n\tif MinFloat32(-1, -2) != -2 {\n\t\tt.Error(\"MinFloat32(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampFloat32\n\tif ClampFloat32(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampFloat32(5, 1, 3) should be 3\")\n\t}\n\tif ClampFloat32(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampFloat32(0, 1, 3) should be 1\")\n\t}\n\tif ClampFloat32(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampFloat32(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsFloat32\n\tif AbsFloat32(-5) != 5 {\n\t\tt.Error(\"AbsFloat32(-5) should be 5\")\n\t}\n\tif AbsFloat32(5) != 5 {\n\t\tt.Error(\"AbsFloat32(5) should be 5\")\n\t}\n\n\t// Test SignFloat32\n\tif SignFloat32(-5) != -1 {\n\t\tt.Error(\"SignFloat32(-5) should be -1\")\n\t}\n\tif SignFloat32(5) != 1 {\n\t\tt.Error(\"SignFloat32(5) should be 1\")\n\t}\n\tif SignFloat32(0.0) != 0 {\n\t\tt.Error(\"SignFloat32(0.0) should be 0\")\n\t}\n\n}\n\nfunc TestFloat64Helpers(t *testing.T) {\n\t// Test MaxFloat64\n\tif MaxFloat64(1, 2) != 2 {\n\t\tt.Error(\"MaxFloat64(1, 2) should be 2\")\n\t}\n\tif MaxFloat64(-1, -2) != -1 {\n\t\tt.Error(\"MaxFloat64(-1, -2) should be -1\")\n\t}\n\n\t// Test MinFloat64\n\tif MinFloat64(1, 2) != 1 {\n\t\tt.Error(\"MinFloat64(1, 2) should be 1\")\n\t}\n\tif MinFloat64(-1, -2) != -2 {\n\t\tt.Error(\"MinFloat64(-1, -2) should be -2\")\n\t}\n\n\t// Test ClampFloat64\n\tif ClampFloat64(5, 1, 3) != 3 {\n\t\tt.Error(\"ClampFloat64(5, 1, 3) should be 3\")\n\t}\n\tif ClampFloat64(0, 1, 3) != 1 {\n\t\tt.Error(\"ClampFloat64(0, 1, 3) should be 1\")\n\t}\n\tif ClampFloat64(2, 1, 3) != 2 {\n\t\tt.Error(\"ClampFloat64(2, 1, 3) should be 2\")\n\t}\n\n\t// Test AbsFloat64\n\tif AbsFloat64(-5) != 5 {\n\t\tt.Error(\"AbsFloat64(-5) should be 5\")\n\t}\n\tif AbsFloat64(5) != 5 {\n\t\tt.Error(\"AbsFloat64(5) should be 5\")\n\t}\n\n\t// Test SignFloat64\n\tif SignFloat64(-5) != -1 {\n\t\tt.Error(\"SignFloat64(-5) should be -1\")\n\t}\n\tif SignFloat64(5) != 1 {\n\t\tt.Error(\"SignFloat64(5) should be 1\")\n\t}\n\tif SignFloat64(0.0) != 0 {\n\t\tt.Error(\"SignFloat64(0.0) should be 0\")\n\t}\n\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "LdfKBnHY4YSApY7aEeszADXEclAH2N26v1sVcA+qTIKjzyT4tmyl6Ib93LZDqOy9HMaCctU9g4saXpHRni02Dw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "chonk", + "path": "gno.land/p/n2p5/chonk", + "files": [ + { + "name": "chonk.gno", + "body": "// Package chonk provides a simple way to store arbitrarily large strings\n// in a linked list across transactions for efficient storage and retrieval.\n// A Chonk support three operations: Add, Flush, and Scanner.\n// - Add appends a string to the Chonk.\n// - Flush clears the Chonk.\n// - Scanner is used to iterate over the chunks in the Chonk.\npackage chonk\n\n// Chonk is a linked list string storage and\n// retrieval system for fine bois.\ntype Chonk struct {\n\tfirst *chunk\n\tlast *chunk\n}\n\n// chunk is a linked list node for Chonk\ntype chunk struct {\n\ttext string\n\tnext *chunk\n}\n\n// New creates a reference to a new Chonk\nfunc New() *Chonk {\n\treturn \u0026Chonk{}\n}\n\n// Add appends a string to the Chonk. If the Chonk is empty,\n// the string will be the first and last chunk. Otherwise,\n// the string will be appended to the end of the Chonk.\nfunc (c *Chonk) Add(text string) {\n\tnext := \u0026chunk{text: text}\n\tif c.first == nil {\n\t\tc.first = next\n\t\tc.last = next\n\t\treturn\n\t}\n\tc.last.next = next\n\tc.last = next\n}\n\n// Flush clears the Chonk by setting the first and last\n// chunks to nil. This will allow the garbage collector to\n// free the memory used by the Chonk.\nfunc (c *Chonk) Flush() {\n\tc.first = nil\n\tc.last = nil\n}\n\n// Scanner returns a new Scanner for the Chonk. The Scanner\n// is used to iterate over the chunks in the Chonk.\nfunc (c *Chonk) Scanner() *Scanner {\n\treturn \u0026Scanner{\n\t\tnext: c.first,\n\t}\n}\n\n// Scanner is a simple string scanner for Chonk. It is used\n// to iterate over the chunks in a Chonk from first to last.\ntype Scanner struct {\n\tcurrent *chunk\n\tnext *chunk\n}\n\n// Scan advances the scanner to the next chunk. It returns\n// true if there is a next chunk, and false if there is not.\nfunc (s *Scanner) Scan() bool {\n\tif s.next != nil {\n\t\ts.current = s.next\n\t\ts.next = s.next.next\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Text returns the current chunk. It is only valid to call\n// this method after a call to Scan returns true. Expected usage:\n//\n//\t\tscanner := chonk.Scanner()\n//\t\t\tfor scanner.Scan() {\n//\t \t\tfmt.Println(scanner.Text())\n//\t\t\t}\nfunc (s *Scanner) Text() string {\n\treturn s.current.text\n}\n" + }, + { + "name": "chonk_test.gno", + "body": "package chonk\n\nimport (\n\t\"testing\"\n)\n\nfunc TestChonk(t *testing.T) {\n\tt.Parallel()\n\tc := New()\n\ttestTable := []struct {\n\t\tname string\n\t\tchunks []string\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tchunks: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"single chunk\",\n\t\t\tchunks: []string{\"a\"},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple chunks\",\n\t\t\tchunks: []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t},\n\t\t{\n\t\t\tname: \"multiline chunks\",\n\t\t\tchunks: []string{\"1a\\nb\\nc\\n\\n\", \"d\\ne\\nf\", \"g\\nh\\ni\", \"j\\nk\\nl\\n\\n\\n\\n\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tchunks: []string{},\n\t\t},\n\t}\n\ttestChonk := func(t *testing.T, c *Chonk, chunks []string) {\n\t\tfor _, chunk := range chunks {\n\t\t\tc.Add(chunk)\n\t\t}\n\t\tscanner := c.Scanner()\n\t\ti := 0\n\t\tfor scanner.Scan() {\n\t\t\tif scanner.Text() != chunks[i] {\n\t\t\t\tt.Errorf(\"expected %s, got %s\", chunks[i], scanner.Text())\n\t\t\t}\n\t\t\ti++\n\t\t}\n\t}\n\tfor _, test := range testTable {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\ttestChonk(t, c, test.chunks)\n\t\t\tc.Flush()\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "EwLAsFUHvEapEsRWh0dBdiV5AKaDaj0Y1b546nDtR4m+iQRgyml7AG55KJxXXDi/D/iG5lR2fBpdLXud7PbyAQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "needle", + "path": "gno.land/p/n2p5/haystack/needle", + "files": [ + { + "name": "needle.gno", + "body": "package needle\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"errors\"\n)\n\nconst (\n\t// HashLength is the length in bytes of the hash prefix in any message\n\tHashLength = 32\n\t// PayloadLength is the length of the remaining bytes of the message.\n\tPayloadLength = 160\n\t// NeedleLength is the number of bytes required for a valid needle.\n\tNeedleLength = HashLength + PayloadLength\n)\n\n// Needle is a container for a 160 byte payload\n// and a 32 byte sha256 hash of the payload.\ntype Needle struct {\n\thash [HashLength]byte\n\tpayload [PayloadLength]byte\n}\n\nvar (\n\t// ErrorInvalidHash is an error for in invalid hash\n\tErrorInvalidHash = errors.New(\"invalid hash\")\n\t// ErrorByteSliceLength is an error for an invalid byte slice length passed in to New or FromBytes\n\tErrorByteSliceLength = errors.New(\"invalid byte slice length\")\n)\n\n// New creates a Needle used for submitting a payload to a Haystack sever. It takes a Payload\n// byte slice that is 160 bytes in length and returns a reference to a\n// Needle and an error. The purpose of this function is to make it\n// easy to create a new Needle from a payload. This function handles creating a sha256\n// hash of the payload, which is used by the Needle to submit to a haystack server.\nfunc New(p []byte) (*Needle, error) {\n\tif len(p) != PayloadLength {\n\t\treturn nil, ErrorByteSliceLength\n\t}\n\tvar n Needle\n\tsum := sha256.Sum256(p)\n\tcopy(n.hash[:], sum[:])\n\tcopy(n.payload[:], p)\n\treturn \u0026n, nil\n}\n\n// FromBytes is intended convert raw bytes (from UDP or storage) into a Needle.\n// It takes a byte slice and expects it to be exactly the length of NeedleLength.\n// The byte slice should consist of the first 32 bytes being the sha256 hash of the\n// payload and the payload bytes. This function verifies the length of the byte slice,\n// copies the bytes into a private [192]byte array, and validates the Needle. It returns\n// a reference to a Needle and an error.\nfunc FromBytes(b []byte) (*Needle, error) {\n\tif len(b) != NeedleLength {\n\t\treturn nil, ErrorByteSliceLength\n\t}\n\tvar n Needle\n\tcopy(n.hash[:], b[:HashLength])\n\tcopy(n.payload[:], b[HashLength:])\n\tif err := n.validate(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026n, nil\n}\n\n// Hash returns a copy of the bytes of the sha256 256 hash of the Needle payload.\nfunc (n *Needle) Hash() []byte {\n\treturn n.Bytes()[:HashLength]\n}\n\n// Payload returns a byte slice of the Needle payload\nfunc (n *Needle) Payload() []byte {\n\treturn n.Bytes()[HashLength:]\n}\n\n// Bytes returns a byte slice of the entire 192 byte hash + payload\nfunc (n *Needle) Bytes() []byte {\n\tb := make([]byte, NeedleLength)\n\tcopy(b, n.hash[:])\n\tcopy(b[HashLength:], n.payload[:])\n\treturn b\n}\n\n// validate checks that a Needle has a valid hash, it returns either nil or an error.\nfunc (n *Needle) validate() error {\n\tif hash := sha256.Sum256(n.Payload()); !bytes.Equal(n.Hash(), hash[:]) {\n\t\treturn ErrorInvalidHash\n\t}\n\treturn nil\n}\n" + }, + { + "name": "needle_test.gno", + "body": "package needle\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"testing\"\n)\n\nfunc TestNeedle(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"Bytes\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp, _ := hex.DecodeString(\"40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\t\tn, _ := New(p)\n\t\tb := n.Bytes()\n\t\tb[0], b[1], b[2], b[3] = 0, 0, 0, 0\n\t\tif bytes.Equal(n.Bytes(), b) {\n\t\t\tt.Error(\"mutating Bytes() changed needle bytes\")\n\t\t}\n\t})\n\tt.Run(\"Payload\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp, _ := hex.DecodeString(\"40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\t\tn, _ := New(p)\n\t\tpayload := n.Payload()\n\t\tif !bytes.Equal(p, payload) {\n\t\t\tt.Error(\"payload imported by New does not match needle.Payload()\")\n\t\t}\n\t\tpayload[0] = 0\n\t\tpl := n.Payload()\n\t\tif bytes.Equal(pl, payload) {\n\t\t\tt.Error(\"mutating Payload() changed needle payload\")\n\t\t}\n\t})\n\tt.Run(\"Hash\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tp, _ := hex.DecodeString(\"40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\t\tn, _ := New(p)\n\t\thash := n.Hash()\n\t\th := sha256.Sum256(p)\n\t\tif !bytes.Equal(h[:], hash) {\n\t\t\tt.Error(\"exported hash is invalid\")\n\t\t}\n\t\thash[0] = 0\n\t\th2 := n.Hash()\n\t\tif bytes.Equal(h2, hash) {\n\t\t\tt.Error(\"mutating Hash() changed needle hash\")\n\t\t}\n\t})\n}\n\nfunc TestNew(t *testing.T) {\n\tt.Parallel()\n\n\tp, _ := hex.DecodeString(\"40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\texpected, _ := hex.DecodeString(\"f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\n\ttestTable := []struct {\n\t\tpayload []byte\n\t\texpected []byte\n\t\thasError bool\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\tpayload: p,\n\t\t\texpected: expected,\n\t\t\thasError: false,\n\t\t\tdescription: \"expected payload\",\n\t\t},\n\t\t{\n\t\t\tpayload: p[:PayloadLength-1],\n\t\t\texpected: nil,\n\t\t\thasError: true,\n\t\t\tdescription: \"payload invalid length (too small)\",\n\t\t},\n\t\t{\n\t\t\tpayload: append(p, byte(1)),\n\t\t\texpected: nil,\n\t\t\thasError: true,\n\t\t\tdescription: \"payload invalid length (too large)\",\n\t\t},\n\t}\n\n\tfor _, test := range testTable {\n\t\tn, err := New(test.payload)\n\t\tif err != nil {\n\t\t\tif !test.hasError {\n\t\t\t\tt.Errorf(\"test: %v had error: %v\", test.description, err)\n\t\t\t}\n\t\t} else if !bytes.Equal(n.Bytes(), test.expected) {\n\t\t\tt.Errorf(\"%v, bytes not equal\\n%x\\n%x\", test.description, n.Bytes(), test.expected)\n\t\t}\n\t}\n}\n\nfunc TestFromBytes(t *testing.T) {\n\tt.Parallel()\n\n\tvalidRaw, _ := hex.DecodeString(\"f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\tvalidExpected, _ := hex.DecodeString(\"f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\tinvalidHash, _ := hex.DecodeString(\"182e0ca0d2fb1da76da6caf36a9d0d2838655632e85891216dc8b545d8f1410940e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1\")\n\n\ttestTable := []struct {\n\t\trawBytes []byte\n\t\texpected []byte\n\t\thasError bool\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\trawBytes: validRaw,\n\t\t\texpected: validExpected,\n\t\t\thasError: false,\n\t\t\tdescription: \"valid raw bytes\",\n\t\t},\n\t\t{\n\t\t\trawBytes: make([]byte, 0),\n\t\t\texpected: nil,\n\t\t\thasError: true,\n\t\t\tdescription: \"empty bytes\",\n\t\t},\n\t\t{\n\t\t\trawBytes: make([]byte, NeedleLength-1),\n\t\t\texpected: nil,\n\t\t\thasError: true,\n\t\t\tdescription: \"too few bytes, one less than expected\",\n\t\t},\n\t\t{\n\t\t\trawBytes: make([]byte, 0),\n\t\t\texpected: nil,\n\t\t\thasError: true,\n\t\t\tdescription: \"too few bytes, no bytes\",\n\t\t},\n\t\t{\n\t\t\trawBytes: make([]byte, NeedleLength+1),\n\t\t\texpected: nil,\n\t\t\thasError: true,\n\t\t\tdescription: \"too many bytes\",\n\t\t},\n\t\t{\n\t\t\trawBytes: invalidHash,\n\t\t\texpected: nil,\n\t\t\thasError: true,\n\t\t\tdescription: \"invalid hash\",\n\t\t},\n\t}\n\tfor _, test := range testTable {\n\t\tn, err := FromBytes(test.rawBytes)\n\t\tif err != nil {\n\t\t\tif !test.hasError {\n\t\t\t\tt.Errorf(\"test: %v had error: %v\", test.description, err)\n\t\t\t}\n\t\t} else if !bytes.Equal(n.Bytes(), test.expected) {\n\t\t\tt.Errorf(\"%v, bytes not equal\\n%x\\n%x\", test.description, n.Bytes(), test.expected)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "8P/EZC4QmtyjtjG2SsFCWXrsMsgcNI3DvEVl4TspHu5VoEi75JVcHaYNl5yFhPegZqxhxQnhDjUwhPqvupQNCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "haystack", + "path": "gno.land/p/n2p5/haystack", + "files": [ + { + "name": "haystack.gno", + "body": "package haystack\n\nimport (\n\t\"encoding/hex\"\n\t\"errors\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/n2p5/haystack/needle\"\n)\n\nvar (\n\t// ErrorNeedleNotFound is returned when a needle is not found in the haystack.\n\tErrorNeedleNotFound = errors.New(\"needle not found\")\n\t// ErrorNeedleLength is returned when a needle is not the correct length.\n\tErrorNeedleLength = errors.New(\"invalid needle length\")\n\t// ErrorHashLength is returned when a needle hash is not the correct length.\n\tErrorHashLength = errors.New(\"invalid hash length\")\n\t// ErrorDuplicateNeedle is returned when a needle already exists in the haystack.\n\tErrorDuplicateNeedle = errors.New(\"needle already exists\")\n\t// ErrorHashMismatch is returned when a needle hash does not match the needle. This should\n\t// never happen and indicates a critical internal storage error.\n\tErrorHashMismatch = errors.New(\"storage error: hash mismatch\")\n\t// ErrorValueInvalidType is returned when a needle value is not a byte slice. This should\n\t// never happen and indicates a critical internal storage error.\n\tErrorValueInvalidType = errors.New(\"storage error: invalid value type, expected []byte\")\n)\n\nconst (\n\t// EncodedHashLength is the length of the hex-encoded needle hash.\n\tEncodedHashLength = needle.HashLength * 2\n\t// EncodedPayloadLength is the length of the hex-encoded needle payload.\n\tEncodedPayloadLength = needle.PayloadLength * 2\n\t// EncodedNeedleLength is the length of the hex-encoded needle.\n\tEncodedNeedleLength = EncodedHashLength + EncodedPayloadLength\n)\n\n// Haystack is a permissionless, append-only, content-addressed key-value store for fix\n// length messages known as needles. A needle is a 192 byte byte slice with a 32 byte\n// hash (sha256) and a 160 byte payload.\ntype Haystack struct{ internal *avl.Tree }\n\n// New creates a new instance of a Haystack key-value store.\nfunc New() *Haystack {\n\treturn \u0026Haystack{\n\t\tinternal: avl.NewTree(),\n\t}\n}\n\n// Add takes a fixed-length hex-encoded needle bytes and adds it to the haystack key-value\n// store. The key is the first 32 bytes of the needle hash (64 bytes hex-encoded) of the\n// sha256 sum of the payload. The value is the 160 byte byte slice of the needle payload.\n// An error is returned if the needle is found to be invalid.\nfunc (h *Haystack) Add(needleHex string) error {\n\tif len(needleHex) != EncodedNeedleLength {\n\t\treturn ErrorNeedleLength\n\t}\n\tb, err := hex.DecodeString(needleHex)\n\tif err != nil {\n\t\treturn err\n\t}\n\tn, err := needle.FromBytes(b)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif h.internal.Has(needleHex[:EncodedHashLength]) {\n\t\treturn ErrorDuplicateNeedle\n\t}\n\th.internal.Set(needleHex[:EncodedHashLength], n.Payload())\n\treturn nil\n}\n\n// Get takes a hex-encoded needle hash and returns the complete hex-encoded needle bytes\n// and an error. Errors covers errors that span from the needle not being found, internal\n// storage error inconsistencies, and invalid value types.\nfunc (h *Haystack) Get(hash string) (string, error) {\n\tif len(hash) != EncodedHashLength {\n\t\treturn \"\", ErrorHashLength\n\t}\n\tif _, err := hex.DecodeString(hash); err != nil {\n\t\treturn \"\", err\n\t}\n\tv, ok := h.internal.Get(hash)\n\tif !ok {\n\t\treturn \"\", ErrorNeedleNotFound\n\t}\n\tb, ok := v.([]byte)\n\tif !ok {\n\t\treturn \"\", ErrorValueInvalidType\n\t}\n\tn, err := needle.New(b)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tneedleHash := hex.EncodeToString(n.Hash())\n\tif needleHash != hash {\n\t\treturn \"\", ErrorHashMismatch\n\t}\n\treturn hex.EncodeToString(n.Bytes()), nil\n}\n" + }, + { + "name": "haystack_test.gno", + "body": "package haystack\n\nimport (\n\t\"encoding/hex\"\n\t\"testing\"\n\n\t\"gno.land/p/n2p5/haystack/needle\"\n)\n\nfunc TestHaystack(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"New\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\th := New()\n\t\tif h == nil {\n\t\t\tt.Error(\"New returned nil\")\n\t\t}\n\t})\n\n\tt.Run(\"Add\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\th := New()\n\t\tn, _ := needle.New(make([]byte, needle.PayloadLength))\n\t\tvalidNeedleHex := hex.EncodeToString(n.Bytes())\n\n\t\ttestTable := []struct {\n\t\t\tneedleHex string\n\t\t\terr error\n\t\t}{\n\t\t\t{validNeedleHex, nil},\n\t\t\t{validNeedleHex, ErrorDuplicateNeedle},\n\t\t\t{\"bad\" + validNeedleHex[3:], needle.ErrorInvalidHash},\n\t\t\t{\"XXX\" + validNeedleHex[3:], hex.InvalidByteError('X')},\n\t\t\t{validNeedleHex[:len(validNeedleHex)-2], ErrorNeedleLength},\n\t\t\t{validNeedleHex + \"00\", ErrorNeedleLength},\n\t\t\t{\"000\", ErrorNeedleLength},\n\t\t}\n\t\tfor _, tt := range testTable {\n\t\t\terr := h.Add(tt.needleHex)\n\t\t\tif err != tt.err {\n\t\t\t\tt.Error(tt.needleHex, err.Error(), \"!=\", tt.err.Error())\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"Get\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\th := New()\n\n\t\t// genNeedleHex returns a hex-encoded needle and its hash for a given index.\n\t\tgenNeedleHex := func(i int) (string, string) {\n\t\t\tb := make([]byte, needle.PayloadLength)\n\t\t\tb[0] = byte(i)\n\t\t\tn, _ := needle.New(b)\n\t\t\treturn hex.EncodeToString(n.Bytes()), hex.EncodeToString(n.Hash())\n\t\t}\n\n\t\t// Add a valid needle to the haystack.\n\t\tvalidNeedleHex, validHash := genNeedleHex(0)\n\t\th.Add(validNeedleHex)\n\n\t\t// Add a needle and break the value type.\n\t\t_, brokenHashValueType := genNeedleHex(1)\n\t\th.internal.Set(brokenHashValueType, 0)\n\n\t\t// Add a needle with invalid hash.\n\t\t_, invalidHash := genNeedleHex(2)\n\t\th.internal.Set(invalidHash, make([]byte, needle.PayloadLength))\n\n\t\ttestTable := []struct {\n\t\t\thash string\n\t\t\texpected string\n\t\t\terr error\n\t\t}{\n\t\t\t{validHash, validNeedleHex, nil},\n\t\t\t{validHash[:len(validHash)-2], \"\", ErrorHashLength},\n\t\t\t{validHash + \"00\", \"\", ErrorHashLength},\n\t\t\t{\"XXX\" + validHash[3:], \"\", hex.InvalidByteError('X')},\n\t\t\t{\"bad\" + validHash[3:], \"\", ErrorNeedleNotFound},\n\t\t\t{brokenHashValueType, \"\", ErrorValueInvalidType},\n\t\t\t{invalidHash, \"\", ErrorHashMismatch},\n\t\t}\n\t\tfor _, tt := range testTable {\n\t\t\tactual, err := h.Get(tt.hash)\n\t\t\tif err != tt.err {\n\t\t\t\tt.Error(tt.hash, err.Error(), \"!=\", tt.err.Error())\n\t\t\t}\n\t\t\tif actual != tt.expected {\n\t\t\t\tt.Error(tt.hash, actual, \"!=\", tt.expected)\n\t\t\t}\n\t\t}\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "s/TV5b3Z4DVOWl11D5y5VkNjaLDL2Rie9S2n51VRn9iuUIvtDcg4FN79fN7/5w2uwHpSrq6Q0TRZaFkYFcDPBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "loci", + "path": "gno.land/p/n2p5/loci", + "files": [ + { + "name": "loci.gno", + "body": "// loci is a single purpose datastore keyed by the caller's address. It has two\n// functions: Set and Get. loci is plural for locus, which is a central or core\n// place where something is found or from which it originates. In this case,\n// it's a simple key-value store where an address (the key) can store exactly\n// one value (in the form of a byte slice). Only the caller can set the value\n// for their address, but anyone can retrieve the value for any address.\npackage loci\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// LociStore is a simple key-value store that uses\n// an AVL tree to store the data.\ntype LociStore struct {\n\tinternal *avl.Tree\n}\n\n// New creates a reference to a new LociStore.\nfunc New() *LociStore {\n\treturn \u0026LociStore{\n\t\tinternal: avl.NewTree(),\n\t}\n}\n\n// Set stores a byte slice in the AVL tree using the `std.PreviousRealm().Address()`\n// string as the key.\nfunc (s *LociStore) Set(value []byte) {\n\tkey := string(std.PreviousRealm().Address())\n\ts.internal.Set(key, value)\n}\n\n// Get retrieves a byte slice from the AVL tree using the provided address.\n// The return values are the byte slice value and a boolean indicating\n// whether the value exists.\nfunc (s *LociStore) Get(addr std.Address) []byte {\n\tvalue, exists := s.internal.Get(string(addr))\n\tif !exists {\n\t\treturn nil\n\t}\n\treturn value.([]byte)\n}\n" + }, + { + "name": "loci_test.gno", + "body": "package loci\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n)\n\nfunc TestLociStore(t *testing.T) {\n\tt.Run(\"TestSet\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tstore := New()\n\t\tu1 := testutils.TestAddress(\"u1\")\n\n\t\tm1 := []byte(\"hello\")\n\t\tm2 := []byte(\"world\")\n\t\ttesting.SetOriginCaller(u1)\n\n\t\t// Ensure that the value is nil before setting it.\n\t\tr1 := store.Get(u1)\n\t\tif r1 != nil {\n\t\t\tt.Errorf(\"expected value to be nil, got '%s'\", r1)\n\t\t}\n\t\tstore.Set(m1)\n\t\t// Ensure that the value is correct after setting it.\n\t\tr2 := store.Get(u1)\n\t\tif string(r2) != \"hello\" {\n\t\t\tt.Errorf(\"expected value to be 'hello', got '%s'\", r2)\n\t\t}\n\t\tstore.Set(m2)\n\t\t// Ensure that the value is correct after overwriting it.\n\t\tr3 := store.Get(u1)\n\t\tif string(r3) != \"world\" {\n\t\t\tt.Errorf(\"expected value to be 'world', got '%s'\", r3)\n\t\t}\n\t})\n\tt.Run(\"TestGet\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tstore := New()\n\t\tu1 := testutils.TestAddress(\"u1\")\n\t\tu2 := testutils.TestAddress(\"u2\")\n\t\tu3 := testutils.TestAddress(\"u3\")\n\t\tu4 := testutils.TestAddress(\"u4\")\n\n\t\tm1 := []byte(\"hello\")\n\t\tm2 := []byte(\"world\")\n\t\tm3 := []byte(\"goodbye\")\n\n\t\ttesting.SetOriginCaller(u1)\n\t\tstore.Set(m1)\n\t\ttesting.SetOriginCaller(u2)\n\t\tstore.Set(m2)\n\t\ttesting.SetOriginCaller(u3)\n\t\tstore.Set(m3)\n\n\t\t// Ensure that the value is correct after setting it.\n\t\tr0 := store.Get(u4)\n\t\tif r0 != nil {\n\t\t\tt.Errorf(\"expected value to be nil, got '%s'\", r0)\n\t\t}\n\t\t// Ensure that the value is correct after setting it.\n\t\tr1 := store.Get(u1)\n\t\tif string(r1) != \"hello\" {\n\t\t\tt.Errorf(\"expected value to be 'hello', got '%s'\", r1)\n\t\t}\n\t\t// Ensure that the value is correct after setting it.\n\t\tr2 := store.Get(u2)\n\t\tif string(r2) != \"world\" {\n\t\t\tt.Errorf(\"expected value to be 'world', got '%s'\", r2)\n\t\t}\n\t\t// Ensure that the value is correct after setting it.\n\t\tr3 := store.Get(u3)\n\t\tif string(r3) != \"goodbye\" {\n\t\t\tt.Errorf(\"expected value to be 'goodbye', got '%s'\", r3)\n\t\t}\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "wd0Vjp0S2c8Bwlyk0XHXWrl9PkVZhxTezWOHEi0Tce73/zlQp1oTkMNdbr7NAQUoQaIv1XD0J/d3qP8+84zVDw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "mgroup", + "path": "gno.land/p/n2p5/mgroup", + "files": [ + { + "name": "mgroup.gno", + "body": "// Package mgroup is a simple managed group managing ownership and membership\n// for authorization in gno realms. The ManagedGroup struct is used to manage\n// the owner, backup owners, and members of a group. The owner is the primary\n// owner of the group and can add and remove backup owners and members. Backup\n// owners can claim ownership of the group. This is meant to provide backup\n// accounts for the owner in case the owner account is lost or compromised.\n// Members are used to authorize actions across realms.\npackage mgroup\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\nvar (\n\tErrCannotRemoveOwner = errors.New(\"mgroup: cannot remove owner\")\n\tErrNotBackupOwner = errors.New(\"mgroup: not a backup owner\")\n\tErrNotMember = errors.New(\"mgroup: not a member\")\n\tErrInvalidAddress = errors.New(\"mgroup: address is invalid\")\n)\n\ntype ManagedGroup struct {\n\towner *ownable.Ownable\n\tbackupOwners *avl.Tree\n\tmembers *avl.Tree\n}\n\n// New creates a new ManagedGroup with the owner set to the provided address.\n// The owner is automatically added as a backup owner and member of the group.\nfunc New(ownerAddress std.Address) *ManagedGroup {\n\tg := \u0026ManagedGroup{\n\t\towner: ownable.NewWithAddress(ownerAddress),\n\t\tbackupOwners: avl.NewTree(),\n\t\tmembers: avl.NewTree(),\n\t}\n\tg.AddBackupOwner(ownerAddress)\n\tg.AddMember(ownerAddress)\n\treturn g\n}\n\n// AddBackupOwner adds a backup owner to the group by std.Address.\n// If the caller is not the owner, an error is returned.\nfunc (g *ManagedGroup) AddBackupOwner(addr std.Address) error {\n\tif !g.owner.CallerIsOwner() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tg.backupOwners.Set(addr.String(), struct{}{})\n\treturn nil\n}\n\n// RemoveBackupOwner removes a backup owner from the group by std.Address.\n// The owner cannot be removed. If the caller is not the owner, an error is returned.\nfunc (g *ManagedGroup) RemoveBackupOwner(addr std.Address) error {\n\tif !g.owner.CallerIsOwner() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif addr == g.Owner() {\n\t\treturn ErrCannotRemoveOwner\n\t}\n\tg.backupOwners.Remove(addr.String())\n\treturn nil\n}\n\n// ClaimOwnership allows a backup owner to claim ownership of the group.\n// If the caller is not a backup owner, an error is returned.\n// The caller is automatically added as a member of the group.\nfunc (g *ManagedGroup) ClaimOwnership() error {\n\tcaller := std.PreviousRealm().Address()\n\t// already owner, skip\n\tif caller == g.Owner() {\n\t\treturn nil\n\t}\n\tif !g.IsBackupOwner(caller) {\n\t\treturn ErrNotMember\n\t}\n\tg.owner = ownable.NewWithAddress(caller)\n\tg.AddMember(caller)\n\treturn nil\n}\n\n// AddMember adds a member to the group by std.Address.\n// If the caller is not the owner, an error is returned.\nfunc (g *ManagedGroup) AddMember(addr std.Address) error {\n\tif !g.owner.CallerIsOwner() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tg.members.Set(addr.String(), struct{}{})\n\treturn nil\n}\n\n// RemoveMember removes a member from the group by std.Address.\n// The owner cannot be removed. If the caller is not the owner,\n// an error is returned.\nfunc (g *ManagedGroup) RemoveMember(addr std.Address) error {\n\tif !g.owner.CallerIsOwner() {\n\t\treturn ownable.ErrUnauthorized\n\t}\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\tif addr == g.Owner() {\n\t\treturn ErrCannotRemoveOwner\n\t}\n\tg.members.Remove(addr.String())\n\treturn nil\n}\n\n// MemberCount returns the number of members in the group.\nfunc (g *ManagedGroup) MemberCount() int {\n\treturn g.members.Size()\n}\n\n// BackupOwnerCount returns the number of backup owners in the group.\nfunc (g *ManagedGroup) BackupOwnerCount() int {\n\treturn g.backupOwners.Size()\n}\n\n// IsMember checks if an address is a member of the group.\nfunc (g *ManagedGroup) IsMember(addr std.Address) bool {\n\treturn g.members.Has(addr.String())\n}\n\n// IsBackupOwner checks if an address is a backup owner in the group.\nfunc (g *ManagedGroup) IsBackupOwner(addr std.Address) bool {\n\treturn g.backupOwners.Has(addr.String())\n}\n\n// Owner returns the owner of the group.\nfunc (g *ManagedGroup) Owner() std.Address {\n\treturn g.owner.Owner()\n}\n\n// BackupOwners returns a slice of all backup owners in the group, using the underlying\n// avl.Tree to iterate over the backup owners. If you have a large group, you may\n// want to use BackupOwnersWithOffset to iterate over backup owners in chunks.\nfunc (g *ManagedGroup) BackupOwners() []string {\n\treturn g.BackupOwnersWithOffset(0, g.BackupOwnerCount())\n}\n\n// Members returns a slice of all members in the group, using the underlying\n// avl.Tree to iterate over the members. If you have a large group, you may\n// want to use MembersWithOffset to iterate over members in chunks.\nfunc (g *ManagedGroup) Members() []string {\n\treturn g.MembersWithOffset(0, g.MemberCount())\n}\n\n// BackupOwnersWithOffset returns a slice of backup owners in the group, using the underlying\n// avl.Tree to iterate over the backup owners. The offset and count parameters allow you\n// to iterate over backup owners in chunks to support patterns such as pagination.\nfunc (g *ManagedGroup) BackupOwnersWithOffset(offset, count int) []string {\n\treturn sliceWithOffset(g.backupOwners, offset, count)\n}\n\n// MembersWithOffset returns a slice of members in the group, using the underlying\n// avl.Tree to iterate over the members. The offset and count parameters allow you\n// to iterate over members in chunks to support patterns such as pagination.\nfunc (g *ManagedGroup) MembersWithOffset(offset, count int) []string {\n\treturn sliceWithOffset(g.members, offset, count)\n}\n\n// sliceWithOffset is a helper function to iterate over an avl.Tree with an offset and count.\nfunc sliceWithOffset(t *avl.Tree, offset, count int) []string {\n\tvar result []string\n\tt.IterateByOffset(offset, count, func(k string, _ any) bool {\n\t\tif k == \"\" {\n\t\t\treturn true\n\t\t}\n\t\tresult = append(result, k)\n\t\treturn false\n\t})\n\treturn result\n}\n" + }, + { + "name": "mgroup_test.gno", + "body": "package mgroup\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/testutils\"\n)\n\nfunc TestManagedGroup(t *testing.T) {\n\tt.Parallel()\n\n\tu1 := testutils.TestAddress(\"u1\")\n\tu2 := testutils.TestAddress(\"u2\")\n\tu3 := testutils.TestAddress(\"u3\")\n\n\tt.Run(\"AddBackupOwner\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tg := New(u1)\n\t\t// happy path\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\terr := g.AddBackupOwner(u2)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t}\n\t\t// ensure checking for authorized caller\n\t\t{\n\t\t\ttesting.SetOriginCaller(u2)\n\t\t\terr := g.AddBackupOwner(u3)\n\t\t\tif err != ownable.ErrUnauthorized {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrNotBackupOwner.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t\t// ensure invalid address is caught\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\tvar badAddr std.Address\n\t\t\terr := g.AddBackupOwner(badAddr)\n\t\t\tif err != ErrInvalidAddress {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrInvalidAddress.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t})\n\tt.Run(\"RemoveBackupOwner\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tg := New(u1)\n\t\t// happy path\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\tg.AddBackupOwner(u2)\n\t\t\terr := g.RemoveBackupOwner(u2)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t}\n\t\t// running this twice should not error.\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\terr := g.RemoveBackupOwner(u2)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t}\n\t\t// ensure checking for authorized caller\n\t\t{\n\t\t\ttesting.SetOriginCaller(u2)\n\t\t\terr := g.RemoveBackupOwner(u3)\n\t\t\tif err != ownable.ErrUnauthorized {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrNotBackupOwner.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\tvar badAddr std.Address\n\t\t\terr := g.RemoveBackupOwner(badAddr)\n\t\t\tif err != ErrInvalidAddress {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrInvalidAddress.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\terr := g.RemoveBackupOwner(u1)\n\t\t\tif err != ErrCannotRemoveOwner {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrCannotRemoveOwner.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t})\n\tt.Run(\"ClaimOwnership\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tg := New(u1)\n\t\tg.AddBackupOwner(u2)\n\t\t// happy path\n\t\t{\n\t\t\ttesting.SetOriginCaller(u2)\n\t\t\terr := g.ClaimOwnership()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t\tif g.Owner() != u2 {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", u2, g.Owner())\n\t\t\t}\n\t\t\tif !g.IsMember(u2) {\n\t\t\t\tt.Errorf(\"expected %v to be a member\", u2)\n\t\t\t}\n\t\t}\n\t\t// running this twice should not error.\n\t\t{\n\t\t\ttesting.SetOriginCaller(u2)\n\t\t\terr := g.ClaimOwnership()\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t}\n\t\t// ensure checking for authorized caller\n\t\t{\n\t\t\ttesting.SetOriginCaller(u3)\n\t\t\terr := g.ClaimOwnership()\n\t\t\tif err != ErrNotMember {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrNotMember.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t})\n\tt.Run(\"AddMember\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tg := New(u1)\n\t\t// happy path\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\terr := g.AddMember(u2)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t\tif !g.IsMember(u2) {\n\t\t\t\tt.Errorf(\"expected %v to be a member\", u2)\n\t\t\t}\n\t\t}\n\t\t// ensure checking for authorized caller\n\t\t{\n\t\t\ttesting.SetOriginCaller(u2)\n\t\t\terr := g.AddMember(u3)\n\t\t\tif err != ownable.ErrUnauthorized {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ownable.ErrUnauthorized.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t\t// ensure invalid address is caught\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\tvar badAddr std.Address\n\t\t\terr := g.AddMember(badAddr)\n\t\t\tif err != ErrInvalidAddress {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrInvalidAddress.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t})\n\tt.Run(\"RemoveMember\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tg := New(u1)\n\t\t// happy path\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\tg.AddMember(u2)\n\t\t\terr := g.RemoveMember(u2)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t\tif g.IsMember(u2) {\n\t\t\t\tt.Errorf(\"expected %v to not be a member\", u2)\n\t\t\t}\n\t\t}\n\t\t// running this twice should not error.\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\terr := g.RemoveMember(u2)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"expected nil, got %v\", err.Error())\n\t\t\t}\n\t\t}\n\t\t// ensure checking for authorized caller\n\t\t{\n\t\t\ttesting.SetOriginCaller(u2)\n\t\t\terr := g.RemoveMember(u3)\n\t\t\tif err != ownable.ErrUnauthorized {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ownable.ErrUnauthorized.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t\t// ensure invalid address is caught\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\tvar badAddr std.Address\n\t\t\terr := g.RemoveMember(badAddr)\n\t\t\tif err != ErrInvalidAddress {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrInvalidAddress.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t\t// ensure owner cannot be removed\n\t\t{\n\t\t\ttesting.SetOriginCaller(u1)\n\t\t\terr := g.RemoveMember(u1)\n\t\t\tif err != ErrCannotRemoveOwner {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", ErrCannotRemoveOwner.Error(), err.Error())\n\t\t\t}\n\t\t}\n\t})\n\tt.Run(\"MemberCount\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tg := New(u1)\n\t\tif g.MemberCount() != 1 {\n\t\t\tt.Errorf(\"expected 0, got %v\", g.MemberCount())\n\t\t}\n\t\tg.AddMember(u2)\n\t\tif g.MemberCount() != 2 {\n\t\t\tt.Errorf(\"expected 1, got %v\", g.MemberCount())\n\t\t}\n\t\tg.AddMember(u3)\n\t\tif g.MemberCount() != 3 {\n\t\t\tt.Errorf(\"expected 2, got %v\", g.MemberCount())\n\t\t}\n\t\tg.RemoveMember(u2)\n\t\tif g.MemberCount() != 2 {\n\t\t\tt.Errorf(\"expected 1, got %v\", g.MemberCount())\n\t\t}\n\t})\n\tt.Run(\"BackupOwnerCount\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tg := New(u1)\n\t\tif g.BackupOwnerCount() != 1 {\n\t\t\tt.Errorf(\"expected 0, got %v\", g.BackupOwnerCount())\n\t\t}\n\t\tg.AddBackupOwner(u2)\n\t\tif g.BackupOwnerCount() != 2 {\n\t\t\tt.Errorf(\"expected 1, got %v\", g.BackupOwnerCount())\n\t\t}\n\t\tg.AddBackupOwner(u3)\n\t\tif g.BackupOwnerCount() != 3 {\n\t\t\tt.Errorf(\"expected 2, got %v\", g.BackupOwnerCount())\n\t\t}\n\t\tg.RemoveBackupOwner(u2)\n\t\tif g.BackupOwnerCount() != 2 {\n\t\t\tt.Errorf(\"expected 1, got %v\", g.BackupOwnerCount())\n\t\t}\n\t})\n\tt.Run(\"IsMember\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tg := New(u1)\n\t\tif !g.IsMember(u1) {\n\t\t\tt.Errorf(\"expected %v to be a member\", u1)\n\t\t}\n\t\tif g.IsMember(u2) {\n\t\t\tt.Errorf(\"expected %v to not be a member\", u2)\n\t\t}\n\t\tg.AddMember(u2)\n\t\tif !g.IsMember(u2) {\n\t\t\tt.Errorf(\"expected %v to be a member\", u2)\n\t\t}\n\t})\n\tt.Run(\"IsBackupOwner\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tg := New(u1)\n\t\tif !g.IsBackupOwner(u1) {\n\t\t\tt.Errorf(\"expected %v to be a backup owner\", u1)\n\t\t}\n\t\tif g.IsBackupOwner(u2) {\n\t\t\tt.Errorf(\"expected %v to not be a backup owner\", u2)\n\t\t}\n\t\tg.AddBackupOwner(u2)\n\t\tif !g.IsBackupOwner(u2) {\n\t\t\tt.Errorf(\"expected %v to be a backup owner\", u2)\n\t\t}\n\t})\n\tt.Run(\"Owner\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tg := New(u1)\n\t\tif g.Owner() != u1 {\n\t\t\tt.Errorf(\"expected %v, got %v\", u1, g.Owner())\n\t\t}\n\t\tg.AddBackupOwner(u2)\n\t\tif g.Owner() != u1 {\n\t\t\tt.Errorf(\"expected %v, got %v\", u1, g.Owner())\n\t\t}\n\t\ttesting.SetOriginCaller(u2)\n\t\tg.ClaimOwnership()\n\t\tif g.Owner() != u2 {\n\t\t\tt.Errorf(\"expected %v, got %v\", u2, g.Owner())\n\t\t}\n\t})\n\tt.Run(\"BackupOwners\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttesting.SetOriginCaller(u1)\n\t\tg := New(u1)\n\t\tg.AddBackupOwner(u2)\n\t\tg.AddBackupOwner(u3)\n\t\towners := g.BackupOwners()\n\t\tif len(owners) != 3 {\n\t\t\tt.Errorf(\"expected 2, got %v\", len(owners))\n\t\t}\n\t\tif owners[0] != u1.String() {\n\t\t\tt.Errorf(\"expected %v, got %v\", u2, owners[0])\n\t\t}\n\t\tif owners[1] != u3.String() {\n\t\t\tt.Errorf(\"expected %v, got %v\", u3, owners[1])\n\t\t}\n\t\tif owners[2] != u2.String() {\n\t\t\tt.Errorf(\"expected %v, got %v\", u3, owners[1])\n\t\t}\n\t})\n\tt.Run(\"Members\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttesting.SetOriginCaller(u1)\n\t\tg := New(u1)\n\t\tg.AddMember(u2)\n\t\tg.AddMember(u3)\n\t\tmembers := g.Members()\n\t\tif len(members) != 3 {\n\t\t\tt.Errorf(\"expected 2, got %v\", len(members))\n\t\t}\n\t\tif members[0] != u1.String() {\n\t\t\tt.Errorf(\"expected %v, got %v\", u2, members[0])\n\t\t}\n\t\tif members[1] != u3.String() {\n\t\t\tt.Errorf(\"expected %v, got %v\", u3, members[1])\n\t\t}\n\t\tif members[2] != u2.String() {\n\t\t\tt.Errorf(\"expected %v, got %v\", u3, members[1])\n\t\t}\n\t})\n}\n\nfunc TestSliceWithOffset(t *testing.T) {\n\tt.Parallel()\n\ttestTable := []struct {\n\t\tname string\n\t\tslice []string\n\t\toffset int\n\t\tcount int\n\t\texpected []string\n\t\texpectedCount int\n\t}{\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tslice: []string{},\n\t\t\toffset: 0,\n\t\t\tcount: 0,\n\t\t\texpected: []string{},\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"single\",\n\t\t\tslice: []string{\"a\"},\n\t\t\toffset: 0,\n\t\t\tcount: 1,\n\t\t\texpected: []string{\"a\"},\n\t\t\texpectedCount: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"single offset\",\n\t\t\tslice: []string{\"a\"},\n\t\t\toffset: 1,\n\t\t\tcount: 1,\n\t\t\texpected: []string{},\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple\",\n\t\t\tslice: []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\toffset: 0,\n\t\t\tcount: 10,\n\t\t\texpected: []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\texpectedCount: 10,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple offset\",\n\t\t\tslice: []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\toffset: 5,\n\t\t\tcount: 5,\n\t\t\texpected: []string{\"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\texpectedCount: 5,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple offset end\",\n\t\t\tslice: []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\toffset: 10,\n\t\t\tcount: 5,\n\t\t\texpected: []string{},\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple offset past end\",\n\t\t\tslice: []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\toffset: 11,\n\t\t\tcount: 5,\n\t\t\texpected: []string{},\n\t\t\texpectedCount: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple offset count past end\",\n\t\t\tslice: []string{\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\toffset: 5,\n\t\t\tcount: 20,\n\t\t\texpected: []string{\"f\", \"g\", \"h\", \"i\", \"j\"},\n\t\t\texpectedCount: 5,\n\t\t},\n\t}\n\tfor _, test := range testTable {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\ttree := avl.NewTree()\n\t\t\tfor _, s := range test.slice {\n\t\t\t\ttree.Set(s, struct{}{})\n\t\t\t}\n\t\t\tslice := sliceWithOffset(tree, test.offset, test.count)\n\t\t\tif len(slice) != test.expectedCount {\n\t\t\t\tt.Errorf(\"expected %v, got %v\", test.expectedCount, len(slice))\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "B7PlRHNbmUT5gcnfaj7RxwhQUgr1a85K/7GaO6xErxMCEIXE5gCG4uUSm6xn03567mFJ/ToRZ+dOPKYDAnSXAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "validators", + "path": "gno.land/p/sys/validators", + "files": [ + { + "name": "types.gno", + "body": "package validators\n\nimport (\n\t\"errors\"\n\t\"std\"\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 std.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 std.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 std.Address) bool\n\n\t// GetValidator returns the validator using the given address\n\tGetValidator(address std.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 std.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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Ur/gkqJf59OvlCWEtrKYaMxQeTm+zNzA5peQVUJzOX4g6zOZLo3thjS0rwEX4yQNE+NT/soza/YvDYlF3S4dAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "poa", + "path": "gno.land/p/nt/poa", + "files": [ + { + "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\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\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 // std.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 std.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) {\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,\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.String(), v)\n\n\treturn v, nil\n}\n\nfunc (p *PoA) RemoveValidator(address std.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)\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.String())\n\n\treturn validator, nil\n}\n\nfunc (p *PoA) IsValidator(address std.Address) bool {\n\t_, exists := p.validators.Get(address.String())\n\n\treturn exists\n}\n\nfunc (p *PoA) GetValidator(address std.Address) (validators.Validator, error) {\n\tvalidatorRaw, exists := p.validators.Get(address.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/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n\t\"gno.land/p/sys/validators\"\n\n\t\"gno.land/p/demo/ufmt\"\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 = 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\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)\n\t\turequire.NoError(t, err)\n\n\t\t// Validate the address\n\t\tif val.Address != address {\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "LW87SellAJLitX3IyoLaBtea8PLiBvME4Tg04CUqDFakM7ohtPT2eaHEkQCcZQqsghc9/6JVAkdpzVAWUPe/DA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "ownable2step", + "path": "gno.land/p/oxtekgrinder/ownable2step", + "files": [ + { + "name": "errors.gno", + "body": "package ownable2step\n\nimport \"errors\"\n\nvar (\n\tErrNoPendingOwner = errors.New(\"ownable2step: no pending owner\")\n\tErrUnauthorized = errors.New(\"ownable2step: caller is not owner\")\n\tErrPendingUnauthorized = errors.New(\"ownable2step: caller is not pending owner\")\n\tErrInvalidAddress = errors.New(\"ownable2step: new owner address is invalid\")\n)\n" + }, + { + "name": "ownable.gno", + "body": "package ownable2step\n\nimport (\n\t\"std\"\n)\n\nconst OwnershipTransferEvent = \"OwnershipTransfer\"\n\n// Ownable2Step is a two-step ownership transfer package\n// It allows the current owner to set a new owner and the new owner will need to accept the ownership before it is transferred\ntype Ownable2Step struct {\n\towner std.Address\n\tpendingOwner std.Address\n}\n\nfunc New() *Ownable2Step {\n\treturn \u0026Ownable2Step{\n\t\towner: std.PreviousRealm().Address(),\n\t\tpendingOwner: \"\",\n\t}\n}\n\nfunc NewWithAddress(addr std.Address) *Ownable2Step {\n\treturn \u0026Ownable2Step{\n\t\towner: addr,\n\t\tpendingOwner: \"\",\n\t}\n}\n\n// TransferOwnership initiate the transfer of the ownership to a new address by setting the PendingOwner\nfunc (o *Ownable2Step) TransferOwnership(newOwner std.Address) error {\n\tif !o.CallerIsOwner() {\n\t\treturn ErrUnauthorized\n\t}\n\tif !newOwner.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\n\to.pendingOwner = newOwner\n\treturn nil\n}\n\n// AcceptOwnership accepts the pending ownership transfer\nfunc (o *Ownable2Step) AcceptOwnership() error {\n\tif o.pendingOwner.String() == \"\" {\n\t\treturn ErrNoPendingOwner\n\t}\n\tif std.PreviousRealm().Address() != o.pendingOwner {\n\t\treturn ErrPendingUnauthorized\n\t}\n\n\to.owner = o.pendingOwner\n\to.pendingOwner = \"\"\n\n\treturn nil\n}\n\n// DropOwnership removes the owner, effectively disabling any owner-related actions\n// Top-level usage: disables all only-owner actions/functions,\n// Embedded usage: behaves like a burn functionality, removing the owner from the struct\nfunc (o *Ownable2Step) DropOwnership() error {\n\tif !o.CallerIsOwner() {\n\t\treturn ErrUnauthorized\n\t}\n\n\tprevOwner := o.owner\n\to.owner = \"\"\n\n\tstd.Emit(\n\t\tOwnershipTransferEvent,\n\t\t\"from\", prevOwner.String(),\n\t\t\"to\", \"\",\n\t)\n\n\treturn nil\n}\n\n// Owner returns the owner address from Ownable\nfunc (o *Ownable2Step) Owner() std.Address {\n\treturn o.owner\n}\n\n// PendingOwner returns the pending owner address from Ownable2Step\nfunc (o *Ownable2Step) PendingOwner() std.Address {\n\treturn o.pendingOwner\n}\n\n// CallerIsOwner checks if the caller of the function is the Realm's owner\nfunc (o *Ownable2Step) CallerIsOwner() bool {\n\treturn std.PreviousRealm().Address() == o.owner\n}\n\n// AssertCallerIsOwner panics if the caller is not the owner\nfunc (o *Ownable2Step) AssertCallerIsOwner() {\n\tif std.PreviousRealm().Address() != o.owner {\n\t\tpanic(ErrUnauthorized)\n\t}\n}\n" + }, + { + "name": "ownable_test.gno", + "body": "package ownable2step\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n)\n\nfunc TestNew(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\tgot := o.Owner()\n\tpendingOwner := o.PendingOwner()\n\n\tuassert.Equal(t, got, alice)\n\tuassert.Equal(t, pendingOwner.String(), \"\")\n}\n\nfunc TestNewWithAddress(t *testing.T) {\n\to := NewWithAddress(alice)\n\n\tgot := o.Owner()\n\tpendingOwner := o.PendingOwner()\n\n\tuassert.Equal(t, got, alice)\n\tuassert.Equal(t, pendingOwner.String(), \"\")\n}\n\nfunc TestInitiateTransferOwnership(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\n\terr := o.TransferOwnership(bob)\n\turequire.NoError(t, err)\n\n\towner := o.Owner()\n\tpendingOwner := o.PendingOwner()\n\n\tuassert.Equal(t, owner, alice)\n\tuassert.Equal(t, pendingOwner, bob)\n}\n\nfunc TestTransferOwnership(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\n\terr := o.TransferOwnership(bob)\n\turequire.NoError(t, err)\n\n\towner := o.Owner()\n\tpendingOwner := o.PendingOwner()\n\n\tuassert.Equal(t, owner, alice)\n\tuassert.Equal(t, pendingOwner, bob)\n\n\ttesting.SetOriginCaller(bob)\n\n\terr = o.AcceptOwnership()\n\turequire.NoError(t, err)\n\n\towner = o.Owner()\n\tpendingOwner = o.PendingOwner()\n\n\tuassert.Equal(t, owner, bob)\n\tuassert.Equal(t, pendingOwner.String(), \"\")\n}\n\nfunc TestCallerIsOwner(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\tunauthorizedCaller := bob\n\n\ttesting.SetOriginCaller(unauthorizedCaller)\n\n\tuassert.False(t, o.CallerIsOwner())\n}\n\nfunc TestDropOwnership(t *testing.T) {\n\ttesting.SetOriginCaller(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\n// Errors\n\nfunc TestErrUnauthorized(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\n\ttesting.SetOriginCaller(bob)\n\n\tuassert.ErrorContains(t, o.TransferOwnership(alice), ErrUnauthorized.Error())\n\tuassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error())\n}\n\nfunc TestErrInvalidAddress(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\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 TestErrNoPendingOwner(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\n\terr := o.AcceptOwnership()\n\tuassert.ErrorContains(t, err, ErrNoPendingOwner.Error())\n}\n\nfunc TestErrPendingUnauthorized(t *testing.T) {\n\ttesting.SetOriginCaller(alice)\n\n\to := New()\n\n\terr := o.TransferOwnership(bob)\n\turequire.NoError(t, err)\n\n\ttesting.SetOriginCaller(alice)\n\n\terr = o.AcceptOwnership()\n\tuassert.ErrorContains(t, err, ErrPendingUnauthorized.Error())\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "YRO86qWxS2bZy3bh59e8XkBp4c0K+H+OR5Ute0xcsC1GAXPWvL/BvGJYfs44a7l6VbF7NIboRlERO6ruwYcZBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "table", + "path": "gno.land/p/sunspirit/table", + "files": [ + { + "name": "table.gno", + "body": "package table\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\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/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "NrKQbvXElLIiNSIAlmKLMuSi15FAvcEe3WhB7M9S4KE2yIvgxEZAdIXzw6z+N/5UrIR3Ynvhg3Njw3tTGlXkDw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "md", + "path": "gno.land/p/sunspirit/md", + "files": [ + { + "name": "md.gno", + "body": "package md\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\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/demo/uassert\"\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "f+icdIGEO/1K51vnsGqYpqFHaoAsaDV6q+98KJmZCTnfZfKAJUCsj/H3WYgc1ExAuozFeTSSxFc3Ikyvq0cGCQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "accesscontrol", + "path": "gno.land/p/thox/accesscontrol", + "files": [ + { + "name": "accesscontrol.gno", + "body": "package accesscontrol\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\nconst (\n\tRoleCreatedEvent = \"RoleCreated\"\n\tRoleGrantedEvent = \"RoleGranted\"\n\tRoleRevokedEvent = \"RoleRevoked\"\n\tRoleRenouncedEvent = \"RoleRenounced\"\n\tRoleSetEvent = \"RoleSet\"\n)\n\n// Role struct to store role information\ntype Role struct {\n\tName string\n\tHolders *avl.Tree // std.Address -\u003e struct{}\n\tOwnable *ownable.Ownable\n}\n\n// Roles struct to store all Roles information\ntype Roles struct {\n\tRoles []*Role\n\tUserToRoles avl.Tree // std.Address -\u003e []*Role\n\tOwnable *ownable.Ownable\n}\n\nfunc validRoleName(name string) error {\n\tif len(name) \u003e 30 || name == \"\" {\n\t\treturn ErrNameRole\n\t}\n\treturn nil\n}\n\n// NewRole creates a new instance of Role\nfunc NewRole(name string, admin std.Address) (*Role, error) {\n\tif err := validRoleName(name); err != nil {\n\t\treturn nil, ErrNameRole\n\t}\n\n\treturn \u0026Role{\n\t\tName: name,\n\t\tHolders: avl.NewTree(),\n\t\tOwnable: ownable.NewWithAddress(admin),\n\t}, nil\n}\n\n// CreateRole create a new role within the realm\nfunc (rs *Roles) CreateRole(name string) (*Role, error) {\n\tif err := validRoleName(name); err != nil {\n\t\treturn nil, ErrNameRole\n\t}\n\n\tif !rs.Ownable.CallerIsOwner() {\n\t\treturn nil, ErrNotOwner\n\t}\n\n\tfor _, role := range rs.Roles {\n\t\tif role.Name == name {\n\t\t\treturn nil, ErrRoleSameName\n\t\t}\n\t}\n\n\trole, err := NewRole(name, rs.Ownable.Owner())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trs.Roles = append(rs.Roles, role)\n\n\tstd.Emit(\n\t\tRoleCreatedEvent,\n\t\t\"roleName\", name,\n\t\t\"sender\", rs.Ownable.Owner().String(),\n\t)\n\n\treturn role, nil\n}\n\n// HasAccount check if an account has a specific role\nfunc (r *Role) HasAccount(account std.Address) bool {\n\treturn r.Holders.Has(account.String())\n}\n\n// FindRole searches for a role by its name\nfunc (rs *Roles) FindRole(name string) (*Role, error) {\n\tfor _, role := range rs.Roles {\n\t\tif role.Name == name {\n\t\t\treturn role, nil\n\t\t}\n\t}\n\n\treturn nil, ErrRoleNotFound\n}\n\n// GrantRole grants a role to an account\nfunc (rs *Roles) GrantRole(name string, account std.Address) error {\n\tr, err := rs.FindRole(name)\n\tif err != nil {\n\t\treturn ErrRoleNotFound\n\t}\n\n\tif !r.Ownable.CallerIsOwner() {\n\t\treturn ErrNotOwner\n\t}\n\n\tr.Holders.Set(account.String(), struct{}{})\n\n\t// Add in UserToRoles\n\troles, found := rs.UserToRoles.Get(account.String())\n\tif !found {\n\t\troles = []*Role{}\n\t}\n\troles = append(roles.([]*Role), r)\n\trs.UserToRoles.Set(account.String(), roles)\n\n\tstd.Emit(\n\t\tRoleGrantedEvent,\n\t\t\"roleName\", r.Name,\n\t\t\"account\", account.String(),\n\t\t\"sender\", std.PreviousRealm().Address().String(),\n\t)\n\n\treturn nil\n}\n\n// RevokeRole revokes a role from an account\nfunc (rs *Roles) RevokeRole(name string, account std.Address) error {\n\tr, err := rs.FindRole(name)\n\tif err != nil {\n\t\treturn ErrRoleNotFound\n\t}\n\n\tif !r.Ownable.CallerIsOwner() {\n\t\treturn ErrNotOwner\n\t}\n\n\tr.Holders.Remove(account.String())\n\n\t// Remove in UserToRoles\n\troles, found := rs.UserToRoles.Get(account.String())\n\tif found {\n\t\tupdatedRoles := []*Role{}\n\t\tfor _, role := range roles.([]*Role) {\n\t\t\tif role != r {\n\t\t\t\tupdatedRoles = append(updatedRoles, role)\n\t\t\t}\n\t\t}\n\t\trs.UserToRoles.Set(account.String(), updatedRoles)\n\t}\n\n\tstd.Emit(\n\t\tRoleRevokedEvent,\n\t\t\"roleName\", r.Name,\n\t\t\"account\", account.String(),\n\t\t\"sender\", std.PreviousRealm().Address().String(),\n\t)\n\n\treturn nil\n}\n\n// RenounceRole allows an account to renounce a role it holds\nfunc (rs *Roles) RenounceRole(name string) error {\n\tr, err := rs.FindRole(name)\n\tif err != nil {\n\t\treturn ErrRoleNotFound\n\t}\n\n\tcaller := std.OriginCaller()\n\n\tif !r.HasAccount(caller) {\n\t\treturn ErrAccountNotRole\n\t}\n\n\tr.Holders.Remove(caller.String())\n\n\tstd.Emit(\n\t\tRoleRenouncedEvent,\n\t\t\"roleName\", r.Name,\n\t\t\"account\", caller.String(),\n\t\t\"sender\", caller.String(),\n\t)\n\n\treturn nil\n}\n\n// SetRoleAdmin transfers the ownership of the role to a new administrator\nfunc (r *Role) SetRoleAdmin(admin std.Address) error {\n\tif err := r.Ownable.TransferOwnership(admin); err != nil {\n\t\treturn err\n\t}\n\n\tstd.Emit(\n\t\tRoleSetEvent,\n\t\t\"roleName\", r.Name,\n\t\t\"newAdminRole\", r.Ownable.Owner().String(),\n\t)\n\n\treturn nil\n}\n" + }, + { + "name": "accesscontrol_test.gno", + "body": "package accesscontrol\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\tadmin = testutils.TestAddress(\"admin1\")\n\tnewAdmin = testutils.TestAddress(\"admin2\")\n\tuser1 = testutils.TestAddress(\"user1\")\n\tuser2 = testutils.TestAddress(\"user2\")\n\n\troleName = \"TestRole\"\n)\n\nfunc initSetup(admin std.Address) *Roles {\n\treturn \u0026Roles{\n\t\tRoles: []*Role{},\n\t\tOwnable: ownable.NewWithAddress(admin),\n\t}\n}\n\nfunc TestCreateRole(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\trole, err := roles.CreateRole(roleName)\n\tuassert.NoError(t, err)\n\tuassert.True(t, role != nil, \"role should not be nil\")\n\tuassert.Equal(t, role.Name, roleName)\n\n\t_, err = roles.CreateRole(roleName)\n\tuassert.Error(t, err, \"should fail on duplicate role creation\")\n}\n\nfunc TestGrantRole(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\t_, err := roles.CreateRole(roleName)\n\tuassert.NoError(t, err)\n\n\terr = roles.GrantRole(roleName, user1)\n\tuassert.NoError(t, err)\n\n\trole, err := roles.FindRole(roleName)\n\tuassert.NoError(t, err)\n\tuassert.True(t, role.HasAccount(user1), \"user1 should have the TestRole\")\n\n\trolesList, found := roles.UserToRoles.Get(user1.String())\n\tuassert.True(t, found, \"user1 should be in UserToRoles\")\n\tuassert.True(t, containsRole(rolesList.([]*Role), role), \"UserToRoles should contain TestRole for user1\")\n}\n\nfunc TestGrantRoleByNonOwner(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\t_, err := roles.CreateRole(roleName)\n\tuassert.NoError(t, err)\n\n\ttesting.SetOriginCaller(user2)\n\troles.Ownable.TransferOwnership(user2)\n\terr = roles.GrantRole(roleName, user1)\n\tuassert.Error(t, err, \"non-owner should not be able to grant roles\")\n\n\troles.Ownable.TransferOwnership(admin)\n}\n\nfunc TestRevokeRole(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\t_, err := roles.CreateRole(roleName)\n\tuassert.NoError(t, err)\n\terr = roles.GrantRole(roleName, user1)\n\tuassert.NoError(t, err)\n\n\terr = roles.RevokeRole(roleName, user1)\n\tuassert.NoError(t, err)\n\n\trole, err := roles.FindRole(roleName)\n\tuassert.NoError(t, err)\n\tuassert.False(t, role.HasAccount(user1), \"user1 should no longer have the TestRole\")\n\n\trolesList, found := roles.UserToRoles.Get(user1.String())\n\tif found {\n\t\tuassert.False(t, containsRole(rolesList.([]*Role), role), \"UserToRoles should not contain TestRole for user1 after revocation\")\n\t}\n}\n\nfunc TestRenounceRole(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\trole, err := roles.CreateRole(roleName)\n\tuassert.NoError(t, err)\n\terr = roles.GrantRole(roleName, user1)\n\tuassert.NoError(t, err)\n\n\t// Pas besoin de transférer la propriété pour renoncer à un rôle\n\ttesting.SetOriginCaller(user1)\n\terr = roles.RenounceRole(roleName)\n\tuassert.NoError(t, err)\n\n\trole, err = roles.FindRole(roleName)\n\tuassert.NoError(t, err)\n\tuassert.False(t, role.HasAccount(user1), \"user1 should have renounced the TestRole\")\n}\n\nfunc TestSetRoleAdmin(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\trole, err := roles.CreateRole(roleName)\n\tuassert.NoError(t, err)\n\n\terr = role.SetRoleAdmin(newAdmin)\n\tuassert.NoError(t, err, \"admin change should succeed\")\n\n\ttesting.SetOriginCaller(newAdmin)\n\tuassert.Equal(t, role.Ownable.Owner(), newAdmin, \"the new admin should be newAdmin\")\n\n\ttesting.SetOriginCaller(admin)\n\tuassert.NotEqual(t, role.Ownable.Owner(), admin, \"the old admin should no longer be the owner\")\n}\n\nfunc TestCreateRoleInvalidName(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\t_, err := roles.CreateRole(\"\")\n\tuassert.Error(t, err, \"should fail on empty role name\")\n\n\tlongRoleName := \"thisisaverylongrolenamethatexceedsthenormallimitfortestingpurposes\"\n\t_, err = roles.CreateRole(longRoleName)\n\tuassert.Error(t, err, \"should fail on very long role name\")\n}\n\nfunc TestRevokeRoleByNonOwner(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\t_, err := roles.CreateRole(roleName)\n\tuassert.NoError(t, err)\n\terr = roles.GrantRole(roleName, user1)\n\tuassert.NoError(t, err)\n\n\ttesting.SetOriginCaller(user2)\n\terr = roles.RevokeRole(roleName, user1)\n\tuassert.Error(t, err, \"non-owner should not be able to revoke roles\")\n}\n\nfunc TestGrantRoleToNonExistentRole(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\terr := roles.GrantRole(\"NonExistentRole\", user1)\n\tuassert.Error(t, err, \"should fail when granting non-existent role\")\n}\n\nfunc TestRevokeRoleFromNonExistentRole(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\terr := roles.RevokeRole(\"NonExistentRole\", user1)\n\tuassert.Error(t, err, \"should fail when revoking non-existent role\")\n}\n\nfunc TestRenounceNonExistentRole(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(user1)\n\n\terr := roles.RenounceRole(\"NonExistentRole\")\n\tuassert.Error(t, err, \"should fail when renouncing non-existent role\")\n}\n\nfunc TestDeleteRole(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\trole, err := roles.CreateRole(roleName)\n\tuassert.NoError(t, err)\n\tuassert.True(t, role != nil, \"role should not be nil\")\n\n\troles.Roles = []*Role{} // Clear roles for testing purpose\n\t_, err = roles.FindRole(roleName)\n\tuassert.Error(t, err, \"should fail when trying to find deleted role\")\n}\n\nfunc TestUserToRolesWithMultipleRoles(t *testing.T) {\n\troles := initSetup(admin)\n\n\ttesting.SetOriginCaller(admin)\n\n\troleName1 := \"Role1\"\n\troleName2 := \"Role2\"\n\n\t// Create two roles\n\t_, err := roles.CreateRole(roleName1)\n\tuassert.NoError(t, err)\n\t_, err = roles.CreateRole(roleName2)\n\tuassert.NoError(t, err)\n\n\t// Grant both roles to user1\n\terr = roles.GrantRole(roleName1, user1)\n\tuassert.NoError(t, err)\n\terr = roles.GrantRole(roleName2, user1)\n\tuassert.NoError(t, err)\n\n\t// Check if user1 has both roles\n\trolesList, found := roles.UserToRoles.Get(user1.String())\n\tuassert.True(t, found, \"user1 should be in UserToRoles\")\n\trole1, _ := roles.FindRole(roleName1)\n\trole2, _ := roles.FindRole(roleName2)\n\tuassert.True(t, containsRole(rolesList.([]*Role), role1), \"UserToRoles should contain Role1 for user1\")\n\tuassert.True(t, containsRole(rolesList.([]*Role), role2), \"UserToRoles should contain Role2 for user1\")\n}\n\n// func test for check if a role is in a list of roles\nfunc containsRole(roles []*Role, target *Role) bool {\n\tfor _, role := range roles {\n\t\tif role == target {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n" + }, + { + "name": "doc.gno", + "body": "// Package accesscontrol implements a role-based access control (RBAC) system for Gno applications.\n// It provides functionality to create, assign, revoke, and transfer roles.\n//\n// # Usage\n//\n// Import the `gno.land/p/demo/accesscontrol` package to manage roles within your Gno realm. You can create roles,\n// assign them to users, revoke them, and transfer role ownership.\n//\n// Roles can be created by the contract owner using `CreateRole`. Each role is uniquely identified by its name.\n//\n//\tCreateRole(\"editor\")\n//\n// Use `GrantRole` to assign a role to a user, and `RevokeRole` to remove a role from a user.\n//\n//\tGrantRole(\"editor\", userAddress)\n//\n//\tRevokeRole(\"editor\", userAddress)\n//\n// Users can renounce their roles using `RenounceRole`, voluntarily removing themselves from a role.\n//\n//\tRenounceRole(\"editor\")\n//\n// You can look up a role by name with `FindRole`.\n//\n// FindRole(\"editor\")\n//\n// Role ownership can be transferred using `SetRoleAdmin`.\n//\n// SetRoleAdmin(newAdminAddress)\n//\n// Key events\n// - `RoleCreatedEvent`: Triggered when a new role is created\n// Key includes:\n// `roleName` (name of the role)\n// `sender` (address of the sender)\n//\n// - `RoleGrantedEvent`: Triggered when a role is granted to an account\n// Key includes:\n// `roleName` (name of the role)\n// `account` (address of the account)\n// `sender` (address of the sender)\n//\n// - `RoleRevokedEvent`: Triggered when a role is revoked from an account\n// Key includes:\n// `roleName` (name of the role)\n// `account` (address of the account)\n// `sender` (address of the sender)\n//\n// - `RoleRenouncedEvent`: Triggered when a role is renounced by an account\n// Key includes:\n// `roleName` (name of the role)\n// `account` (address of the account)\n//\n// - `RoleSetEvent`: Triggered when a role's administrator is set or changed\n// Key includes:\n// `roleName` (name of the role)\n// `newAdmin` (address of the new administrator)\n// `sender` (address of the sender)\npackage accesscontrol\n" + }, + { + "name": "errors.gno", + "body": "package accesscontrol\n\nimport \"errors\"\n\nvar (\n\tErrNotMatchAccount = errors.New(\"accesscontrol: caller confirmation does not match account\")\n\tErrRoleSameName = errors.New(\"accesscontrol: role already exists with the same name\")\n\tErrRoleNotFound = errors.New(\"accesscontrol: role not found\")\n\tErrAccountNotRole = errors.New(\"accesscontrol: this account does not have the role\")\n\tErrNameRole = errors.New(\"accesscontrol: role name cannot be empty or exceed 30 characters\")\n\tErrNotOwner = errors.New(\"accesscontrol: caller is not the owner of the role\")\n)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "tXpjCDEoXZ1xEQUbHWt16xoQseom/Qw4NQtHSDtPRSOc+L7d7T8BXkH1Sueoc1fu5e+vrz/+8ANrvs+X3DlDDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "timelock", + "path": "gno.land/p/thox/timelock", + "files": [ + { + "name": "doc.gno", + "body": "// Package timelock provides a library for scheduling, cancelling, and\n// executing time-locked operations in Gno. It ensures that\n// operations are only carried out after a specified delay and offers\n// mechanisms for managing and verifying the status of these operations.\n// This package leverages an AVL tree for efficient management of timestamps\n// and integrates role-based access control for administrative tasks.\n//\n// # Usage:\n//\n//\timport \"gno.land/p/demo/timelock\"\n//\timport \"gno.land/p/demo/accesscontrol\"\n//\n//\tInitialize timelock utility with an AVL tree and access control.\n//\ttimestamps := avl.NewTree()\n//\tadminRole := accesscontrol.NewRole(\"admin\", std.Address(\"admin-address\"))\n//\ttimeLockUtil := timelock.NewTimeLockUtil(timestamps, adminRole, 30)\n//\n//\tSchedule an operation with a delay of 60 seconds.\n//\tid := seqid.ID()\n//\ttimeLockUtil.Schedule(id, 60)\n//\n//\tCheck if an operation is pending.\n//\tisPending := timeLockUtil.IsPending(id)\n//\n//\tExecute the operation when it is pending.\n//\tif timeLockUtil.IsPending(id) {\n//\t timeLockUtil.Execute(id)\n//\t}\n//\n//\tUpdate the minimum delay for future operations.\n//\ttimeLockUtil.UpdateDelay(45)\npackage timelock\n" + }, + { + "name": "errors.gno", + "body": "package timelock\n\nimport \"errors\"\n\nvar (\n\tErrNilTimestampsOrAccessControl = errors.New(\"timelock: timestamps and accesscontrol values must be different from nil\")\n\tErrInsufficientDelay = errors.New(\"timelockutil: Schedule: insufficient delay\")\n\tErrOperationAlreadyScheduled = errors.New(\"timelockutil: Schedule: operation already scheduled\")\n\tErrOperationNotPending = errors.New(\"timelock: operation not pending\")\n\tErrUnexpectedType = errors.New(\"timelockutil: GetTimestamp: unexpected type\")\n\tErrUpadateDelay = errors.New(\"timelock: UpdateDelay: only admin can update delay\")\n\tErrOperationCancelNotPending = errors.New(\"timelock: Cancel: operation not pending\")\n\tErrOperationExecuteNotPending = errors.New(\"timelock: Execute: operation not pending\")\n)\n" + }, + { + "name": "timelock.gno", + "body": "package timelock\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/thox/accesscontrol\"\n)\n\n// Represents the status of a planned operation\ntype OperationState int\n\nconst (\n\tUnset OperationState = iota\n\tPending\n\tReady\n\tDone\n)\n\nfunc (os OperationState) StateToString() string {\n\tswitch os {\n\tcase Unset:\n\t\treturn \"Unset\"\n\tcase Pending:\n\t\treturn \"Pending\"\n\tcase Ready:\n\t\treturn \"Ready\"\n\tcase Done:\n\t\treturn \"Done\"\n\tdefault:\n\t\treturn \"Unknown\"\n\t}\n}\n\n// OperationStatus represents the status of an operation\ntype OperationStatus struct {\n\tsheduleTime int64\n\tisDone bool\n}\n\n// TimeLock stores the necessary parameters for the timelock operations\ntype TimeLock struct {\n\ttimestamps *avl.Tree // id -\u003e time.Time\n\taccessControl *accesscontrol.Role\n\tminDelay uint64\n}\n\n// New instance of TimeLock\nfunc NewTimeLock(timestamps *avl.Tree, accessControl *accesscontrol.Role, minDelay uint64) (*TimeLock, error) {\n\tif timestamps == nil || accessControl == nil {\n\t\treturn nil, ErrNilTimestampsOrAccessControl\n\t}\n\n\treturn \u0026TimeLock{\n\t\ttimestamps: timestamps,\n\t\taccessControl: accessControl,\n\t\tminDelay: minDelay,\n\t}, nil\n}\n\n// Schedules an operation to be carried out after a minimum delay\nfunc (tl *TimeLock) Schedule(id seqid.ID, delay uint64) error {\n\tif delay \u003c tl.minDelay {\n\t\treturn ErrInsufficientDelay\n\t}\n\n\tif tl.timestamps.Has(id.Binary()) {\n\t\treturn ErrOperationAlreadyScheduled\n\t}\n\n\ttimestamp := time.Now().Unix() + int64(delay)\n\tstatus := OperationStatus{sheduleTime: timestamp, isDone: false}\n\ttl.timestamps.Set(id.Binary(), status)\n\n\tstd.Emit(\n\t\t\"TimeLockScheduled\",\n\t\t\"id\", id.String(),\n\t\t\"delay\", strconv.FormatInt(int64(delay), 10),\n\t)\n\n\treturn nil\n}\n\n// Remove operation\nfunc (tl *TimeLock) Remove(id seqid.ID) {\n\ttl.timestamps.Remove(id.Binary())\n\n\tstd.Emit(\n\t\t\"TimeLockRemoved\",\n\t\t\"id\", id.String(),\n\t)\n}\n\n// Cancels a planned operation\nfunc (tl *TimeLock) Cancel(id seqid.ID) error {\n\tif !tl.IsPending(id) {\n\t\treturn ErrOperationCancelNotPending\n\t}\n\n\ttl.timestamps.Remove(id.Binary())\n\n\tstd.Emit(\n\t\t\"TimeLockCancelled\",\n\t\t\"id\", id.String(),\n\t)\n\treturn nil\n}\n\n// Executes a pending operation\nfunc (tl *TimeLock) Execute(id seqid.ID) error {\n\tif !tl.IsPending(id) {\n\t\treturn ErrOperationExecuteNotPending\n\t}\n\n\tstatus, err := tl.GetOperationStatus(id)\n\tif err != nil {\n\t\treturn err\n\t}\n\tstatus.isDone = true\n\ttl.timestamps.Set(id.Binary(), status)\n\n\tstd.Emit(\n\t\t\"TimeLockExecuted\",\n\t\t\"id\", id.String(),\n\t)\n\n\treturn nil\n}\n\n// Update the minimum lead time for future operations\nfunc (tl *TimeLock) UpdateDelay(newDelay uint64) error {\n\tif std.PreviousRealm().Address() != tl.accessControl.Ownable.Owner() {\n\t\treturn ErrUpadateDelay\n\t}\n\n\tstd.Emit(\n\t\t\"TimeLockMinDelayChanged\",\n\t\t\"oldDelay\", strconv.FormatInt(int64(tl.minDelay), 10),\n\t\t\"newDelay\", strconv.FormatInt(int64(newDelay), 10),\n\t)\n\n\ttl.minDelay = newDelay\n\n\treturn nil\n}\n\n// Checks if an operation is pending\nfunc (tl *TimeLock) IsPending(id seqid.ID) bool {\n\tstate, err := tl.GetOperationState(id)\n\tif err != nil {\n\t\t// Handle the error appropriately; for now, we assume the operation is not pending if there's an error\n\t\tufmt.Errorf(\"Error retrieving operation state: %v\", err)\n\t\treturn false\n\t}\n\n\treturn state == Pending\n}\n\n// Checks if an operation is ready\nfunc (tl *TimeLock) IsReady(id seqid.ID) bool {\n\tstate, err := tl.GetOperationState(id)\n\tif err != nil {\n\t\t// Handle the error appropriately; for now, we assume the operation is not pending if there's an error\n\t\tufmt.Errorf(\"Error retrieving operation state: %v\", err)\n\t\treturn false\n\t}\n\n\treturn state == Ready\n}\n\n// Returns the status of an operation\nfunc (tl *TimeLock) GetOperationState(id seqid.ID) (OperationState, error) {\n\tstatus, err := tl.GetOperationStatus(id)\n\tif err != nil {\n\t\treturn Unset, err\n\t}\n\tif status.isDone {\n\t\treturn Done, nil\n\t}\n\tif status.sheduleTime == 0 {\n\t\treturn Unset, nil\n\t}\n\tif status.sheduleTime \u003e time.Now().Unix() {\n\t\treturn Pending, nil\n\t}\n\treturn Ready, nil\n}\n\n// Returns the status of an operation\nfunc (tl *TimeLock) GetOperationStatus(id seqid.ID) (OperationStatus, error) {\n\tvalue, ok := tl.timestamps.Get(id.Binary())\n\n\tif !ok {\n\t\treturn OperationStatus{}, nil // Return an empty status if the operation is not found\n\t}\n\tif status, ok := value.(OperationStatus); ok {\n\t\treturn status, nil\n\t} else {\n\t\treturn OperationStatus{}, ErrUnexpectedType\n\t}\n}\n" + }, + { + "name": "timelock_test.gno", + "body": "package timelock\n\nimport (\n\t\"std\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/thox/accesscontrol\"\n)\n\nfunc TestTimelock(t *testing.T) {\n\t// Initialization\n\ttimestamps := avl.NewTree()\n\tminDelay := uint64(2) // 2 seconds to simplify testing\n\taccessControl, _ := accesscontrol.NewRole(\"admin\", std.OriginCaller())\n\ttimelockUtil, err := NewTimeLock(timestamps, accessControl, minDelay)\n\n\t// Generate a new ID from time.Now().UnixNano() with seconds added to guarantee uniqueness\n\tnewID := func(offset int64) seqid.ID {\n\t\treturn seqid.ID(time.Now().UnixNano() + offset)\n\t}\n\n\tuassert.NoError(t, err, \"Failed to create TimeLock instance\")\n\n\t// Test Schedule\n\tt.Run(\"Schedule\", func(t *testing.T) {\n\t\tid := newID(0)\n\t\tdelay := uint64(3) // 3 seconds\n\n\t\terr := timelockUtil.Schedule(id, delay)\n\n\t\tuassert.NoError(t, err, \"Schedule failed\")\n\n\t\tstatus, err := timelockUtil.GetOperationStatus(id)\n\n\t\tuassert.NoError(t, err, \"failed to get operation status\")\n\t\tuassert.NotEmpty(t, status.sheduleTime, \"operation status not set or invalid\")\n\t})\n\n\t// Test Cancel\n\tt.Run(\"Cancel\", func(t *testing.T) {\n\t\tid := newID(1)\n\n\t\t// Plan a new operation to ensure it is unique\n\t\terr := timelockUtil.Schedule(id, uint64(3))\n\t\tuassert.NoError(t, err, \"Failed to schedule operation for cancellation\")\n\n\t\terr = timelockUtil.Cancel(id)\n\t\tuassert.NoError(t, err, \"Cancel failed\")\n\n\t\tstatus, err := timelockUtil.GetOperationStatus(id)\n\t\tuassert.NoError(t, err, \"failed to get operation status\")\n\t\tuassert.Empty(t, status.sheduleTime, \"operation not cancelled\")\n\t})\n\n\t// Test Execute\n\tt.Run(\"Execute\", func(t *testing.T) {\n\t\tid := newID(2)\n\t\tdelay := uint64(3) // 3 seconds\n\t\tfutureTime := time.Now().Unix() + int64(delay)\n\n\t\t// Schedule the operation with a future timestamp\n\t\terr := timelockUtil.Schedule(id, delay)\n\t\tuassert.NoError(t, err, \"Failed to schedule operation for execution\")\n\n\t\t// Simulates the passage of time by setting the timestamp to a future time\n\t\ttimestamps.Set(id.Binary(), OperationStatus{sheduleTime: futureTime, isDone: false})\n\n\t\terr = timelockUtil.Execute(id)\n\t\tuassert.NoError(t, err, \"Execute failed\")\n\n\t\tstate, err := timelockUtil.GetOperationState(id)\n\t\tuassert.NoError(t, err, \"failed to get operation state\")\n\t\tuassert.Equal(t, Done.StateToString(), state.StateToString(), \"operation not executed\")\n\t})\n\n\t// Test UpdateDelay\n\tt.Run(\"UpdateDelay\", func(t *testing.T) {\n\t\tnewDelay := uint64(4) // 4 seconds\n\n\t\terr := timelockUtil.UpdateDelay(newDelay)\n\t\tuassert.NoError(t, err, \"UpdateDelay failed\")\n\n\t\tuassert.Equal(t, newDelay, timelockUtil.minDelay, \"minDelay not updated\")\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "z70Zp+V8fIf8UTXAk6CrHRA+piw27tPMlUvfdf1Lw7M2D+AMPUROBS+eqRQJ29VVz+awJEAY59Kon1CtTZWrCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "xorshiftr128plus", + "path": "gno.land/p/wyhaines/rand/xorshiftr128plus", + "files": [ + { + "name": "xorshiftr128plus.gno", + "body": "// Xorshiftr128+ is a very fast psuedo-random number generation algorithm with strong\n// statistical properties.\n//\n// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which\n// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations\n// because it has modest seeding requirements, and generates statistically strong randomness.\n//\n// This package provides an implementation of the Xorshiftr128+ PRNG algorithm. This algorithm provides\n// strong statistical performance with most seeds (just don't seed it with zeros), and the performance\n// of this implementation in Gno is more than four times faster than the default PCG implementation in\n// `math/rand`.\n//\n//\tBenchmark\n//\t---------\n//\tPCG: 1000000 Uint64 generated in 15.48s\n//\tXorshiftr128+: 1000000 Uint64 generated in 3.22s\n//\tRatio: x4.81 times faster than PCG\n//\n// Use it directly:\n//\n//\tprng = xorshiftr128plus.New() // pass a uint64 to seed it or pass nothing to seed it with entropy\n//\n// Or use it as a drop-in replacement for the default PRNT in Rand:\n//\n//\tsource = xorshiftr128plus.New()\n//\tprng := rand.New(source)\npackage xorshiftr128plus\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Xorshiftr128Plus struct {\n\tseed [2]uint64 // Seeds\n}\n\nfunc New(seeds ...uint64) *Xorshiftr128Plus {\n\tvar s1, s2 uint64\n\tseed_length := len(seeds)\n\tif seed_length \u003c 2 {\n\t\te := entropy.New()\n\t\tif seed_length == 0 {\n\t\t\ts1 = e.Value64()\n\t\t\ts2 = e.Value64()\n\t\t} else {\n\t\t\ts1 = seeds[0]\n\t\t\ts2 = e.Value64()\n\t\t}\n\t} else {\n\t\ts1 = seeds[0]\n\t\ts2 = seeds[1]\n\t}\n\n\tprng := \u0026Xorshiftr128Plus{}\n\tprng.Seed(s1, s2)\n\treturn prng\n}\n\nfunc (x *Xorshiftr128Plus) Seed(s1, s2 uint64) {\n\tif s1 == 0 \u0026\u0026 s2 == 0 {\n\t\tpanic(\"Seeds must not both be zero\")\n\t}\n\tx.seed[0] = s1\n\tx.seed[1] = s2\n}\n\n// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding.\n// binary.bigEndian.Uint64, copied to avoid dependency\nfunc beUint64(b []byte) uint64 {\n\t_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[7]) | uint64(b[6])\u003c\u003c8 | uint64(b[5])\u003c\u003c16 | uint64(b[4])\u003c\u003c24 |\n\t\tuint64(b[3])\u003c\u003c32 | uint64(b[2])\u003c\u003c40 | uint64(b[1])\u003c\u003c48 | uint64(b[0])\u003c\u003c56\n}\n\n// bePutUint64() encodes a uint64 into a buffer of eight bytes.\n// binary.bigEndian.PutUint64, copied to avoid dependency\nfunc bePutUint64(b []byte, v uint64) {\n\t_ = b[7] // early bounds check to guarantee safety of writes below\n\tb[0] = byte(v \u003e\u003e 56)\n\tb[1] = byte(v \u003e\u003e 48)\n\tb[2] = byte(v \u003e\u003e 40)\n\tb[3] = byte(v \u003e\u003e 32)\n\tb[4] = byte(v \u003e\u003e 24)\n\tb[5] = byte(v \u003e\u003e 16)\n\tb[6] = byte(v \u003e\u003e 8)\n\tb[7] = byte(v)\n}\n\n// A label to identify the marshalled data.\nvar marshalXorshiftr128PlusLabel = []byte(\"xorshiftr128+:\")\n\n// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used\n// with UnmarshalBinary() to restore the state of the PRNG.\n// MarshalBinary implements the encoding.BinaryMarshaler interface.\nfunc (xs *Xorshiftr128Plus) MarshalBinary() ([]byte, error) {\n\tb := make([]byte, 30)\n\tcopy(b, marshalXorshiftr128PlusLabel)\n\tbePutUint64(b[14:], xs.seed[0])\n\tbePutUint64(b[22:], xs.seed[1])\n\treturn b, nil\n}\n\n// errUnmarshalXorshiftr128Plus is returned when unmarshalling fails.\nvar errUnmarshalXorshiftr128Plus = errors.New(\"invalid Xorshiftr128Plus encoding\")\n\n// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary().\n// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.\nfunc (xs *Xorshiftr128Plus) UnmarshalBinary(data []byte) error {\n\tif len(data) != 30 || string(data[:14]) != string(marshalXorshiftr128PlusLabel) {\n\t\treturn errUnmarshalXorshiftr128Plus\n\t}\n\txs.seed[0] = beUint64(data[14:])\n\txs.seed[1] = beUint64(data[22:])\n\treturn nil\n}\n\nfunc (x *Xorshiftr128Plus) Uint64() uint64 {\n\tx0 := x.seed[0]\n\tx1 := x.seed[1]\n\tx.seed[0] = x1\n\tx0 ^= x0 \u003c\u003c 23\n\tx0 ^= x0 \u003e\u003e 17\n\tx0 ^= x1\n\tx.seed[1] = x0 + x1\n\treturn x.seed[1]\n}\n\n// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function.\n// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing\n// for generating a million random uint64 numbers on any unix based system:\n//\n// `time gno run -expr 'benchmarkXorshiftr128Plus()' xorshiftr128plus.gno\nfunc benchmarkXorshiftr128Plus(_iterations ...int) {\n\titerations := 1000000\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\txs128p := New()\n\n\tfor i := 0; i \u003c iterations; i++ {\n\t\t_ = xs128p.Uint64()\n\t}\n\tufmt.Println(ufmt.Sprintf(\"Xorshiftr128Plus: generate %d uint64\\n\", iterations))\n}\n\n// The averageXorshiftr128Plus() function is a simple benchmarking helper to demonstrate\n// the most basic statistical property of the Xorshiftr128+ PRNG.\nfunc averageXorshiftr128Plus(_iterations ...int) {\n\ttarget := uint64(500000)\n\titerations := 1000000\n\tvar squares [1000000]uint64\n\n\tufmt.Println(\n\t\tufmt.Sprintf(\n\t\t\t\"Averaging %d random numbers. The average should be very close to %d.\\n\",\n\t\t\titerations,\n\t\t\ttarget))\n\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\txs128p := New()\n\n\tvar average float64 = 0\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tn := xs128p.Uint64()%(target*2) + 1\n\t\taverage += (float64(n) - average) / float64(i+1)\n\t\tsquares[i] = n\n\t}\n\n\tsum_of_squares := uint64(0)\n\t// transform numbers into their squares of the distance from the average\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tdifference := average - float64(squares[i])\n\t\tsquare := uint64(difference * difference)\n\t\tsum_of_squares += square\n\t}\n\n\tufmt.Println(ufmt.Sprintf(\"Xorshiftr128+ average of %d uint64: %f\\n\", iterations, average))\n\tufmt.Println(ufmt.Sprintf(\"Xorshiftr128+ standard deviation : %f\\n\", math.Sqrt(float64(sum_of_squares)/float64(iterations))))\n\tufmt.Println(ufmt.Sprintf(\"Xorshiftr128+ theoretical perfect deviation: %f\\n\", (float64(target*2)-1)/math.Sqrt(12)))\n}\n" + }, + { + "name": "xorshiftr128plus_test.gno", + "body": "package xorshiftr128plus\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n)\n\nfunc TestXorshift64StarSeeding(t *testing.T) {\n\txs128p := New()\n\tvalue1 := xs128p.Uint64()\n\n\txs128p = New(987654321)\n\tvalue2 := xs128p.Uint64()\n\n\txs128p = New(987654321, 9876543210)\n\tvalue3 := xs128p.Uint64()\n\n\tif value1 != 13970141264473760763 ||\n\t\tvalue2 != 17031892808144362974 ||\n\t\tvalue3 != 8285073084540510 ||\n\t\tvalue1 == value2 ||\n\t\tvalue2 == value3 ||\n\t\tvalue1 == value3 {\n\t\tt.Errorf(\"Expected three different values: 13970141264473760763, 17031892808144362974, and 8285073084540510\\n got: %d, %d, %d\", value1, value2, value3)\n\t}\n}\n\nfunc TestXorshiftr128PlusRand(t *testing.T) {\n\tsource := New(987654321)\n\trng := rand.New(source)\n\n\t// Expected outputs for the first 5 random floats with the given seed\n\texpected := []float64{\n\t\t0.9199548549485674,\n\t\t0.0027491282372705816,\n\t\t0.31493362274701164,\n\t\t0.3531250819119609,\n\t\t0.09957852858060356,\n\t\t0.731941362705936,\n\t\t0.3476937688876708,\n\t\t0.1444018086140385,\n\t\t0.9106467321832331,\n\t\t0.8024870151488901,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rng.Float64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Rand.Float64() at iteration %d: got %g, expected %g\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestXorshiftr128PlusUint64(t *testing.T) {\n\txs128p := New(987654321, 9876543210)\n\n\texpected := []uint64{\n\t\t8285073084540510,\n\t\t97010855169053386,\n\t\t11353359435625603792,\n\t\t10289232744262291728,\n\t\t14019961444418950453,\n\t\t15829492476941720545,\n\t\t2764732928842099222,\n\t\t6871047144273883379,\n\t\t16142204260470661970,\n\t\t11803223757041229095,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := xs128p.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestXorshiftr128PlusMarshalUnmarshal(t *testing.T) {\n\txs128p := New(987654321, 9876543210)\n\n\texpected1 := []uint64{\n\t\t8285073084540510,\n\t\t97010855169053386,\n\t\t11353359435625603792,\n\t\t10289232744262291728,\n\t\t14019961444418950453,\n\t}\n\n\texpected2 := []uint64{\n\t\t15829492476941720545,\n\t\t2764732928842099222,\n\t\t6871047144273883379,\n\t\t16142204260470661970,\n\t\t11803223757041229095,\n\t}\n\n\tfor i, exp := range expected1 {\n\t\tval := xs128p.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n\n\tmarshalled, err := xs128p.MarshalBinary()\n\n\tt.Logf(\"Original State: [%x]\\n\", xs128p.seed)\n\tt.Logf(\"Marshalled State: [%x] -- %v\\n\", marshalled, err)\n\tstate_before := xs128p.seed\n\n\tif err != nil {\n\t\tt.Errorf(\"Xorshiftr128Plus.MarshalBinary() error: %v\", err)\n\t}\n\n\t// Advance state by one number; then check the next 5. The expectation is that they _will_ fail.\n\txs128p.Uint64()\n\n\tfor i, exp := range expected2 {\n\t\tval := xs128p.Uint64()\n\t\tif exp == val {\n\t\t\tt.Errorf(\" Iteration %d matched %d; which is from iteration %d; something strange is happening.\", (i + 6), val, (i + 5))\n\t\t}\n\t}\n\n\tt.Logf(\"State before unmarshall: [%x]\\n\", xs128p.seed)\n\n\t// Now restore the state of the PRNG\n\terr = xs128p.UnmarshalBinary(marshalled)\n\n\tt.Logf(\"State after unmarshall: [%x]\\n\", xs128p.seed)\n\n\tif state_before != xs128p.seed {\n\t\tt.Errorf(\"States before and after marshal/unmarshal are not equal; go %x and %x\", state_before, xs128p.seed)\n\t}\n\n\t// Now we should be back on track for the last 5 numbers\n\tfor i, exp := range expected2 {\n\t\tval := xs128p.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d\", (i + 5), val, exp)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "bdmca5rv/wrxXvuPNQGQ8vTdIsjBtqRA+teoHRxsVDnCTuKB7C9tXth0B0jfAAzqyiUnu9N9ytUwzCgADU3JDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "isaac", + "path": "gno.land/p/wyhaines/rand/isaac", + "files": [ + { + "name": "README.md", + "body": "# package isaac // import \"gno.land/p/demo/math/rand/isaac\"\n\nThis is a port of the ISAAC cryptographically secure PRNG,\noriginally based on the reference implementation found at\nhttps://burtleburtle.net/bob/rand/isaacafa.html\n\nISAAC has excellent statistical properties, with long cycle times, and\nuniformly distributed, unbiased, and unpredictable number generation. It can\nnot be distinguished from real random data, and in three decades of scrutiny,\nno practical attacks have been found.\n\nThe default random number algorithm in gno was ported from Go's v2 rand\nimplementatoon, which defaults to the PCG algorithm. This algorithm is\ncommonly used in language PRNG implementations because it has modest seeding\nrequirements, and generates statistically strong randomness.\n\nThis package provides an implementation of the 32-bit ISAAC PRNG algorithm. This\nalgorithm provides very strong statistical performance, and is cryptographically\nsecure, while still being substantially faster than the default PCG\nimplementation in `math/rand`. Note that this package does implement a `Uint64()`\nfunction in order to generate a 64 bit number out of two 32 bit numbers. Doing this\nmakes the generator only slightly faster than PCG, however,\n\nNote that the approach to seeing with ISAAC is very important for best results,\nand seeding with ISAAC is not as simple as seeding with a single uint64 value.\nThe ISAAC algorithm requires a 256-element seed. If used for cryptographic\npurposes, this will likely require entropy generated off-chain for actual\ncryptographically secure seeding. For other purposes, however, one can utilize\nthe built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to\ngenerate any missing seeds if fewer than 256 are provided.\n\n\n```\nBenchmark\n---------\nPCG: 1000000 Uint64 generated in 15.58s\nISAAC: 1000000 Uint64 generated in 13.23s (uint64)\nISAAC: 1000000 Uint32 generated in 6.43s (uint32)\nRatio: x1.18 times faster than PCG (uint64)\nRatio: x2.42 times faster than PCG (uint32)\n```\n\nUse it directly:\n\n```\nprng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest\n // will be generated using the xorshiftr128plus PRNG.\n```\n\nOr use it as a drop-in replacement for the default PRNT in Rand:\n\n```\nsource = isaac.New()\nprng := rand.New(source)\n```\n\n# TYPES\n\n`\ntype ISAAC struct {\n\t// Has unexported fields.\n}\n`\n\n`func New(seeds ...uint32) *ISAAC`\n ISAAC requires a large, 256-element seed. This implementation will leverage\n the entropy package combined with the the xorshiftr128plus PRNG to generate\n any missing seeds of fewer than the required number of arguments are\n provided.\n\n`func (isaac *ISAAC) MarshalBinary() ([]byte, error)`\n MarshalBinary() returns a byte array that encodes the state of the PRNG.\n This can later be used with UnmarshalBinary() to restore the state of the\n PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface.\n\n`func (isaac *ISAAC) Seed(seed [256]uint32)`\n\n`func (isaac *ISAAC) Uint32() uint32`\n\n`func (isaac *ISAAC) Uint64() uint64`\n\n`func (isaac *ISAAC) UnmarshalBinary(data []byte) error`\n UnmarshalBinary() restores the state of the PRNG from a byte array\n that was created with MarshalBinary(). UnmarshalBinary implements the\n encoding.BinaryUnmarshaler interface.\n\n" + }, + { + "name": "isaac.gno", + "body": "// This is a port of the ISAAC cryptographically secure PRNG, originally based on the reference\n// implementation found at https://burtleburtle.net/bob/rand/isaacafa.html\n//\n// ISAAC has excellent statistical properties, with long cycle times, and uniformly distributed,\n// unbiased, and unpredictable number generation. It can not be distinguished from real random\n// data, and in three decades of scrutiny, no practical attacks have been found.\n//\n// The default random number algorithm in gno was ported from Go's v2 rand implementation, which\n// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations\n// because it has modest seeding requirements, and generates statistically strong randomness.\n//\n// This package provides an implementation of the 32-bit ISAAC PRNG algorithm. This\n// algorithm provides very strong statistical performance, and is cryptographically\n// secure, while still being substantially faster than the default PCG\n// implementation in `math/rand`. Note that this package does implement a `Uint64()`\n// function in order to generate a 64 bit number out of two 32 bit numbers. Doing this\n// makes the generator only slightly faster than PCG, however,\n//\n// Note that the approach to seeing with ISAAC is very important for best results, and seeding with\n// ISAAC is not as simple as seeding with a single uint64 value. The ISAAC algorithm requires a\n// 256-element seed. If used for cryptographic purposes, this will likely require entropy generated\n// off-chain for actual cryptographically secure seeding. For other purposes, however, one can\n// utilize the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to generate\n// any missing seeds if fewer than 256 are provided.\n//\n//\t\tBenchmark\n//\t\t---------\n//\t\tPCG: 1000000 Uint64 generated in 15.58s\n//\t\tISAAC: 1000000 Uint64 generated in 13.23s\n//\t\tISAAC: 1000000 Uint32 generated in 6.43s\n//\t Ratio: x1.18 times faster than PCG (uint64)\n//\t Ratio: x2.42 times faster than PCG (uint32)\n//\n// Use it directly:\n//\n//\t\tprng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest\n//\t // will be generated using the xorshiftr128plus PRNG.\n//\n// Or use it as a drop-in replacement for the default PRNG in Rand:\n//\n//\tsource = isaac.New()\n//\tprng := rand.New(source)\npackage isaac\n\nimport (\n\t\"errors\"\n\t\"math\"\n\t\"math/rand\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/wyhaines/rand/xorshiftr128plus\"\n)\n\ntype ISAAC struct {\n\trandrsl [256]uint32\n\trandcnt uint32\n\tmm [256]uint32\n\taa, bb, cc uint32\n\tseed [256]uint32\n}\n\n// ISAAC requires a large, 256-element seed. This implementation will leverage the entropy\n// package combined with the the xorshiftr128plus PRNG to generate any missing seeds of\n// fewer than the required number of arguments are provided.\nfunc New(seeds ...uint32) *ISAAC {\n\tisaac := \u0026ISAAC{}\n\tseed := [256]uint32{}\n\n\tindex := 0\n\tfor index = 0; index \u003c len(seeds); index++ {\n\t\tseed[index] = seeds[index]\n\t}\n\n\tif index \u003c 4 {\n\t\te := entropy.New()\n\t\tfor ; index \u003c 4; index++ {\n\t\t\tseed[index] = e.Value()\n\t\t}\n\t}\n\n\t// Use up to the first four seeds as seeding inputs for xorshiftr128+, in order to\n\t// use it to provide any remaining missing seeds.\n\tprng := xorshiftr128plus.New(\n\t\t(uint64(seed[0])\u003c\u003c32)|uint64(seed[1]),\n\t\t(uint64(seed[2])\u003c\u003c32)|uint64(seed[3]),\n\t)\n\tfor ; index \u003c 256; index += 2 {\n\t\tval := prng.Uint64()\n\t\tseed[index] = uint32(val \u0026 0xffffffff)\n\t\tif index+1 \u003c 256 {\n\t\t\tseed[index+1] = uint32(val \u003e\u003e 32)\n\t\t}\n\t}\n\tisaac.Seed(seed)\n\treturn isaac\n}\n\nfunc (isaac *ISAAC) Seed(seed [256]uint32) {\n\tisaac.randrsl = seed\n\tisaac.seed = seed\n\tisaac.randinit(true)\n}\n\n// beUint32() decodes a uint32 from a set of four bytes, assuming big endian encoding.\n// binary.bigEndian.Uint32, copied to avoid dependency\nfunc beUint32(b []byte) uint32 {\n\t_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint32(b[3]) | uint32(b[2])\u003c\u003c8 | uint32(b[1])\u003c\u003c16 | uint32(b[0])\u003c\u003c24\n}\n\n// bePutUint32() encodes a uint64 into a buffer of eight bytes.\n// binary.bigEndian.PutUint32, copied to avoid dependency\nfunc bePutUint32(b []byte, v uint32) {\n\t_ = b[3] // early bounds check to guarantee safety of writes below\n\tb[0] = byte(v \u003e\u003e 24)\n\tb[1] = byte(v \u003e\u003e 16)\n\tb[2] = byte(v \u003e\u003e 8)\n\tb[3] = byte(v)\n}\n\n// A label to identify the marshalled data.\nvar marshalISAACLabel = []byte(\"isaac:\")\n\n// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used\n// with UnmarshalBinary() to restore the state of the PRNG.\n// MarshalBinary implements the encoding.BinaryMarshaler interface.\nfunc (isaac *ISAAC) MarshalBinary() ([]byte, error) {\n\tb := make([]byte, 3094) // 6 + 1024 + 1024 + 1024 + 4 + 4 + 4 + 4 == 3090\n\tcopy(b, marshalISAACLabel)\n\tfor i := 0; i \u003c 256; i++ {\n\t\tbePutUint32(b[6+i*4:], isaac.seed[i])\n\t}\n\tfor i := 256; i \u003c 512; i++ {\n\t\tbePutUint32(b[6+i*4:], isaac.randrsl[i-256])\n\t}\n\tfor i := 512; i \u003c 768; i++ {\n\t\tbePutUint32(b[6+i*4:], isaac.mm[i-512])\n\t}\n\tbePutUint32(b[3078:], isaac.aa)\n\tbePutUint32(b[3082:], isaac.bb)\n\tbePutUint32(b[3086:], isaac.cc)\n\tbePutUint32(b[3090:], isaac.randcnt)\n\n\treturn b, nil\n}\n\n// errUnmarshalISAAC is returned when unmarshalling fails.\nvar errUnmarshalISAAC = errors.New(\"invalid ISAAC encoding\")\n\n// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary().\n// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.\nfunc (isaac *ISAAC) UnmarshalBinary(data []byte) error {\n\tif len(data) != 3094 || string(data[:6]) != string(marshalISAACLabel) {\n\t\treturn errUnmarshalISAAC\n\t}\n\tfor i := 0; i \u003c 256; i++ {\n\t\tisaac.seed[i] = beUint32(data[6+i*4:])\n\t}\n\tfor i := 256; i \u003c 512; i++ {\n\t\tisaac.randrsl[i-256] = beUint32(data[6+i*4:])\n\t}\n\tfor i := 512; i \u003c 768; i++ {\n\t\tisaac.mm[i-512] = beUint32(data[6+i*4:])\n\t}\n\tisaac.aa = beUint32(data[3078:])\n\tisaac.bb = beUint32(data[3082:])\n\tisaac.cc = beUint32(data[3086:])\n\tisaac.randcnt = beUint32(data[3090:])\n\treturn nil\n}\n\nfunc (isaac *ISAAC) randinit(flag bool) {\n\tisaac.aa = 0\n\tisaac.bb = 0\n\tisaac.cc = 0\n\n\tvar a, b, c, d, e, f, g, h uint32 = 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9\n\n\tfor i := 0; i \u003c 4; i++ {\n\t\ta ^= b \u003c\u003c 11\n\t\td += a\n\t\tb += c\n\t\tb ^= c \u003e\u003e 2\n\t\te += b\n\t\tc += d\n\t\tc ^= d \u003c\u003c 8\n\t\tf += c\n\t\td += e\n\t\td ^= e \u003e\u003e 16\n\t\tg += d\n\t\te += f\n\t\te ^= f \u003c\u003c 10\n\t\th += e\n\t\tf += g\n\t\tf ^= g \u003e\u003e 4\n\t\ta += f\n\t\tg += h\n\t\tg ^= h \u003c\u003c 8\n\t\tb += g\n\t\th += a\n\t\th ^= a \u003e\u003e 9\n\t\tc += h\n\t\ta += b\n\t}\n\n\tfor i := 0; i \u003c 256; i += 8 {\n\t\tif flag {\n\t\t\ta += isaac.randrsl[i]\n\t\t\tb += isaac.randrsl[i+1]\n\t\t\tc += isaac.randrsl[i+2]\n\t\t\td += isaac.randrsl[i+3]\n\t\t\te += isaac.randrsl[i+4]\n\t\t\tf += isaac.randrsl[i+5]\n\t\t\tg += isaac.randrsl[i+6]\n\t\t\th += isaac.randrsl[i+7]\n\t\t}\n\n\t\ta ^= b \u003c\u003c 11\n\t\td += a\n\t\tb += c\n\t\tb ^= c \u003e\u003e 2\n\t\te += b\n\t\tc += d\n\t\tc ^= d \u003c\u003c 8\n\t\tf += c\n\t\td += e\n\t\td ^= e \u003e\u003e 16\n\t\tg += d\n\t\te += f\n\t\te ^= f \u003c\u003c 10\n\t\th += e\n\t\tf += g\n\t\tf ^= g \u003e\u003e 4\n\t\ta += f\n\t\tg += h\n\t\tg ^= h \u003c\u003c 8\n\t\tb += g\n\t\th += a\n\t\th ^= a \u003e\u003e 9\n\t\tc += h\n\t\ta += b\n\n\t\tisaac.mm[i] = a\n\t\tisaac.mm[i+1] = b\n\t\tisaac.mm[i+2] = c\n\t\tisaac.mm[i+3] = d\n\t\tisaac.mm[i+4] = e\n\t\tisaac.mm[i+5] = f\n\t\tisaac.mm[i+6] = g\n\t\tisaac.mm[i+7] = h\n\t}\n\n\tif flag {\n\t\tfor i := 0; i \u003c 256; i += 8 {\n\t\t\ta += isaac.mm[i]\n\t\t\tb += isaac.mm[i+1]\n\t\t\tc += isaac.mm[i+2]\n\t\t\td += isaac.mm[i+3]\n\t\t\te += isaac.mm[i+4]\n\t\t\tf += isaac.mm[i+5]\n\t\t\tg += isaac.mm[i+6]\n\t\t\th += isaac.mm[i+7]\n\n\t\t\ta ^= b \u003c\u003c 11\n\t\t\td += a\n\t\t\tb += c\n\t\t\tb ^= c \u003e\u003e 2\n\t\t\te += b\n\t\t\tc += d\n\t\t\tc ^= d \u003c\u003c 8\n\t\t\tf += c\n\t\t\td += e\n\t\t\td ^= e \u003e\u003e 16\n\t\t\tg += d\n\t\t\te += f\n\t\t\te ^= f \u003c\u003c 10\n\t\t\th += e\n\t\t\tf += g\n\t\t\tf ^= g \u003e\u003e 4\n\t\t\ta += f\n\t\t\tg += h\n\t\t\tg ^= h \u003c\u003c 8\n\t\t\tb += g\n\t\t\th += a\n\t\t\th ^= a \u003e\u003e 9\n\t\t\tc += h\n\t\t\ta += b\n\n\t\t\tisaac.mm[i] = a\n\t\t\tisaac.mm[i+1] = b\n\t\t\tisaac.mm[i+2] = c\n\t\t\tisaac.mm[i+3] = d\n\t\t\tisaac.mm[i+4] = e\n\t\t\tisaac.mm[i+5] = f\n\t\t\tisaac.mm[i+6] = g\n\t\t\tisaac.mm[i+7] = h\n\t\t}\n\t}\n\n\tisaac.isaac()\n\tisaac.randcnt = uint32(256)\n}\n\nfunc (isaac *ISAAC) isaac() {\n\tisaac.cc++\n\tisaac.bb += isaac.cc\n\n\tfor i := 0; i \u003c 256; i++ {\n\t\tx := isaac.mm[i]\n\t\tswitch i % 4 {\n\t\tcase 0:\n\t\t\tisaac.aa ^= isaac.aa \u003c\u003c 13\n\t\tcase 1:\n\t\t\tisaac.aa ^= isaac.aa \u003e\u003e 6\n\t\tcase 2:\n\t\t\tisaac.aa ^= isaac.aa \u003c\u003c 2\n\t\tcase 3:\n\t\t\tisaac.aa ^= isaac.aa \u003e\u003e 16\n\t\t}\n\t\tisaac.aa += isaac.mm[(i+128)\u00260xff]\n\n\t\ty := isaac.mm[(x\u003e\u003e2)\u00260xff] + isaac.aa + isaac.bb\n\t\tisaac.mm[i] = y\n\t\tisaac.bb = isaac.mm[(y\u003e\u003e10)\u00260xff] + x\n\t\tisaac.randrsl[i] = isaac.bb\n\t}\n}\n\n// Returns a random uint32.\nfunc (isaac *ISAAC) Uint32() uint32 {\n\tif isaac.randcnt == uint32(0) {\n\t\tisaac.isaac()\n\t\tisaac.randcnt = uint32(256)\n\t}\n\tisaac.randcnt--\n\treturn isaac.randrsl[isaac.randcnt]\n}\n\n// Returns a random uint64 by combining two uint32s.\nfunc (isaac *ISAAC) Uint64() uint64 {\n\treturn uint64(isaac.Uint32()) | (uint64(isaac.Uint32()) \u003c\u003c 32)\n}\n\n// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function.\n// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing\n// for generating a million random uint64 numbers on any unix based system:\n//\n// `time gno run -expr 'benchmarkISAAC()' xorshift64star.gno\nfunc benchmarkISAAC(_iterations ...int) {\n\titerations := 1000000\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\tisaac := New()\n\n\tfor i := 0; i \u003c iterations; i++ {\n\t\t_ = isaac.Uint64()\n\t}\n\tufmt.Println(ufmt.Sprintf(\"ISAAC: generate %d uint64\\n\", iterations))\n}\n\n// The averageISAAC() function is a simple benchmarking helper to demonstrate\n// the most basic statistical property of the ISAAC PRNG.\nfunc averageISAAC(_iterations ...int) {\n\ttarget := uint64(500000)\n\titerations := 1000000\n\tvar squares [1000000]uint64\n\n\tufmt.Println(\n\t\tufmt.Sprintf(\n\t\t\t\"Averaging %d random numbers. The average should be very close to %d.\\n\",\n\t\t\titerations,\n\t\t\ttarget))\n\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\tisaac := New(987654321, 123456789, 999999999, 111111111)\n\n\tvar average float64 = 0\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tn := isaac.Uint64()%(target*2) + 1\n\t\taverage += (float64(n) - average) / float64(i+1)\n\t\tsquares[i] = n\n\t}\n\n\tsum_of_squares := uint64(0)\n\t// transform numbers into their squares of the distance from the average\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tdifference := average - float64(squares[i])\n\t\tsquare := uint64(difference * difference)\n\t\tsum_of_squares += square\n\t}\n\n\tufmt.Println(ufmt.Sprintf(\"ISAAC average of %d uint64: %f\\n\", iterations, average))\n\tufmt.Println(ufmt.Sprintf(\"ISAAC standard deviation : %f\\n\", math.Sqrt(float64(sum_of_squares)/float64(iterations))))\n\tufmt.Println(ufmt.Sprintf(\"ISAAC theoretical perfect deviation: %f\\n\", (float64(target*2)-1)/math.Sqrt(12)))\n}\n\nfunc averagePCG(_iterations ...int) {\n\ttarget := uint64(500000)\n\titerations := 1000000\n\tvar squares [1000000]uint64\n\n\tufmt.Println(\n\t\tufmt.Sprintf(\n\t\t\t\"Averaging %d random numbers. The average should be very close to %d.\\n\",\n\t\t\titerations,\n\t\t\ttarget))\n\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\tisaac := rand.NewPCG(987654321, 123456789)\n\n\tvar average float64 = 0\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tn := isaac.Uint64()%(target*2) + 1\n\t\taverage += (float64(n) - average) / float64(i+1)\n\t\tsquares[i] = n\n\t}\n\n\tsum_of_squares := uint64(0)\n\t// transform numbers into their squares of the distance from the average\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tdifference := average - float64(squares[i])\n\t\tsquare := uint64(difference * difference)\n\t\tsum_of_squares += square\n\t}\n\n\tufmt.Println(ufmt.Sprintf(\"PCG average of %d uint64: %f\\n\", iterations, average))\n\tufmt.Println(ufmt.Sprintf(\"PCG standard deviation : %f\\n\", math.Sqrt(float64(sum_of_squares)/float64(iterations))))\n\tufmt.Println(ufmt.Sprintf(\"PCG theoretical perfect deviation: %f\\n\", (float64(target*2)-1)/math.Sqrt(12)))\n}\n" + }, + { + "name": "isaac_test.gno", + "body": "package isaac\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n)\n\ntype OpenISAAC struct {\n\tRandrsl [256]uint32\n\tRandcnt uint32\n\tMm [256]uint32\n\tAa, Bb, Cc uint32\n\tSeed [256]uint32\n}\n\nfunc TestISAACSeeding(t *testing.T) {\n\t_ = New()\n}\n\nfunc TestISAACRand(t *testing.T) {\n\tsource := New(987654321)\n\trng := rand.New(source)\n\n\t// Expected outputs for the first 5 random floats with the given seed\n\texpected := []float64{\n\t\t0.17828173023837635,\n\t\t0.7327795780287832,\n\t\t0.4850369074875177,\n\t\t0.9474842397428482,\n\t\t0.6747135561813891,\n\t\t0.7522507082868403,\n\t\t0.041115261836534356,\n\t\t0.7405243709084567,\n\t\t0.672863376128768,\n\t\t0.11866211399980553,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rng.Float64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Rand.Float64() at iteration %d: got %g, expected %g\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestISAACUint64(t *testing.T) {\n\tisaac := New()\n\n\texpected := []uint64{\n\t\t5986068031949215749,\n\t\t10437354066128700566,\n\t\t13478007513323023970,\n\t\t8969511410255984224,\n\t\t3869229557962857982,\n\t\t1762449743873204415,\n\t\t5292356290662282456,\n\t\t7893982194485405616,\n\t\t4296136494566588699,\n\t\t12414349056998262772,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := isaac.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc dupState(i *ISAAC) *OpenISAAC {\n\tstate := \u0026OpenISAAC{}\n\tstate.Seed = i.seed\n\tstate.Randrsl = i.randrsl\n\tstate.Mm = i.mm\n\tstate.Aa = i.aa\n\tstate.Bb = i.bb\n\tstate.Cc = i.cc\n\tstate.Randcnt = i.randcnt\n\n\treturn state\n}\n\nfunc TestISAACMarshalUnmarshal(t *testing.T) {\n\tisaac := New()\n\n\texpected1 := []uint64{\n\t\t5986068031949215749,\n\t\t10437354066128700566,\n\t\t13478007513323023970,\n\t\t8969511410255984224,\n\t\t3869229557962857982,\n\t}\n\n\texpected2 := []uint64{\n\t\t1762449743873204415,\n\t\t5292356290662282456,\n\t\t7893982194485405616,\n\t\t4296136494566588699,\n\t\t12414349056998262772,\n\t}\n\n\tfor i, exp := range expected1 {\n\t\tval := isaac.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n\n\tmarshalled, err := isaac.MarshalBinary()\n\n\tt.Logf(\"State: [%v]\\n\", dupState(isaac))\n\tt.Logf(\"Marshalled State: [%x] -- %v\\n\", marshalled, err)\n\tstate_before := dupState(isaac)\n\n\tif err != nil {\n\t\tt.Errorf(\"ISAAC.MarshalBinary() error: %v\", err)\n\t}\n\n\t// Advance state by one number; then check the next 5. The expectation is that they _will_ fail.\n\tisaac.Uint64()\n\n\tfor i, exp := range expected2 {\n\t\tval := isaac.Uint64()\n\t\tif exp == val {\n\t\t\tt.Errorf(\" Iteration %d matched %d; which is from iteration %d; something strange is happening.\", (i + 6), val, (i + 5))\n\t\t}\n\t}\n\n\tt.Logf(\"State before unmarshall: [%v]\\n\", dupState(isaac))\n\n\t// Now restore the state of the PRNG\n\terr = isaac.UnmarshalBinary(marshalled)\n\n\tt.Logf(\"State after unmarshall: [%v]\\n\", dupState(isaac))\n\n\tif state_before.Seed != dupState(isaac).Seed {\n\t\tt.Errorf(\"Seed mismatch\")\n\t}\n\tif state_before.Randrsl != dupState(isaac).Randrsl {\n\t\tt.Errorf(\"Randrsl mismatch\")\n\t}\n\tif state_before.Mm != dupState(isaac).Mm {\n\t\tt.Errorf(\"Mm mismatch\")\n\t}\n\tif state_before.Aa != dupState(isaac).Aa {\n\t\tt.Errorf(\"Aa mismatch\")\n\t}\n\tif state_before.Bb != dupState(isaac).Bb {\n\t\tt.Errorf(\"Bb mismatch\")\n\t}\n\tif state_before.Cc != dupState(isaac).Cc {\n\t\tt.Errorf(\"Cc mismatch\")\n\t}\n\tif state_before.Randcnt != dupState(isaac).Randcnt {\n\t\tt.Errorf(\"Randcnt mismatch\")\n\t}\n\n\t// Now we should be back on track for the last 5 numbers\n\tfor i, exp := range expected2 {\n\t\tval := isaac.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", (i + 5), val, exp)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Io2eigmgFB5VSoTShgemSl2+JVJnp1z8gkpAIhqiZoY9CO2MqZzTMkhqPQDfujqOLHnAwl+8C1c+XkFHbo37Ag==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "isaac64", + "path": "gno.land/p/wyhaines/rand/isaac64", + "files": [ + { + "name": "README.md", + "body": "# package isaac64 // import \"gno.land/p/demo/math/rand/isaac64\"\n\nThis is a port of the 64-bit version of the ISAAC cryptographically\nsecure PRNG, originally based on the reference implementation found at\nhttps://burtleburtle.net/bob/rand/isaacafa.html\n\nISAAC has excellent statistical properties, with long cycle times, and\nuniformly distributed, unbiased, and unpredictable number generation. It can\nnot be distinguished from real random data, and in three decades of scrutiny,\nno practical attacks have been found.\n\nThe default random number algorithm in gno was ported from Go's v2 rand\nimplementatoon, which defaults to the PCG algorithm. This algorithm is\ncommonly used in language PRNG implementations because it has modest seeding\nrequirements, and generates statistically strong randomness.\n\nThis package provides an implementation of the 64-bit ISAAC PRNG algorithm. This\nalgorithm provides very strong statistical performance, and is cryptographically\nsecure, while still being substantially faster than the default PCG\nimplementation in `math/rand`.\n\nNote that the approach to seeing with ISAAC is very important for best results,\nand seeding with ISAAC is not as simple as seeding with a single uint64 value.\nThe ISAAC algorithm requires a 256-element seed. If used for cryptographic\npurposes, this will likely require entropy generated off-chain for actual\ncryptographically secure seeding. For other purposes, however, one can utilize\nthe built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to\ngenerate any missing seeds if fewer than 256 are provided.\n\n\n```\nBenchmark\n---------\nPCG: 1000000 Uint64 generated in 15.58s\nISAAC: 1000000 Uint64 generated in 8.95s\nISAAC: 1000000 Uint32 generated in 7.66s\nRatio: x1.74 times faster than PCG (uint64)\nRatio: x2.03 times faster than PCG (uint32)\n```\n\nUse it directly:\n\n\n```\nprng = isaac.New() // pass 0 to 256 uint64 seeds; if fewer than 256 are provided, the rest\n // will be generated using the xorshiftr128plus PRNG.\n```\n\nOr use it as a drop-in replacement for the default PRNT in Rand:\n\n```\nsource = isaac64.New()\nprng := rand.New(source)\n```\n\n## CONSTANTS\n\n\n```\nconst (\n\tRANDSIZL = 8\n\tRANDSIZ = 1 \u003c\u003c RANDSIZL // 256\n)\n```\n\n## TYPES\n\n\n```\ntype ISAAC struct {\n\t// Has unexported fields.\n}\n```\n\n`func New(seeds ...uint64) *ISAAC`\nISAAC requires a large, 256-element seed. This implementation will leverage\nthe entropy package combined with the xorshiftr128plus PRNG to generate any\nmissing seeds if fewer than the required number of arguments are provided.\n\n`func (isaac *ISAAC) MarshalBinary() ([]byte, error)`\nMarshalBinary() returns a byte array that encodes the state of the PRNG.\nThis can later be used with UnmarshalBinary() to restore the state of the\nPRNG. MarshalBinary implements the encoding.BinaryMarshaler interface.\n\n`func (isaac *ISAAC) Seed(seed [256]uint64)`\nReinitialize the generator with a new seed. A seed must be composed of 256 uint64.\n\n`func (isaac *ISAAC) Uint32() uint32`\nReturn a 32 bit random integer, composed of the high 32 bits of the generated 32 bit result.\n\n`func (isaac *ISAAC) Uint64() uint64`\nReturn a 64 bit random integer.\n\n`func (isaac *ISAAC) UnmarshalBinary(data []byte) error`\nUnmarshalBinary() restores the state of the PRNG from a byte array\nthat was created with MarshalBinary(). UnmarshalBinary implements the\nencoding.BinaryUnmarshaler interface.\n" + }, + { + "name": "isaac64.gno", + "body": "// This is a port of the 64-bit version of the ISAAC cryptographically secure PRNG, originally\n// based on the reference implementation found at https://burtleburtle.net/bob/rand/isaacafa.html\n//\n// ISAAC has excellent statistical properties, with long cycle times, and uniformly distributed,\n// unbiased, and unpredictable number generation. It can not be distinguished from real random\n// data, and in three decades of scrutiny, no practical attacks have been found.\n//\n// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which\n// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations\n// because it has modest seeding requirements, and generates statistically strong randomness.\n//\n// This package provides an implementation of the 64-bit ISAAC PRNG algorithm. This algorithm\n// provides very strong statistical performance, and is cryptographically secure, while still\n// being substantially faster than the default PCG implementation in `math/rand`.\n//\n// Note that the approach to seeing with ISAAC is very important for best results, and seeding with\n// ISAAC is not as simple as seeding with a single uint64 value. The ISAAC algorithm requires a\n// 256-element seed. If used for cryptographic purposes, this will likely require entropy generated\n// off-chain for actual cryptographically secure seeding. For other purposes, however, one can\n// utilize the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to generate\n// any missing seeds if fewer than 256 are provided.\n//\n//\t\tBenchmark\n//\t\t---------\n//\t\tPCG: 1000000 Uint64 generated in 15.58s\n//\t\tISAAC: 1000000 Uint64 generated in 8.95s\n//\t ISAAC: 1000000 Uint32 generated in 7.66s\n//\t\tRatio: x1.74 times faster than PCG (uint64)\n//\t Ratio: x2.03 times faster than PCG (uint32)\n//\n// Use it directly:\n//\n//\t\tprng = isaac.New() // pass 0 to 256 uint64 seeds; if fewer than 256 are provided, the rest\n//\t // will be generated using the xorshiftr128plus PRNG.\n//\n// Or use it as a drop-in replacement for the default PRNT in Rand:\n//\n//\tsource = isaac64.New()\n//\tprng := rand.New(source)\npackage isaac64\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/wyhaines/rand/xorshiftr128plus\"\n)\n\nconst (\n\tRANDSIZL = 8\n\tRANDSIZ = 1 \u003c\u003c RANDSIZL // 256\n)\n\ntype ISAAC struct {\n\trandrsl [256]uint64\n\trandcnt uint64\n\tmm [256]uint64\n\taa, bb, cc uint64\n\tseed [256]uint64\n}\n\n// ISAAC requires a large, 256-element seed. This implementation will leverage the entropy\n// package combined with the xorshiftr128plus PRNG to generate any missing seeds if fewer than\n// the required number of arguments are provided.\nfunc New(seeds ...uint64) *ISAAC {\n\tisaac := \u0026ISAAC{}\n\tseed := [256]uint64{}\n\n\tindex := 0\n\tfor index = 0; index \u003c len(seeds) \u0026\u0026 index \u003c 256; index++ {\n\t\tseed[index] = seeds[index]\n\t}\n\n\tif index \u003c 2 {\n\t\te := entropy.New()\n\t\tfor ; index \u003c 2; index++ {\n\t\t\tseed[index] = e.Value64()\n\t\t}\n\t}\n\n\t// Use the first two seeds as seeding inputs for xorshiftr128plus, in order to\n\t// use it to provide any remaining missing seeds.\n\tprng := xorshiftr128plus.New(\n\t\tseed[0],\n\t\tseed[1],\n\t)\n\tfor ; index \u003c 256; index++ {\n\t\tseed[index] = prng.Uint64()\n\t}\n\tisaac.Seed(seed)\n\treturn isaac\n}\n\n// Reinitialize the generator with a new seed. A seed must be composed of 256 uint64.\nfunc (isaac *ISAAC) Seed(seed [256]uint64) {\n\tisaac.randrsl = seed\n\tisaac.seed = seed\n\tisaac.randinit(true)\n}\n\n// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding.\nfunc beUint64(b []byte) uint64 {\n\t_ = b[7] // bounds check hint to compiler\n\treturn uint64(b[7]) | uint64(b[6])\u003c\u003c8 | uint64(b[5])\u003c\u003c16 | uint64(b[4])\u003c\u003c24 |\n\t\tuint64(b[3])\u003c\u003c32 | uint64(b[2])\u003c\u003c40 | uint64(b[1])\u003c\u003c48 | uint64(b[0])\u003c\u003c56\n}\n\n// bePutUint64() encodes a uint64 into a buffer of eight bytes.\nfunc bePutUint64(b []byte, v uint64) {\n\t_ = b[7] // early bounds check to guarantee safety of writes below\n\tb[0] = byte(v \u003e\u003e 56)\n\tb[1] = byte(v \u003e\u003e 48)\n\tb[2] = byte(v \u003e\u003e 40)\n\tb[3] = byte(v \u003e\u003e 32)\n\tb[4] = byte(v \u003e\u003e 24)\n\tb[5] = byte(v \u003e\u003e 16)\n\tb[6] = byte(v \u003e\u003e 8)\n\tb[7] = byte(v)\n}\n\n// A label to identify the marshalled data.\nvar marshalISAACLabel = []byte(\"isaac:\")\n\n// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used\n// with UnmarshalBinary() to restore the state of the PRNG.\n// MarshalBinary implements the encoding.BinaryMarshaler interface.\nfunc (isaac *ISAAC) MarshalBinary() ([]byte, error) {\n\tb := make([]byte, 6+2048*3+8*3+8) // 6 + 2048*3 + 8*3 + 8 == 6182\n\tcopy(b, marshalISAACLabel)\n\toffset := 6\n\tfor i := 0; i \u003c 256; i++ {\n\t\tbePutUint64(b[offset:], isaac.seed[i])\n\t\toffset += 8\n\t}\n\tfor i := 0; i \u003c 256; i++ {\n\t\tbePutUint64(b[offset:], isaac.randrsl[i])\n\t\toffset += 8\n\t}\n\tfor i := 0; i \u003c 256; i++ {\n\t\tbePutUint64(b[offset:], isaac.mm[i])\n\t\toffset += 8\n\t}\n\tbePutUint64(b[offset:], isaac.aa)\n\toffset += 8\n\tbePutUint64(b[offset:], isaac.bb)\n\toffset += 8\n\tbePutUint64(b[offset:], isaac.cc)\n\toffset += 8\n\tbePutUint64(b[offset:], isaac.randcnt)\n\treturn b, nil\n}\n\n// errUnmarshalISAAC is returned when unmarshalling fails.\nvar errUnmarshalISAAC = errors.New(\"invalid ISAAC encoding\")\n\n// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary().\n// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.\nfunc (isaac *ISAAC) UnmarshalBinary(data []byte) error {\n\tif len(data) != 6182 || string(data[:6]) != string(marshalISAACLabel) {\n\t\treturn errUnmarshalISAAC\n\t}\n\toffset := 6\n\tfor i := 0; i \u003c 256; i++ {\n\t\tisaac.seed[i] = beUint64(data[offset:])\n\t\toffset += 8\n\t}\n\tfor i := 0; i \u003c 256; i++ {\n\t\tisaac.randrsl[i] = beUint64(data[offset:])\n\t\toffset += 8\n\t}\n\tfor i := 0; i \u003c 256; i++ {\n\t\tisaac.mm[i] = beUint64(data[offset:])\n\t\toffset += 8\n\t}\n\tisaac.aa = beUint64(data[offset:])\n\toffset += 8\n\tisaac.bb = beUint64(data[offset:])\n\toffset += 8\n\tisaac.cc = beUint64(data[offset:])\n\toffset += 8\n\tisaac.randcnt = beUint64(data[offset:])\n\treturn nil\n}\n\nfunc (isaac *ISAAC) randinit(flag bool) {\n\tvar a, b, c, d, e, f, g, h uint64\n\tisaac.aa = 0\n\tisaac.bb = 0\n\tisaac.cc = 0\n\n\ta = 0x9e3779b97f4a7c13\n\tb = 0x9e3779b97f4a7c13\n\tc = 0x9e3779b97f4a7c13\n\td = 0x9e3779b97f4a7c13\n\te = 0x9e3779b97f4a7c13\n\tf = 0x9e3779b97f4a7c13\n\tg = 0x9e3779b97f4a7c13\n\th = 0x9e3779b97f4a7c13\n\n\t// scramble it\n\tfor i := 0; i \u003c 4; i++ {\n\t\tmix(\u0026a, \u0026b, \u0026c, \u0026d, \u0026e, \u0026f, \u0026g, \u0026h)\n\t}\n\n\t// fill in mm[] with messy stuff\n\tfor i := 0; i \u003c RANDSIZ; i += 8 {\n\t\tif flag {\n\t\t\ta += isaac.randrsl[i]\n\t\t\tb += isaac.randrsl[i+1]\n\t\t\tc += isaac.randrsl[i+2]\n\t\t\td += isaac.randrsl[i+3]\n\t\t\te += isaac.randrsl[i+4]\n\t\t\tf += isaac.randrsl[i+5]\n\t\t\tg += isaac.randrsl[i+6]\n\t\t\th += isaac.randrsl[i+7]\n\t\t}\n\t\tmix(\u0026a, \u0026b, \u0026c, \u0026d, \u0026e, \u0026f, \u0026g, \u0026h)\n\t\tisaac.mm[i] = a\n\t\tisaac.mm[i+1] = b\n\t\tisaac.mm[i+2] = c\n\t\tisaac.mm[i+3] = d\n\t\tisaac.mm[i+4] = e\n\t\tisaac.mm[i+5] = f\n\t\tisaac.mm[i+6] = g\n\t\tisaac.mm[i+7] = h\n\t}\n\n\tif flag {\n\t\t// do a second pass to make all of the seed affect all of mm\n\t\tfor i := 0; i \u003c RANDSIZ; i += 8 {\n\t\t\ta += isaac.mm[i]\n\t\t\tb += isaac.mm[i+1]\n\t\t\tc += isaac.mm[i+2]\n\t\t\td += isaac.mm[i+3]\n\t\t\te += isaac.mm[i+4]\n\t\t\tf += isaac.mm[i+5]\n\t\t\tg += isaac.mm[i+6]\n\t\t\th += isaac.mm[i+7]\n\t\t\tmix(\u0026a, \u0026b, \u0026c, \u0026d, \u0026e, \u0026f, \u0026g, \u0026h)\n\t\t\tisaac.mm[i] = a\n\t\t\tisaac.mm[i+1] = b\n\t\t\tisaac.mm[i+2] = c\n\t\t\tisaac.mm[i+3] = d\n\t\t\tisaac.mm[i+4] = e\n\t\t\tisaac.mm[i+5] = f\n\t\t\tisaac.mm[i+6] = g\n\t\t\tisaac.mm[i+7] = h\n\t\t}\n\t}\n\n\tisaac.isaac()\n\tisaac.randcnt = RANDSIZ\n}\n\nfunc mix(a, b, c, d, e, f, g, h *uint64) {\n\t*a -= *e\n\t*f ^= *h \u003e\u003e 9\n\t*h += *a\n\n\t*b -= *f\n\t*g ^= *a \u003c\u003c 9\n\t*a += *b\n\n\t*c -= *g\n\t*h ^= *b \u003e\u003e 23\n\t*b += *c\n\n\t*d -= *h\n\t*a ^= *c \u003c\u003c 15\n\t*c += *d\n\n\t*e -= *a\n\t*b ^= *d \u003e\u003e 14\n\t*d += *e\n\n\t*f -= *b\n\t*c ^= *e \u003c\u003c 20\n\t*e += *f\n\n\t*g -= *c\n\t*d ^= *f \u003e\u003e 17\n\t*f += *g\n\n\t*h -= *d\n\t*e ^= *g \u003c\u003c 14\n\t*g += *h\n}\n\nfunc ind(mm []uint64, x uint64) uint64 {\n\treturn mm[(x\u003e\u003e3)\u0026(RANDSIZ-1)]\n}\n\nfunc (isaac *ISAAC) isaac() {\n\tvar a, b, x, y uint64\n\ta = isaac.aa\n\tb = isaac.bb + isaac.cc + 1\n\tisaac.cc++\n\n\tm := isaac.mm[:]\n\tr := isaac.randrsl[:]\n\n\tvar i, m2Index int\n\n\t// First half\n\tfor i = 0; i \u003c RANDSIZ/2; i++ {\n\t\tm2Index = i + RANDSIZ/2\n\t\tswitch i % 4 {\n\t\tcase 0:\n\t\t\ta = ^(a ^ (a \u003c\u003c 21)) + m[m2Index]\n\t\tcase 1:\n\t\t\ta = (a ^ (a \u003e\u003e 5)) + m[m2Index]\n\t\tcase 2:\n\t\t\ta = (a ^ (a \u003c\u003c 12)) + m[m2Index]\n\t\tcase 3:\n\t\t\ta = (a ^ (a \u003e\u003e 33)) + m[m2Index]\n\t\t}\n\t\tx = m[i]\n\t\ty = ind(m, x) + a + b\n\t\tm[i] = y\n\t\tb = ind(m, y\u003e\u003eRANDSIZL) + x\n\t\tr[i] = b\n\t}\n\n\t// Second half\n\tfor i = RANDSIZ / 2; i \u003c RANDSIZ; i++ {\n\t\tm2Index = i - RANDSIZ/2\n\t\tswitch i % 4 {\n\t\tcase 0:\n\t\t\ta = ^(a ^ (a \u003c\u003c 21)) + m[m2Index]\n\t\tcase 1:\n\t\t\ta = (a ^ (a \u003e\u003e 5)) + m[m2Index]\n\t\tcase 2:\n\t\t\ta = (a ^ (a \u003c\u003c 12)) + m[m2Index]\n\t\tcase 3:\n\t\t\ta = (a ^ (a \u003e\u003e 33)) + m[m2Index]\n\t\t}\n\t\tx = m[i]\n\t\ty = ind(m, x) + a + b\n\t\tm[i] = y\n\t\tb = ind(m, y\u003e\u003eRANDSIZL) + x\n\t\tr[i] = b\n\t}\n\n\tisaac.bb = b\n\tisaac.aa = a\n}\n\n// Return a 64 bit random integer.\nfunc (isaac *ISAAC) Uint64() uint64 {\n\tif isaac.randcnt == 0 {\n\t\tisaac.isaac()\n\t\tisaac.randcnt = RANDSIZ\n\t}\n\tisaac.randcnt--\n\treturn isaac.randrsl[isaac.randcnt]\n}\n\nvar gencycle int = 0\nvar bufferFor32 uint64 = uint64(0)\n\n// Return a 32 bit random integer, composed of the high 32 bits of the generated 32 bit result.\nfunc (isaac *ISAAC) Uint32() uint32 {\n\tif gencycle == 0 {\n\t\tbufferFor32 = isaac.Uint64()\n\t\tgencycle = 1\n\t\treturn uint32(bufferFor32 \u003e\u003e 32)\n\t}\n\n\tgencycle = 0\n\treturn uint32(bufferFor32 \u0026 0xffffffff)\n}\n\n// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function.\n// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing\n// for generating a million random uint64 numbers on any unix based system:\n//\n// `time gno run -expr 'benchmarkISAAC()' isaac64.gno\nfunc benchmarkISAAC(_iterations ...int) {\n\titerations := 1000000\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\tisaac := New()\n\n\tfor i := 0; i \u003c iterations; i++ {\n\t\t_ = isaac.Uint64()\n\t}\n\tufmt.Println(ufmt.Sprintf(\"ISAAC: generated %d uint64\\n\", iterations))\n}\n\n// The averageISAAC() function is a simple benchmarking helper to demonstrate\n// the most basic statistical property of the ISAAC PRNG.\nfunc averageISAAC(_iterations ...int) {\n\ttarget := uint64(500000)\n\titerations := 1000000\n\n\tufmt.Println(\n\t\tufmt.Sprintf(\n\t\t\t\"Averaging %d random numbers. The average should be very close to %d.\\n\",\n\t\t\titerations,\n\t\t\ttarget))\n\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\tisaac := New(987654321987654321, 123456789987654321, 1, 997755331886644220)\n\n\tvar average float64 = 0\n\tvar squares []uint64 = make([]uint64, iterations)\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tn := isaac.Uint64()%(target*2) + 1\n\t\taverage += (float64(n) - average) / float64(i+1)\n\t\tsquares[i] = n\n\t}\n\n\tsum_of_squares := uint64(0)\n\t// transform numbers into their squares of the distance from the average\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tdifference := average - float64(squares[i])\n\t\tsquare := uint64(difference * difference)\n\t\tsum_of_squares += square\n\t}\n\n\tufmt.Println(ufmt.Sprintf(\"ISAAC average of %d uint64: %f\\n\", iterations, average))\n\tufmt.Println(ufmt.Sprintf(\"ISAAC standard deviation : %f\\n\", math.Sqrt(float64(sum_of_squares)/float64(iterations))))\n\tufmt.Println(ufmt.Sprintf(\"ISAAC theoretical perfect deviation: %f\\n\", (float64(target*2)-1)/math.Sqrt(12)))\n}\n" + }, + { + "name": "isaac64_test.gno", + "body": "package isaac64\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n)\n\ntype OpenISAAC struct {\n\tRandrsl [256]uint64\n\tRandcnt uint64\n\tMm [256]uint64\n\tAa, Bb, Cc uint64\n\tSeed [256]uint64\n}\n\nfunc TestISAACSeeding(t *testing.T) {\n\t_ = New()\n}\n\nfunc TestISAACRand(t *testing.T) {\n\tsource := New(987654321)\n\trng := rand.New(source)\n\n\t// Expected outputs for the first 5 random floats with the given seed\n\texpected := []float64{\n\t\t0.9273376778618531,\n\t\t0.327620245173309,\n\t\t0.49315436150113456,\n\t\t0.9222536383598948,\n\t\t0.2999297342641162,\n\t\t0.4050531597269049,\n\t\t0.5321357451089953,\n\t\t0.19478000239059667,\n\t\t0.5156043950865713,\n\t\t0.9233494881511063,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rng.Float64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Rand.Float64() at iteration %d: got %g, expected %g\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestISAACUint64(t *testing.T) {\n\tisaac := New()\n\n\texpected := []uint64{\n\t\t6781932227698873623,\n\t\t14800945299485332986,\n\t\t4114322996297394168,\n\t\t5328012296808356526,\n\t\t12789214124608876433,\n\t\t17611101631239575547,\n\t\t6877490613942924608,\n\t\t15954522518901325556,\n\t\t14180160756719376887,\n\t\t4977949063252893357,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := isaac.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc dupState(i *ISAAC) *OpenISAAC {\n\tstate := \u0026OpenISAAC{}\n\tstate.Seed = i.seed\n\tstate.Randrsl = i.randrsl\n\tstate.Mm = i.mm\n\tstate.Aa = i.aa\n\tstate.Bb = i.bb\n\tstate.Cc = i.cc\n\tstate.Randcnt = i.randcnt\n\n\treturn state\n}\n\nfunc TestISAACMarshalUnmarshal(t *testing.T) {\n\tisaac := New()\n\n\texpected1 := []uint64{\n\t\t6781932227698873623,\n\t\t14800945299485332986,\n\t\t4114322996297394168,\n\t\t5328012296808356526,\n\t\t12789214124608876433,\n\t}\n\n\texpected2 := []uint64{\n\t\t17611101631239575547,\n\t\t6877490613942924608,\n\t\t15954522518901325556,\n\t\t14180160756719376887,\n\t\t4977949063252893357,\n\t}\n\n\tfor i, exp := range expected1 {\n\t\tval := isaac.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n\n\tmarshalled, err := isaac.MarshalBinary()\n\n\tt.Logf(\"State: [%v]\\n\", dupState(isaac))\n\tt.Logf(\"Marshalled State: [%x] -- %v\\n\", marshalled, err)\n\tstate_before := dupState(isaac)\n\n\tif err != nil {\n\t\tt.Errorf(\"ISAAC.MarshalBinary() error: %v\", err)\n\t}\n\n\t// Advance state by one number; then check the next 5. The expectation is that they _will_ fail.\n\tisaac.Uint64()\n\n\tfor i, exp := range expected2 {\n\t\tval := isaac.Uint64()\n\t\tif exp == val {\n\t\t\tt.Errorf(\" Iteration %d matched %d; which is from iteration %d; something strange is happening.\", (i + 6), val, (i + 5))\n\t\t}\n\t}\n\n\tt.Logf(\"State before unmarshall: [%v]\\n\", dupState(isaac))\n\n\t// Now restore the state of the PRNG\n\terr = isaac.UnmarshalBinary(marshalled)\n\n\tt.Logf(\"State after unmarshall: [%v]\\n\", dupState(isaac))\n\n\tif state_before.Seed != dupState(isaac).Seed {\n\t\tt.Errorf(\"Seed mismatch\")\n\t}\n\tif state_before.Randrsl != dupState(isaac).Randrsl {\n\t\tt.Errorf(\"Randrsl mismatch\")\n\t}\n\tif state_before.Mm != dupState(isaac).Mm {\n\t\tt.Errorf(\"Mm mismatch\")\n\t}\n\tif state_before.Aa != dupState(isaac).Aa {\n\t\tt.Errorf(\"Aa mismatch\")\n\t}\n\tif state_before.Bb != dupState(isaac).Bb {\n\t\tt.Errorf(\"Bb mismatch\")\n\t}\n\tif state_before.Cc != dupState(isaac).Cc {\n\t\tt.Errorf(\"Cc mismatch\")\n\t}\n\tif state_before.Randcnt != dupState(isaac).Randcnt {\n\t\tt.Errorf(\"Randcnt mismatch\")\n\t}\n\n\t// Now we should be back on track for the last 5 numbers\n\tfor i, exp := range expected2 {\n\t\tval := isaac.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"ISAAC.Uint64() at iteration %d: got %d, expected %d\", (i + 5), val, exp)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "WrbeocTzfqnP5lb0VQ7xO8HmhUJnRnnVEXJxOcGpbAykT/Y7RZxhXhRRXkk6XU7Nxu1GfCUmv1nRF7KVt4pLDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "xorshift64star", + "path": "gno.land/p/wyhaines/rand/xorshift64star", + "files": [ + { + "name": "xorshift64star.gno", + "body": "// Xorshift64* is a very fast psuedo-random number generation algorithm with strong\n// statistical properties.\n//\n// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which\n// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations\n// because it has modest seeding requirements, and generates statistically strong randomness.\n//\n// This package provides an implementation of the Xorshift64* PRNG algorithm. This algorithm provides\n// strong statistical performance with most seeds (just don't seed it with zero), and the performance\n// of this implementation in Gno is more than four times faster than the default PCG implementation in\n// `math/rand`.\n//\n//\tBenchmark\n//\t---------\n//\tPCG: 1000000 Uint64 generated in 15.58s\n//\tXorshift64*: 1000000 Uint64 generated in 3.77s\n//\tRatio: x4.11 times faster than PCG\n//\n// Use it directly:\n//\n//\tprng = xorshift64star.New() // pass a uint64 to seed it or pass nothing to seed it with entropy\n//\n// Or use it as a drop-in replacement for the default PRNT in Rand:\n//\n//\tsource = xorshift64star.New()\n//\tprng := rand.New(source)\npackage xorshift64star\n\nimport (\n\t\"errors\"\n\t\"math\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Xorshift64Star is a PRNG that implements the Xorshift64* algorithm.\ntype Xorshift64Star struct {\n\tseed uint64\n}\n\n// New() creates a new instance of the PRNG with a given seed, which\n// should be a uint64. If no seed is provided, the PRNG will be seeded via the\n// gno.land/p/demo/entropy package.\nfunc New(seed ...uint64) *Xorshift64Star {\n\txs := \u0026Xorshift64Star{}\n\txs.Seed(seed...)\n\treturn xs\n}\n\n// Seed() implements the rand.Source interface. It provides a way to set the seed for the PRNG.\nfunc (xs *Xorshift64Star) Seed(seed ...uint64) {\n\tif len(seed) == 0 {\n\t\te := entropy.New()\n\t\txs.seed = e.Value64()\n\t} else {\n\t\txs.seed = seed[0]\n\t}\n}\n\n// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding.\n// binary.bigEndian.Uint64, copied to avoid dependency\nfunc beUint64(b []byte) uint64 {\n\t_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808\n\treturn uint64(b[7]) | uint64(b[6])\u003c\u003c8 | uint64(b[5])\u003c\u003c16 | uint64(b[4])\u003c\u003c24 |\n\t\tuint64(b[3])\u003c\u003c32 | uint64(b[2])\u003c\u003c40 | uint64(b[1])\u003c\u003c48 | uint64(b[0])\u003c\u003c56\n}\n\n// bePutUint64() encodes a uint64 into a buffer of eight bytes.\n// binary.bigEndian.PutUint64, copied to avoid dependency\nfunc bePutUint64(b []byte, v uint64) {\n\t_ = b[7] // early bounds check to guarantee safety of writes below\n\tb[0] = byte(v \u003e\u003e 56)\n\tb[1] = byte(v \u003e\u003e 48)\n\tb[2] = byte(v \u003e\u003e 40)\n\tb[3] = byte(v \u003e\u003e 32)\n\tb[4] = byte(v \u003e\u003e 24)\n\tb[5] = byte(v \u003e\u003e 16)\n\tb[6] = byte(v \u003e\u003e 8)\n\tb[7] = byte(v)\n}\n\n// A label to identify the marshalled data.\nvar marshalXorshift64StarLabel = []byte(\"xorshift64*:\")\n\n// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used\n// with UnmarshalBinary() to restore the state of the PRNG.\n// MarshalBinary implements the encoding.BinaryMarshaler interface.\nfunc (xs *Xorshift64Star) MarshalBinary() ([]byte, error) {\n\tb := make([]byte, 20)\n\tcopy(b, marshalXorshift64StarLabel)\n\tbePutUint64(b[12:], xs.seed)\n\treturn b, nil\n}\n\n// errUnmarshalXorshift64Star is returned when unmarshalling fails.\nvar errUnmarshalXorshift64Star = errors.New(\"invalid Xorshift64* encoding\")\n\n// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary().\n// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.\nfunc (xs *Xorshift64Star) UnmarshalBinary(data []byte) error {\n\tif len(data) != 20 || string(data[:12]) != string(marshalXorshift64StarLabel) {\n\t\treturn errUnmarshalXorshift64Star\n\t}\n\txs.seed = beUint64(data[12:])\n\treturn nil\n}\n\n// Uint64() generates the next random uint64 value.\nfunc (xs *Xorshift64Star) Uint64() uint64 {\n\txs.seed ^= xs.seed \u003e\u003e 12\n\txs.seed ^= xs.seed \u003c\u003c 25\n\txs.seed ^= xs.seed \u003e\u003e 27\n\txs.seed *= 2685821657736338717\n\treturn xs.seed // Operations naturally wrap around in uint64\n}\n\n// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function.\n// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing\n// for generating a million random uint64 numbers on any unix based system:\n//\n// `time gno run -expr 'benchmarkXorshift64Star()' xorshift64star.gno\nfunc benchmarkXorshift64Star(_iterations ...int) {\n\titerations := 1000000\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\txs64s := New()\n\n\tfor i := 0; i \u003c iterations; i++ {\n\t\t_ = xs64s.Uint64()\n\t}\n\tufmt.Println(ufmt.Sprintf(\"Xorshift64*: generate %d uint64\\n\", iterations))\n}\n\n// The averageXorshift64Star() function is a simple benchmarking helper to demonstrate\n// the most basic statistical property of the Xorshift64* PRNG.\nfunc averageXorshift64Star(_iterations ...int) {\n\ttarget := uint64(500000)\n\titerations := 1000000\n\tvar squares [1000000]uint64\n\n\tufmt.Println(\n\t\tufmt.Sprintf(\n\t\t\t\"Averaging %d random numbers. The average should be very close to %d.\\n\",\n\t\t\titerations,\n\t\t\ttarget))\n\n\tif len(_iterations) \u003e 0 {\n\t\titerations = _iterations[0]\n\t}\n\txs64s := New()\n\n\tvar average float64 = 0\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tn := xs64s.Uint64()%(target*2) + 1\n\t\taverage += (float64(n) - average) / float64(i+1)\n\t\tsquares[i] = n\n\t}\n\n\tsum_of_squares := uint64(0)\n\t// transform numbers into their squares of the distance from the average\n\tfor i := 0; i \u003c iterations; i++ {\n\t\tdifference := average - float64(squares[i])\n\t\tsquare := uint64(difference * difference)\n\t\tsum_of_squares += square\n\t}\n\n\tufmt.Println(ufmt.Sprintf(\"Xorshift64* average of %d uint64: %f\\n\", iterations, average))\n\tufmt.Println(ufmt.Sprintf(\"Xorshift64* standard deviation : %f\\n\", math.Sqrt(float64(sum_of_squares)/float64(iterations))))\n\tufmt.Println(ufmt.Sprintf(\"Xorshift64* theoretical perfect deviation: %f\\n\", (float64(target*2)-1)/math.Sqrt(12)))\n}\n" + }, + { + "name": "xorshift64star_test.gno", + "body": "package xorshift64star\n\nimport (\n\t\"math/rand\"\n\t\"testing\"\n)\n\nfunc TestXorshift64StarSeeding(t *testing.T) {\n\txs64s := New()\n\tvalue1 := xs64s.Uint64()\n\n\txs64s = New(987654321)\n\tvalue2 := xs64s.Uint64()\n\n\tif value1 != 5083824587905981259 || value2 != 18211065302896784785 || value1 == value2 {\n\t\tt.Errorf(\"Expected 5083824587905981259 to be != to 18211065302896784785; got: %d == %d\", value1, value2)\n\t}\n}\n\nfunc TestXorshift64StarRand(t *testing.T) {\n\tsource := New(987654321)\n\trng := rand.New(source)\n\n\t// Expected outputs for the first 5 random floats with the given seed\n\texpected := []float64{\n\t\t.8344002228310946,\n\t\t0.01777174153236205,\n\t\t0.23521769507865276,\n\t\t0.5387610198576143,\n\t\t0.631539862225968,\n\t\t0.9369068148346704,\n\t\t0.6387002315083188,\n\t\t0.5047507613688854,\n\t\t0.5208486273732391,\n\t\t0.25023746271541747,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := rng.Float64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Rand.Float64() at iteration %d: got %g, expected %g\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestXorshift64StarUint64(t *testing.T) {\n\txs64s := New()\n\n\texpected := []uint64{\n\t\t5083824587905981259,\n\t\t4607286371009545754,\n\t\t2070557085263023674,\n\t\t14094662988579565368,\n\t\t2910745910478213381,\n\t\t18037409026311016155,\n\t\t17169624916429864153,\n\t\t10459214929523155306,\n\t\t11840179828060641081,\n\t\t1198750959721587199,\n\t}\n\n\tfor i, exp := range expected {\n\t\tval := xs64s.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshift64Star.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n}\n\nfunc TestXorshift64StarMarshalUnmarshal(t *testing.T) {\n\txs64s := New()\n\n\texpected1 := []uint64{\n\t\t5083824587905981259,\n\t\t4607286371009545754,\n\t\t2070557085263023674,\n\t\t14094662988579565368,\n\t\t2910745910478213381,\n\t}\n\n\texpected2 := []uint64{\n\t\t18037409026311016155,\n\t\t17169624916429864153,\n\t\t10459214929523155306,\n\t\t11840179828060641081,\n\t\t1198750959721587199,\n\t}\n\n\tfor i, exp := range expected1 {\n\t\tval := xs64s.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshift64Star.Uint64() at iteration %d: got %d, expected %d\", i, val, exp)\n\t\t}\n\t}\n\n\tmarshalled, err := xs64s.MarshalBinary()\n\n\tt.Logf(\"Original State: [%x]\\n\", xs64s.seed)\n\tt.Logf(\"Marshalled State: [%x] -- %v\\n\", marshalled, err)\n\tstate_before := xs64s.seed\n\n\tif err != nil {\n\t\tt.Errorf(\"Xorshift64Star.MarshalBinary() error: %v\", err)\n\t}\n\n\t// Advance state by one number; then check the next 5. The expectation is that they _will_ fail.\n\txs64s.Uint64()\n\n\tfor i, exp := range expected2 {\n\t\tval := xs64s.Uint64()\n\t\tif exp == val {\n\t\t\tt.Errorf(\" Iteration %d matched %d; which is from iteration %d; something strange is happening.\", (i + 6), val, (i + 5))\n\t\t}\n\t}\n\n\tt.Logf(\"State before unmarshall: [%x]\\n\", xs64s.seed)\n\n\t// Now restore the state of the PRNG\n\terr = xs64s.UnmarshalBinary(marshalled)\n\n\tt.Logf(\"State after unmarshall: [%x]\\n\", xs64s.seed)\n\n\tif state_before != xs64s.seed {\n\t\tt.Errorf(\"States before and after marshal/unmarshal are not equal; go %x and %x\", state_before, xs64s.seed)\n\t}\n\n\t// Now we should be back on track for the last 5 numbers\n\tfor i, exp := range expected2 {\n\t\tval := xs64s.Uint64()\n\t\tif exp != val {\n\t\t\tt.Errorf(\"Xorshift64Star.Uint64() at iteration %d: got %d, expected %d\", (i + 5), val, exp)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "PuNYZS5Td1IUnFCoT3UZdUWxYItQuUQxUXmkf3VB75zzBTFYS5URwi1qBJ7v24LjWaT+1E70l2PBhRm0MkRIAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "config", + "path": "gno.land/r/leon/config", + "files": [ + { + "name": "config.gno", + "body": "package config\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/seqid\"\n)\n\nvar (\n\tcfgID seqid.ID\n\tconfigs = avl.NewTree()\n\n\tabsPath = strings.TrimPrefix(std.CurrentRealm().PkgPath(), std.ChainDomain())\n\n\t// SafeObjects\n\tOwnableMain = ownable.NewWithAddress(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\")\n\tOwnableBackup = ownable.NewWithAddress(\"g1lavlav7zwsjqlzzl3qdl3nl242qtf638vnhdjh\")\n\n\tErrUnauthorized = errors.New(\"leon's config: unauthorized\")\n)\n\ntype Config struct {\n\tid seqid.ID\n\tname string\n\tlines string\n\tupdated time.Time\n}\n\nfunc AddConfig(name, lines string) {\n\tif !IsAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(ErrUnauthorized)\n\t}\n\n\tid := cfgID.Next()\n\tconfigs.Set(id.String(), Config{\n\t\tid: id,\n\t\tname: name,\n\t\tlines: lines,\n\t\tupdated: time.Now(),\n\t})\n}\n\nfunc EditConfig(id string, name, lines string) {\n\tif !IsAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(ErrUnauthorized)\n\t}\n\n\traw, ok := configs.Remove(id)\n\tif !ok {\n\t\tpanic(\"no config with that id\")\n\t}\n\n\tconf := raw.(Config)\n\t// Overwrites data\n\tconf.lines = lines\n\tconf.name = name\n\tconf.updated = time.Now()\n}\n\nfunc RemoveConfig(id string) {\n\tif !IsAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(ErrUnauthorized)\n\t}\n\n\tif _, ok := configs.Remove(id); !ok {\n\t\tpanic(\"no config with that id\")\n\t}\n}\n\nfunc UpdateBanner(newBanner string) {\n\tif !IsAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(ErrUnauthorized)\n\t}\n\n\tbanner = newBanner\n}\n\nfunc IsAuthorized(addr std.Address) bool {\n\treturn addr == OwnableMain.Owner() || addr == OwnableBackup.Owner()\n}\n" + }, + { + "name": "render.gno", + "body": "package config\n\nimport (\n\t\"strconv\"\n\n\tp \"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/realmpath\"\n\t\"gno.land/p/moul/txlink\"\n)\n\nvar (\n\tbanner = \"---\\n[[Leon's Home page]](/r/leon/home) | [[Leon's snippets]](/r/leon/config) | [[GitHub: @leohhhn]](https://github.com/leohhhn)\\n\\n---\"\n\tpager = p.NewPager(configs, 10, true)\n)\n\nfunc Banner() string {\n\treturn banner\n}\n\nfunc Render(path string) (out string) {\n\treq := realmpath.Parse(path)\n\tif req.Path == \"\" {\n\t\tout += md.H1(\"Leon's configs \u0026 snippets\")\n\n\t\tout += ufmt.Sprintf(\"Leon's main address: %s\\n\\n\", OwnableMain.Owner().String())\n\t\tout += ufmt.Sprintf(\"Leon's backup address: %s\\n\\n\", OwnableBackup.Owner().String())\n\n\t\tout += md.H2(\"Snippets\")\n\n\t\tif configs.Size() == 0 {\n\t\t\tout += \"No configs yet :c\\n\\n\"\n\t\t} else {\n\t\t\tpage := pager.MustGetPageByPath(path)\n\t\t\tfor _, item := range page.Items {\n\t\t\t\tout += ufmt.Sprintf(\"- [%s](%s:%s)\\n\\n\", item.Value.(Config).name, absPath, item.Key)\n\t\t\t}\n\n\t\t\tout += page.Picker(path)\n\t\t\tout += \"\\n\\n\"\n\t\t\tout += \"Page \" + strconv.Itoa(page.PageNumber) + \" of \" + strconv.Itoa(page.TotalPages) + \"\\n\\n\"\n\t\t}\n\n\t\tout += Banner()\n\n\t\treturn out\n\t}\n\n\treturn renderConfPage(req.Path)\n}\n\nfunc renderConfPage(id string) (out string) {\n\traw, ok := configs.Get(id)\n\tif !ok {\n\t\tout += md.H1(\"404\")\n\t\tout += \"That config does not exist :/\"\n\t\treturn out\n\t}\n\n\tconf := raw.(Config)\n\tout += md.H1(conf.name)\n\tout += ufmt.Sprintf(\"```\\n%s\\n```\\n\\n\", conf.lines)\n\tout += ufmt.Sprintf(\"_Last updated on %s_\\n\\n\", conf.updated.Format(\"02 Jan, 2006\"))\n\tout += md.HorizontalRule()\n\tout += ufmt.Sprintf(\"[[EDIT]](%s) - [[DELETE]](%s)\", txlink.Call(\"EditConfig\", \"id\", conf.id.String()), txlink.Call(\"RemoveConfig\", \"id\", conf.id.String()))\n\n\treturn out\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "XBgzxSexVpFwHUsuL1tPv1qVaLqPvvTCsg1H1WXODqkD/HNkTV3xYyT0mgnNyKrC6CV7Pekul8d4qIMwh58NAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "hof", + "path": "gno.land/r/leon/hof", + "files": [ + { + "name": "datasource.gno", + "body": "package hof\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/jeronimoalbi/datasource\"\n\t\"gno.land/p/moul/md\"\n)\n\nfunc NewDatasource() Datasource {\n\treturn Datasource{exhibition}\n}\n\ntype Datasource struct {\n\texhibition *Exhibition\n}\n\nfunc (ds Datasource) Size() int { return ds.exhibition.items.Size() }\n\nfunc (ds Datasource) Records(q datasource.Query) datasource.Iterator {\n\treturn \u0026iterator{\n\t\texhibition: ds.exhibition,\n\t\tindex: q.Offset,\n\t\tmaxIndex: q.Offset + q.Count,\n\t}\n}\n\nfunc (ds Datasource) Record(id string) (datasource.Record, error) {\n\tv, found := ds.exhibition.items.Get(id)\n\tif !found {\n\t\treturn nil, errors.New(\"realm submission not found\")\n\t}\n\treturn record{v.(*Item)}, nil\n}\n\ntype record struct {\n\titem *Item\n}\n\nfunc (r record) ID() string { return r.item.id.String() }\nfunc (r record) String() string { return r.item.pkgpath }\n\nfunc (r record) Fields() (datasource.Fields, error) {\n\tfields := avl.NewTree()\n\tfields.Set(\n\t\t\"details\",\n\t\tufmt.Sprintf(\"Votes: ⏶ %d - ⏷ %d\", r.item.upvote.Size(), r.item.downvote.Size()),\n\t)\n\treturn fields, nil\n}\n\nfunc (r record) Content() (string, error) {\n\tcontent := md.H1(r.item.title)\n\tcontent += md.H2(r.item.description)\n\tcontent += r.item.Render(false)\n\treturn content, nil\n}\n\ntype iterator struct {\n\texhibition *Exhibition\n\tindex, maxIndex int\n\trecord *record\n}\n\nfunc (it iterator) Record() datasource.Record { return it.record }\nfunc (it iterator) Err() error { return nil }\n\nfunc (it *iterator) Next() bool {\n\tif it.index \u003e= it.maxIndex || it.index \u003e= it.exhibition.items.Size() {\n\t\treturn false\n\t}\n\n\t_, v := it.exhibition.items.GetByIndex(it.index)\n\tit.record = \u0026record{v.(*Item)}\n\tit.index++\n\treturn true\n}\n" + }, + { + "name": "datasource_test.gno", + "body": "package hof\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/urequire\"\n\t\"gno.land/p/jeronimoalbi/datasource\"\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/txlink\"\n)\n\nvar (\n\t_ datasource.Datasource = (*Datasource)(nil)\n\t_ datasource.Record = (*record)(nil)\n\t_ datasource.ContentRecord = (*record)(nil)\n\t_ datasource.Iterator = (*iterator)(nil)\n)\n\nfunc TestDatasourceRecords(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\titems []*Item\n\t\trecordIDs []string\n\t\toptions []datasource.QueryOption\n\t}{\n\t\t{\n\t\t\tname: \"all items\",\n\t\t\titems: []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\trecordIDs: []string{\"0000001\", \"0000002\", \"0000003\"},\n\t\t},\n\t\t{\n\t\t\tname: \"with offset\",\n\t\t\titems: []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\trecordIDs: []string{\"0000002\", \"0000003\"},\n\t\t\toptions: []datasource.QueryOption{datasource.WithOffset(1)},\n\t\t},\n\t\t{\n\t\t\tname: \"with count\",\n\t\t\titems: []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\trecordIDs: []string{\"0000001\", \"0000002\"},\n\t\t\toptions: []datasource.QueryOption{datasource.WithCount(2)},\n\t\t},\n\t\t{\n\t\t\tname: \"with offset and count\",\n\t\t\titems: []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\trecordIDs: []string{\"0000002\"},\n\t\t\toptions: []datasource.QueryOption{\n\t\t\t\tdatasource.WithOffset(1),\n\t\t\t\tdatasource.WithCount(1),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Initialize a local instance of exhibition\n\t\t\texhibition := \u0026Exhibition{items: avl.NewTree()}\n\t\t\tfor _, item := range tc.items {\n\t\t\t\texhibition.items.Set(item.id.String(), item)\n\t\t\t}\n\n\t\t\t// Get a records iterator\n\t\t\tds := Datasource{exhibition}\n\t\t\tquery := datasource.NewQuery(tc.options...)\n\t\t\titer := ds.Records(query)\n\n\t\t\t// Start asserting\n\t\t\turequire.Equal(t, len(tc.items), ds.Size(), \"datasource size\")\n\n\t\t\tvar records []datasource.Record\n\t\t\tfor iter.Next() {\n\t\t\t\trecords = append(records, iter.Record())\n\t\t\t}\n\t\t\turequire.Equal(t, len(tc.recordIDs), len(records), \"record count\")\n\n\t\t\tfor i, r := range records {\n\t\t\t\tuassert.Equal(t, tc.recordIDs[i], r.ID())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDatasourceRecord(t *testing.T) {\n\tcases := []struct {\n\t\tname string\n\t\titems []*Item\n\t\tid string\n\t\terr string\n\t}{\n\t\t{\n\t\t\tname: \"found\",\n\t\t\titems: []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\tid: \"0000001\",\n\t\t},\n\t\t{\n\t\t\tname: \"no found\",\n\t\t\titems: []*Item{{id: 1}, {id: 2}, {id: 3}},\n\t\t\tid: \"42\",\n\t\t\terr: \"realm submission not found\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t// Initialize a local instance of exhibition\n\t\t\texhibition := \u0026Exhibition{items: avl.NewTree()}\n\t\t\tfor _, item := range tc.items {\n\t\t\t\texhibition.items.Set(item.id.String(), item)\n\t\t\t}\n\n\t\t\t// Get a single record\n\t\t\tds := Datasource{exhibition}\n\t\t\tr, err := ds.Record(tc.id)\n\n\t\t\t// Start asserting\n\t\t\tif tc.err != \"\" {\n\t\t\t\tuassert.ErrorContains(t, err, tc.err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\turequire.NoError(t, err, \"no error\")\n\t\t\turequire.NotEqual(t, nil, r, \"record not nil\")\n\t\t\tuassert.Equal(t, tc.id, r.ID())\n\t\t})\n\t}\n}\n\nfunc TestItemRecord(t *testing.T) {\n\tpkgpath := \"gno.land/r/demo/test\"\n\titem := Item{\n\t\tid: 1,\n\t\tpkgpath: pkgpath,\n\t\ttitle: \"Test Realm\",\n\t\tdescription: \"This is a test realm in the Hall of Fame\",\n\t\tblockNum: 42,\n\t\tupvote: \u0026addrset.Set{},\n\t\tdownvote: \u0026addrset.Set{},\n\t}\n\titem.downvote.Add(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\titem.upvote.Add(\"g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn\")\n\titem.upvote.Add(\"g1w4ek2u3jta047h6lta047h6lta047h6l9huexc\")\n\n\tr := record{\u0026item}\n\n\tuassert.Equal(t, \"0000001\", r.ID())\n\tuassert.Equal(t, pkgpath, r.String())\n\n\tfields, _ := r.Fields()\n\tdetails, found := fields.Get(\"details\")\n\turequire.True(t, found, \"details field\")\n\tuassert.Equal(t, \"Votes: ⏶ 2 - ⏷ 1\", details)\n\n\tcontent, _ := r.Content()\n\twantContent := md.H1(item.title) +\n\t\tmd.H2(r.item.description) +\n\t\tufmt.Sprintf(\"\\n%s\\n\\n\", md.CodeBlock(item.pkgpath)) +\n\t\tufmt.Sprintf(\"%s\\n\\n\", item.description) +\n\t\tufmt.Sprintf(\"by %s\\n\\n\", strings.Split(item.pkgpath, \"/\")[2]) +\n\t\tmd.Link(\"View Realm\", strings.TrimPrefix(item.pkgpath, \"gno.land\")) + \"\\n\\n\" +\n\t\tufmt.Sprintf(\"Submitted at Block #%d\\n\\n\", item.blockNum) +\n\t\tmd.Bold(ufmt.Sprintf(\"[%d👍](%s) - [%d👎](%s)\",\n\t\t\titem.upvote.Size(), txlink.Call(\"Upvote\", \"pkgpath\", item.pkgpath),\n\t\t\titem.downvote.Size(), txlink.Call(\"Downvote\", \"pkgpath\", item.pkgpath),\n\t\t))\n\tuassert.Equal(t, wantContent, content)\n}\n" + }, + { + "name": "errors.gno", + "body": "package hof\n\nimport (\n\t\"gno.land/p/leon/pkgerr\"\n)\n\nvar (\n\tErrNoSuchItem = pkgerr.New(\"no such item exists\")\n\tErrDoubleUpvote = pkgerr.New(\"cannot upvote twice\")\n\tErrDoubleDownvote = pkgerr.New(\"cannot downvote twice\")\n)\n" + }, + { + "name": "hof.gno", + "body": "// Package hof is the hall of fame realm.\n// The Hall of Fame is an exhibition that holds items. Users can add their realms to the Hall of Fame by\n// importing the Hall of Fame realm and calling hof.Register() from their init function.\npackage hof\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/pausable\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/r/leon/config\"\n)\n\nconst (\n\tmaxTitleLength = 30\n\tmaxDescriptionLength = 50\n)\n\nvar (\n\texhibition *Exhibition\n\n\t// Safe objects\n\tOwnable *ownable.Ownable\n\tPausable *pausable.Pausable\n)\n\ntype (\n\tExhibition struct {\n\t\titemCounter seqid.ID\n\t\tdescription string\n\t\titems *avl.Tree // pkgPath \u003e *Item\n\t\titemsSortedByCreation *avl.Tree // same data but sorted by creation time\n\t\titemsSortedByUpvotes *avl.Tree // same data but sorted by upvotes\n\t\titemsSortedByDownvotes *avl.Tree // same data but sorted by downvotes\n\t}\n\n\tItem struct {\n\t\tid seqid.ID\n\t\ttitle string\n\t\tdescription string\n\t\tpkgpath string\n\t\tblockNum int64\n\t\tupvote *addrset.Set\n\t\tdownvote *addrset.Set\n\t}\n)\n\nfunc init() {\n\texhibition = \u0026Exhibition{\n\t\titems: avl.NewTree(),\n\t\titemsSortedByCreation: avl.NewTree(),\n\t\titemsSortedByUpvotes: avl.NewTree(),\n\t\titemsSortedByDownvotes: avl.NewTree(),\n\t}\n\n\tOwnable = ownable.NewWithAddress(config.OwnableMain.Owner()) // OrigSendOwnable?\n\tPausable = pausable.NewFromOwnable(Ownable)\n}\n\n// Register registers your realm to the Hall of Fame\n// Should be called from within code\nfunc Register(title, description string) {\n\tif Pausable.IsPaused() {\n\t\treturn\n\t}\n\n\tsubmission := std.PreviousRealm()\n\tpkgpath := submission.PkgPath()\n\n\t// Must be called from code\n\tif submission.IsUser() {\n\t\treturn\n\t}\n\n\t// Must not yet exist\n\tif exhibition.items.Has(pkgpath) {\n\t\treturn\n\t}\n\n\t// Title must be between 1 maxTitleLength long\n\tif title == \"\" || len(title) \u003e maxTitleLength {\n\t\treturn\n\t}\n\n\t// Description must be between 1 maxDescriptionLength long\n\tif len(description) \u003e maxDescriptionLength {\n\t\treturn\n\t}\n\n\tid := exhibition.itemCounter.Next()\n\ti := \u0026Item{\n\t\tid: id,\n\t\ttitle: title,\n\t\tdescription: description,\n\t\tpkgpath: pkgpath,\n\t\tblockNum: std.ChainHeight(),\n\t\tupvote: \u0026addrset.Set{},\n\t\tdownvote: \u0026addrset.Set{},\n\t}\n\n\texhibition.items.Set(pkgpath, i)\n\texhibition.itemsSortedByCreation.Set(getCreationSortKey(i.blockNum, i.id), i)\n\texhibition.itemsSortedByUpvotes.Set(getVoteSortKey(i.upvote.Size(), i.id), i)\n\texhibition.itemsSortedByDownvotes.Set(getVoteSortKey(i.downvote.Size(), i.id), i)\n\n\tstd.Emit(\"Registration\")\n}\n\nfunc Upvote(pkgpath string) {\n\trawItem, ok := exhibition.items.Get(pkgpath)\n\tif !ok {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\titem := rawItem.(*Item)\n\tcaller := std.PreviousRealm().Address()\n\n\tif item.upvote.Has(caller) {\n\t\tpanic(ErrDoubleUpvote)\n\t}\n\n\tif _, exists := exhibition.itemsSortedByUpvotes.Remove(getVoteSortKey(item.upvote.Size(), item.id)); !exists {\n\t\tpanic(\"error removing old upvote entry\")\n\t}\n\n\titem.upvote.Add(caller)\n\n\texhibition.itemsSortedByUpvotes.Set(getVoteSortKey(item.upvote.Size(), item.id), item)\n}\n\nfunc Downvote(pkgpath string) {\n\trawItem, ok := exhibition.items.Get(pkgpath)\n\tif !ok {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\titem := rawItem.(*Item)\n\tcaller := std.PreviousRealm().Address()\n\n\tif item.downvote.Has(caller) {\n\t\tpanic(ErrDoubleDownvote)\n\t}\n\n\tif _, exist := exhibition.itemsSortedByDownvotes.Remove(getVoteSortKey(item.downvote.Size(), item.id)); !exist {\n\t\tpanic(\"error removing old downvote entry\")\n\n\t}\n\n\titem.downvote.Add(caller)\n\n\texhibition.itemsSortedByDownvotes.Set(getVoteSortKey(item.downvote.Size(), item.id), item)\n}\n\nfunc Delete(pkgpath string) {\n\tif !Ownable.CallerIsOwner() {\n\t\tpanic(ownable.ErrUnauthorized)\n\t}\n\n\ti, ok := exhibition.items.Get(pkgpath)\n\tif !ok {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\titem := i.(*Item)\n\tupvoteKey := getVoteSortKey(item.upvote.Size(), item.id)\n\tdownvoteKey := getVoteSortKey(item.downvote.Size(), item.id)\n\n\tif _, removed := exhibition.items.Remove(pkgpath); !removed {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\tif _, removed := exhibition.itemsSortedByUpvotes.Remove(upvoteKey); !removed {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\tif _, removed := exhibition.itemsSortedByDownvotes.Remove(downvoteKey); !removed {\n\t\tpanic(ErrNoSuchItem)\n\t}\n\n\tif _, removed := exhibition.itemsSortedByCreation.Remove(getCreationSortKey(item.blockNum, item.id)); !removed {\n\t\tpanic(ErrNoSuchItem)\n\t}\n}\n\nfunc getVoteSortKey(votes int, id seqid.ID) string {\n\tvotesStr := strconv.Itoa(votes)\n\tpaddedVotes := strings.Repeat(\"0\", 10-len(votesStr)) + votesStr\n\treturn paddedVotes + \":\" + strconv.FormatUint(uint64(id), 10)\n}\n\nfunc getCreationSortKey(blockNum int64, id seqid.ID) string {\n\tblockNumStr := strconv.Itoa(int(blockNum))\n\tpaddedBlockNum := strings.Repeat(\"0\", 10-len(blockNumStr)) + blockNumStr\n\treturn paddedBlockNum + \":\" + strconv.FormatUint(uint64(id), 10)\n}\n" + }, + { + "name": "hof_test.gno", + "body": "package hof\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n\t\"gno.land/p/moul/addrset\"\n)\n\nconst (\n\trlmPath = \"gno.land/r/gnoland/home\"\n\trlmPath2 = \"gno.land/r/gnoland/test2\"\n\n\trlmPath3 = \"gno.land/r/gnoland/test3\"\n\trlmPath4 = \"gno.land/r/gnoland/test4\"\n\trlmPath5 = \"gno.land/r/gnoland/test5\"\n\n\tvalidTitle = \"valid title\"\n\tinvalidTitle = \"This title is very very very long, longer than 30 characters\"\n\tvalidDesc = \"valid description\"\n\tinvalidDescription = \"This description is very very very long, longer than 50 characters\"\n)\n\nvar (\n\tadmin = Ownable.Owner()\n\tadminRealm = std.NewUserRealm(admin)\n\talice = testutils.TestAddress(\"alice\")\n)\n\nfunc TestRegister(t *testing.T) {\n\t// Test user realm register\n\taliceRealm := std.NewUserRealm(alice)\n\ttesting.SetRealm(aliceRealm)\n\n\tRegister(validTitle, validDesc)\n\tuassert.False(t, itemExists(t, rlmPath))\n\n\t// Test register while paused\n\ttesting.SetRealm(adminRealm)\n\tPausable.Pause()\n\n\t// Set legitimate caller\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath))\n\n\tRegister(validTitle, validDesc)\n\tuassert.False(t, itemExists(t, rlmPath))\n\n\t// Unpause\n\ttesting.SetRealm(adminRealm)\n\tPausable.Unpause()\n\n\t// Set legitimate caller\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath))\n\tRegister(validTitle, validDesc)\n\n\t// Find registered items\n\tuassert.True(t, itemExists(t, rlmPath))\n\n\t// Test register with invalid title\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath2))\n\tRegister(invalidTitle, validDesc)\n\tuassert.False(t, itemExists(t, rlmPath2))\n\n\t// Test register with invalid description\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath2))\n\tRegister(validTitle, invalidDescription)\n\tuassert.False(t, itemExists(t, rlmPath2))\n}\n\nfunc TestUpvote(t *testing.T) {\n\traw, _ := exhibition.items.Get(rlmPath)\n\titem := raw.(*Item)\n\n\t// 0 upvotes by default\n\turequire.Equal(t, item.upvote.Size(), 0)\n\n\ttesting.SetRealm(adminRealm)\n\n\turequire.NotPanics(t, func() {\n\t\tUpvote(rlmPath)\n\t})\n\n\t// Check both trees for 1 upvote\n\tuassert.Equal(t, item.upvote.Size(), 1)\n\n\t// Check double upvote\n\tuassert.PanicsWithMessage(t, ErrDoubleUpvote.Error(), func() {\n\t\tUpvote(rlmPath)\n\t})\n}\n\nfunc TestDownvote(t *testing.T) {\n\traw, _ := exhibition.items.Get(rlmPath)\n\titem := raw.(*Item)\n\n\t// 0 downvotes by default\n\turequire.Equal(t, item.downvote.Size(), 0)\n\n\tuserRealm := std.NewUserRealm(alice)\n\ttesting.SetRealm(userRealm)\n\n\turequire.NotPanics(t, func() {\n\t\tDownvote(rlmPath)\n\t})\n\n\t// Check both trees for 1 upvote\n\tuassert.Equal(t, item.downvote.Size(), 1)\n\n\t// Check double downvote\n\tuassert.PanicsWithMessage(t, ErrDoubleDownvote.Error(), func() {\n\t\tDownvote(rlmPath)\n\t})\n}\n\nfunc TestDelete(t *testing.T) {\n\tuserRealm := std.NewUserRealm(admin)\n\ttesting.SetRealm(userRealm)\n\ttesting.SetOriginCaller(admin)\n\n\tuassert.PanicsWithMessage(t, ErrNoSuchItem.Error(), func() {\n\t\tDelete(\"nonexistentpkgpath\")\n\t})\n\n\ti, _ := exhibition.items.Get(rlmPath)\n\tid := i.(*Item).id\n\n\tuassert.NotPanics(t, func() {\n\t\tDelete(rlmPath)\n\t})\n\n\tuassert.False(t, exhibition.items.Has(rlmPath))\n}\n\nfunc itemExists(t *testing.T, rlmPath string) bool {\n\tt.Helper()\n\n\ti, ok1 := exhibition.items.Get(rlmPath)\n\n\treturn ok1\n}\n\nfunc TestgetVoteSortKey(t *testing.T) {\n\ti := \u0026Item{\n\t\tid: 1,\n\t\ttitle: validTitle,\n\t\tdescription: validDesc,\n\t\tpkgpath: rlmPath,\n\t\tblockNum: std.ChainHeight(),\n\t\tupvote: \u0026addrset.Set{},\n\t\tdownvote: \u0026addrset.Set{},\n\t}\n\n\ti.upvote.Add(alice)\n\n\tgeneratedKey := getVoteSortKey(i.upvote.Size(), i.id)\n\texpectedKey := \"0000000001:1\"\n\n\turequire.Equal(t, generatedKey, expectedKey)\n}\n\nfunc TestSortByUpvote(t *testing.T) {\n\t// Remove all items from all trees\n\texhibition.items.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.items.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByUpvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByUpvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByDownvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByDownvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByCreation.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByCreation.Remove(key)\n\t\treturn false\n\t})\n\n\t// Add items\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath3))\n\tRegister(validTitle, validDesc)\n\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath4))\n\tRegister(validTitle, validDesc)\n\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath5))\n\tRegister(validTitle, validDesc)\n\n\tuser1 := testutils.TestAddress(\"user1\")\n\tuser2 := testutils.TestAddress(\"user2\")\n\tuser3 := testutils.TestAddress(\"user3\")\n\n\ttesting.SetOriginCaller(user1)\n\ttesting.SetRealm(std.NewUserRealm(user1))\n\tUpvote(rlmPath3)\n\tUpvote(rlmPath4)\n\tUpvote(rlmPath5)\n\n\ttesting.SetOriginCaller(user2)\n\ttesting.SetRealm(std.NewUserRealm(user2))\n\tUpvote(rlmPath4)\n\tUpvote(rlmPath5)\n\n\ttesting.SetOriginCaller(user3)\n\ttesting.SetRealm(std.NewUserRealm(user3))\n\tUpvote(rlmPath5)\n\n\t// We are displaying data in reverse order in render, so items should be sorted in reverse order\n\tfirstKey, firstRawValue := exhibition.itemsSortedByUpvotes.GetByIndex(0)\n\tfirstValue := firstRawValue.(*Item)\n\tuassert.Equal(t, firstValue.pkgpath, rlmPath3)\n\n\tsecondKey, secondRawValue := exhibition.itemsSortedByUpvotes.GetByIndex(1)\n\tsecondValue := secondRawValue.(*Item)\n\tuassert.Equal(t, secondValue.pkgpath, rlmPath4)\n}\n\nfunc TestSortByDownvote(t *testing.T) {\n\t// Remove all items from all trees\n\texhibition.items.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.items.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByUpvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByUpvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByDownvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByDownvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByCreation.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByCreation.Remove(key)\n\t\treturn false\n\t})\n\n\t// Add items\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath3))\n\tRegister(validTitle, validDesc)\n\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath4))\n\tRegister(validTitle, validDesc)\n\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath5))\n\tRegister(validTitle, validDesc)\n\n\tuser1 := testutils.TestAddress(\"user1\")\n\tuser2 := testutils.TestAddress(\"user2\")\n\tuser3 := testutils.TestAddress(\"user3\")\n\n\ttesting.SetOriginCaller(user1)\n\ttesting.SetRealm(std.NewUserRealm(user1))\n\tDownvote(rlmPath3)\n\tDownvote(rlmPath4)\n\tDownvote(rlmPath5)\n\n\ttesting.SetOriginCaller(user2)\n\ttesting.SetRealm(std.NewUserRealm(user2))\n\tDownvote(rlmPath4)\n\tDownvote(rlmPath5)\n\n\ttesting.SetOriginCaller(user3)\n\ttesting.SetRealm(std.NewUserRealm(user3))\n\tDownvote(rlmPath5)\n\n\t// We are dispalying data is reverse order in render, so items should be sorted in reverse order\n\tfirstKey, firstRawValue := exhibition.itemsSortedByDownvotes.GetByIndex(0)\n\n\tfirstValue := firstRawValue.(*Item)\n\n\tuassert.Equal(t, firstValue.pkgpath, rlmPath3)\n\n\tsecondKey, secondRawValue := exhibition.itemsSortedByDownvotes.GetByIndex(1)\n\n\tsecondValue := secondRawValue.(*Item)\n\n\tuassert.Equal(t, secondValue.pkgpath, rlmPath4)\n}\n\nfunc TestSortByCreation(t *testing.T) {\n\t// Remove all items from all trees\n\texhibition.items.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.items.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByUpvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByUpvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByDownvotes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByDownvotes.Remove(key)\n\t\treturn false\n\t})\n\texhibition.itemsSortedByCreation.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\texhibition.itemsSortedByCreation.Remove(key)\n\t\treturn false\n\t})\n\n\ttesting.SkipHeights(10)\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath3))\n\tRegister(validTitle, validDesc)\n\n\ttesting.SkipHeights(10)\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath4))\n\tRegister(validTitle, validDesc)\n\n\ttesting.SkipHeights(10)\n\ttesting.SetRealm(std.NewCodeRealm(rlmPath5))\n\tRegister(validTitle, validDesc)\n\n\t// We are dispalying data is reverse order in render, so items should be sorted in reverse order\n\tfirstKey, firstRawValue := exhibition.itemsSortedByCreation.GetByIndex(0)\n\n\tfirstValue := firstRawValue.(*Item)\n\n\tuassert.Equal(t, firstValue.pkgpath, rlmPath3)\n\n\tsecondKey, secondRawValue := exhibition.itemsSortedByCreation.GetByIndex(1)\n\n\tsecondValue := secondRawValue.(*Item)\n\n\tuassert.Equal(t, secondValue.pkgpath, rlmPath4)\n}\n" + }, + { + "name": "render.gno", + "body": "package hof\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/fqname\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/txlink\"\n)\n\nconst (\n\tpageSize = 5\n)\n\nfunc Render(path string) string {\n\tout := md.H1(\"Hall of Fame\\n\\n\")\n\tout += md.Link(\"Reset Sort\", \"?\") + \" | \"\n\tout += md.Link(\"Sort by Upvotes\", \"?sort=upvotes\") + \" | \"\n\tout += md.Link(\"Sort by Downvotes\", \"?sort=downvotes\") + \" | \"\n\tout += md.Link(\"Sort by Most Recent\", \"?sort=creation\") + \" | \"\n\tout += md.Link(\"Sort by Oldest\", \"?sort=oldest\") + \"\\n\\n\"\n\n\tdashboardEnabled := path == \"dashboard\"\n\n\tif dashboardEnabled {\n\t\tout += renderDashboard()\n\t}\n\n\tout += exhibition.Render(path, dashboardEnabled)\n\n\treturn out\n}\n\nfunc (e Exhibition) Render(path string, dashboard bool) string {\n\ttree := getTreeByPath(\u0026e, path)\n\n\tu, _ := url.Parse(path)\n\treversed := u.Query().Get(\"sort\") != \"oldest\"\n\n\tpage := pager.NewPager(tree, pageSize, reversed).MustGetPageByPath(path)\n\n\tout := ufmt.Sprintf(\"%s\\n\\n\", e.description)\n\n\tif e.items.Size() == 0 {\n\t\tout += \"No items in this exhibition currently.\\n\\n\"\n\t\treturn out\n\t}\n\n\tout += \"\u003cdiv class='columns-2'\u003e\\n\\n\"\n\n\tfor _, item := range page.Items {\n\t\tout += \"\u003cdiv\u003e\\n\\n\"\n\t\titemValue := item.Value.(*Item)\n\t\tout += md.H3(itemValue.title + \"\\n\\n\")\n\t\tout += itemValue.Render(dashboard)\n\t\tout += \"\u003c/div\u003e\"\n\t}\n\n\tout += \"\u003c/div\u003e\u003c!-- /columns-2 --\u003e\\n\\n\"\n\n\tout += page.Picker(path)\n\n\treturn out\n}\n\nfunc (i Item) Render(dashboard bool) string {\n\tout := ufmt.Sprintf(\"\\n%s\\n\\n\", md.CodeBlock(i.pkgpath))\n\tout += ufmt.Sprintf(\"%s\\n\\n\", i.description)\n\tout += ufmt.Sprintf(\"by %s\\n\\n\", strings.Split(i.pkgpath, \"/\")[2])\n\tout += md.Link(\"View Realm\", strings.TrimPrefix(i.pkgpath, \"gno.land\")) + \"\\n\\n\"\n\tout += ufmt.Sprintf(\"Submitted at Block #%d\\n\\n\", i.blockNum)\n\n\tout += md.Bold(ufmt.Sprintf(\"[%d👍](%s) - [%d👎](%s)\",\n\t\ti.upvote.Size(), txlink.Call(\"Upvote\", \"pkgpath\", i.pkgpath),\n\t\ti.downvote.Size(), txlink.Call(\"Downvote\", \"pkgpath\", i.pkgpath),\n\t))\n\n\tif dashboard {\n\t\tout += md.Link(\"Delete\", txlink.Call(\"Delete\", \"pkgpath\", i.pkgpath))\n\t}\n\n\treturn out\n}\n\nfunc renderDashboard() string {\n\tout := md.HorizontalRule()\n\tout += md.H2(\"Dashboard\\n\\n\")\n\tout += ufmt.Sprintf(\"Total submissions: %d\\n\\n\", exhibition.items.Size())\n\n\tout += ufmt.Sprintf(\"Exhibition admin: %s\\n\\n\", Ownable.Owner().String())\n\n\tif !Pausable.IsPaused() {\n\t\tout += md.Link(\"Pause exhibition\", txlink.Call(\"Pause\"))\n\t} else {\n\t\tout += md.Link(\"Unpause exhibition\", txlink.Call(\"Unpause\"))\n\t}\n\n\tout += md.HorizontalRule()\n\n\treturn out\n}\n\nfunc RenderExhibWidget(itemsToRender int) string {\n\tif itemsToRender \u003c 1 {\n\t\treturn \"\"\n\t}\n\n\tout := \"\"\n\ti := 0\n\texhibition.items.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\titem := value.(*Item)\n\n\t\tout += ufmt.Sprintf(\"- %s\\n\", fqname.RenderLink(item.pkgpath, \"\"))\n\n\t\ti++\n\t\treturn i \u003e= itemsToRender\n\t})\n\n\treturn out\n}\n\nfunc getTreeByPath(e *Exhibition, path string) *avl.Tree {\n\tu, _ := url.Parse(path)\n\tswitch u.Query().Get(\"sort\") {\n\tcase \"upvotes\":\n\t\treturn e.itemsSortedByUpvotes\n\tcase \"downvotes\":\n\t\treturn e.itemsSortedByDownvotes\n\tcase \"creation\":\n\t\treturn e.itemsSortedByCreation\n\tcase \"oldest\":\n\t\treturn e.itemsSortedByCreation\n\tdefault:\n\t\treturn e.items\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "o4XMt63NneA+nYZAsHSb8GVBrUaaH3y2POnijXEwTFl8uwECc0Z6lqBtrohnVZRMsOe9ikQ4Mq1Pr06zOcn5Bw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "forms", + "path": "gno.land/r/agherasie/forms", + "files": [ + { + "name": "forms.gno", + "body": "package forms\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/agherasie/forms\"\n\t\"gno.land/p/demo/json\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/leon/hof\"\n)\n\nvar db *forms.FormDB\n\nfunc init() {\n\thof.Register(\"Ahgerasle's forms\", \"\")\n\tdb = forms.NewDB()\n}\n\nfunc CreateForm(title string, description string, openAt string, closeAt string, data string) string {\n\tid, err := db.CreateForm(title, description, openAt, closeAt, data)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn id\n}\n\nfunc GetForms() string {\n\tbuilder := forms.FormNodeBuilder{json.Builder()}\n\n\tbuilder.WriteArray(\"forms\", func(builder *forms.FormArrayBuilder) {\n\t\tfor _, form := range db.Forms {\n\t\t\tbuilder.WriteObject(func(builder *forms.FormNodeBuilder) {\n\t\t\t\tbuilder.WriteForm(\"form\", form)\n\t\t\t})\n\t\t}\n\t})\n\n\tencoded, err := json.Marshal(builder.Node())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(encoded)\n}\n\nfunc GetFormByID(id string) string {\n\tform, err := db.GetForm(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tbuilder := forms.FormNodeBuilder{json.Builder()}\n\n\tbuilder.WriteForm(\"form\", form).\n\t\tWriteObject(\"submissions\", func(builder *forms.FormNodeBuilder) {\n\t\t\tformSubmissions := db.GetSubmissionsByFormID(form.ID)\n\t\t\tfor _, submission := range formSubmissions {\n\t\t\t\tbuilder.WriteFormSubmission(submission.Author.String(), submission)\n\t\t\t}\n\t\t})\n\n\topenAt, err := form.OpenAt()\n\tif err == nil {\n\t\tbuilder.WriteString(\"openAt\", openAt.Format(\"2006-01-02 15:04:05\"))\n\t}\n\tcloseAt, err := form.CloseAt()\n\tif err == nil {\n\t\tbuilder.WriteString(\"closeAt\", closeAt.Format(\"2006-01-02 15:04:05\"))\n\t}\n\n\tencoded, err := json.Marshal(builder.Node())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(encoded)\n}\n\nfunc GetAnswer(formID string, authorID string) string {\n\t_, err := db.GetForm(formID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tanswer, err := db.GetAnswer(formID, std.Address(authorID))\n\tif answer != nil {\n\t\tpanic(err)\n\t}\n\n\treturn answer.Answers\n}\n\nfunc SubmitForm(formID string, answers string) {\n\t_, err := db.GetForm(formID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdb.SubmitForm(formID, answers)\n}\n\nfunc Render(path string) string {\n\tif len(db.Forms) == 0 {\n\t\tresponse := \"No forms yet !\"\n\t\treturn response\n\t}\n\n\tresponse := \"Forms:\\n\\n\"\n\tfor _, form := range db.Forms {\n\t\tresponse += ufmt.Sprintf(\"- %s\\n\\n\", GetFormByID(form.ID))\n\t}\n\tresponse += \"Answers:\\n\\n\"\n\tfor _, answer := range db.Answers {\n\t\tresponse += ufmt.Sprintf(\"- Form ID: %s\\nAuthor: %s\\nSubmitted At: %s\\n\u003eAnswers: %s\\n\\n\", answer.FormID, answer.Author, answer.SubmittedAt, answer.Answers)\n\t}\n\n\treturn response\n}\n" + }, + { + "name": "forms_test.gno", + "body": "package forms\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestGetFormByID(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\ttitle := \"Simple Form\"\n\tdescription := \"This is a form\"\n\topenAt := \"2021-01-01T00:00:00Z\"\n\tcloseAt := \"2021-01-02T00:00:00Z\"\n\tdata := `[{\"label\":\"Name\",\"fieldType\":\"string\",\"required\":true},{\"label\":\"Age\",\"fieldType\":\"number\",\"required\":false},{\"label\":\"Is this a test?\",\"fieldType\":\"boolean\",\"required\":false},{\"label\":\"Favorite Food\",\"fieldType\":\"['Pizza', 'Schnitzel', 'Burger']\",\"required\":true},{\"label\":\"Favorite Foods\",\"fieldType\":\"{'Pizza', 'Schnitzel', 'Burger'}\",\"required\":true}]`\n\n\turequire.NotPanics(t, func() {\n\t\tid := CreateForm(title, description, openAt, closeAt, data)\n\n\t\tform := GetFormByID(id)\n\n\t\turequire.True(t, strings.Contains(form, data), \"Form JSON was not rebuilt properly\")\n\t})\n}\n\nfunc TestGetForms(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\tdescription := \"This is a form\"\n\topenAt := \"2021-01-01T00:00:00Z\"\n\tcloseAt := \"2021-01-02T00:00:00Z\"\n\n\turequire.NotPanics(t, func() {\n\t\tdata1 := `[{\"label\":\"Name\",\"fieldType\":\"string\",\"required\":true}]`\n\t\tCreateForm(\"NameForm\", description, openAt, closeAt, data1)\n\t\tdata2 := `[{\"label\":\"Age\",\"fieldType\":\"number\",\"required\":false}]`\n\t\tCreateForm(\"AgeForm\", description, openAt, closeAt, data2)\n\n\t\tforms := GetForms()\n\n\t\turequire.True(t, strings.Contains(forms, data1) \u0026\u0026 strings.Contains(forms, data2), \"Forms JSON were not rebuilt properly\")\n\t})\n\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "MofPplK3fGaKRuhe0ti1gq5ciUUTBkEdDgPUd1EL9HjIPC5bkhF2PGaI1efJYR7Xt/zIun8oXwl43pG0LMSeDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "gnoface", + "path": "gno.land/r/demo/art/gnoface", + "files": [ + { + "name": "gnoface.gno", + "body": "package gnoface\n\nimport (\n\t\"math/rand\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc Render(path string) string {\n\tseed := uint64(entropy.New().Value())\n\n\tpath = strings.TrimSpace(path)\n\tif path != \"\" {\n\t\ts, err := strconv.Atoi(path)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tseed = uint64(s)\n\t}\n\n\toutput := ufmt.Sprintf(\"Gnoface #%d\\n\", seed)\n\toutput += \"```\\n\" + Draw(seed) + \"```\\n\"\n\treturn output\n}\n\nfunc Draw(seed uint64) string {\n\tvar (\n\t\thairs = []string{\n\t\t\t\" s\",\n\t\t\t\" .......\",\n\t\t\t\" s s s\",\n\t\t\t\" /\\\\ /\\\\\",\n\t\t\t\" |||||||\",\n\t\t}\n\t\theadtop = []string{\n\t\t\t\" /-------\\\\\",\n\t\t\t\" /~~~~~~~\\\\\",\n\t\t\t\" /|||||||\\\\\",\n\t\t\t\" ////////\\\\\",\n\t\t\t\" |||||||||\",\n\t\t\t\" /\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\",\n\t\t}\n\t\theadspace = []string{\n\t\t\t\" | |\",\n\t\t}\n\t\teyebrow = []string{\n\t\t\t\"~\",\n\t\t\t\"*\",\n\t\t\t\"_\",\n\t\t\t\".\",\n\t\t}\n\t\tear = []string{\n\t\t\t\"o\",\n\t\t\t\" \",\n\t\t\t\"D\",\n\t\t\t\"O\",\n\t\t\t\"\u003c\",\n\t\t\t\"\u003e\",\n\t\t\t\".\",\n\t\t\t\"|\",\n\t\t\t\")\",\n\t\t\t\"(\",\n\t\t}\n\t\teyesmiddle = []string{\n\t\t\t\"| o o |\",\n\t\t\t\"| o _ |\",\n\t\t\t\"| _ o |\",\n\t\t\t\"| . . |\",\n\t\t\t\"| O O |\",\n\t\t\t\"| v v |\",\n\t\t\t\"| X X |\",\n\t\t\t\"| x X |\",\n\t\t\t\"| X D |\",\n\t\t\t\"| ~ ~ |\",\n\t\t}\n\t\tnose = []string{\n\t\t\t\" | o |\",\n\t\t\t\" | O |\",\n\t\t\t\" | V |\",\n\t\t\t\" | L |\",\n\t\t\t\" | C |\",\n\t\t\t\" | ~ |\",\n\t\t\t\" | . . |\",\n\t\t\t\" | . |\",\n\t\t}\n\t\tmouth = []string{\n\t\t\t\" | __/ |\",\n\t\t\t\" | \\\\_/ |\",\n\t\t\t\" | . |\",\n\t\t\t\" | ___ |\",\n\t\t\t\" | ~~~ |\",\n\t\t\t\" | === |\",\n\t\t\t\" | \u003c=\u003e |\",\n\t\t}\n\t\theadbottom = []string{\n\t\t\t\" \\\\-------/\",\n\t\t\t\" \\\\~~~~~~~/\",\n\t\t\t\" \\\\_______/\",\n\t\t}\n\t)\n\n\tr := rand.New(rand.NewPCG(seed, 0xdeadbeef))\n\n\treturn pick(r, hairs) + \"\\n\" +\n\t\tpick(r, headtop) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\t\" | \" + pick(r, eyebrow) + \" \" + pick(r, eyebrow) + \" |\\n\" +\n\t\tpick(r, ear) + pick(r, eyesmiddle) + pick(r, ear) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, nose) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, mouth) + \"\\n\" +\n\t\tpick(r, headspace) + \"\\n\" +\n\t\tpick(r, headbottom) + \"\\n\"\n}\n\nfunc pick(r *rand.Rand, slice []string) string {\n\treturn slice[r.IntN(len(slice))]\n}\n\n// based on https://github.com/moul/pipotron/blob/master/dict/ascii-face.yml\n" + }, + { + "name": "gnoface_test.gno", + "body": "package gnoface\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestDraw(t *testing.T) {\n\tcases := []struct {\n\t\tseed uint64\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tseed: 42,\n\t\t\texpected: `\n |||||||\n |||||||||\n | |\n | . ~ |\n)| v v |O\n | |\n | L |\n | |\n | ___ |\n | |\n \\~~~~~~~/\n`[1:],\n\t\t},\n\t\t{\n\t\t\tseed: 1337,\n\t\t\texpected: `\n .......\n |||||||||\n | |\n | . _ |\nD| x X |O\n | |\n | ~ |\n | |\n | ~~~ |\n | |\n \\~~~~~~~/\n`[1:],\n\t\t},\n\t\t{\n\t\t\tseed: 123456789,\n\t\t\texpected: `\n .......\n ////////\\\n | |\n | ~ * |\n|| x X |o\n | |\n | V |\n | |\n | . |\n | |\n \\-------/\n`[1:],\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\tname := ufmt.Sprintf(\"%d\", tc.seed)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot := Draw(tc.seed)\n\t\t\tuassert.Equal(t, string(tc.expected), got)\n\t\t})\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\tcases := []struct {\n\t\tpath string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tpath: \"42\",\n\t\t\texpected: \"Gnoface #42\\n```\" + `\n |||||||\n |||||||||\n | |\n | . ~ |\n)| v v |O\n | |\n | L |\n | |\n | ___ |\n | |\n \\~~~~~~~/\n` + \"```\\n\",\n\t\t},\n\t\t{\n\t\t\tpath: \"1337\",\n\t\t\texpected: \"Gnoface #1337\\n```\" + `\n .......\n |||||||||\n | |\n | . _ |\nD| x X |O\n | |\n | ~ |\n | |\n | ~~~ |\n | |\n \\~~~~~~~/\n` + \"```\\n\",\n\t\t},\n\t\t{\n\t\t\tpath: \"123456789\",\n\t\t\texpected: \"Gnoface #123456789\\n```\" + `\n .......\n ////////\\\n | |\n | ~ * |\n|| x X |o\n | |\n | V |\n | |\n | . |\n | |\n \\-------/\n` + \"```\\n\",\n\t\t},\n\t}\n\tfor _, tc := range cases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tgot := Render(tc.path)\n\t\t\tuassert.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "yyRTw0fhPR+pqXDXvsc5CGU2YIdn3RvpB57MRQ7es5tzTOIf408QhcN8st2AXKF5w1orVLKt9KXaAKuS6a+bAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "millipede", + "path": "gno.land/r/demo/art/millipede", + "files": [ + { + "name": "millipede.gno", + "body": "package millipede\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tminSize = 1\n\tdefaultSize = 20\n\tmaxSize = 100\n)\n\nfunc Draw(size int) string {\n\tif size \u003c minSize || size \u003e maxSize {\n\t\tpanic(\"invalid millipede size\")\n\t}\n\tpaddings := []string{\" \", \" \", \"\", \" \", \" \", \" \", \" \", \" \", \" \"}\n\tvar b strings.Builder\n\tb.WriteString(\" ╚⊙ ⊙╝\\n\")\n\tfor i := 0; i \u003c size; i++ {\n\t\tb.WriteString(paddings[i%9] + \"╚═(███)═╝\\n\")\n\t}\n\treturn b.String()\n}\n\nfunc Render(path string) string {\n\tsize := defaultSize\n\n\tpath = strings.TrimSpace(path)\n\tif path != \"\" {\n\t\tvar err error\n\t\tsize, err = strconv.Atoi(path)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\toutput := \"```\\n\" + Draw(size) + \"```\\n\"\n\tif size \u003e minSize {\n\t\toutput += ufmt.Sprintf(\"[%d](/r/demo/art/millipede:%d)\u003c \", size-1, size-1)\n\t}\n\tif size \u003c maxSize {\n\t\toutput += ufmt.Sprintf(\" \u003e[%d](/r/demo/art/millipede:%d)\", size+1, size+1)\n\t}\n\treturn output\n}\n\n// based on https://github.com/getmillipede/millipede-go/blob/977f046c39c35a650eac0fd30245e96b22c7803c/main.go\n" + }, + { + "name": "millipede_test.gno", + "body": "package millipede\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestRender(t *testing.T) {\n\tcases := []struct {\n\t\tpath string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tpath: \"\",\n\t\t\texpected: \"```\" + `\n ╚⊙ ⊙╝\n ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n ╚═(███)═╝\n` + \"```\\n[19](/r/demo/art/millipede:19)\u003c \u003e[21](/r/demo/art/millipede:21)\",\n\t\t},\n\t\t{\n\t\t\tpath: \"4\",\n\t\t\texpected: \"```\" + `\n ╚⊙ ⊙╝\n ╚═(███)═╝\n ╚═(███)═╝\n╚═(███)═╝\n ╚═(███)═╝\n` + \"```\\n[3](/r/demo/art/millipede:3)\u003c \u003e[5](/r/demo/art/millipede:5)\",\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tgot := Render(tc.path)\n\t\t\tuassert.Equal(t, tc.expected, got)\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "T/2hpSWAFotgvpOtd+tVkIOxiTdTGuYwAeg7U51qEc4XQWslv36rjpBS8bqAutCueR6AvRfkQARORJz6Dvv3Cg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "grc20reg", + "path": "gno.land/r/demo/grc20reg", + "files": [ + { + "name": "grc20reg.gno", + "body": "package grc20reg\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/rotree\"\n\t\"gno.land/p/demo/fqname\"\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar registry = avl.NewTree() // rlmPath[.slug] -\u003e TokenGetter (slug is optional)\n\nfunc Register(tokenGetter grc20.TokenGetter, slug string) {\n\trlmPath := std.PreviousRealm().PkgPath()\n\tkey := fqname.Construct(rlmPath, slug)\n\tregistry.Set(key, tokenGetter)\n\tstd.Emit(\n\t\tregisterEvent,\n\t\t\"pkgpath\", rlmPath,\n\t\t\"slug\", slug,\n\t)\n}\n\nfunc Get(key string) grc20.TokenGetter {\n\ttokenGetter, ok := registry.Get(key)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn tokenGetter.(grc20.TokenGetter)\n}\n\nfunc MustGet(key string) grc20.TokenGetter {\n\ttokenGetter := Get(key)\n\tif tokenGetter == nil {\n\t\tpanic(\"unknown token: \" + key)\n\t}\n\treturn tokenGetter\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\ttokenGetter := tokenI.(grc20.TokenGetter)\n\t\t\ttoken := tokenGetter()\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\ttokenGetter := MustGet(key)\n\t\ttoken := tokenGetter()\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\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestRegistry(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/demo/foo\"))\n\trealmAddr := std.CurrentRealm().PkgPath()\n\ttoken, ledger := grc20.NewToken(\"TestToken\", \"TST\", 4)\n\tledger.Mint(std.CurrentRealm().Address(), 1234567)\n\ttokenGetter := func() *grc20.Token { return token }\n\t// register\n\tRegister(tokenGetter, \"\")\n\tregTokenGetter := Get(realmAddr)\n\tregToken := regTokenGetter()\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(tokenGetter, \"mySlug\")\n\tregTokenGetter = Get(realmAddr + \".mySlug\")\n\tregToken = regTokenGetter()\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(tokenGetter, \"\")\n\tregTokenGetter = Get(realmAddr + \"\")\n\tregToken = regTokenGetter()\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "N+iyzCGqyLT1ve49tkHsg9cBwuNFBhysPFinTn6g6DDThD+Wb//WZALzuaL370CgC9yqCtNNIZ+D98OplS8MDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "test20", + "path": "gno.land/r/demo/tests/test20", + "files": [ + { + "name": "test20.gno", + "body": "// Package test20 implements a deliberately insecure ERC20 token for testing purposes.\n// The Test20 token allows anyone to mint any amount of tokens to any address, making\n// it unsuitable for production use. The primary goal of this package is to facilitate\n// testing and experimentation without any security measures or restrictions.\n//\n//\tWARNING: This token is highly insecure and should not be used in any\n//\t production environment. It is intended solely for testing and\n//\t educational purposes.\npackage test20\n\nimport (\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/r/demo/grc20reg\"\n)\n\nvar Token, PrivateLedger = grc20.NewToken(\"Test20\", \"TST\", 4)\n\nfunc init() {\n\tgrc20reg.Register(Token.Getter(), \"\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "vFi1lrLdmC76c8gQhAD3slqqvAq4KMqX68bCBA33NXw1F4JOAAwb+9SRBBadRQRzIgVmnqx9xK3SekRdEyRGAA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "atomicswap", + "path": "gno.land/r/demo/atomicswap", + "files": [ + { + "name": "atomicswap.gno", + "body": "// Package atomicswap implements a hash time-locked contract (HTLC) for atomic swaps\n// between native coins (ugnot) or GRC20 tokens.\n//\n// An atomic swap allows two parties to exchange assets in a trustless way, where\n// either both transfers happen or neither does. The process works as follows:\n//\n// 1. Alice wants to swap with Bob. She generates a secret and creates a swap with\n// Bob's address and the hash of the secret (hashlock).\n//\n// 2. Bob can claim the assets by providing the correct secret before the timelock expires.\n// The secret proves Bob knows the preimage of the hashlock.\n//\n// 3. If Bob doesn't claim in time, Alice can refund the assets back to herself.\n//\n// Example usage for native coins:\n//\n//\t// Alice creates a swap with 1000ugnot for Bob\n//\tsecret := \"mysecret\"\n//\thashlock := hex.EncodeToString(sha256.Sum256([]byte(secret)))\n//\tid, _ := atomicswap.NewCoinSwap(bobAddr, hashlock) // -send 1000ugnot\n//\n//\t// Bob claims the swap by providing the secret\n//\tatomicswap.Claim(id, \"mysecret\")\n//\n// Example usage for GRC20 tokens:\n//\n//\t// Alice approves the swap contract to spend her tokens\n//\ttoken.Approve(swapAddr, 1000)\n//\n//\t// Alice creates a swap with 1000 tokens for Bob\n//\tid, _ := atomicswap.NewGRC20Swap(bobAddr, hashlock, \"gno.land/r/demo/token\")\n//\n//\t// Bob claims the swap by providing the secret\n//\tatomicswap.Claim(id, \"mysecret\")\n//\n// If Bob doesn't claim in time (default 1 week), Alice can refund:\n//\n//\tatomicswap.Refund(id)\npackage atomicswap\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/grc20reg\"\n)\n\nconst defaultTimelockDuration = 7 * 24 * time.Hour // 1w\n\nvar (\n\tswaps avl.Tree // id -\u003e *Swap\n\tcounter int\n)\n\n// NewCoinSwap creates a new atomic swap contract for native coins.\n// It uses a default timelock duration.\nfunc NewCoinSwap(recipient std.Address, hashlock string) (int, *Swap) {\n\ttimelock := time.Now().Add(defaultTimelockDuration)\n\treturn NewCustomCoinSwap(recipient, hashlock, timelock)\n}\n\n// NewGRC20Swap creates a new atomic swap contract for grc20 tokens.\n// It uses gno.land/r/demo/grc20reg to lookup for a registered token.\nfunc NewGRC20Swap(recipient std.Address, hashlock string, tokenRegistryKey string) (int, *Swap) {\n\ttimelock := time.Now().Add(defaultTimelockDuration)\n\ttokenGetter := grc20reg.MustGet(tokenRegistryKey)\n\ttoken := tokenGetter()\n\treturn NewCustomGRC20Swap(recipient, hashlock, timelock, token)\n}\n\n// NewCoinSwapWithTimelock creates a new atomic swap contract for native coin.\n// It allows specifying a custom timelock duration.\n// It is not callable with `gnokey maketx call`, but can be imported by another contract or `gnokey maketx run`.\nfunc NewCustomCoinSwap(recipient std.Address, hashlock string, timelock time.Time) (int, *Swap) {\n\tsender := std.PreviousRealm().Address()\n\tsent := std.OriginSend()\n\trequire(len(sent) != 0, \"at least one coin needs to be sent\")\n\n\t// Create the swap\n\tsendFn := func(to std.Address) {\n\t\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\t\tpkgAddr := std.CurrentRealm().Address()\n\t\tbanker.SendCoins(pkgAddr, to, sent)\n\t}\n\tamountStr := sent.String()\n\tswap := newSwap(sender, recipient, hashlock, timelock, amountStr, sendFn)\n\n\tcounter++\n\tid := strconv.Itoa(counter)\n\tswaps.Set(id, swap)\n\treturn counter, swap\n}\n\n// NewCustomGRC20Swap creates a new atomic swap contract for grc20 tokens.\n// It is not callable with `gnokey maketx call`, but can be imported by another contract or `gnokey maketx run`.\nfunc NewCustomGRC20Swap(recipient std.Address, hashlock string, timelock time.Time, token *grc20.Token) (int, *Swap) {\n\tsender := std.PreviousRealm().Address()\n\tcurAddr := std.CurrentRealm().Address()\n\n\tallowance := token.Allowance(sender, curAddr)\n\trequire(allowance \u003e 0, \"no allowance\")\n\n\tuserTeller := token.CallerTeller()\n\terr := userTeller.TransferFrom(sender, curAddr, allowance)\n\trequire(err == nil, \"cannot retrieve tokens from allowance\")\n\n\tamountStr := ufmt.Sprintf(\"%d%s\", allowance, token.GetSymbol())\n\tsendFn := func(to std.Address) {\n\t\terr := userTeller.Transfer(to, allowance)\n\t\trequire(err == nil, \"cannot transfer tokens\")\n\t}\n\n\tswap := newSwap(sender, recipient, hashlock, timelock, amountStr, sendFn)\n\n\tcounter++\n\tid := strconv.Itoa(counter)\n\tswaps.Set(id, swap)\n\n\treturn counter, swap\n}\n\n// Claim loads a registered swap and tries to claim it.\nfunc Claim(id int, secret string) {\n\tswap := mustGet(id)\n\tswap.Claim(secret)\n}\n\n// Refund loads a registered swap and tries to refund it.\nfunc Refund(id int) {\n\tswap := mustGet(id)\n\tswap.Refund()\n}\n\n// Render returns a list of swaps (simplified) for the homepage, and swap details when specifying a swap ID.\nfunc Render(path string) string {\n\tif path == \"\" { // home\n\t\toutput := \"\"\n\t\tsize := swaps.Size()\n\t\tmax := 10\n\t\tswaps.ReverseIterateByOffset(size-max, max, func(key string, value any) bool {\n\t\t\tswap := value.(*Swap)\n\t\t\toutput += ufmt.Sprintf(\"- %s: %s -(%s)\u003e %s - %s\\n\",\n\t\t\t\tkey, swap.sender, swap.amountStr, swap.recipient, swap.Status())\n\t\t\treturn false\n\t\t})\n\t\treturn output\n\t} else { // by id\n\t\tswap, ok := swaps.Get(path)\n\t\tif !ok {\n\t\t\treturn \"404\"\n\t\t}\n\t\treturn swap.(*Swap).String()\n\t}\n}\n\n// require checks a condition and panics with a message if the condition is false.\nfunc require(check bool, msg string) {\n\tif !check {\n\t\tpanic(msg)\n\t}\n}\n\n// mustGet retrieves a swap by its id or panics.\nfunc mustGet(id int) *Swap {\n\tkey := strconv.Itoa(id)\n\tswap, ok := swaps.Get(key)\n\tif !ok {\n\t\tpanic(\"unknown swap ID\")\n\t}\n\treturn swap.(*Swap)\n}\n" + }, + { + "name": "atomicswap_test.gno", + "body": "package atomicswap\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"std\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/r/demo/tests/test20\"\n)\n\nvar testRun bool\n\nfunc TestNewCustomCoinSwap_Claim(t *testing.T) {\n\tt.Skip(\"skipping due to bad support for unit-test driven banker\")\n\tdefer resetTestState()\n\n\t// Setup\n\tsender := testutils.TestAddress(\"sender1\")\n\trecipient := testutils.TestAddress(\"recipient1\")\n\tamount := std.Coins{{Denom: \"ugnot\", Amount: 1}}\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(1 * time.Hour)\n\n\t// Create a new swap\n\ttesting.SetRealm(std.NewUserRealm(sender))\n\ttesting.SetOriginSend(amount)\n\tid, swap := NewCustomCoinSwap(recipient, hashlockHex, timelock)\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wgc47h6lta047h6lta047h6l56jtjc\n- recipient: g1wfjkx6tsd9jkuap3ta047h6lta047h6lkk20gv\n- amount: 1ugnot\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test initial state\n\tuassert.Equal(t, sender, swap.sender, \"expected sender to match\")\n\tuassert.Equal(t, recipient, swap.recipient, \"expected recipient to match\")\n\tuassert.Equal(t, swap.amountStr, amount.String(), \"expected amount to match\")\n\tuassert.Equal(t, hashlockHex, swap.hashlock, \"expected hashlock to match\")\n\tuassert.True(t, swap.timelock.Equal(timelock), \"expected timelock to match\")\n\tuassert.False(t, swap.claimed, \"expected claimed to be false\")\n\tuassert.False(t, swap.refunded, \"expected refunded to be false\")\n\n\t// Test claim\n\ttesting.SetRealm(std.NewUserRealm(recipient))\n\tuassert.PanicsWithMessage(t, \"invalid preimage\", func() { swap.Claim(\"invalid\") })\n\tswap.Claim(\"secret\")\n\tuassert.True(t, swap.claimed, \"expected claimed to be true\")\n\n\t// Test refund (should fail because already claimed)\n\tuassert.PanicsWithMessage(t, \"already claimed\", swap.Refund)\n\tuassert.PanicsWithMessage(t, \"already claimed\", func() { swap.Claim(\"secret\") })\n\n\texpected = `- status: claimed\n- sender: g1wdjkuer9wgc47h6lta047h6lta047h6l56jtjc\n- recipient: g1wfjkx6tsd9jkuap3ta047h6lta047h6lkk20gv\n- amount: 1ugnot\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestNewCustomCoinSwap_Refund(t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tsender := testutils.TestAddress(\"sender2\")\n\trecipient := testutils.TestAddress(\"recipient2\")\n\tamount := std.Coins{{Denom: \"ugnot\", Amount: 1}}\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(1 * time.Hour)\n\n\t// Create a new swap\n\ttesting.SetRealm(std.NewUserRealm(sender))\n\ttesting.SetOriginSend(amount)\n\tid, swap := NewCustomCoinSwap(recipient, hashlockHex, timelock) // Create a new swap\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wge97h6lta047h6lta047h6ltfacad\n- recipient: g1wfjkx6tsd9jkuapjta047h6lta047h6lducc3v\n- amount: 1ugnot\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test Refund\n\tpkgAddr := std.DerivePkgAddr(\"gno.land/r/demo/atomicswap\")\n\ttesting.IssueCoins(pkgAddr, std.Coins{{\"ugnot\", 100000000}})\n\tuassert.PanicsWithMessage(t, \"timelock not expired\", swap.Refund)\n\tswap.timelock = time.Now().Add(-1 * time.Hour) // override timelock\n\tswap.Refund()\n\tuassert.True(t, swap.refunded, \"expected refunded to be true\")\n\texpected = `- status: refunded\n- sender: g1wdjkuer9wge97h6lta047h6lta047h6ltfacad\n- recipient: g1wfjkx6tsd9jkuapjta047h6lta047h6lducc3v\n- amount: 1ugnot\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-13T22:31:30Z\n- remaining: 0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestNewCustomGRC20Swap_Claim(t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tsender := testutils.TestAddress(\"sender3\")\n\trecipient := testutils.TestAddress(\"recipient3\")\n\trlm := std.DerivePkgAddr(\"gno.land/r/demo/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(1 * time.Hour)\n\n\ttest20.PrivateLedger.Mint(sender, 100_000)\n\ttest20.PrivateLedger.Approve(sender, rlm, 70_000)\n\n\t// Create a new swap\n\ttesting.SetRealm(std.NewUserRealm(sender))\n\tid, swap := NewCustomGRC20Swap(recipient, hashlockHex, timelock, test20.Token)\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wge47h6lta047h6lta047h6l5rk38l\n- recipient: g1wfjkx6tsd9jkuapnta047h6lta047h6ly6k4pv\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test initial state\n\tuassert.Equal(t, sender, swap.sender, \"expected sender to match\")\n\tuassert.Equal(t, recipient, swap.recipient, \"expected recipient to match\")\n\tbal := test20.Token.BalanceOf(sender)\n\tuassert.Equal(t, bal, uint64(30_000))\n\tbal = test20.Token.BalanceOf(rlm)\n\tuassert.Equal(t, bal, uint64(70_000))\n\tbal = test20.Token.BalanceOf(recipient)\n\tuassert.Equal(t, bal, uint64(0))\n\n\t// uassert.Equal(t, swap.amountStr, amount.String(), \"expected amount to match\")\n\tuassert.Equal(t, hashlockHex, swap.hashlock, \"expected hashlock to match\")\n\tuassert.True(t, swap.timelock.Equal(timelock), \"expected timelock to match\")\n\tuassert.False(t, swap.claimed, \"expected claimed to be false\")\n\tuassert.False(t, swap.refunded, \"expected refunded to be false\")\n\n\t// Test claim\n\ttesting.SetRealm(std.NewUserRealm(recipient))\n\tuassert.PanicsWithMessage(t, \"invalid preimage\", func() { swap.Claim(\"invalid\") })\n\tswap.Claim(\"secret\")\n\tuassert.True(t, swap.claimed, \"expected claimed to be true\")\n\n\tbal = test20.Token.BalanceOf(sender)\n\tuassert.Equal(t, bal, uint64(30_000))\n\tbal = test20.Token.BalanceOf(rlm)\n\tuassert.Equal(t, bal, uint64(0))\n\tbal = test20.Token.BalanceOf(recipient)\n\tuassert.Equal(t, bal, uint64(70_000))\n\n\t// Test refund (should fail because already claimed)\n\tuassert.PanicsWithMessage(t, \"already claimed\", swap.Refund)\n\tuassert.PanicsWithMessage(t, \"already claimed\", func() { swap.Claim(\"secret\") })\n\n\texpected = `- status: claimed\n- sender: g1wdjkuer9wge47h6lta047h6lta047h6l5rk38l\n- recipient: g1wfjkx6tsd9jkuapnta047h6lta047h6ly6k4pv\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestNewCustomGRC20Swap_Refund(t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tsender := testutils.TestAddress(\"sender5\")\n\trecipient := testutils.TestAddress(\"recipient5\")\n\trlm := std.DerivePkgAddr(\"gno.land/r/demo/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(1 * time.Hour)\n\n\ttest20.PrivateLedger.Mint(sender, 100_000)\n\ttest20.PrivateLedger.Approve(sender, rlm, 70_000)\n\n\t// Create a new swap\n\ttesting.SetRealm(std.NewUserRealm(sender))\n\tid, swap := NewCustomGRC20Swap(recipient, hashlockHex, timelock, test20.Token)\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wg647h6lta047h6lta047h6l5p6k3k\n- recipient: g1wfjkx6tsd9jkuap4ta047h6lta047h6lmwmj6v\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-14T00:31:30Z\n- remaining: 1h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test initial state\n\tuassert.Equal(t, sender, swap.sender, \"expected sender to match\")\n\tuassert.Equal(t, recipient, swap.recipient, \"expected recipient to match\")\n\tbal := test20.Token.BalanceOf(sender)\n\tuassert.Equal(t, bal, uint64(30_000))\n\tbal = test20.Token.BalanceOf(rlm)\n\tuassert.Equal(t, bal, uint64(70_000))\n\tbal = test20.Token.BalanceOf(recipient)\n\tuassert.Equal(t, bal, uint64(0))\n\n\t// Test Refund\n\tpkgAddr := std.DerivePkgAddr(\"gno.land/r/demo/atomicswap\")\n\ttesting.IssueCoins(pkgAddr, std.Coins{{\"ugnot\", 100000000}})\n\tuassert.PanicsWithMessage(t, \"timelock not expired\", swap.Refund)\n\tswap.timelock = time.Now().Add(-1 * time.Hour) // override timelock\n\tswap.Refund()\n\tuassert.True(t, swap.refunded, \"expected refunded to be true\")\n\n\tbal = test20.Token.BalanceOf(sender)\n\tuassert.Equal(t, bal, uint64(100_000))\n\tbal = test20.Token.BalanceOf(rlm)\n\tuassert.Equal(t, bal, uint64(0))\n\tbal = test20.Token.BalanceOf(recipient)\n\tuassert.Equal(t, bal, uint64(0))\n\n\texpected = `- status: refunded\n- sender: g1wdjkuer9wg647h6lta047h6lta047h6l5p6k3k\n- recipient: g1wfjkx6tsd9jkuap4ta047h6lta047h6lmwmj6v\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-13T22:31:30Z\n- remaining: 0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestNewGRC20Swap_Claim(t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tsender := testutils.TestAddress(\"sender4\")\n\trecipient := testutils.TestAddress(\"recipient4\")\n\trlm := std.DerivePkgAddr(\"gno.land/r/demo/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(defaultTimelockDuration)\n\n\ttest20.PrivateLedger.Mint(sender, 100_000)\n\ttest20.PrivateLedger.Approve(sender, rlm, 70_000)\n\n\t// Create a new swap\n\ttesting.SetRealm(std.NewUserRealm(sender))\n\tid, swap := NewGRC20Swap(recipient, hashlockHex, \"gno.land/r/demo/tests/test20\")\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wg697h6lta047h6lta047h6ltt3lty\n- recipient: g1wfjkx6tsd9jkuap5ta047h6lta047h6ljg4l2v\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-20T23:31:30Z\n- remaining: 168h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test initial state\n\tuassert.Equal(t, sender, swap.sender, \"expected sender to match\")\n\tuassert.Equal(t, recipient, swap.recipient, \"expected recipient to match\")\n\tbal := test20.Token.BalanceOf(sender)\n\tuassert.Equal(t, bal, uint64(30_000))\n\tbal = test20.Token.BalanceOf(rlm)\n\tuassert.Equal(t, bal, uint64(70_000))\n\tbal = test20.Token.BalanceOf(recipient)\n\tuassert.Equal(t, bal, uint64(0))\n\n\t// uassert.Equal(t, swap.amountStr, amount.String(), \"expected amount to match\")\n\tuassert.Equal(t, hashlockHex, swap.hashlock, \"expected hashlock to match\")\n\tuassert.True(t, swap.timelock.Equal(timelock), \"expected timelock to match\")\n\tuassert.False(t, swap.claimed, \"expected claimed to be false\")\n\tuassert.False(t, swap.refunded, \"expected refunded to be false\")\n\n\t// Test claim\n\ttesting.SetRealm(std.NewUserRealm(recipient))\n\tuassert.PanicsWithMessage(t, \"invalid preimage\", func() { swap.Claim(\"invalid\") })\n\tswap.Claim(\"secret\")\n\tuassert.True(t, swap.claimed, \"expected claimed to be true\")\n\n\tbal = test20.Token.BalanceOf(sender)\n\tuassert.Equal(t, bal, uint64(30_000))\n\tbal = test20.Token.BalanceOf(rlm)\n\tuassert.Equal(t, bal, uint64(0))\n\tbal = test20.Token.BalanceOf(recipient)\n\tuassert.Equal(t, bal, uint64(70_000))\n\n\t// Test refund (should fail because already claimed)\n\tuassert.PanicsWithMessage(t, \"already claimed\", swap.Refund)\n\tuassert.PanicsWithMessage(t, \"already claimed\", func() { swap.Claim(\"secret\") })\n\n\texpected = `- status: claimed\n- sender: g1wdjkuer9wg697h6lta047h6lta047h6ltt3lty\n- recipient: g1wfjkx6tsd9jkuap5ta047h6lta047h6ljg4l2v\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-20T23:31:30Z\n- remaining: 168h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestNewGRC20Swap_Refund(t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\tsender := testutils.TestAddress(\"sender6\")\n\trecipient := testutils.TestAddress(\"recipient6\")\n\trlm := std.DerivePkgAddr(\"gno.land/r/demo/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(defaultTimelockDuration)\n\n\ttest20.PrivateLedger.Mint(sender, 100_000)\n\ttest20.PrivateLedger.Approve(sender, rlm, 70_000)\n\n\t// Create a new swap\n\ttesting.SetRealm(std.NewUserRealm(sender))\n\tid, swap := NewGRC20Swap(recipient, hashlockHex, \"gno.land/r/demo/tests/test20\")\n\tuassert.Equal(t, 1, id)\n\n\texpected := `- status: active\n- sender: g1wdjkuer9wgm97h6lta047h6lta047h6ltj497r\n- recipient: g1wfjkx6tsd9jkuapkta047h6lta047h6lqyf9rv\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-20T23:31:30Z\n- remaining: 168h0m0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n\n\t// Test initial state\n\tuassert.Equal(t, sender, swap.sender, \"expected sender to match\")\n\tuassert.Equal(t, recipient, swap.recipient, \"expected recipient to match\")\n\tbal := test20.Token.BalanceOf(sender)\n\tuassert.Equal(t, bal, uint64(30_000))\n\tbal = test20.Token.BalanceOf(rlm)\n\tuassert.Equal(t, bal, uint64(70_000))\n\tbal = test20.Token.BalanceOf(recipient)\n\tuassert.Equal(t, bal, uint64(0))\n\n\t// Test Refund\n\tpkgAddr := std.DerivePkgAddr(\"gno.land/r/demo/atomicswap\")\n\ttesting.IssueCoins(pkgAddr, std.Coins{{\"ugnot\", 100000000}})\n\tuassert.PanicsWithMessage(t, \"timelock not expired\", swap.Refund)\n\tswap.timelock = time.Now().Add(-1 * time.Hour) // override timelock\n\tswap.Refund()\n\tuassert.True(t, swap.refunded, \"expected refunded to be true\")\n\n\tbal = test20.Token.BalanceOf(sender)\n\tuassert.Equal(t, bal, uint64(100_000))\n\tbal = test20.Token.BalanceOf(rlm)\n\tuassert.Equal(t, bal, uint64(0))\n\tbal = test20.Token.BalanceOf(recipient)\n\tuassert.Equal(t, bal, uint64(0))\n\n\texpected = `- status: refunded\n- sender: g1wdjkuer9wgm97h6lta047h6lta047h6ltj497r\n- recipient: g1wfjkx6tsd9jkuapkta047h6lta047h6lqyf9rv\n- amount: 70000TST\n- hashlock: 2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b\n- timelock: 2009-02-13T22:31:30Z\n- remaining: 0s`\n\tuassert.Equal(t, expected, swap.String())\n\tuassert.Equal(t, expected, Render(\"1\"))\n}\n\nfunc TestRender(t *testing.T) {\n\tdefer resetTestState()\n\n\t// Setup\n\talice := testutils.TestAddress(\"alice\")\n\tbob := testutils.TestAddress(\"bob\")\n\tcharly := testutils.TestAddress(\"charly\")\n\trlm := std.DerivePkgAddr(\"gno.land/r/demo/atomicswap\")\n\thashlock := sha256.Sum256([]byte(\"secret\"))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\ttimelock := time.Now().Add(1 * time.Hour)\n\n\ttest20.PrivateLedger.Mint(alice, 100_000)\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\n\tuserTeller := test20.Token.CallerTeller()\n\tuserTeller.Approve(rlm, 10_000)\n\t_, bobSwap := NewCustomGRC20Swap(bob, hashlockHex, timelock, test20.Token)\n\n\tuserTeller.Approve(rlm, 20_000)\n\t_, _ = NewCustomGRC20Swap(charly, hashlockHex, timelock, test20.Token)\n\n\ttesting.SetRealm(std.NewUserRealm(bob))\n\tbobSwap.Claim(\"secret\")\n\n\texpected := `- 2: g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh -(20000TST)\u003e g1vd5xzunv09047h6lta047h6lta047h6lhsyveh - active\n- 1: g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh -(10000TST)\u003e g1vfhkyh6lta047h6lta047h6lta047h6l03vdhu - claimed\n`\n\tuassert.Equal(t, expected, Render(\"\"))\n}\n\nfunc resetTestState() {\n\tswaps = avl.Tree{}\n\tcounter = 0\n}\n" + }, + { + "name": "swap.gno", + "body": "package atomicswap\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Swap represents an atomic swap contract.\ntype Swap struct {\n\tsender std.Address\n\trecipient std.Address\n\thashlock string\n\ttimelock time.Time\n\tclaimed bool\n\trefunded bool\n\tamountStr string\n\tsendFn func(to std.Address)\n}\n\nfunc newSwap(\n\tsender std.Address,\n\trecipient std.Address,\n\thashlock string,\n\ttimelock time.Time,\n\tamountStr string,\n\tsendFn func(std.Address),\n) *Swap {\n\trequire(time.Now().Before(timelock), \"timelock must be in the future\")\n\trequire(hashlock != \"\", \"hashlock must not be empty\")\n\treturn \u0026Swap{\n\t\trecipient: recipient,\n\t\tsender: sender,\n\t\thashlock: hashlock,\n\t\ttimelock: timelock,\n\t\tclaimed: false,\n\t\trefunded: false,\n\t\tsendFn: sendFn,\n\t\tamountStr: amountStr,\n\t}\n}\n\n// Claim allows the recipient to claim the funds if they provide the correct preimage.\nfunc (s *Swap) Claim(preimage string) {\n\trequire(!s.claimed, \"already claimed\")\n\trequire(!s.refunded, \"already refunded\")\n\trequire(std.PreviousRealm().Address() == s.recipient, \"unauthorized\")\n\n\thashlock := sha256.Sum256([]byte(preimage))\n\thashlockHex := hex.EncodeToString(hashlock[:])\n\trequire(hashlockHex == s.hashlock, \"invalid preimage\")\n\n\ts.claimed = true\n\ts.sendFn(s.recipient)\n}\n\n// Refund allows the sender to refund the funds after the timelock has expired.\nfunc (s *Swap) Refund() {\n\trequire(!s.claimed, \"already claimed\")\n\trequire(!s.refunded, \"already refunded\")\n\trequire(std.PreviousRealm().Address() == s.sender, \"unauthorized\")\n\trequire(time.Now().After(s.timelock), \"timelock not expired\")\n\n\ts.refunded = true\n\ts.sendFn(s.sender)\n}\n\nfunc (s Swap) Status() string {\n\tswitch {\n\tcase s.refunded:\n\t\treturn \"refunded\"\n\tcase s.claimed:\n\t\treturn \"claimed\"\n\tcase s.TimeRemaining() \u003c 0:\n\t\treturn \"expired\"\n\tdefault:\n\t\treturn \"active\"\n\t}\n}\n\nfunc (s Swap) TimeRemaining() time.Duration {\n\tremaining := time.Until(s.timelock)\n\tif remaining \u003c 0 {\n\t\treturn 0\n\t}\n\treturn remaining\n}\n\n// String returns the current state of the swap.\nfunc (s Swap) String() string {\n\treturn ufmt.Sprintf(\n\t\t\"- status: %s\\n- sender: %s\\n- recipient: %s\\n- amount: %s\\n- hashlock: %s\\n- timelock: %s\\n- remaining: %s\",\n\t\ts.Status(), s.sender, s.recipient, s.amountStr, s.hashlock, s.timelock.Format(time.RFC3339), s.TimeRemaining().String(),\n\t)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "ozfLG9H9wvrEdPCyQImhf2BtieBI4cX/9p9kQhcwQIwed+LFz+h3jBXX22m2bnwWVD51isuWg7vepSkA3ZLVCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "banktest", + "path": "gno.land/r/demo/banktest", + "files": [ + { + "name": "README.md", + "body": "This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.go](/r/demo/banktest/banktest.go) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n \"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e Self explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n caller std.Address\n sent std.Coins\n returned std.Coins\n time time.Time\n}\n\nfunc (act *activity) String() string {\n return act.caller.String() + \" \" +\n act.sent.String() + \" sent, \" +\n act.returned.String() + \" returned, at \" +\n act.time.Format(\"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract. Notice that the \"latest\" variable is defined \"globally\" within the context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package are encapsulated within this \"data realm\", where the data is mutated based on transactions that can potentially cross many realm and non-realm package boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n std.AssertOriginCall()\n caller := std.OriginCaller()\n send := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named \"Deposit\". `std.AssertOriginCall() asserts that this function was called by a gno transactional Message. The caller is the user who signed off on this transactional message. Send is the amount of deposit sent along with this message.\n\n```go\n // record activity\n act := \u0026activity{\n caller: caller,\n sent: std.OriginSend(),\n returned: send,\n time: time.Now(),\n }\n for i := len(latest) - 2; i \u003e= 0; i-- {\n latest[i+1] = latest[i] // shift by +1.\n }\n latest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n // return if any.\n if returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n banker := std.NewBanker(std.BankerTypeOriginSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n pkgaddr := std.CurrentRealm().Address()\n // TODO: use std.Coins constructors, this isn't generally safe.\n banker.SendCoins(pkgaddr, caller, send)\n return \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/demo/banktest:](/r/demo/banktest:).\n\n```go\nfunc Render(path string) string {\n // get realm coins.\n banker := std.NewBanker(std.BankerTypeReadonly)\n coins := banker.GetCoins(std.CurrentRealm().Address())\n\n // render\n res := \"\"\n res += \"## recent activity\\n\"\n res += \"\\n\"\n for _, act := range latest {\n if act == nil {\n break\n }\n res += \" * \" + act.String() + \"\\n\"\n }\n res += \"\\n\"\n res += \"## total deposits\\n\"\n res += coins.String()\n return res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:gnolang/4).\n" + }, + { + "name": "banktest.gno", + "body": "package banktest\n\nimport (\n\t\"std\"\n\t\"time\"\n)\n\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime time.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tact.time.Format(\"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.OriginCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.OriginSend(),\n\t\treturned: send,\n\t\ttime: time.Now(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n\t// return if any.\n\tif returnAmount \u003e 0 {\n\t\tbanker := std.NewBanker(std.BankerTypeOriginSend)\n\t\tpkgaddr := std.CurrentRealm().Address()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n\t} else {\n\t\treturn \"thank you!\"\n\t}\n}\n\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.NewBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.CurrentRealm().Address())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\n// SEND: 100000000ugnot\n\npackage bank1\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/banktest\"\n)\n\nfunc main() {\n\t// set up main address and banktest addr.\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/bank1\")\n\ttesting.SetOriginCaller(mainaddr)\n\n\t// get and print balance of mainaddr.\n\t// with the SEND, + 200 gnot given by the TestContext, main should have 300gnot.\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\t// simulate a Deposit call. use Send + OriginSend to simulate -send.\n\tbanker.SendCoins(mainaddr, banktestAddr, std.Coins{{\"ugnot\", 100_000_000}})\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 100_000_000}})\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/demo/banktest\"))\n\tres := banktest.Deposit(\"ugnot\", 50_000_000)\n\tprintln(\"Deposit():\", res)\n\n\t// print main balance after.\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal)\n\n\t// simulate a Render(). banker should have given back all coins.\n\tres = banktest.Render(\"\")\n\tprintln(res)\n}\n\n// Output:\n// main before: 100000000ugnot\n// Deposit(): returned!\n// main after: 50000000ugnot\n// ## recent activity\n//\n// * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 50000000ugnot returned, at 2009-02-13 11:31pm UTC\n//\n// ## total deposits\n// 50000000ugnot\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/banktest\"\n)\n\nfunc main() {\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\n\t// simulate a Deposit call.\n\ttesting.IssueCoins(banktestAddr, std.Coins{{\"ugnot\", 100000000}})\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 100000000}})\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/demo/banktest\"))\n\tres := banktest.Deposit(\"ugnot\", 101000000)\n\tprintln(res)\n}\n\n// Error:\n// cannot send \"101000000ugnot\", limit \"100000000ugnot\" exceeded with \"\" already spent\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/banktest\"\n)\n\nfunc main() {\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\n\t// print main balance before.\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/bank1\")\n\ttesting.SetOriginCaller(mainaddr)\n\n\tbanker := std.NewBanker(std.BankerTypeReadonly)\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal) // plus OriginSend equals 300.\n\n\t// simulate a Deposit call.\n\ttesting.IssueCoins(banktestAddr, std.Coins{{\"ugnot\", 100000000}})\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 100000000}})\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/demo/banktest\"))\n\tres := banktest.Deposit(\"ugnot\", 55000000)\n\tprintln(\"Deposit():\", res)\n\n\t// print main balance after.\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal) // now 255.\n\n\t// simulate a Render().\n\tres = banktest.Render(\"\")\n\tprintln(res)\n}\n\n// Output:\n// main before:\n// Deposit(): returned!\n// main after: 55000000ugnot\n// ## recent activity\n//\n// * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 55000000ugnot returned, at 2009-02-13 11:31pm UTC\n//\n// ## total deposits\n// 45000000ugnot\n" + }, + { + "name": "z_3_filetest.gno", + "body": "// Empty line between the directives is important for them to be parsed\n// independently. :facepalm:\n\n// PKGPATH: gno.land/r/demo/bank1\n\npackage bank1\n\nimport (\n\t\"std\"\n\t\"testing\"\n)\n\nfunc main() {\n\tbanktestAddr := std.DerivePkgAddr(\"gno.land/r/demo/banktest\")\n\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/bank1\")\n\ttesting.SetOriginCaller(mainaddr)\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\tsend := std.Coins{{\"ugnot\", 123}}\n\tbanker.SendCoins(banktestAddr, mainaddr, send)\n}\n\n// Error:\n// can only send coins from realm that created banker \"g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk\", not \"g1dv3435088tlrgggf745kaud0ptrkc9v42k8llz\"\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "xllEDuVAct2RvEIrsPfxcovXBjDwc7UMnOP64hkGvORjg+RwGfdUYDo1/2E79yLHzoDg3sCK73ZznpT7XzLqDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "bar20", + "path": "gno.land/r/demo/bar20", + "files": [ + { + "name": "bar20.gno", + "body": "// Package bar20 is similar to gno.land/r/demo/foo20 but exposes a safe-object\n// that can be used by `maketx run`, another contract importing foo20, and in\n// the future when we'll support `maketx call Token.XXX`.\npackage bar20\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/grc20reg\"\n)\n\nvar (\n\tToken, adm = grc20.NewToken(\"Bar\", \"BAR\", 4)\n\tUserTeller = Token.CallerTeller()\n)\n\nfunc init() {\n\tgrc20reg.Register(Token.Getter(), \"\")\n}\n\nfunc Faucet() string {\n\tcaller := std.PreviousRealm().Address()\n\tif err := adm.Mint(caller, 1_000_000); err != nil {\n\t\treturn \"error: \" + err.Error()\n\t}\n\treturn \"OK\"\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn Token.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := std.Address(parts[1])\n\t\tbalance := Token.BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\\n\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n" + }, + { + "name": "bar20_test.gno", + "body": "package bar20\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestPackage(t *testing.T) {\n\talice := testutils.TestAddress(\"alice\")\n\ttesting.SetOriginCaller(alice)\n\n\turequire.Equal(t, UserTeller.BalanceOf(alice), uint64(0))\n\turequire.Equal(t, Faucet(), \"OK\")\n\turequire.Equal(t, UserTeller.BalanceOf(alice), uint64(1_000_000))\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "AkjtzOlukRelA3T0cBpEXSJc2rMLMRIH4xlr1cY4XDDM73JOUeTFEJlS/C3n1UvCDIjZiXfYwOjP/IXBnqsABA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "bridge", + "path": "gno.land/r/gov/dao/bridge", + "files": [ + { + "name": "bridge.gno", + "body": "package bridge\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/ownable\"\n)\n\nconst (\n\tinitialOwner = std.Address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n\tloader = \"gno.land/r/gov/dao/init\"\n)\n\nvar (\n\tb *Bridge\n\tOwnable = ownable.NewWithAddress(initialOwner)\n)\n\n// Bridge is the active GovDAO\n// implementation bridge\ntype Bridge struct {\n\tdao DAO\n}\n\n// init constructs the initial GovDAO implementation\nfunc init() {\n\tb = \u0026Bridge{\n\t\tdao: nil, // initially set via r/gov/dao/init\n\t}\n}\n\n// LoadGovDAO loads the initial version of GovDAO into the bridge\n// All changes to b.dao need to be done via GovDAO proposals after\nfunc LoadGovDAO(d DAO) {\n\tif std.PreviousRealm().PkgPath() != loader {\n\t\tpanic(\"unauthorized\")\n\t}\n\n\tb.dao = d\n}\n\n// NewGovDAOImplChangeExecutor allows creating a GovDAO proposal\n// Which will upgrade the GovDAO version inside the bridge\nfunc NewGovDAOImplChangeExecutor(newImpl DAO) dao.Executor {\n\tcallback := func() error {\n\t\tb.dao = newImpl\n\t\treturn nil\n\t}\n\n\treturn b.dao.NewGovDAOExecutor(callback)\n}\n\n// SetGovDAO allows the admin to set the GovDAO version manually\n// This functionality can be fully disabled by Ownable.DropOwnership(),\n// making this realm fully managed by GovDAO.\nfunc SetGovDAO(d DAO) {\n\tOwnable.AssertCallerIsOwner()\n\tb.dao = d\n}\n\n// GovDAO returns the current GovDAO implementation\nfunc GovDAO() DAO {\n\treturn b.dao\n}\n" + }, + { + "name": "bridge_test.gno", + "body": "package bridge\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestBridge_DAO(t *testing.T) {\n\tvar (\n\t\tproposalID = uint64(10)\n\t\tmockDAO = \u0026mockDAO{\n\t\t\tproposeFn: func(_ dao.ProposalRequest) uint64 {\n\t\t\t\treturn proposalID\n\t\t\t},\n\t\t}\n\t)\n\n\tb.dao = mockDAO\n\n\tuassert.Equal(t, proposalID, GovDAO().Propose(dao.ProposalRequest{}))\n}\n\nfunc TestBridge_LoadGovDAO(t *testing.T) {\n\tt.Run(\"invalid initializer path\", func(t *testing.T) {\n\t\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/demo/init\")) // invalid loader\n\n\t\t// Attempt to set a new DAO implementation\n\t\tuassert.PanicsWithMessage(t, \"unauthorized\", func() {\n\t\t\tLoadGovDAO(\u0026mockDAO{})\n\t\t})\n\t})\n\n\tt.Run(\"valid loader\", func(t *testing.T) {\n\t\tvar (\n\t\t\tinitializer = \"gno.land/r/gov/dao/init\"\n\t\t\tproposalID = uint64(10)\n\t\t\tmockDAO = \u0026mockDAO{\n\t\t\t\tproposeFn: func(_ dao.ProposalRequest) uint64 {\n\t\t\t\t\treturn proposalID\n\t\t\t\t},\n\t\t\t}\n\t\t)\n\n\t\ttesting.SetRealm(std.NewCodeRealm(initializer))\n\n\t\t// Attempt to set a new DAO implementation\n\t\tuassert.NotPanics(t, func() {\n\t\t\tLoadGovDAO(mockDAO)\n\t\t})\n\n\t\tuassert.Equal(\n\t\t\tt,\n\t\t\tmockDAO.Propose(dao.ProposalRequest{}),\n\t\t\tGovDAO().Propose(dao.ProposalRequest{}),\n\t\t)\n\t})\n}\n\nfunc TestBridge_SetDAO(t *testing.T) {\n\tt.Run(\"invalid owner\", func(t *testing.T) {\n\t\t// Attempt to set a new DAO implementation\n\t\tuassert.PanicsWithMessage(t, ownable.ErrUnauthorized.Error(), func() {\n\t\t\tSetGovDAO(\u0026mockDAO{})\n\t\t})\n\t})\n\n\tt.Run(\"valid owner\", func(t *testing.T) {\n\t\tvar (\n\t\t\taddr = testutils.TestAddress(\"owner\")\n\n\t\t\tproposalID = uint64(10)\n\t\t\tmockDAO = \u0026mockDAO{\n\t\t\t\tproposeFn: func(_ dao.ProposalRequest) uint64 {\n\t\t\t\t\treturn proposalID\n\t\t\t\t},\n\t\t\t}\n\t\t)\n\n\t\ttesting.SetOriginCaller(addr)\n\n\t\tOwnable = ownable.NewWithAddress(addr)\n\n\t\turequire.NotPanics(t, func() {\n\t\t\tSetGovDAO(mockDAO)\n\t\t})\n\n\t\tuassert.Equal(\n\t\t\tt,\n\t\t\tmockDAO.Propose(dao.ProposalRequest{}),\n\t\t\tGovDAO().Propose(dao.ProposalRequest{}),\n\t\t)\n\t})\n}\n" + }, + { + "name": "doc.gno", + "body": "// Package bridge represents a GovDAO implementation wrapper, used by other Realms and Packages to\n// always fetch the most active GovDAO implementation, instead of directly referencing it, and having to\n// update it each time the GovDAO implementation changes\npackage bridge\n" + }, + { + "name": "mock_test.gno", + "body": "package bridge\n\nimport (\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n)\n\ntype (\n\tproposeDelegate func(dao.ProposalRequest) uint64\n\tvoteOnProposalDelegate func(uint64, dao.VoteOption)\n\texecuteProposalDelegate func(uint64)\n\tgetPropStoreDelegate func() dao.PropStore\n\tgetMembStoreDelegate func() membstore.MemberStore\n\tnewGovDAOExecutorDelegate func(func() error) dao.Executor\n)\n\ntype mockDAO struct {\n\tproposeFn proposeDelegate\n\tvoteOnProposalFn voteOnProposalDelegate\n\texecuteProposalFn executeProposalDelegate\n\tgetPropStoreFn getPropStoreDelegate\n\tgetMembStoreFn getMembStoreDelegate\n\tnewGovDAOExecutorFn newGovDAOExecutorDelegate\n}\n\nfunc (m *mockDAO) Propose(request dao.ProposalRequest) uint64 {\n\tif m.proposeFn != nil {\n\t\treturn m.proposeFn(request)\n\t}\n\n\treturn 0\n}\n\nfunc (m *mockDAO) VoteOnProposal(id uint64, option dao.VoteOption) {\n\tif m.voteOnProposalFn != nil {\n\t\tm.voteOnProposalFn(id, option)\n\t}\n}\n\nfunc (m *mockDAO) ExecuteProposal(id uint64) {\n\tif m.executeProposalFn != nil {\n\t\tm.executeProposalFn(id)\n\t}\n}\n\nfunc (m *mockDAO) GetPropStore() dao.PropStore {\n\tif m.getPropStoreFn != nil {\n\t\treturn m.getPropStoreFn()\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockDAO) GetMembStore() membstore.MemberStore {\n\tif m.getMembStoreFn != nil {\n\t\treturn m.getMembStoreFn()\n\t}\n\n\treturn nil\n}\n\nfunc (m *mockDAO) NewGovDAOExecutor(cb func() error) dao.Executor {\n\tif m.newGovDAOExecutorFn != nil {\n\t\treturn m.newGovDAOExecutorFn(cb)\n\t}\n\n\treturn nil\n}\n" + }, + { + "name": "types.gno", + "body": "package bridge\n\nimport (\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n)\n\n// DAO abstracts the commonly used DAO interface\ntype DAO interface {\n\tPropose(dao.ProposalRequest) uint64\n\tVoteOnProposal(uint64, dao.VoteOption)\n\tExecuteProposal(uint64)\n\tGetPropStore() dao.PropStore\n\tGetMembStore() membstore.MemberStore\n\n\tNewGovDAOExecutor(func() error) dao.Executor\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "+POKLRmSvJbommnBVcr0pOHbGdMkYA43N4GNRsesmvOMlp9K8IDJ/7mNizB/euYl6ZCOwp5u2tUY3QoJTpcBAQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "users", + "path": "gno.land/r/sys/users", + "files": [ + { + "name": "admin.gno", + "body": "package users\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/moul/addrset\"\n\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\nconst gusersv1 = \"gno.land/r/gnoland/users/v1\" // preregistered with store write perms\n\nvar controllers = addrset.Set{} // caller whitelist\n\nfunc init() {\n\tcontrollers.Add(std.DerivePkgAddr(gusersv1)) // initially whitelisted\n}\n\n// ProposeNewController allows GovDAO to add a whitelisted caller\nfunc ProposeNewController(addr std.Address) dao.Executor {\n\tcb := func() error {\n\t\treturn addToWhitelist(addr)\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(cb)\n}\n\n// ProposeControllerRemoval allows GovDAO to add a whitelisted caller\nfunc ProposeControllerRemoval(addr std.Address) dao.Executor {\n\tcb := func() error {\n\t\treturn deleteFromwhitelist(addr)\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(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 std.Address) dao.Executor {\n\tcb := func() error {\n\t\terr := addToWhitelist(toAdd)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\terr = deleteFromwhitelist(toRemove)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(cb)\n}\n\n// Helpers\n\nfunc deleteFromwhitelist(addr std.Address) error {\n\tif !controllers.Has(addr) {\n\t\treturn ErrNotWhitelisted\n\t}\n\n\tif ok := controllers.Remove(addr); !ok {\n\t\tpanic(\"failed to remove address from whitelist\")\n\t}\n\n\treturn nil\n}\n\nfunc addToWhitelist(newCaller std.Address) error {\n\tif !newCaller.IsValid() {\n\t\treturn ErrInvalidAddress\n\t}\n\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 \"errors\"\n\nconst prefix = \"r/sys/users: \"\n\nvar (\n\tErrNotWhitelisted = errors.New(prefix + \"does not exist in whitelist\")\n\tErrAlreadyWhitelisted = errors.New(prefix + \"already whitelisted\")\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" + }, + { + "name": "render.gno", + "body": "package users\n\nimport \"gno.land/p/demo/ufmt\"\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 += \"Users should use [`gno.land/r/gnoland/users`](/r/gnoland/users) implementations to register their usernames.\\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\"regexp\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\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 std.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() std.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\t// TODO switch to /u/username once the gnoweb page is ready.\n\tif linkText == \"\" {\n\t\treturn ufmt.Sprintf(\"[@%s](/r/gnoland/users/v1:%s)\", u.username, u.username)\n\t}\n\n\treturn ufmt.Sprintf(\"[%s](/r/gnoland/users/v1:%s)\", linkText, u.username)\n}\n\n// RegisterUser adds a new user to the system.\nfunc RegisterUser(name string, address std.Address) error {\n\t// Validate caller\n\tif !controllers.Has(std.PreviousRealm().Address()) {\n\t\treturn ErrNotWhitelisted\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.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.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,\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.String(), data)\n\n\tstd.Emit(RegisterUserEvent,\n\t\t\"name\", name,\n\t\t\"address\", address.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(std.PreviousRealm().Address()) {\n\t\treturn ErrNotWhitelisted\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\tstd.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(std.PreviousRealm().Address()) {\n\t\treturn ErrNotWhitelisted\n\t}\n\n\tu.deleted = true\n\n\tstd.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 std.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\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\talice = \"alice\"\n\taliceAddr = testutils.TestAddress(alice)\n\tbob = \"bob\"\n\tbobAddr = testutils.TestAddress(bob)\n\n\twhitelistedCallerAddr = std.DerivePkgAddr(gusersv1)\n)\n\nfunc TestRegister(t *testing.T) {\n\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\n\tt.Run(\"valid_registration\", func(t *testing.T) {\n\t\turequire.NoError(t, RegisterUser(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(\"\", aliceAddr), ErrEmptyUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(alice, \"\"), ErrInvalidAddress.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(alice, \"invalidaddress\"), ErrInvalidAddress.Error())\n\n\t\tuassert.ErrorContains(t, RegisterUser(\"username with a space\", aliceAddr), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t,\n\t\t\tRegisterUser(\"verylongusernameverylongusernameverylongusernameverylongusername1\", aliceAddr),\n\t\t\tErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, RegisterUser(\"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(alice, aliceAddr))\n\n\t\t// Try registering again\n\t\tuassert.ErrorContains(t, RegisterUser(\"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(alice, aliceAddr))\n\n\t\t// Try registering alice's name with bob's address\n\t\tuassert.ErrorContains(t, RegisterUser(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(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(\"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(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\", aliceAddr), ErrNameLikeAddress.Error())\n\t\t// Beginning of address as username\n\t\tuassert.ErrorContains(t, RegisterUser(\"g1jg8mtutu9khhfwc4nxmu\", aliceAddr), ErrNameLikeAddress.Error())\n\t\tuassert.NoError(t, RegisterUser(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress\", aliceAddr))\n\t})\n}\n\nfunc TestUpdateName(t *testing.T) {\n\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\tt.Run(\"valid_direct_alias\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\tuassert.NoError(t, data.UpdateName(\"alice1\"))\n\t})\n\n\tt.Run(\"valid_double_alias\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\tuassert.NoError(t, data.UpdateName(\"alice2\"))\n\t\tuassert.NoError(t, data.UpdateName(\"alice3\"))\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(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(alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\t\turequire.NoError(t, data.Delete())\n\n\t\tdata = ResolveAddress(aliceAddr)\n\t\tuassert.ErrorContains(t, data.UpdateName(\"newalice\"), ErrUserNotExistOrDeleted.Error())\n\t})\n\n\tt.Run(\"invalid_inputs\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\n\t\tuassert.ErrorContains(t, data.UpdateName(\"\"), ErrEmptyUsername.Error())\n\t\tuassert.ErrorContains(t, data.UpdateName(\"username with a space\"), ErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t,\n\t\t\tdata.UpdateName(\"verylongusernameverylongusernameverylongusernameverylongusername1\"),\n\t\t\tErrInvalidUsername.Error())\n\t\tuassert.ErrorContains(t, data.UpdateName(\"namewith^\u0026()\"), ErrInvalidUsername.Error())\n\t})\n\n\tt.Run(\"address_lookalike\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(alice, aliceAddr))\n\t\tdata := ResolveAddress(aliceAddr)\n\n\t\t// Address as username\n\t\tuassert.ErrorContains(t, data.UpdateName(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"), ErrNameLikeAddress.Error())\n\t\t// Beginning of address as username\n\t\tuassert.ErrorContains(t, data.UpdateName(\"g1jg8mtutu9khhfwc4nxmu\"), ErrNameLikeAddress.Error())\n\t\tuassert.NoError(t, data.UpdateName(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5longerthananaddress\"))\n\t})\n}\n\nfunc TestDelete(t *testing.T) {\n\ttesting.SetOriginCaller(whitelistedCallerAddr)\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(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(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 (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl/rotree\"\n)\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 std.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// 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/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestResolveName(t *testing.T) {\n\ttesting.SetOriginCaller(whitelistedCallerAddr)\n\n\tt.Run(\"single_name\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(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(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(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.SetOriginCaller(whitelistedCallerAddr)\n\n\tt.Run(\"single_name\", func(t *testing.T) {\n\t\tcleanStore(t)\n\n\t\turequire.NoError(t, RegisterUser(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(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(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.SetOriginCaller(whitelistedCallerAddr)\n\tcleanStore(t)\n\n\turequire.NoError(t, RegisterUser(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 (nilin 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\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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "gJGiMynmk7lCSDTb+P0hPhq6GWn4JkAnU/ArkM/gdksRJk/q5p/BqZP5MoLy35IzkJEtbIUwvGu5DJv9nls7BQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "boards", + "path": "gno.land/r/demo/boards", + "files": [ + { + "name": "board.gno", + "body": "package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/moul/txlink\"\n)\n\n//----------------------------------------\n// Board\n\ntype BoardID uint64\n\nfunc (bid BoardID) String() string {\n\treturn strconv.Itoa(int(bid))\n}\n\ntype Board struct {\n\tid BoardID // only set for public boards.\n\turl string\n\tname string\n\tcreator std.Address\n\tthreads avl.Tree // Post.id -\u003e *Post\n\tpostsCtr uint64 // increments Post.id\n\tcreatedAt time.Time\n\tdeleted avl.Tree // TODO reserved for fast-delete.\n}\n\nfunc newBoard(id BoardID, url string, name string, creator std.Address) *Board {\n\tif !reName.MatchString(name) {\n\t\tpanic(\"invalid name: \" + name)\n\t}\n\texists := gBoardsByName.Has(name)\n\tif exists {\n\t\tpanic(\"board already exists\")\n\t}\n\treturn \u0026Board{\n\t\tid: id,\n\t\turl: url,\n\t\tname: name,\n\t\tcreator: creator,\n\t\tthreads: avl.Tree{},\n\t\tcreatedAt: time.Now(),\n\t\tdeleted: avl.Tree{},\n\t}\n}\n\n/* TODO support this once we figure out how to ensure URL correctness.\n// A private board is not tracked by gBoards*,\n// but must be persisted by the caller's realm.\n// Private boards have 0 id and does not ping\n// back the remote board on reposts.\nfunc NewPrivateBoard(url string, name string, creator std.Address) *Board {\n\treturn newBoard(0, url, name, creator)\n}\n*/\n\nfunc (board *Board) IsPrivate() bool {\n\treturn board.id == 0\n}\n\nfunc (board *Board) GetThread(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\tpostI, exists := board.threads.Get(pidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\treturn postI.(*Post)\n}\n\nfunc (board *Board) AddThread(creator std.Address, title string, body string) *Post {\n\tpid := board.incGetPostID()\n\tpidkey := postIDKey(pid)\n\tthread := newPost(board, pid, creator, title, body, pid, 0, 0)\n\tboard.threads.Set(pidkey, thread)\n\treturn thread\n}\n\n// NOTE: this can be potentially very expensive for threads with many replies.\n// TODO: implement optional fast-delete where thread is simply moved.\nfunc (board *Board) DeleteThread(pid PostID) {\n\tpidkey := postIDKey(pid)\n\t_, removed := board.threads.Remove(pidkey)\n\tif !removed {\n\t\tpanic(\"thread does not exist with id \" + pid.String())\n\t}\n}\n\nfunc (board *Board) HasPermission(addr std.Address, perm Permission) bool {\n\tif board.creator == addr {\n\t\tswitch perm {\n\t\tcase EditPermission:\n\t\t\treturn true\n\t\tcase DeletePermission:\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn false\n}\n\n// Renders the board for display suitable as plaintext in\n// console. This is suitable for demonstration or tests,\n// but not for prod.\nfunc (board *Board) RenderBoard() string {\n\tstr := \"\"\n\tstr += \"\\\\[[post](\" + board.GetPostFormURL() + \")]\\n\\n\"\n\tif board.threads.Size() \u003e 0 {\n\t\tboard.threads.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tif str != \"\" {\n\t\t\t\tstr += \"----------------------------------------\\n\"\n\t\t\t}\n\t\t\tstr += value.(*Post).RenderSummary() + \"\\n\"\n\t\t\treturn false\n\t\t})\n\t}\n\treturn str\n}\n\nfunc (board *Board) incGetPostID() PostID {\n\tboard.postsCtr++\n\treturn PostID(board.postsCtr)\n}\n\nfunc (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string {\n\tif replyID == 0 {\n\t\treturn board.url + \"/\" + threadID.String()\n\t} else {\n\t\treturn board.url + \"/\" + threadID.String() + \"/\" + replyID.String()\n\t}\n}\n\nfunc (board *Board) GetPostFormURL() string {\n\treturn txlink.Call(\"CreateThread\", \"bid\", board.id.String())\n}\n" + }, + { + "name": "boards.gno", + "body": "package boards\n\nimport (\n\t\"regexp\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n//----------------------------------------\n// Realm (package) state\n\nvar (\n\tgBoards avl.Tree // id -\u003e *Board\n\tgBoardsCtr int // increments Board.id\n\tgBoardsByName avl.Tree // name -\u003e *Board\n\tgDefaultAnonFee = 100000000 // minimum fee required if anonymous\n)\n\n//----------------------------------------\n// Constants\n\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)\n" + }, + { + "name": "misc.gno", + "body": "package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n//----------------------------------------\n// private utility methods\n// XXX ensure these cannot be called from public.\n\nfunc getBoard(bid BoardID) *Board {\n\tbidkey := boardIDKey(bid)\n\tboard_, exists := gBoards.Get(bidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\tboard := board_.(*Board)\n\treturn board\n}\n\nfunc incGetBoardID() BoardID {\n\tgBoardsCtr++\n\treturn BoardID(gBoardsCtr)\n}\n\nfunc padLeft(str string, length int) string {\n\tif len(str) \u003e= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\" \", length-len(str)) + str\n\t}\n}\n\nfunc padZero(u64 uint64, length int) string {\n\tstr := strconv.Itoa(int(u64))\n\tif len(str) \u003e= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\"0\", length-len(str)) + str\n\t}\n}\n\nfunc boardIDKey(bid BoardID) string {\n\treturn padZero(uint64(bid), 10)\n}\n\nfunc postIDKey(pid PostID) string {\n\treturn padZero(uint64(pid), 10)\n}\n\nfunc indentBody(indent string, body string) string {\n\tlines := strings.Split(body, \"\\n\")\n\tres := \"\"\n\tfor i, line := range lines {\n\t\tif i \u003e 0 {\n\t\t\tres += \"\\n\"\n\t\t}\n\t\tres += indent + line\n\t}\n\treturn res\n}\n\n// NOTE: length must be greater than 3.\nfunc summaryOf(str string, length int) string {\n\tlines := strings.SplitN(str, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\t// len(line) \u003c= 80\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n\nfunc displayAddressMD(addr std.Address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user == nil {\n\t\treturn \"[\" + addr.String() + \"](/u/\" + addr.String() + \")\"\n\t} else {\n\t\treturn \"[@\" + user.Name() + \"](/u/\" + user.Name() + \")\"\n\t}\n}\n\nfunc usernameOf(addr std.Address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user == nil {\n\t\treturn \"\"\n\t}\n\treturn user.Name()\n}\n" + }, + { + "name": "post.gno", + "body": "package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/moul/txlink\"\n)\n\n//----------------------------------------\n// Post\n\n// NOTE: a PostID is relative to the board.\ntype PostID uint64\n\nfunc (pid PostID) String() string {\n\treturn strconv.Itoa(int(pid))\n}\n\n// A Post is a \"thread\" or a \"reply\" depending on context.\n// A thread is a Post of a Board that holds other replies.\ntype Post struct {\n\tboard *Board\n\tid PostID\n\tcreator std.Address\n\ttitle string // optional\n\tbody string\n\treplies avl.Tree // Post.id -\u003e *Post\n\trepliesAll avl.Tree // Post.id -\u003e *Post (all replies, for top-level posts)\n\treposts avl.Tree // Board.id -\u003e Post.id\n\tthreadID PostID // original Post.id\n\tparentID PostID // parent Post.id (if reply or repost)\n\trepostBoard BoardID // original Board.id (if repost)\n\tcreatedAt time.Time\n\tupdatedAt time.Time\n}\n\nfunc newPost(board *Board, id PostID, creator std.Address, title, body string, threadID, parentID PostID, repostBoard BoardID) *Post {\n\treturn \u0026Post{\n\t\tboard: board,\n\t\tid: id,\n\t\tcreator: creator,\n\t\ttitle: title,\n\t\tbody: body,\n\t\treplies: avl.Tree{},\n\t\trepliesAll: avl.Tree{},\n\t\treposts: avl.Tree{},\n\t\tthreadID: threadID,\n\t\tparentID: parentID,\n\t\trepostBoard: repostBoard,\n\t\tcreatedAt: time.Now(),\n\t}\n}\n\nfunc (post *Post) IsThread() bool {\n\treturn post.parentID == 0\n}\n\nfunc (post *Post) GetPostID() PostID {\n\treturn post.id\n}\n\nfunc (post *Post) AddReply(creator std.Address, body string) *Post {\n\tboard := post.board\n\tpid := board.incGetPostID()\n\tpidkey := postIDKey(pid)\n\treply := newPost(board, pid, creator, \"\", body, post.threadID, post.id, 0)\n\tpost.replies.Set(pidkey, reply)\n\tif post.threadID == post.id {\n\t\tpost.repliesAll.Set(pidkey, reply)\n\t} else {\n\t\tthread := board.GetThread(post.threadID)\n\t\tthread.repliesAll.Set(pidkey, reply)\n\t}\n\treturn reply\n}\n\nfunc (post *Post) Update(title string, body string) {\n\tpost.title = title\n\tpost.body = body\n\tpost.updatedAt = time.Now()\n}\n\nfunc (thread *Post) GetReply(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\treplyI, ok := thread.repliesAll.Get(pidkey)\n\tif !ok {\n\t\treturn nil\n\t} else {\n\t\treturn replyI.(*Post)\n\t}\n}\n\nfunc (post *Post) AddRepostTo(creator std.Address, title, body string, dst *Board) *Post {\n\tif !post.IsThread() {\n\t\tpanic(\"cannot repost non-thread post\")\n\t}\n\tpid := dst.incGetPostID()\n\tpidkey := postIDKey(pid)\n\trepost := newPost(dst, pid, creator, title, body, pid, post.id, post.board.id)\n\tdst.threads.Set(pidkey, repost)\n\tif !dst.IsPrivate() {\n\t\tbidkey := boardIDKey(dst.id)\n\t\tpost.reposts.Set(bidkey, pid)\n\t}\n\treturn repost\n}\n\nfunc (thread *Post) DeletePost(pid PostID) {\n\tif thread.id == pid {\n\t\tpanic(\"should not happen\")\n\t}\n\tpidkey := postIDKey(pid)\n\tpostI, removed := thread.repliesAll.Remove(pidkey)\n\tif !removed {\n\t\tpanic(\"post not found in thread\")\n\t}\n\tpost := postI.(*Post)\n\tif post.parentID != thread.id {\n\t\tparent := thread.GetReply(post.parentID)\n\t\tparent.replies.Remove(pidkey)\n\t} else {\n\t\tthread.replies.Remove(pidkey)\n\t}\n}\n\nfunc (post *Post) HasPermission(addr std.Address, perm Permission) bool {\n\tif post.creator == addr {\n\t\tswitch perm {\n\t\tcase EditPermission:\n\t\t\treturn true\n\t\tcase DeletePermission:\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\t// post notes inherit permissions of the board.\n\treturn post.board.HasPermission(addr, perm)\n}\n\nfunc (post *Post) GetSummary() string {\n\treturn summaryOf(post.body, 80)\n}\n\nfunc (post *Post) GetURL() string {\n\tif post.IsThread() {\n\t\treturn post.board.GetURLFromThreadAndReplyID(\n\t\t\tpost.id, 0)\n\t} else {\n\t\treturn post.board.GetURLFromThreadAndReplyID(\n\t\t\tpost.threadID, post.id)\n\t}\n}\n\nfunc (post *Post) GetReplyFormURL() string {\n\treturn txlink.Call(\"CreateReply\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) GetRepostFormURL() string {\n\treturn txlink.Call(\"CreateRepost\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) GetDeleteFormURL() string {\n\treturn txlink.Call(\"DeletePost\",\n\t\t\"bid\", post.board.id.String(),\n\t\t\"threadid\", post.threadID.String(),\n\t\t\"postid\", post.id.String(),\n\t)\n}\n\nfunc (post *Post) RenderSummary() string {\n\tif post.repostBoard != 0 {\n\t\tdstBoard := getBoard(post.repostBoard)\n\t\tif dstBoard == nil {\n\t\t\tpanic(\"repostBoard does not exist\")\n\t\t}\n\t\tthread := dstBoard.GetThread(PostID(post.parentID))\n\t\tif thread == nil {\n\t\t\treturn \"reposted post does not exist\"\n\t\t}\n\t\treturn \"Repost: \" + post.GetSummary() + \"\\n\" + thread.RenderSummary()\n\t}\n\tstr := \"\"\n\tif post.title != \"\" {\n\t\tstr += \"## [\" + summaryOf(post.title, 80) + \"](\" + post.GetURL() + \")\\n\"\n\t\tstr += \"\\n\"\n\t}\n\tstr += post.GetSummary() + \"\\n\"\n\tstr += \"\\\\- \" + displayAddressMD(post.creator) + \",\"\n\tstr += \" [\" + post.createdAt.Format(\"2006-01-02 3:04pm MST\") + \"](\" + post.GetURL() + \")\"\n\tstr += \" \\\\[[x](\" + post.GetDeleteFormURL() + \")]\"\n\tstr += \" (\" + strconv.Itoa(post.replies.Size()) + \" replies)\"\n\tstr += \" (\" + strconv.Itoa(post.reposts.Size()) + \" reposts)\" + \"\\n\"\n\treturn str\n}\n\nfunc (post *Post) RenderPost(indent string, levels int) string {\n\tif post == nil {\n\t\treturn \"nil post\"\n\t}\n\tstr := \"\"\n\tif post.title != \"\" {\n\t\tstr += indent + \"# \" + post.title + \"\\n\"\n\t\tstr += indent + \"\\n\"\n\t}\n\tstr += indentBody(indent, post.body) + \"\\n\" // TODO: indent body lines.\n\tstr += indent + \"\\\\- \" + displayAddressMD(post.creator) + \", \"\n\tstr += \"[\" + post.createdAt.Format(\"2006-01-02 3:04pm (MST)\") + \"](\" + post.GetURL() + \")\"\n\tstr += \" \\\\[[reply](\" + post.GetReplyFormURL() + \")]\"\n\tif post.IsThread() {\n\t\tstr += \" \\\\[[repost](\" + post.GetRepostFormURL() + \")]\"\n\t}\n\tstr += \" \\\\[[x](\" + post.GetDeleteFormURL() + \")]\\n\"\n\tif levels \u003e 0 {\n\t\tif post.replies.Size() \u003e 0 {\n\t\t\tpost.replies.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\t\tstr += indent + \"\\n\"\n\t\t\t\tstr += value.(*Post).RenderPost(indent+\"\u003e \", levels-1)\n\t\t\t\treturn false\n\t\t\t})\n\t\t}\n\t} else {\n\t\tif post.replies.Size() \u003e 0 {\n\t\t\tstr += indent + \"\\n\"\n\t\t\tstr += indent + \"_[see all \" + strconv.Itoa(post.replies.Size()) + \" replies](\" + post.GetURL() + \")_\\n\"\n\t\t}\n\t}\n\treturn str\n}\n\n// render reply and link to context thread\nfunc (post *Post) RenderInner() string {\n\tif post.IsThread() {\n\t\tpanic(\"unexpected thread\")\n\t}\n\tthreadID := post.threadID\n\t// replyID := post.id\n\tparentID := post.parentID\n\tstr := \"\"\n\tstr += \"_[see thread](\" + post.board.GetURLFromThreadAndReplyID(\n\t\tthreadID, 0) + \")_\\n\\n\"\n\tthread := post.board.GetThread(post.threadID)\n\tvar parent *Post\n\tif thread.id == parentID {\n\t\tparent = thread\n\t} else {\n\t\tparent = thread.GetReply(parentID)\n\t}\n\tstr += parent.RenderPost(\"\", 0)\n\tstr += \"\\n\"\n\tstr += post.RenderPost(\"\u003e \", 5)\n\treturn str\n}\n" + }, + { + "name": "public.gno", + "body": "package boards\n\nimport (\n\t\"std\"\n\t\"strconv\"\n)\n\n//----------------------------------------\n// Public facing functions\n\nfunc GetBoardIDFromName(name string) (BoardID, bool) {\n\tboardI, exists := gBoardsByName.Get(name)\n\tif !exists {\n\t\treturn 0, false\n\t}\n\treturn boardI.(*Board).id, true\n}\n\nfunc CreateBoard(name string) BoardID {\n\tif !std.PreviousRealm().IsUser() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\n\tbid := incGetBoardID()\n\tcaller := std.OriginCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tpanic(\"unauthorized\")\n\t}\n\turl := \"/r/demo/boards:\" + name\n\tboard := newBoard(bid, url, name, caller)\n\tbidkey := boardIDKey(bid)\n\tgBoards.Set(bidkey, board)\n\tgBoardsByName.Set(name, board)\n\treturn board.id\n}\n\nfunc checkAnonFee() bool {\n\tsent := std.OriginSend()\n\tanonFeeCoin := std.NewCoin(\"ugnot\", int64(gDefaultAnonFee))\n\tif len(sent) == 1 \u0026\u0026 sent[0].IsGTE(anonFeeCoin) {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc CreateThread(bid BoardID, title string, body string) PostID {\n\tif !std.PreviousRealm().IsUser() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.OriginCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.AddThread(caller, title, body)\n\treturn thread.id\n}\n\nfunc CreateReply(bid BoardID, threadid, postid PostID, body string) PostID {\n\tif !std.PreviousRealm().IsUser() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.OriginCaller()\n\tif usernameOf(caller) == \"\" {\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\treply := thread.AddReply(caller, body)\n\t\treturn reply.id\n\t} else {\n\t\tpost := thread.GetReply(postid)\n\t\treply := post.AddReply(caller, body)\n\t\treturn reply.id\n\t}\n}\n\n// If dstBoard is private, does not ping back.\n// If board specified by bid is private, panics.\nfunc CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID {\n\tif !std.PreviousRealm().IsUser() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.OriginCaller()\n\tif usernameOf(caller) == \"\" {\n\t\t// TODO: allow with gDefaultAnonFee payment.\n\t\tif !checkAnonFee() {\n\t\t\tpanic(\"please register, otherwise minimum fee \" + strconv.Itoa(gDefaultAnonFee) + \" is required if anonymous\")\n\t\t}\n\t}\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"src board not exist\")\n\t}\n\tif board.IsPrivate() {\n\t\tpanic(\"cannot repost from a private board\")\n\t}\n\tdst := getBoard(dstBoardID)\n\tif dst == nil {\n\t\tpanic(\"dst board not exist\")\n\t}\n\tthread := board.GetThread(postid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\trepost := thread.AddRepostTo(caller, title, body, dst)\n\treturn repost.id\n}\n\nfunc DeletePost(bid BoardID, threadid, postid PostID, reason string) {\n\tif !std.PreviousRealm().IsUser() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.OriginCaller()\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\t// delete thread\n\t\tif !thread.HasPermission(caller, DeletePermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tboard.DeleteThread(threadid)\n\t} else {\n\t\t// delete thread's post\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"post not exist\")\n\t\t}\n\t\tif !post.HasPermission(caller, DeletePermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tthread.DeletePost(postid)\n\t}\n}\n\nfunc EditPost(bid BoardID, threadid, postid PostID, title, body string) {\n\tif !std.PreviousRealm().IsUser() {\n\t\tpanic(\"invalid non-user call\")\n\t}\n\tcaller := std.OriginCaller()\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\tpanic(\"board not exist\")\n\t}\n\tthread := board.GetThread(threadid)\n\tif thread == nil {\n\t\tpanic(\"thread not exist\")\n\t}\n\tif postid == threadid {\n\t\t// edit thread\n\t\tif !thread.HasPermission(caller, EditPermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tthread.Update(title, body)\n\t} else {\n\t\t// edit thread's post\n\t\tpost := thread.GetReply(postid)\n\t\tif post == nil {\n\t\t\tpanic(\"post not exist\")\n\t\t}\n\t\tif !post.HasPermission(caller, EditPermission) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t\tpost.Update(title, body)\n\t}\n}\n" + }, + { + "name": "render.gno", + "body": "package boards\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n//----------------------------------------\n// Render functions\n\nfunc RenderBoard(bid BoardID) string {\n\tboard := getBoard(bid)\n\tif board == nil {\n\t\treturn \"missing board\"\n\t}\n\treturn board.RenderBoard()\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tstr := \"These are all the boards of this realm:\\n\\n\"\n\t\tgBoards.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tboard := value.(*Board)\n\t\t\tstr += \" * [\" + board.url + \"](\" + board.url + \")\\n\"\n\t\t\treturn false\n\t\t})\n\t\treturn str\n\t}\n\tparts := strings.Split(path, \"/\")\n\tif len(parts) == 1 {\n\t\t// /r/demo/boards:BOARD_NAME\n\t\tname := parts[0]\n\t\tboardI, exists := gBoardsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\treturn boardI.(*Board).RenderBoard()\n\t} else if len(parts) == 2 {\n\t\t// /r/demo/boards:BOARD_NAME/THREAD_ID\n\t\tname := parts[0]\n\t\tboardI, exists := gBoardsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\tpid, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn \"invalid thread id: \" + parts[1]\n\t\t}\n\t\tboard := boardI.(*Board)\n\t\tthread := board.GetThread(PostID(pid))\n\t\tif thread == nil {\n\t\t\treturn \"thread does not exist with id: \" + parts[1]\n\t\t}\n\t\treturn thread.RenderPost(\"\", 5)\n\t} else if len(parts) == 3 {\n\t\t// /r/demo/boards:BOARD_NAME/THREAD_ID/REPLY_ID\n\t\tname := parts[0]\n\t\tboardI, exists := gBoardsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"board does not exist: \" + name\n\t\t}\n\t\tpid, err := strconv.Atoi(parts[1])\n\t\tif err != nil {\n\t\t\treturn \"invalid thread id: \" + parts[1]\n\t\t}\n\t\tboard := boardI.(*Board)\n\t\tthread := board.GetThread(PostID(pid))\n\t\tif thread == nil {\n\t\t\treturn \"thread does not exist with id: \" + parts[1]\n\t\t}\n\t\trid, err := strconv.Atoi(parts[2])\n\t\tif err != nil {\n\t\t\treturn \"invalid reply id: \" + parts[2]\n\t\t}\n\t\treply := thread.GetReply(PostID(rid))\n\t\tif reply == nil {\n\t\t\treturn \"reply does not exist with id: \" + parts[2]\n\t\t}\n\t\treturn reply.RenderInner()\n\t} else {\n\t\treturn \"unrecognized path \" + path\n\t}\n}\n" + }, + { + "name": "role.gno", + "body": "package boards\n\ntype Permission string\n\nconst (\n\tDeletePermission Permission = \"role:delete\"\n\tEditPermission Permission = \"role:edit\"\n)\n" + }, + { + "name": "z_0_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\tcaller := testutils.TestAddress(\"caller\")\n\ttesting.SetRealm(std.NewUserRealm(caller))\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid := boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// unauthorized\n" + }, + { + "name": "z_0_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\tboards.CreateThread(1, \"First Post (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// board not exist\n" + }, + { + "name": "z_0_d_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateReply(bid, 0, 0, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// thread not exist\n" + }, + { + "name": "z_0_e_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\tboards.CreateReply(bid, 0, 0, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Error:\n// board not exist\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar bid boards.BoardID\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid := boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Output:\n// \\[[post](/r/demo/boards$help\u0026func=CreateThread\u0026bid=1)]\n//\n// ----------------------------------------\n// ## [First Post (title)](/r/demo/boards:test_board/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)] (0 replies) (0 reposts)\n//\n// ----------------------------------------\n// ## [Second Post (title)](/r/demo/boards:test_board/2)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)] (1 replies) (0 reposts)\n//\n//\n" + }, + { + "name": "z_10_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// boardId 2 not exist\n\tboards.DeletePost(2, pid, pid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// invalid non-user call\n" + }, + { + "name": "z_10_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// pid of 2 not exist\n\tboards.DeletePost(bid, 2, 2, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// invalid non-user call\n" + }, + { + "name": "z_10_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"First reply of the First post\\n\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.DeletePost(bid, pid, rid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// \u003e First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=1)]\n//\n// ----------------------------------------------------\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n" + }, + { + "name": "z_10_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.DeletePost(bid, pid, pid, \"\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// ----------------------------------------------------\n// thread does not exist with id: 1\n" + }, + { + "name": "z_11_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// board 2 not exist\n\tboards.EditPost(2, pid, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// invalid non-user call\n" + }, + { + "name": "z_11_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// thread 2 not exist\n\tboards.EditPost(bid, 2, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// invalid non-user call\n" + }, + { + "name": "z_11_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\t// post 2 not exist\n\tboards.EditPost(bid, pid, 2, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// invalid non-user call\n" + }, + { + "name": "z_11_d_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"First reply of the First post\\n\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.EditPost(bid, pid, rid, \"\", \"Edited: First reply of the First post\\n\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// \u003e First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=1)]\n//\n// ----------------------------------------------------\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// \u003e Edited: First reply of the First post\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=1)]\n//\n" + }, + { + "name": "z_11_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tpid = boards.CreateThread(bid, \"First Post in (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.EditPost(bid, pid, pid, \"Edited: First Post in (title)\", \"Edited: Body of the first post. (body)\")\n\tprintln(\"----------------------------------------------------\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// ----------------------------------------------------\n// # Edited: First Post in (title)\n//\n// Edited: Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n" + }, + { + "name": "z_12_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\t// create a post via registered user\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tpid := boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(\"test_board2\")\n\n\t// create a repost via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 9000000}})\n\n\trid := boards.CreateRepost(bid1, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n" + }, + { + "name": "z_12_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tpid := boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(\"test_board2\")\n\n\t// create a repost to a non-existing board\n\trid := boards.CreateRepost(5, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// src board not exist\n" + }, + { + "name": "z_12_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tboards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 := boards.CreateBoard(\"test_board2\")\n\n\t// create a repost to a non-existing thread\n\trid := boards.CreateRepost(bid1, 5, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// thread not exist\n" + }, + { + "name": "z_12_d_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\tbid1 := boards.CreateBoard(\"test_board1\")\n\tpid := boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tboards.CreateBoard(\"test_board2\")\n\n\t// create a repost to a non-existing destination board\n\trid := boards.CreateRepost(bid1, pid, \"\", \"Check this out\", 5)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board1\"))\n}\n\n// Error:\n// dst board not exist\n" + }, + { + "name": "z_12_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid1 boards.BoardID\n\tbid2 boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid1 = boards.CreateBoard(\"test_board1\")\n\tpid = boards.CreateThread(bid1, \"First Post (title)\", \"Body of the first post. (body)\")\n\tbid2 = boards.CreateBoard(\"test_board2\")\n}\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\trid := boards.CreateRepost(bid1, pid, \"\", \"Check this out\", bid2)\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board2\"))\n}\n\n// Output:\n// 1\n// \\[[post](/r/demo/boards$help\u0026func=CreateThread\u0026bid=2)]\n//\n// ----------------------------------------\n// Repost: Check this out\n// ## [First Post (title)](/r/demo/boards:test_board1/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)] (0 replies) (1 reposts)\n//\n//\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar board *boards.Board\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\t_ = boards.CreateBoard(\"test_board_1\")\n\t_ = boards.CreateBoard(\"test_board_2\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"\"))\n}\n\n// Output:\n// These are all the boards of this realm:\n//\n// * [/r/demo/boards:test_board_1](/r/demo/boards:test_board_1)\n// * [/r/demo/boards:test_board_2](/r/demo/boards:test_board_2)\n//\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n//\n" + }, + { + "name": "z_3_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n}\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\trid := boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n\tprintln(rid)\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// 3\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n//\n" + }, + { + "name": "z_4_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid := boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n\tprintln(rid)\n}\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\trid2 := boards.CreateReply(bid, pid, pid, \"Second reply of the second post\")\n\tprintln(rid2)\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// 3\n// 4\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n//\n// \u003e Second reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=4\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=4\u0026threadid=2)]\n//\n\n// Realm:\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/demo/boards\"]\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111]=\n// @@ -1,8 +1,8 @@\n// {\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111\",\n// - \"ModTime\": \"0\",\n// - \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:106\",\n// + \"ModTime\": \"123\",\n// + \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:125]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Post\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:125\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"3f34ac77289aa1d5f9a2f8b6d083138325816fb0\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:125\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"94a6665a44bac6ede7f3e3b87173e537b12f9532\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"bc8e5b4e782a0bbc4ac9689681f119beb7b34d59\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:124\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:106\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"9957eadbc91dd32f33b0d815e041a32dbdea0671\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:123\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:128]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:128\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:129]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:129\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:130]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:130\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:131]={\n// \"Fields\": [\n// {\n// \"N\": \"AAAAgJSeXbo=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"65536\"\n// }\n// },\n// {\n// \"N\": \"AbSNdvQQIhE=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"1024\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Location\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"336074805fc853987abe6f7fe3ad97a6a6f3077a:2\"\n// },\n// \"Index\": \"182\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:131\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:132]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"65536\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"1024\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Location\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:132\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Board\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"N\": \"BAAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.PostID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"Second reply of the second post\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"f91e355bd19240f0f3350a7fa0e6a82b72225916\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:128\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"9ee9c4117be283fc51ffcc5ecd65b75ecef5a9dd\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:129\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Tree\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"eb768b0140a5fe95f9c58747f0960d647dacfd42\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:130\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.PostID\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.PostID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.BoardID\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Time\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"0fd3352422af0a56a77ef2c9e88f479054e3d51f\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:131\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"time.Time\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"bed4afa8ffdbbf775451c947fc68b27a345ce32a\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:132\"\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\",\n// \"IsEscaped\": true,\n// \"ModTime\": \"0\",\n// \"RefCount\": \"2\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Post\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"c45bbd47a46681a63af973db0ec2180922e4a8ae\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:127\"\n// }\n// }\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:120]=\n// @@ -1,8 +1,8 @@\n// {\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:120\",\n// - \"ModTime\": \"0\",\n// - \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:107\",\n// + \"ModTime\": \"134\",\n// + \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:136]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/boards.Post\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Escaped\": true,\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:126\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:136\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"96b86b4585c7f1075d7794180a5581f72733a7ab\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:136\"\n// }\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"0000000004\"\n// }\n// },\n// {},\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AgAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"32274e1f28fb2b97d67a1262afd362d370de7faa\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:120\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"c2cfd6aec36a462f35bf02e5bf4a127aa1bb7ac2\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:135\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133]={\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:107\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"5cb875179e86d32c517322af7a323b2a5f3e6cc5\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:134\"\n// }\n// }\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:85]=\n// @@ -49,7 +49,7 @@\n// }\n// },\n// {\n// - \"N\": \"AwAAAAAAAAA=\",\n// + \"N\": \"BAAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"65536\"\n// @@ -80,7 +80,7 @@\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:85\",\n// - \"ModTime\": \"110\",\n// + \"ModTime\": \"121\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84\",\n// \"RefCount\": \"1\"\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:106]=\n// @@ -12,8 +12,8 @@\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// - \"Hash\": \"3d6aa1e96f126aba3cfd48f0203b85a287a6f1c0\",\n// - \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:111\"\n// + \"Hash\": \"9809329dc1ddc5d3556f7a8fa3c2cebcbf65560b\",\n// + \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:122\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// @@ -22,7 +22,7 @@\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:106\",\n// - \"ModTime\": \"110\",\n// + \"ModTime\": \"121\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:105\",\n// \"RefCount\": \"1\"\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:107]=\n// @@ -12,8 +12,8 @@\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// - \"Hash\": \"96a7d0b7b5bf5d366d10c559464ba56a50f78bbc\",\n// - \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:120\"\n// + \"Hash\": \"ceae9a1c4ed28bb51062e6ccdccfad0caafd1c4f\",\n// + \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:133\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// @@ -22,7 +22,7 @@\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:107\",\n// - \"ModTime\": \"110\",\n// + \"ModTime\": \"121\",\n// \"OwnerID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:105\",\n// \"RefCount\": \"1\"\n// }\n// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84]=\n// @@ -2,8 +2,8 @@\n// \"ObjectInfo\": {\n// \"ID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84\",\n// \"IsEscaped\": true,\n// - \"ModTime\": \"114\",\n// - \"RefCount\": \"5\"\n// + \"ModTime\": \"127\",\n// + \"RefCount\": \"6\"\n// },\n// \"Value\": {\n// \"T\": {\n// @@ -12,7 +12,7 @@\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// - \"Hash\": \"058a49e6ee26f75cc44901754d4c298fd1a1655c\",\n// + \"Hash\": \"a88a9b837af217656ee27084309f7cd02cd94cb3\",\n// \"ObjectID\": \"f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:85\"\n// }\n// }\n// u[336074805fc853987abe6f7fe3ad97a6a6f3077a:2]=\n// @@ -3,8 +3,8 @@\n// \"ObjectInfo\": {\n// \"ID\": \"336074805fc853987abe6f7fe3ad97a6a6f3077a:2\",\n// \"IsEscaped\": true,\n// - \"ModTime\": \"118\",\n// - \"RefCount\": \"11\"\n// + \"ModTime\": \"131\",\n// + \"RefCount\": \"12\"\n// },\n// \"Parent\": null,\n// \"Source\": {\n// switchrealm[\"gno.land/r/demo/boards\"]\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/sys/users\"]\n// switchrealm[\"gno.land/r/demo/boards\"]\n// switchrealm[\"gno.land/r/demo/boards_test\"]\n" + }, + { + "name": "z_5_b_filetest.gno", + "body": "package main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nconst admin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\t// create board via registered user\n\tbid := boards.CreateBoard(\"test_board\")\n\n\t// create post via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 9000000}})\n\n\tpid := boards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n" + }, + { + "name": "z_5_c_filetest.gno", + "body": "package main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nconst admin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\t// create board via registered user\n\tbid := boards.CreateBoard(\"test_board\")\n\n\t// create post via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 101000000}})\n\n\tpid := boards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tboards.CreateReply(bid, pid, pid, \"Reply of the first post\")\n\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post (title)\n//\n// Body of the first post. (body)\n// \\- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/u/g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=1\u0026threadid=1)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)]\n//\n// \u003e Reply of the first post\n// \u003e \\- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/u/g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=1)]\n//\n" + }, + { + "name": "z_5_d_filetest.gno", + "body": "package main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nconst admin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\t// create board via registered user\n\tbid := boards.CreateBoard(\"test_board\")\n\tpid := boards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\n\t// create reply via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 9000000}})\n\tboards.CreateReply(bid, pid, pid, \"Reply of the first post\")\n\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Error:\n// please register, otherwise minimum fee 100000000 is required if anonymous\n" + }, + { + "name": "z_5_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid := boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\trid2 := boards.CreateReply(bid, pid, pid, \"Second reply of the second post\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n//\n// \u003e Second reply of the second post\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=4\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=4\u0026threadid=2)]\n//\n" + }, + { + "name": "z_6_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.CreateReply(bid, pid, pid, \"Second reply of the second post\\n\")\n\tboards.CreateReply(bid, pid, rid, \"First reply of the first reply\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # Second Post (title)\n//\n// Body of the second post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=2\u0026threadid=2)] \\[[repost](/r/demo/boards$help\u0026func=CreateRepost\u0026bid=1\u0026postid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=2\u0026threadid=2)]\n//\n// \u003e Reply of the second post\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n// \u003e\n// \u003e \u003e First reply of the first reply\n// \u003e \u003e\n// \u003e \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=5\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=5\u0026threadid=2)]\n//\n// \u003e Second reply of the second post\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=4\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=4\u0026threadid=2)]\n//\n" + }, + { + "name": "z_7_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nfunc init() {\n\t// register\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\t// create board and post\n\tbid := boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n}\n\nfunc main() {\n\tprintln(boards.Render(\"test_board\"))\n}\n\n// Output:\n// \\[[post](/r/demo/boards$help\u0026func=CreateThread\u0026bid=1)]\n//\n// ----------------------------------------\n// ## [First Post (title)](/r/demo/boards:test_board/1)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=1\u0026threadid=1)] (0 replies) (0 reposts)\n//\n//\n" + }, + { + "name": "z_8_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tbid boards.BoardID\n\tpid boards.PostID\n\trid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tbid = boards.CreateBoard(\"test_board\")\n\tboards.CreateThread(bid, \"First Post (title)\", \"Body of the first post. (body)\")\n\tpid = boards.CreateThread(bid, \"Second Post (title)\", \"Body of the second post. (body)\")\n\trid = boards.CreateReply(bid, pid, pid, \"Reply of the second post\")\n}\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\")))\n\tboards.CreateReply(bid, pid, pid, \"Second reply of the second post\\n\")\n\trid2 := boards.CreateReply(bid, pid, rid, \"First reply of the first reply\\n\")\n\tprintln(boards.Render(\"test_board/\" + strconv.Itoa(int(pid)) + \"/\" + strconv.Itoa(int(rid2))))\n}\n\n// Output:\n// _[see thread](/r/demo/boards:test_board/2)_\n//\n// Reply of the second post\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=3\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=3\u0026threadid=2)]\n//\n// _[see all 1 replies](/r/demo/boards:test_board/2/3)_\n//\n// \u003e First reply of the first reply\n// \u003e\n// \u003e \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=1\u0026postid=5\u0026threadid=2)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=1\u0026postid=5\u0026threadid=2)]\n//\n" + }, + { + "name": "z_9_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar dstBoard boards.BoardID\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tdstBoard = boards.CreateBoard(\"dst_board\")\n\n\tboards.CreateRepost(0, 0, \"First Post in (title)\", \"Body of the first post. (body)\", dstBoard)\n}\n\nfunc main() {\n}\n\n// Error:\n// src board not exist\n" + }, + { + "name": "z_9_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tsrcBoard boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tsrcBoard = boards.CreateBoard(\"first_board\")\n\tpid = boards.CreateThread(srcBoard, \"First Post in (title)\", \"Body of the first post. (body)\")\n\n\tboards.CreateRepost(srcBoard, pid, \"First Post in (title)\", \"Body of the first post. (body)\", 0)\n}\n\nfunc main() {\n}\n\n// Error:\n// dst board not exist\n" + }, + { + "name": "z_9_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/boards_test\npackage boards_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/boards\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar (\n\tfirstBoard boards.BoardID\n\tsecondBoard boards.BoardID\n\tpid boards.PostID\n)\n\nfunc init() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\n\tfirstBoard = boards.CreateBoard(\"first_board\")\n\tsecondBoard = boards.CreateBoard(\"second_board\")\n\tpid = boards.CreateThread(firstBoard, \"First Post in (title)\", \"Body of the first post. (body)\")\n\n\tboards.CreateRepost(firstBoard, pid, \"First Post in (title)\", \"Body of the first post. (body)\", secondBoard)\n}\n\nfunc main() {\n\tprintln(boards.Render(\"second_board/\" + strconv.Itoa(int(pid))))\n}\n\n// Output:\n// # First Post in (title)\n//\n// Body of the first post. (body)\n// \\- [@gnouser123](/u/gnouser123), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \\[[reply](/r/demo/boards$help\u0026func=CreateReply\u0026bid=2\u0026postid=1\u0026threadid=1)] \\[[x](/r/demo/boards$help\u0026func=DeletePost\u0026bid=2\u0026postid=1\u0026threadid=1)]\n//\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "hJXC3s3D0rTiIAj42YtEhfRVuTGQPVflJaZemt+VYCagSSn7xfqN4X7UERRhDQAFq/dqTd0kjUTNoQwiuv1FCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "btree_dao", + "path": "gno.land/r/demo/btree_dao", + "files": [ + { + "name": "btree_dao.gno", + "body": "package btree_dao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/btree\"\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/md\"\n)\n\n// RegistrationDetails holds the details of a user's registration in the BTree DAO.\n// It stores the user's address, registration time, their B-Tree if they planted one,\n// and their NFT ID.\ntype RegistrationDetails struct {\n\tAddress std.Address\n\tRegTime time.Time\n\tUserBTree *btree.BTree\n\tNFTID string\n}\n\n// Less implements the btree.Record interface for RegistrationDetails.\n// It compares two RegistrationDetails based on their registration time.\n// Returns true if the current registration time is before the other registration time.\nfunc (rd *RegistrationDetails) Less(than btree.Record) bool {\n\tother := than.(*RegistrationDetails)\n\treturn rd.RegTime.Before(other.RegTime)\n}\n\nvar (\n\tdao = grc721.NewBasicNFT(\"BTree DAO\", \"BTDAO\")\n\ttokenID = 0\n\tmembers = btree.New()\n)\n\n// PlantTree allows a user to plant their B-Tree in the DAO forest.\n// It mints an NFT to the user and registers their tree in the DAO.\n// Returns an error if the tree is already planted, empty, or if NFT minting fails.\nfunc PlantTree(userBTree *btree.BTree) error {\n\treturn plantImpl(userBTree, \"\")\n}\n\n// PlantSeed allows a user to register as a seed in the DAO with a message.\n// It mints an NFT to the user and registers them as a seed member.\n// Returns an error if the message is empty or if NFT minting fails.\nfunc PlantSeed(message string) error {\n\treturn plantImpl(nil, message)\n}\n\n// plantImpl is the internal implementation that handles both tree planting and seed registration.\n// For tree planting (userBTree != nil), it verifies the tree isn't already planted and isn't empty.\n// For seed planting (userBTree == nil), it verifies the seed message isn't empty.\n// In both cases, it mints an NFT to the user and adds their registration details to the members tree.\n// Returns an error if any validation fails or if NFT minting fails.\nfunc plantImpl(userBTree *btree.BTree, seedMessage string) error {\n\t// Get the caller's address\n\tuserAddress := std.OriginCaller()\n\n\tvar nftID string\n\tvar regDetails *RegistrationDetails\n\n\tif userBTree != nil {\n\t\t// Handle tree planting\n\t\tvar treeExists bool\n\t\tmembers.Ascend(func(record btree.Record) bool {\n\t\t\tregDetails := record.(*RegistrationDetails)\n\t\t\tif regDetails.UserBTree == userBTree {\n\t\t\t\ttreeExists = true\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t\tif treeExists {\n\t\t\treturn errors.New(\"tree is already planted in the forest\")\n\t\t}\n\n\t\tif userBTree.Len() == 0 {\n\t\t\treturn errors.New(\"cannot plant an empty tree\")\n\t\t}\n\n\t\tnftID = ufmt.Sprintf(\"%d\", tokenID)\n\t\tregDetails = \u0026RegistrationDetails{\n\t\t\tAddress: userAddress,\n\t\t\tRegTime: time.Now(),\n\t\t\tUserBTree: userBTree,\n\t\t\tNFTID: nftID,\n\t\t}\n\t} else {\n\t\t// Handle seed planting\n\t\tif seedMessage == \"\" {\n\t\t\treturn errors.New(\"seed message cannot be empty\")\n\t\t}\n\t\tnftID = \"seed_\" + ufmt.Sprintf(\"%d\", tokenID)\n\t\tregDetails = \u0026RegistrationDetails{\n\t\t\tAddress: userAddress,\n\t\t\tRegTime: time.Now(),\n\t\t\tUserBTree: nil,\n\t\t\tNFTID: nftID,\n\t\t}\n\t}\n\n\t// Mint an NFT to the user\n\terr := dao.Mint(userAddress, grc721.TokenID(nftID))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmembers.Insert(regDetails)\n\ttokenID++\n\treturn nil\n}\n\n// Render generates a Markdown representation of the DAO members.\n// It displays:\n// - Total number of NFTs minted\n// - Total number of members\n// - Size of the biggest planted tree\n// - The first 3 members (OGs)\n// - The latest 10 members\n// Each member entry includes their address and owned NFTs (🌳 for trees, 🌱 for seeds).\n// The path parameter is currently unused.\n// Returns a formatted Markdown string.\nfunc Render(path string) string {\n\tvar latestMembers []string\n\tvar ogMembers []string\n\n\t// Get total size and first member\n\ttotalSize := members.Len()\n\tbiggestTree := 0\n\tif maxMember := members.Max(); maxMember != nil {\n\t\tif userBTree := maxMember.(*RegistrationDetails).UserBTree; userBTree != nil {\n\t\t\tbiggestTree = userBTree.Len()\n\t\t}\n\t}\n\n\t// Collect the latest 10 members\n\tmembers.Descend(func(record btree.Record) bool {\n\t\tif len(latestMembers) \u003c 10 {\n\t\t\tregDetails := record.(*RegistrationDetails)\n\t\t\taddr := regDetails.Address\n\t\t\tnftList := \"\"\n\t\t\tbalance, err := dao.BalanceOf(addr)\n\t\t\tif err == nil \u0026\u0026 balance \u003e 0 {\n\t\t\t\tnftList = \" (NFTs: \"\n\t\t\t\tfor i := uint64(0); i \u003c balance; i++ {\n\t\t\t\t\tif i \u003e 0 {\n\t\t\t\t\t\tnftList += \", \"\n\t\t\t\t\t}\n\t\t\t\t\tif regDetails.UserBTree == nil {\n\t\t\t\t\t\tnftList += \"🌱#\" + regDetails.NFTID\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnftList += \"🌳#\" + regDetails.NFTID\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnftList += \")\"\n\t\t\t}\n\t\t\tlatestMembers = append(latestMembers, string(addr)+nftList)\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\n\t// Collect the first 3 members (OGs)\n\tmembers.Ascend(func(record btree.Record) bool {\n\t\tif len(ogMembers) \u003c 3 {\n\t\t\tregDetails := record.(*RegistrationDetails)\n\t\t\taddr := regDetails.Address\n\t\t\tnftList := \"\"\n\t\t\tbalance, err := dao.BalanceOf(addr)\n\t\t\tif err == nil \u0026\u0026 balance \u003e 0 {\n\t\t\t\tnftList = \" (NFTs: \"\n\t\t\t\tfor i := uint64(0); i \u003c balance; i++ {\n\t\t\t\t\tif i \u003e 0 {\n\t\t\t\t\t\tnftList += \", \"\n\t\t\t\t\t}\n\t\t\t\t\tif regDetails.UserBTree == nil {\n\t\t\t\t\t\tnftList += \"🌱#\" + regDetails.NFTID\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnftList += \"🌳#\" + regDetails.NFTID\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tnftList += \")\"\n\t\t\t}\n\t\t\togMembers = append(ogMembers, string(addr)+nftList)\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\n\tvar sb strings.Builder\n\n\tsb.WriteString(md.H1(\"B-Tree DAO Members\"))\n\tsb.WriteString(md.H2(\"Total NFTs Minted\"))\n\tsb.WriteString(ufmt.Sprintf(\"Total NFTs minted: %d\\n\\n\", dao.TokenCount()))\n\tsb.WriteString(md.H2(\"Member Stats\"))\n\tsb.WriteString(ufmt.Sprintf(\"Total members: %d\\n\", totalSize))\n\tif biggestTree \u003e 0 {\n\t\tsb.WriteString(ufmt.Sprintf(\"Biggest tree size: %d\\n\", biggestTree))\n\t}\n\tsb.WriteString(md.H2(\"OG Members\"))\n\tsb.WriteString(md.BulletList(ogMembers))\n\tsb.WriteString(md.H2(\"Latest Members\"))\n\tsb.WriteString(md.BulletList(latestMembers))\n\n\treturn sb.String()\n}\n" + }, + { + "name": "btree_dao_test.gno", + "body": "package btree_dao\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/btree\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc setupTest() {\n\ttesting.SetOriginCaller(std.Address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\"))\n\tmembers = btree.New()\n}\n\ntype TestElement struct {\n\tvalue int\n}\n\nfunc (te *TestElement) Less(than btree.Record) bool {\n\treturn te.value \u003c than.(*TestElement).value\n}\n\nfunc TestPlantTree(t *testing.T) {\n\tsetupTest()\n\n\ttree := btree.New()\n\telements := []int{30, 10, 50, 20, 40}\n\tfor _, val := range elements {\n\t\ttree.Insert(\u0026TestElement{value: val})\n\t}\n\n\terr := PlantTree(tree)\n\turequire.NoError(t, err)\n\n\tfound := false\n\tmembers.Ascend(func(record btree.Record) bool {\n\t\tregDetails := record.(*RegistrationDetails)\n\t\tif regDetails.UserBTree == tree {\n\t\t\tfound = true\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t})\n\tuassert.True(t, found)\n\n\terr = PlantTree(tree)\n\tuassert.Error(t, err)\n\n\temptyTree := btree.New()\n\terr = PlantTree(emptyTree)\n\tuassert.Error(t, err)\n}\n\nfunc TestPlantSeed(t *testing.T) {\n\tsetupTest()\n\n\terr := PlantSeed(\"Hello DAO!\")\n\turequire.NoError(t, err)\n\n\tfound := false\n\tmembers.Ascend(func(record btree.Record) bool {\n\t\tregDetails := record.(*RegistrationDetails)\n\t\tif regDetails.UserBTree == nil {\n\t\t\tfound = true\n\t\t\tuassert.NotEmpty(t, regDetails.NFTID)\n\t\t\tuassert.True(t, strings.Contains(regDetails.NFTID, \"seed_\"))\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t})\n\tuassert.True(t, found)\n\n\terr = PlantSeed(\"\")\n\tuassert.Error(t, err)\n}\n\nfunc TestRegistrationDetailsOrdering(t *testing.T) {\n\tsetupTest()\n\n\trd1 := \u0026RegistrationDetails{\n\t\tAddress: std.Address(\"test1\"),\n\t\tRegTime: time.Now(),\n\t\tNFTID: \"0\",\n\t}\n\trd2 := \u0026RegistrationDetails{\n\t\tAddress: std.Address(\"test2\"),\n\t\tRegTime: time.Now().Add(time.Hour),\n\t\tNFTID: \"1\",\n\t}\n\n\tuassert.True(t, rd1.Less(rd2))\n\tuassert.False(t, rd2.Less(rd1))\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "y9V9mXaNWgorTA3EZC7Cxa69yUibZCen2AFbhl+SsIqveBZptaJTlOXYyakeFIXiXR7BWalhYdBP7Kn3IMfVAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "counter", + "path": "gno.land/r/demo/counter", + "files": [ + { + "name": "counter.gno", + "body": "package counter\n\nimport \"strconv\"\n\nvar counter int\n\nfunc Increment() int {\n\tcounter++\n\treturn counter\n}\n\nfunc Render(_ string) string {\n\treturn strconv.Itoa(counter)\n}\n" + }, + { + "name": "counter_test.gno", + "body": "package counter\n\nimport \"testing\"\n\nfunc TestIncrement(t *testing.T) {\n\tcounter = 0\n\tval := Increment()\n\tif val != 1 {\n\t\tt.Fatalf(\"result from Increment(): %d != 1\", val)\n\t}\n\tif counter != val {\n\t\tt.Fatalf(\"counter (%d) != val (%d)\", counter, val)\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\tcounter = 1337\n\tres := Render(\"\")\n\tif res != \"1337\" {\n\t\tt.Fatalf(\"render result %q != %q\", res, \"1337\")\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "KVb3s8cIy+7+kieFA6ayHYfPl9shGOAEzMXTACnlYQDGvisy4CXgY64EfHReedt+EXiMQ4zSIHJCWC89O2a8CQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "daoweb", + "path": "gno.land/r/demo/daoweb", + "files": [ + { + "name": "daoweb.gno", + "body": "package daoweb\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/json\"\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\n// Proposals returns the paginated GovDAO proposals\nfunc Proposals(offset, count uint64) string {\n\tvar (\n\t\tpropStore = bridge.GovDAO().GetPropStore()\n\t\tsize = propStore.Size()\n\t)\n\n\t// Get the props\n\tprops := propStore.Proposals(offset, count)\n\n\tresp := ProposalsResponse{\n\t\tProposals: make([]Proposal, 0, count),\n\t\tTotal: uint64(size),\n\t}\n\n\tfor _, p := range props {\n\t\tprop := Proposal{\n\t\t\tAuthor: p.Author(),\n\t\t\tDescription: p.Description(),\n\t\t\tStatus: p.Status(),\n\t\t\tStats: p.Stats(),\n\t\t\tIsExpired: p.IsExpired(),\n\t\t}\n\n\t\tresp.Proposals = append(resp.Proposals, prop)\n\t}\n\n\t// Encode the response into JSON\n\tencodedProps, err := json.Marshal(encodeProposalsResponse(resp))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(encodedProps)\n}\n\n// ProposalByID fetches the proposal using the given ID\nfunc ProposalByID(id uint64) string {\n\tpropStore := bridge.GovDAO().GetPropStore()\n\n\tp, err := propStore.ProposalByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Encode the response into JSON\n\tprop := Proposal{\n\t\tAuthor: p.Author(),\n\t\tDescription: p.Description(),\n\t\tStatus: p.Status(),\n\t\tStats: p.Stats(),\n\t\tIsExpired: p.IsExpired(),\n\t}\n\n\tencodedProp, err := json.Marshal(encodeProposal(prop))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn string(encodedProp)\n}\n\n// encodeProposal encodes a proposal into a json node\nfunc encodeProposal(p Proposal) *json.Node {\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"author\": json.StringNode(\"author\", p.Author.String()),\n\t\t\"description\": json.StringNode(\"description\", p.Description),\n\t\t\"status\": json.StringNode(\"status\", p.Status.String()),\n\t\t\"stats\": json.ObjectNode(\"stats\", map[string]*json.Node{\n\t\t\t\"yay_votes\": json.NumberNode(\"yay_votes\", float64(p.Stats.YayVotes)),\n\t\t\t\"nay_votes\": json.NumberNode(\"nay_votes\", float64(p.Stats.NayVotes)),\n\t\t\t\"abstain_votes\": json.NumberNode(\"abstain_votes\", float64(p.Stats.AbstainVotes)),\n\t\t\t\"total_voting_power\": json.NumberNode(\"total_voting_power\", float64(p.Stats.TotalVotingPower)),\n\t\t}),\n\t\t\"is_expired\": json.BoolNode(\"is_expired\", p.IsExpired),\n\t})\n}\n\n// encodeProposalsResponse encodes a proposal response into a JSON node\nfunc encodeProposalsResponse(props ProposalsResponse) *json.Node {\n\tproposals := make([]*json.Node, 0, len(props.Proposals))\n\n\tfor _, p := range props.Proposals {\n\t\tproposals = append(proposals, encodeProposal(p))\n\t}\n\n\treturn json.ObjectNode(\"\", map[string]*json.Node{\n\t\t\"proposals\": json.ArrayNode(\"proposals\", proposals),\n\t\t\"total\": json.NumberNode(\"total\", float64(props.Total)),\n\t})\n}\n\n// ProposalsResponse is a paginated proposal response\ntype ProposalsResponse struct {\n\tProposals []Proposal `json:\"proposals\"`\n\tTotal uint64 `json:\"total\"`\n}\n\n// Proposal is a single GovDAO proposal\ntype Proposal struct {\n\tAuthor std.Address `json:\"author\"`\n\tDescription string `json:\"description\"`\n\tStatus dao.ProposalStatus `json:\"status\"`\n\tStats dao.Stats `json:\"stats\"`\n\tIsExpired bool `json:\"is_expired\"`\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "BzOwgr/GdVPgeOSe0uoWeG7TPf2q0jgNpZwPb2rkhWPS9acpBKcybcksbxIH+qI5gchB00WpyjNVH8Aw73amDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "deep", + "path": "gno.land/r/demo/deep/very/deep", + "files": [ + { + "name": "render.gno", + "body": "package deep\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn \"it works!\"\n\t} else {\n\t\treturn \"hi \" + path\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "x6oQVL/mL9bIjmHil7vs2BQtoKmVGw2XqyzqdJB5n1StEEWeWAb9Pti1aaH4qiLpYTx2gsl0uVkoOK1V3s9kDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "foo20", + "path": "gno.land/r/demo/grc20factory", + "files": [ + { + "name": "grc20factory.gno", + "body": "package foo20\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/grc20reg\"\n)\n\nvar instances avl.Tree // symbol -\u003e instance\n\ntype instance struct {\n\ttoken *grc20.Token\n\tledger *grc20.PrivateLedger\n\tadmin *ownable.Ownable\n\tfaucet uint64 // per-request amount. disabled if 0.\n}\n\nfunc New(name, symbol string, decimals uint, initialMint, faucet uint64) {\n\tcaller := std.PreviousRealm().Address()\n\tNewWithAdmin(name, symbol, decimals, initialMint, faucet, caller)\n}\n\nfunc NewWithAdmin(name, symbol string, decimals uint, initialMint, faucet uint64, admin std.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.NewWithAddress(admin),\n\t\tfaucet: faucet,\n\t}\n\tinstances.Set(symbol, \u0026inst)\n\tgrc20reg.Register(token.Getter(), symbol)\n}\n\nfunc (inst instance) Token() *grc20.Token {\n\treturn inst.token\n}\n\nfunc (inst instance) CallerTeller() grc20.Teller {\n\treturn inst.token.CallerTeller()\n}\n\nfunc Bank(symbol string) *grc20.Token {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token\n}\n\nfunc TotalSupply(symbol string) uint64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().TotalSupply()\n}\n\nfunc BalanceOf(symbol string, owner std.Address) uint64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().BalanceOf(owner)\n}\n\nfunc Allowance(symbol string, owner, spender std.Address) uint64 {\n\tinst := mustGetInstance(symbol)\n\treturn inst.token.ReadonlyTeller().Allowance(owner, spender)\n}\n\nfunc Transfer(symbol string, to std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tcaller := std.PreviousRealm().Address()\n\tteller := inst.ledger.ImpersonateTeller(caller)\n\tcheckErr(teller.Transfer(to, amount))\n}\n\nfunc Approve(symbol string, spender std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tcaller := std.PreviousRealm().Address()\n\tteller := inst.ledger.ImpersonateTeller(caller)\n\tcheckErr(teller.Approve(spender, amount))\n}\n\nfunc TransferFrom(symbol string, from, to std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tcaller := std.PreviousRealm().Address()\n\tteller := inst.ledger.ImpersonateTeller(caller)\n\tcheckErr(teller.TransferFrom(from, to, amount))\n}\n\n// faucet.\nfunc Faucet(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 := std.PreviousRealm().Address()\n\tcheckErr(inst.ledger.Mint(caller, inst.faucet))\n}\n\nfunc Mint(symbol string, to std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tinst.admin.AssertCallerIsOwner()\n\tcheckErr(inst.ledger.Mint(to, amount))\n}\n\nfunc Burn(symbol string, from std.Address, amount uint64) {\n\tinst := mustGetInstance(symbol)\n\tinst.admin.AssertCallerIsOwner()\n\tcheckErr(inst.ledger.Burn(from, amount))\n}\n\n// instance admin functionality\nfunc DropInstanceOwnership(symbol string) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.admin.DropOwnership())\n}\n\nfunc TransferInstanceOwnership(symbol string, newOwner std.Address) {\n\tinst := mustGetInstance(symbol)\n\tcheckErr(inst.admin.TransferOwnership(newOwner))\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 \"TODO: list existing tokens and admins\"\n\tcase c == 1:\n\t\tsymbol := parts[0]\n\t\tinst := mustGetInstance(symbol)\n\t\treturn inst.token.RenderHome()\n\tcase c == 3 \u0026\u0026 parts[1] == \"balance\":\n\t\tsymbol := parts[0]\n\t\tinst := mustGetInstance(symbol)\n\t\towner := std.Address(parts[2])\n\t\tbalance := inst.token.CallerTeller().BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\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/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\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 uint64\n\t\tfn func() uint64\n\t}\n\n\tcheckBalances := func(step string, totSup, balAdm, balBob, allowAdmBob, balCarl uint64) {\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", totSup, func() uint64 { return TotalSupply(\"FOO\") }},\n\t\t\t{\"BalanceOf(admin)\", balAdm, func() uint64 { return BalanceOf(\"FOO\", admin) }},\n\t\t\t{\"BalanceOf(bob)\", balBob, func() uint64 { return BalanceOf(\"FOO\", bob) }},\n\t\t\t{\"Allowance(admin, bob)\", allowAdmBob, func() uint64 { return Allowance(\"FOO\", admin, bob) }},\n\t\t\t{\"BalanceOf(carl)\", balCarl, func() uint64 { 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(\"Foo\", \"FOO\", 3, 1_111_111_000, 5_555, admin)\n\tNewWithAdmin(\"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(\"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(\"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(\"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(\"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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "mU5WaWES/FF/eN9pgIaoYdzZDRfHjGEkXvHGjq4RjTn1m5h5WNFYu5Ig0zt3bCLjnt/lGuNh39CKLBrou139AA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "disperse", + "path": "gno.land/r/demo/disperse", + "files": [ + { + "name": "disperse.gno", + "body": "package disperse\n\nimport (\n\t\"std\"\n\n\ttokens \"gno.land/r/demo/grc20factory\"\n)\n\n// Get address of Disperse realm\nvar realmAddr = std.CurrentRealm().Address()\n\n// DisperseUgnot parses receivers and amounts and sends out ugnot\n// The function will send out the coins to the addresses and return the leftover coins to the caller\n// if there are any to return\nfunc DisperseUgnot(addresses []std.Address, coins std.Coins) {\n\tcoinSent := std.OriginSend()\n\tcaller := std.PreviousRealm().Address()\n\tbanker := std.NewBanker(std.BankerTypeOriginSend)\n\n\tif len(addresses) != len(coins) {\n\t\tpanic(ErrNumAddrValMismatch)\n\t}\n\n\tfor _, coin := range coins {\n\t\tif coin.Amount \u003c= 0 {\n\t\t\tpanic(ErrNegativeCoinAmount)\n\t\t}\n\n\t\tif banker.GetCoins(realmAddr).AmountOf(coin.Denom) \u003c coin.Amount {\n\t\t\tpanic(ErrMismatchBetweenSentAndParams)\n\t\t}\n\t}\n\n\t// Send coins\n\tfor i, _ := range addresses {\n\t\tbanker.SendCoins(realmAddr, addresses[i], std.NewCoins(coins[i]))\n\t}\n\n\t// Return possible leftover coins\n\tfor _, coin := range coinSent {\n\t\tleftoverAmt := banker.GetCoins(realmAddr).AmountOf(coin.Denom)\n\t\tif leftoverAmt \u003e 0 {\n\t\t\tsend := std.Coins{std.NewCoin(coin.Denom, leftoverAmt)}\n\t\t\tbanker.SendCoins(realmAddr, caller, send)\n\t\t}\n\t}\n}\n\n// DisperseGRC20 disperses tokens to multiple addresses\n// Note that it is necessary to approve the realm to spend the tokens before calling this function\n// see the corresponding filetests for examples\nfunc DisperseGRC20(addresses []std.Address, amounts []uint64, symbols []string) {\n\tcaller := std.PreviousRealm().Address()\n\n\tif (len(addresses) != len(amounts)) || (len(amounts) != len(symbols)) {\n\t\tpanic(ErrArgLenAndSentLenMismatch)\n\t}\n\n\tfor i := 0; i \u003c len(addresses); i++ {\n\t\ttokens.TransferFrom(symbols[i], caller, addresses[i], amounts[i])\n\t}\n}\n\n// DisperseGRC20String receives a string of addresses and a string of tokens\n// and parses them to be used in DisperseGRC20\nfunc DisperseGRC20String(addresses string, tokens string) {\n\tparsedAddresses, err := parseAddresses(addresses)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tparsedAmounts, parsedSymbols, err := parseTokens(tokens)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tDisperseGRC20(parsedAddresses, parsedAmounts, parsedSymbols)\n}\n\n// DisperseUgnotString receives a string of addresses and a string of amounts\n// and parses them to be used in DisperseUgnot\nfunc DisperseUgnotString(addresses string, amounts string) {\n\tparsedAddresses, err := parseAddresses(addresses)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tparsedAmounts, err := parseAmounts(amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tcoins := make(std.Coins, len(parsedAmounts))\n\tfor i, amount := range parsedAmounts {\n\t\tcoins[i] = std.NewCoin(\"ugnot\", amount)\n\t}\n\n\tDisperseUgnot(parsedAddresses, coins)\n}\n" + }, + { + "name": "doc.gno", + "body": "// Package disperse provides methods to disperse coins or GRC20 tokens among multiple addresses.\n//\n// The disperse package is an implementation of an existing service that allows users to send coins or GRC20 tokens to multiple addresses\n// on the Ethereum blockchain.\n//\n// Usage:\n// To use disperse, you can either use `DisperseUgnot` to send coins or `DisperseGRC20` to send GRC20 tokens to multiple addresses.\n//\n// Example:\n// Dispersing 200 coins to two addresses:\n// - DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n// Dispersing 200 worth of a GRC20 token \"TEST\" to two addresses:\n// - DisperseGRC20String(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150TEST,50TEST\")\n//\n// Reference:\n// - [the original dispere app](https://disperse.app/)\n// - [the original disperse app on etherscan](https://etherscan.io/address/0xd152f549545093347a162dce210e7293f1452150#code)\n// - [the gno disperse web app](https://gno-disperse.netlify.app/)\npackage disperse // import \"gno.land/r/demo/disperse\"\n" + }, + { + "name": "errors.gno", + "body": "package disperse\n\nimport \"errors\"\n\nvar (\n\tErrNotEnoughCoin = errors.New(\"disperse: not enough coin sent in\")\n\tErrNumAddrValMismatch = errors.New(\"disperse: number of addresses and values to send doesn't match\")\n\tErrInvalidAddress = errors.New(\"disperse: invalid address\")\n\tErrNegativeCoinAmount = errors.New(\"disperse: coin amount cannot be negative\")\n\tErrMismatchBetweenSentAndParams = errors.New(\"disperse: mismatch between coins sent and params called\")\n\tErrArgLenAndSentLenMismatch = errors.New(\"disperse: mismatch between coins sent and args called\")\n)\n" + }, + { + "name": "util.gno", + "body": "package disperse\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unicode\"\n)\n\nfunc parseAddresses(addresses string) ([]std.Address, error) {\n\tvar ret []std.Address\n\n\tfor _, str := range strings.Split(addresses, \",\") {\n\t\taddr := std.Address(str)\n\t\tif !addr.IsValid() {\n\t\t\treturn nil, ErrInvalidAddress\n\t\t}\n\n\t\tret = append(ret, addr)\n\t}\n\n\treturn ret, nil\n}\n\nfunc splitString(input string) (string, string) {\n\tvar pos int\n\tfor i, char := range input {\n\t\tif !unicode.IsDigit(char) {\n\t\t\tpos = i\n\t\t\tbreak\n\t\t}\n\t}\n\treturn input[:pos], input[pos:]\n}\n\nfunc parseTokens(tokens string) ([]uint64, []string, error) {\n\tvar amounts []uint64\n\tvar symbols []string\n\n\tfor _, token := range strings.Split(tokens, \",\") {\n\t\tamountStr, symbol := splitString(token)\n\t\tamount, _ := strconv.Atoi(amountStr)\n\t\tif amount \u003c 0 {\n\t\t\treturn nil, nil, ErrNegativeCoinAmount\n\t\t}\n\n\t\tamounts = append(amounts, uint64(amount))\n\t\tsymbols = append(symbols, symbol)\n\t}\n\n\treturn amounts, symbols, nil\n}\n\nfunc parseAmounts(amounts string) ([]int64, error) {\n\tvar ret []int64\n\n\tfor _, amt := range strings.Split(amounts, \",\") {\n\t\tamount, _ := strconv.Atoi(amt)\n\t\tif amount \u003c 0 {\n\t\t\treturn nil, ErrNegativeCoinAmount\n\t\t}\n\n\t\tret = append(ret, int64(amount))\n\t}\n\n\treturn ret, nil\n}\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/main\n\n// SEND: 200ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\n\ttesting.SetOriginCaller(mainaddr)\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\tbanker.SendCoins(mainaddr, disperseAddr, std.Coins{{\"ugnot\", 200}})\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/demo/disperse\"))\n\tdisperse.DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal)\n}\n\n// Output:\n// main before: 200ugnot\n// main after:\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\n\ttesting.SetOriginCaller(mainaddr)\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\n\tmainbal := banker.GetCoins(mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\tbanker.SendCoins(mainaddr, disperseAddr, std.Coins{{\"ugnot\", 300}})\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/demo/disperse\"))\n\tdisperse.DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n\n\tmainbal = banker.GetCoins(mainaddr)\n\tprintln(\"main after:\", mainbal)\n}\n\n// Output:\n// main before: 300ugnot\n// main after: 100ugnot\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/disperse\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\n\ttesting.SetOriginCaller(mainaddr)\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\n\tbanker.SendCoins(mainaddr, disperseAddr, std.Coins{{\"ugnot\", 100}})\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/demo/disperse\"))\n\tdisperse.DisperseUgnotString(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150,50\")\n}\n\n// Error:\n// disperse: mismatch between coins sent and params called\n" + }, + { + "name": "z_3_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/disperse\"\n\ttokens \"gno.land/r/demo/grc20factory\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\tbeneficiary1 := std.Address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := std.Address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\ttesting.SetOriginCaller(mainaddr)\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\n\ttokens.New(\"test\", \"TEST\", 4, 0, 0)\n\ttokens.Mint(\"TEST\", mainaddr, 200)\n\n\tmainbal := tokens.BalanceOf(\"TEST\", mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\ttokens.Approve(\"TEST\", disperseAddr, 200)\n\n\tdisperse.DisperseGRC20String(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"150TEST,50TEST\")\n\n\tmainbal = tokens.BalanceOf(\"TEST\", mainaddr)\n\tprintln(\"main after:\", mainbal)\n\tben1bal := tokens.BalanceOf(\"TEST\", beneficiary1)\n\tprintln(\"beneficiary1:\", ben1bal)\n\tben2bal := tokens.BalanceOf(\"TEST\", beneficiary2)\n\tprintln(\"beneficiary2:\", ben2bal)\n}\n\n// Output:\n// main before: 200\n// main after: 0\n// beneficiary1: 150\n// beneficiary2: 50\n" + }, + { + "name": "z_4_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/main\n\n// SEND: 300ugnot\n\npackage main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/disperse\"\n\ttokens \"gno.land/r/demo/grc20factory\"\n)\n\nfunc main() {\n\tdisperseAddr := std.DerivePkgAddr(\"gno.land/r/demo/disperse\")\n\tmainaddr := std.DerivePkgAddr(\"gno.land/r/demo/main\")\n\tbeneficiary1 := std.Address(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0\")\n\tbeneficiary2 := std.Address(\"g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\")\n\n\ttesting.SetOriginCaller(mainaddr)\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\n\ttokens.New(\"test1\", \"TEST1\", 4, 0, 0)\n\ttokens.Mint(\"TEST1\", mainaddr, 200)\n\ttokens.New(\"test2\", \"TEST2\", 4, 0, 0)\n\ttokens.Mint(\"TEST2\", mainaddr, 200)\n\n\tmainbal := tokens.BalanceOf(\"TEST1\", mainaddr) + tokens.BalanceOf(\"TEST2\", mainaddr)\n\tprintln(\"main before:\", mainbal)\n\n\ttokens.Approve(\"TEST1\", disperseAddr, 200)\n\ttokens.Approve(\"TEST2\", disperseAddr, 200)\n\n\tdisperse.DisperseGRC20String(\"g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0,g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c\", \"200TEST1,200TEST2\")\n\n\tmainbal = tokens.BalanceOf(\"TEST1\", mainaddr) + tokens.BalanceOf(\"TEST2\", mainaddr)\n\tprintln(\"main after:\", mainbal)\n\tben1bal := tokens.BalanceOf(\"TEST1\", beneficiary1) + tokens.BalanceOf(\"TEST2\", beneficiary1)\n\tprintln(\"beneficiary1:\", ben1bal)\n\tben2bal := tokens.BalanceOf(\"TEST1\", beneficiary2) + tokens.BalanceOf(\"TEST2\", beneficiary2)\n\tprintln(\"beneficiary2:\", ben2bal)\n}\n\n// Output:\n// main before: 400\n// main after: 0\n// beneficiary1: 200\n// beneficiary2: 200\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "CxjB102Vco0hAb31UPky6lodJTd28Ib7eT9agcX5jhkMpVf4X6rfNe/rNvtvShC6qXbEI8E2KYkcB7UyLe3iCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "echo", + "path": "gno.land/r/demo/echo", + "files": [ + { + "name": "echo.gno", + "body": "package echo\n\n/*\n * This realm echoes the `path` argument it received.\n * Can be used by developers as a simple endpoint to test\n * forbidden characters, for pentesting or simply to\n * test it works.\n *\n * See also r/demo/print (to print various thing like user address)\n */\nfunc Render(path string) string {\n\treturn path\n}\n" + }, + { + "name": "echo_test.gno", + "body": "package echo\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc Test(t *testing.T) {\n\turequire.Equal(t, \"aa\", Render(\"aa\"))\n\turequire.Equal(t, \"\", Render(\"\"))\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "2eiUQO+8dV6g7q2IWT6qUETZ2pH0KZ1bEU0xNJfUb7Sh0M2X2RqcHLNFssaJAz3Vts+5F1Ej/Ic9tNSTqR+xAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "emit", + "path": "gno.land/r/demo/emit", + "files": [ + { + "name": "emit.gno", + "body": "// Package emit demonstrates how to use the std.Emit() function\n// to emit Gno events that can be used to track data changes off-chain.\n// std.Emit is variadic; apart from the event name, it can take in any number of key-value pairs to emit.\npackage emit\n\nimport (\n\t\"std\"\n)\n\nfunc Emit(value string) {\n\tstd.Emit(\"EventName\", \"key\", value)\n}\n" + }, + { + "name": "z1_filetest.gno", + "body": "package main\n\nimport \"gno.land/r/demo/emit\"\n\nfunc main() {\n\temit.Emit(\"foo\")\n\temit.Emit(\"bar\")\n}\n\n// Events:\n// [\n// {\n// \"type\": \"EventName\",\n// \"attrs\": [\n// {\n// \"key\": \"key\",\n// \"value\": \"foo\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/demo/emit\",\n// \"func\": \"Emit\"\n// },\n// {\n// \"type\": \"EventName\",\n// \"attrs\": [\n// {\n// \"key\": \"key\",\n// \"value\": \"bar\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/demo/emit\",\n// \"func\": \"Emit\"\n// }\n// ]\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "jPuBcwuiMvK6NAgGSpne+bIgGTg+ykYkgV9RX5J04fX53n33esZILr8CA3ErlqrA1g7iuA7IWZipAi70UHBWCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "foo1155", + "path": "gno.land/r/demo/foo1155", + "files": [ + { + "name": "foo1155.gno", + "body": "package foo1155\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/grc1155\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tdummyURI = \"ipfs://xyz\"\n\tadmin std.Address = \"g10x5phu0k6p64cwrhfpsc8tk43st9kug6wft530\"\n\tfoo = grc1155.NewBasicGRC1155Token(dummyURI)\n)\n\nfunc init() {\n\tmintGRC1155Token(admin) // @administrator (10)\n}\n\nfunc mintGRC1155Token(owner std.Address) {\n\tfor i := 1; i \u003c= 10; i++ {\n\t\ttid := grc1155.TokenID(ufmt.Sprintf(\"%d\", i))\n\t\tfoo.SafeMint(owner, tid, 100)\n\t}\n}\n\n// Getters\n\nfunc BalanceOf(user std.Address, tid grc1155.TokenID) uint64 {\n\tbalance, err := foo.BalanceOf(user, tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balance\n}\n\nfunc BalanceOfBatch(ul []std.Address, batch []grc1155.TokenID) []uint64 {\n\tbalanceBatch, err := foo.BalanceOfBatch(ul, batch)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balanceBatch\n}\n\nfunc IsApprovedForAll(owner, user std.Address) bool {\n\treturn foo.IsApprovedForAll(owner, user)\n}\n\n// Setters\n\nfunc SetApprovalForAll(user std.Address, approved bool) {\n\terr := foo.SetApprovalForAll(user, approved)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TransferFrom(from, to std.Address, tid grc1155.TokenID, amount uint64) {\n\terr := foo.SafeTransferFrom(from, to, tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc BatchTransferFrom(from, to std.Address, batch []grc1155.TokenID, amounts []uint64) {\n\terr := foo.SafeBatchTransferFrom(from, to, batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Admin\n\nfunc Mint(to std.Address, tid grc1155.TokenID, amount uint64) {\n\tcaller := std.OriginCaller()\n\tassertIsAdmin(caller)\n\terr := foo.SafeMint(to, tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc MintBatch(to std.Address, batch []grc1155.TokenID, amounts []uint64) {\n\tcaller := std.OriginCaller()\n\tassertIsAdmin(caller)\n\terr := foo.SafeBatchMint(to, batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Burn(from std.Address, tid grc1155.TokenID, amount uint64) {\n\tcaller := std.OriginCaller()\n\tassertIsAdmin(caller)\n\terr := foo.Burn(from, tid, amount)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc BurnBatch(from std.Address, batch []grc1155.TokenID, amounts []uint64) {\n\tcaller := std.OriginCaller()\n\tassertIsAdmin(caller)\n\terr := foo.BatchBurn(from, batch, amounts)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Render\n\nfunc Render(path string) string {\n\tswitch {\n\tcase path == \"\":\n\t\treturn foo.RenderHome()\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\n// Util\n\nfunc assertIsAdmin(address std.Address) {\n\tif address != admin {\n\t\tpanic(\"restricted access\")\n\t}\n}\n" + }, + { + "name": "foo1155_test.gno", + "body": "package foo1155\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/grc/grc1155\"\n)\n\nfunc TestFoo721(t *testing.T) {\n\tadmin := std.Address(\"g10x5phu0k6p64cwrhfpsc8tk43st9kug6wft530\")\n\tbob := std.Address(\"g1ze6et22ces5atv79y4xh38s4kuraey4y2fr6tw\")\n\ttid1 := grc1155.TokenID(\"1\")\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\texpected any\n\t\tfn func() any\n\t}{\n\t\t{\"BalanceOf(admin, tid1)\", uint64(100), func() any { return BalanceOf(admin, tid1) }},\n\t\t{\"BalanceOf(bob, tid1)\", uint64(0), func() any { return BalanceOf(bob, tid1) }},\n\t\t{\"IsApprovedForAll(admin, bob)\", false, func() any { return IsApprovedForAll(admin, bob) }},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := tc.fn()\n\t\t\tif tc.expected != got {\n\t\t\t\tt.Errorf(\"expected: %v got: %v\", tc.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "b4xN7WBX2vrygOcV8WRVnc0+5o5lsPnP39nsyH+NhWeM8X5e3z2CxrqIog7kr9SpbkgX0h0RmaP/iZGE3wySAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "foo20", + "path": "gno.land/r/demo/foo20", + "files": [ + { + "name": "foo20.gno", + "body": "// foo20 is a GRC20 token contract where all the grc20.Teller methods are\n// proxified with top-level functions. see also gno.land/r/demo/bar20.\npackage foo20\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/grc20reg\"\n)\n\nvar (\n\tToken, privateLedger = grc20.NewToken(\"Foo\", \"FOO\", 4)\n\tUserTeller = Token.CallerTeller()\n\tOwnable = ownable.NewWithAddress(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @manfred\n)\n\nfunc init() {\n\tprivateLedger.Mint(Ownable.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M)\n\tgrc20reg.Register(Token.Getter(), \"\")\n}\n\nfunc TotalSupply() uint64 {\n\treturn UserTeller.TotalSupply()\n}\n\nfunc BalanceOf(owner std.Address) uint64 {\n\treturn UserTeller.BalanceOf(owner)\n}\n\nfunc Allowance(owner, spender std.Address) uint64 {\n\treturn UserTeller.Allowance(owner, spender)\n}\n\nfunc Transfer(to std.Address, amount uint64) {\n\tcheckErr(UserTeller.Transfer(to, amount))\n}\n\nfunc Approve(spender std.Address, amount uint64) {\n\tcheckErr(UserTeller.Approve(spender, amount))\n}\n\nfunc TransferFrom(from, to std.Address, amount uint64) {\n\tcheckErr(UserTeller.TransferFrom(from, to, amount))\n}\n\n// Faucet is distributing foo20 tokens without restriction (unsafe).\n// For a real token faucet, you should take care of setting limits are asking payment.\nfunc Faucet() {\n\tcaller := std.PreviousRealm().Address()\n\tamount := uint64(1_000 * 10_000) // 1k\n\tcheckErr(privateLedger.Mint(caller, amount))\n}\n\nfunc Mint(to std.Address, amount uint64) {\n\tOwnable.AssertCallerIsOwner()\n\tcheckErr(privateLedger.Mint(to, amount))\n}\n\nfunc Burn(from std.Address, amount uint64) {\n\tOwnable.AssertCallerIsOwner()\n\tcheckErr(privateLedger.Burn(from, amount))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn Token.RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"balance\":\n\t\towner := std.Address(parts[1])\n\t\tbalance := UserTeller.BalanceOf(owner)\n\t\treturn ufmt.Sprintf(\"%d\\n\", balance)\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n" + }, + { + "name": "foo20_test.gno", + "body": "package foo20\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestReadOnlyPublicMethods(t *testing.T) {\n\tvar (\n\t\tadmin = std.Address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\")\n\t\talice = testutils.TestAddress(\"alice\")\n\t\tbob = testutils.TestAddress(\"bob\")\n\t)\n\n\ttype test struct {\n\t\tname string\n\t\tbalance uint64\n\t\tfn func() uint64\n\t}\n\n\t// check balances #1.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_000_000_000, func() uint64 { return TotalSupply() }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint64 { return BalanceOf(admin) }},\n\t\t\t{\"BalanceOf(alice)\", 0, func() uint64 { return BalanceOf(alice) }},\n\t\t\t{\"Allowance(admin, alice)\", 0, func() uint64 { return Allowance(admin, alice) }},\n\t\t\t{\"BalanceOf(bob)\", 0, func() uint64 { return BalanceOf(bob) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tgot := tc.fn()\n\t\t\tuassert.Equal(t, got, tc.balance)\n\t\t}\n\t}\n\n\t// bob uses the faucet.\n\ttesting.SetOriginCaller(bob)\n\tFaucet()\n\n\t// check balances #2.\n\t{\n\t\ttests := []test{\n\t\t\t{\"TotalSupply\", 10_010_000_000, func() uint64 { return TotalSupply() }},\n\t\t\t{\"BalanceOf(admin)\", 10_000_000_000, func() uint64 { return BalanceOf(admin) }},\n\t\t\t{\"BalanceOf(alice)\", 0, func() uint64 { return BalanceOf(alice) }},\n\t\t\t{\"Allowance(admin, alice)\", 0, func() uint64 { return Allowance(admin, alice) }},\n\t\t\t{\"BalanceOf(bob)\", 10_000_000, func() uint64 { return BalanceOf(bob) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tgot := tc.fn()\n\t\t\tuassert.Equal(t, got, tc.balance)\n\t\t}\n\t}\n}\n\nfunc TestErrConditions(t *testing.T) {\n\tvar (\n\t\tadmin = std.Address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\")\n\t\tempty = std.Address(\"\")\n\t)\n\n\ttype test struct {\n\t\tname string\n\t\tmsg string\n\t\tfn func()\n\t}\n\n\tprivateLedger.Mint(std.Address(admin), 10000)\n\t{\n\t\ttests := []test{\n\t\t\t{\"Transfer(admin, 1)\", \"cannot send transfer to self\", func() {\n\t\t\t\t// XXX: should replace with: Transfer(admin, 1)\n\t\t\t\t// but there is currently a limitation in manipulating the frame stack and simulate\n\t\t\t\t// calling this package from an outside point of view.\n\t\t\t\tadminAddr := std.Address(admin)\n\t\t\t\tif err := privateLedger.Transfer(adminAddr, adminAddr, 1); err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t}},\n\t\t\t{\"Approve(empty, 1))\", \"invalid address\", func() { Approve(empty, 1) }},\n\t\t}\n\t\tfor _, tc := range tests {\n\t\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t\tuassert.PanicsWithMessage(t, tc.msg, tc.fn)\n\t\t\t})\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "GAhrutyy6hZU5nwTsIxga1eFNRRm70I5/ItMhgF3OXLkte64hVHcZmDMnvo/JoYfmrZj5UNbU6kmdmOwGGt9Bw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "foo721", + "path": "gno.land/r/demo/foo721", + "files": [ + { + "name": "foo721.gno", + "body": "package foo721\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tadmin std.Address = \"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"\n\tfoo = grc721.NewBasicNFT(\"FooNFT\", \"FNFT\")\n)\n\nfunc init() {\n\tmintNNFT(admin, 10) // @administrator (10)\n\tmintNNFT(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\", 5) // @hariom (5)\n}\n\nfunc mintNNFT(owner std.Address, n uint64) {\n\tcount := foo.TokenCount()\n\tfor i := count; i \u003c count+n; i++ {\n\t\ttid := grc721.TokenID(ufmt.Sprintf(\"%d\", i))\n\t\tfoo.Mint(owner, tid)\n\t}\n}\n\n// Getters\n\nfunc BalanceOf(user std.Address) uint64 {\n\tbalance, err := foo.BalanceOf(user)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn balance\n}\n\nfunc OwnerOf(tid grc721.TokenID) std.Address {\n\towner, err := foo.OwnerOf(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn owner\n}\n\nfunc IsApprovedForAll(owner, user std.Address) bool {\n\treturn foo.IsApprovedForAll(owner, user)\n}\n\nfunc GetApproved(tid grc721.TokenID) std.Address {\n\taddr, err := foo.GetApproved(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn addr\n}\n\n// Setters\n\nfunc Approve(user std.Address, tid grc721.TokenID) {\n\terr := foo.Approve(user, tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc SetApprovalForAll(user std.Address, approved bool) {\n\terr := foo.SetApprovalForAll(user, approved)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc TransferFrom(from, to std.Address, tid grc721.TokenID) {\n\terr := foo.TransferFrom(from, to, tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Admin\n\nfunc Mint(to std.Address, tid grc721.TokenID) {\n\tcaller := std.PreviousRealm().Address()\n\tassertIsAdmin(caller)\n\terr := foo.Mint(to, tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Burn(tid grc721.TokenID) {\n\tcaller := std.PreviousRealm().Address()\n\tassertIsAdmin(caller)\n\terr := foo.Burn(tid)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Render\n\nfunc Render(path string) string {\n\tswitch {\n\tcase path == \"\":\n\t\treturn foo.RenderHome()\n\tdefault:\n\t\treturn \"404\\n\"\n\t}\n}\n\n// Util\n\nfunc assertIsAdmin(address std.Address) {\n\tif address != admin {\n\t\tpanic(\"restricted access\")\n\t}\n}\n" + }, + { + "name": "foo721_test.gno", + "body": "package foo721\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/grc/grc721\"\n)\n\nfunc TestFoo721(t *testing.T) {\n\tadmin := std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\")\n\thariom := std.Address(\"g1var589z07ppjsjd24ukm4uguzwdt0tw7g47cgm\")\n\n\tfor _, tc := range []struct {\n\t\tname string\n\t\texpected any\n\t\tfn func() any\n\t}{\n\t\t{\"BalanceOf(admin)\", uint64(10), func() interface{} { return BalanceOf(admin) }},\n\t\t{\"BalanceOf(hariom)\", uint64(5), func() interface{} { return BalanceOf(hariom) }},\n\t\t{\"OwnerOf(0)\", admin, func() interface{} { return OwnerOf(grc721.TokenID(\"0\")) }},\n\t\t{\"IsApprovedForAll(admin, hariom)\", false, func() interface{} { return IsApprovedForAll(admin, hariom) }},\n\t} {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := tc.fn()\n\t\t\tif tc.expected != got {\n\t\t\t\tt.Errorf(\"expected: %v got: %v\", tc.expected, got)\n\t\t\t}\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "N/OInuqfOhxPbdb2USxwnOr0dZtw7bhE0870/hq8VMbnQ9VTxzZ3aK52r0pKjwIlz7bjC0Z5gsAn5sER68NcBA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "dice_roller", + "path": "gno.land/r/demo/games/dice_roller", + "files": [ + { + "name": "dice_roller.gno", + "body": "package dice_roller\n\nimport (\n\t\"errors\"\n\t\"math/rand\"\n\t\"sort\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/entropy\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n\n\t\"gno.land/r/sys/users\"\n)\n\ntype (\n\t// game represents a Dice Roller game between two players\n\tgame struct {\n\t\tplayer1, player2 std.Address\n\t\troll1, roll2 int\n\t}\n\n\t// player holds the information about each player including their stats\n\tplayer struct {\n\t\taddr std.Address\n\t\twins, losses, draws, points int\n\t}\n\n\t// leaderBoard is a slice of players, used to sort players by rank\n\tleaderBoard []player\n)\n\nconst (\n\t// Constants to represent game result outcomes\n\tongoing = iota\n\twin\n\tdraw\n\tloss\n)\n\nvar (\n\tgames avl.Tree // AVL tree for storing game states\n\tgameId seqid.ID // Sequence ID for games\n\n\tplayers avl.Tree // AVL tree for storing player data\n\n\tseed = uint64(entropy.New().Seed())\n\tr = rand.New(rand.NewPCG(seed, 0xdeadbeef))\n)\n\n// rollDice generates a random dice roll between 1 and 6\nfunc rollDice() int {\n\treturn r.IntN(6) + 1\n}\n\n// NewGame initializes a new game with the provided opponent's address\nfunc NewGame(addr std.Address) int {\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid opponent's address\")\n\t}\n\n\tgames.Set(gameId.Next().String(), \u0026game{\n\t\tplayer1: std.PreviousRealm().Address(),\n\t\tplayer2: addr,\n\t})\n\n\treturn int(gameId)\n}\n\n// Play allows a player to roll the dice and updates the game state accordingly\nfunc Play(idx int) int {\n\tg, err := getGame(idx)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\troll := rollDice() // Random the player's dice roll\n\n\t// Play the game and update the player's roll\n\tif err := g.play(std.PreviousRealm().Address(), roll); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// If both players have rolled, update the results and leaderboard\n\tif g.isFinished() {\n\t\t// If the player is playing against themselves, no points are awarded\n\t\tif g.player1 == g.player2 {\n\t\t\treturn roll\n\t\t}\n\n\t\tplayer1 := getPlayer(g.player1)\n\t\tplayer2 := getPlayer(g.player2)\n\n\t\tif g.roll1 \u003e g.roll2 {\n\t\t\tplayer1.updateStats(win)\n\t\t\tplayer2.updateStats(loss)\n\t\t} else if g.roll2 \u003e g.roll1 {\n\t\t\tplayer2.updateStats(win)\n\t\t\tplayer1.updateStats(loss)\n\t\t} else {\n\t\t\tplayer1.updateStats(draw)\n\t\t\tplayer2.updateStats(draw)\n\t\t}\n\t}\n\n\treturn roll\n}\n\n// play processes a player's roll and updates their score\nfunc (g *game) play(player std.Address, roll int) error {\n\tif player != g.player1 \u0026\u0026 player != g.player2 {\n\t\treturn errors.New(\"invalid player\")\n\t}\n\n\tif g.isFinished() {\n\t\treturn errors.New(\"game over\")\n\t}\n\n\tif player == g.player1 \u0026\u0026 g.roll1 == 0 {\n\t\tg.roll1 = roll\n\t\treturn nil\n\t}\n\n\tif player == g.player2 \u0026\u0026 g.roll2 == 0 {\n\t\tg.roll2 = roll\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"already played\")\n}\n\n// isFinished checks if the game has ended\nfunc (g *game) isFinished() bool {\n\treturn g.roll1 != 0 \u0026\u0026 g.roll2 != 0\n}\n\n// checkResult returns the game status as a formatted string\nfunc (g *game) status() string {\n\tif !g.isFinished() {\n\t\treturn resultIcon(ongoing) + \" Game still in progress\"\n\t}\n\n\tif g.roll1 \u003e g.roll2 {\n\t\treturn resultIcon(win) + \" Player1 Wins !\"\n\t} else if g.roll2 \u003e g.roll1 {\n\t\treturn resultIcon(win) + \" Player2 Wins !\"\n\t} else {\n\t\treturn resultIcon(draw) + \" It's a Draw !\"\n\t}\n}\n\n// Render provides a summary of the current state of games and leader board\nfunc Render(path string) string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(`# 🎲 **Dice Roller Game**\n\nWelcome to Dice Roller! Challenge your friends to a simple yet exciting dice rolling game. Roll the dice and see who gets the highest score !\n\n---\n\n## **How to Play**:\n1. **Create a game**: Challenge an opponent using [NewGame](./dice_roller$help\u0026func=NewGame)\n2. **Roll the dice**: Play your turn by rolling a dice using [Play](./dice_roller$help\u0026func=Play)\n\n---\n\n## **Scoring Rules**:\n- **Win** 🏆: +3 points\n- **Draw** 🤝: +1 point each\n- **Lose** ❌: No points\n- **Playing against yourself**: No points or stats changes for you\n\n---\n\n## **Recent Games**:\nBelow are the results from the most recent games. Up to 10 recent games are displayed\n\n| Game | Player 1 | 🎲 Roll 1 | Player 2 | 🎲 Roll 2 | 🏆 Winner |\n|------|----------|-----------|----------|-----------|-----------|\n`)\n\n\tmaxGames := 10\n\tfor n := int(gameId); n \u003e 0 \u0026\u0026 int(gameId)-n \u003c maxGames; n-- {\n\t\tg, err := getGame(n)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tsb.WriteString(strconv.Itoa(n) + \" | \" +\n\t\t\t\"\u003cspan title=\\\"\" + string(g.player1) + \"\\\"\u003e\" + shortName(g.player1) + \"\u003c/span\u003e\" + \" | \" + diceIcon(g.roll1) + \" | \" +\n\t\t\t\"\u003cspan title=\\\"\" + string(g.player2) + \"\\\"\u003e\" + shortName(g.player2) + \"\u003c/span\u003e\" + \" | \" + diceIcon(g.roll2) + \" | \" +\n\t\t\tg.status() + \"\\n\")\n\t}\n\n\tsb.WriteString(`\n---\n\n## **Leaderboard**:\nThe top players are ranked by performance. Games played against oneself are not counted in the leaderboard\n\n| Rank | Player | Wins | Losses | Draws | Points |\n|------|-----------------------|------|--------|-------|--------|\n`)\n\n\tfor i, player := range getLeaderBoard() {\n\t\tsb.WriteString(ufmt.Sprintf(\"| %s | \u003cspan title=\\\"\"+string(player.addr)+\"\\\"\u003e**%s**\u003c/span\u003e | %d | %d | %d | %d |\\n\",\n\t\t\trankIcon(i+1),\n\t\t\tshortName(player.addr),\n\t\t\tplayer.wins,\n\t\t\tplayer.losses,\n\t\t\tplayer.draws,\n\t\t\tplayer.points,\n\t\t))\n\t}\n\n\tsb.WriteString(\"\\n---\\n**Good luck and have fun !** 🎉\")\n\treturn sb.String()\n}\n\n// shortName returns a shortened name for the given address\nfunc shortName(addr std.Address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user != nil {\n\t\treturn user.Name()\n\t}\n\tif len(addr) \u003c 10 {\n\t\treturn string(addr)\n\t}\n\treturn string(addr)[:10] + \"...\"\n}\n\n// getGame retrieves the game state by its ID\nfunc getGame(idx int) (*game, error) {\n\tv, ok := games.Get(seqid.ID(idx).String())\n\tif !ok {\n\t\treturn nil, errors.New(\"game not found\")\n\t}\n\treturn v.(*game), nil\n}\n\n// updateResult updates the player's stats and points based on the game outcome\nfunc (p *player) updateStats(result int) {\n\tswitch result {\n\tcase win:\n\t\tp.wins++\n\t\tp.points += 3\n\tcase loss:\n\t\tp.losses++\n\tcase draw:\n\t\tp.draws++\n\t\tp.points++\n\t}\n}\n\n// getPlayer retrieves a player or initializes a new one if they don't exist\nfunc getPlayer(addr std.Address) *player {\n\tv, ok := players.Get(addr.String())\n\tif !ok {\n\t\tplayer := \u0026player{\n\t\t\taddr: addr,\n\t\t}\n\t\tplayers.Set(addr.String(), player)\n\t\treturn player\n\t}\n\n\treturn v.(*player)\n}\n\n// getLeaderBoard generates a leaderboard sorted by points\nfunc getLeaderBoard() leaderBoard {\n\tboard := leaderBoard{}\n\tplayers.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tplayer := value.(*player)\n\t\tboard = append(board, *player)\n\t\treturn false\n\t})\n\n\tsort.Sort(board)\n\n\treturn board\n}\n\n// Methods for sorting the leaderboard\nfunc (r leaderBoard) Len() int {\n\treturn len(r)\n}\n\nfunc (r leaderBoard) Less(i, j int) bool {\n\tif r[i].points != r[j].points {\n\t\treturn r[i].points \u003e r[j].points\n\t}\n\n\tif r[i].wins != r[j].wins {\n\t\treturn r[i].wins \u003e r[j].wins\n\t}\n\n\tif r[i].draws != r[j].draws {\n\t\treturn r[i].draws \u003e r[j].draws\n\t}\n\n\treturn false\n}\n\nfunc (r leaderBoard) Swap(i, j int) {\n\tr[i], r[j] = r[j], r[i]\n}\n" + }, + { + "name": "dice_roller_test.gno", + "body": "package dice_roller\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\tplayer1 = testutils.TestAddress(\"alice\")\n\tplayer2 = testutils.TestAddress(\"bob\")\n\tunknownPlayer = testutils.TestAddress(\"unknown\")\n)\n\n// resetGameState resets the game state for testing\nfunc resetGameState() {\n\tgames = avl.Tree{}\n\tgameId = seqid.ID(0)\n\tplayers = avl.Tree{}\n}\n\n// TestNewGame tests the initialization of a new game\nfunc TestNewGame(t *testing.T) {\n\tresetGameState()\n\n\ttesting.SetOriginCaller(player1)\n\tgameID := NewGame(player2)\n\n\t// Verify that the game has been correctly initialized\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\turequire.Equal(t, player1.String(), g.player1.String())\n\turequire.Equal(t, player2.String(), g.player2.String())\n\turequire.Equal(t, 0, g.roll1)\n\turequire.Equal(t, 0, g.roll2)\n}\n\n// TestPlay tests the dice rolling functionality for both players\nfunc TestPlay(t *testing.T) {\n\tresetGameState()\n\n\ttesting.SetOriginCaller(player1)\n\tgameID := NewGame(player2)\n\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\n\t// Simulate rolling dice for player 1\n\troll1 := Play(gameID)\n\n\t// Verify player 1's roll\n\turequire.NotEqual(t, 0, g.roll1)\n\turequire.Equal(t, g.roll1, roll1)\n\turequire.Equal(t, 0, g.roll2) // Player 2 hasn't rolled yet\n\n\t// Simulate rolling dice for player 2\n\ttesting.SetOriginCaller(player2)\n\troll2 := Play(gameID)\n\n\t// Verify player 2's roll\n\turequire.NotEqual(t, 0, g.roll2)\n\turequire.Equal(t, g.roll1, roll1)\n\turequire.Equal(t, g.roll2, roll2)\n}\n\n// TestPlayAgainstSelf tests the scenario where a player plays against themselves\nfunc TestPlayAgainstSelf(t *testing.T) {\n\tresetGameState()\n\n\ttesting.SetOriginCaller(player1)\n\tgameID := NewGame(player1)\n\n\t// Simulate rolling dice twice by the same player\n\troll1 := Play(gameID)\n\troll2 := Play(gameID)\n\n\tg, err := getGame(gameID)\n\turequire.NoError(t, err)\n\turequire.Equal(t, g.roll1, roll1)\n\turequire.Equal(t, g.roll2, roll2)\n}\n\n// TestPlayInvalidPlayer tests the scenario where an invalid player tries to play\nfunc TestPlayInvalidPlayer(t *testing.T) {\n\tresetGameState()\n\n\ttesting.SetOriginCaller(player1)\n\tgameID := NewGame(player1)\n\n\t// Attempt to play as an invalid player\n\ttesting.SetOriginCaller(unknownPlayer)\n\turequire.PanicsWithMessage(t, \"invalid player\", func() {\n\t\tPlay(gameID)\n\t})\n}\n\n// TestPlayAlreadyPlayed tests the scenario where a player tries to play again after already playing\nfunc TestPlayAlreadyPlayed(t *testing.T) {\n\tresetGameState()\n\n\ttesting.SetOriginCaller(player1)\n\tgameID := NewGame(player2)\n\n\t// Player 1 rolls\n\tPlay(gameID)\n\n\t// Player 1 tries to roll again\n\turequire.PanicsWithMessage(t, \"already played\", func() {\n\t\tPlay(gameID)\n\t})\n}\n\n// TestPlayBeyondGameEnd tests that playing after both players have finished their rolls fails\nfunc TestPlayBeyondGameEnd(t *testing.T) {\n\tresetGameState()\n\n\ttesting.SetOriginCaller(player1)\n\tgameID := NewGame(player2)\n\n\t// Play for both players\n\ttesting.SetOriginCaller(player1)\n\tPlay(gameID)\n\ttesting.SetOriginCaller(player2)\n\tPlay(gameID)\n\n\t// Check if the game is over\n\t_, err := getGame(gameID)\n\turequire.NoError(t, err)\n\n\t// Attempt to play more should fail\n\ttesting.SetOriginCaller(player1)\n\turequire.PanicsWithMessage(t, \"game over\", func() {\n\t\tPlay(gameID)\n\t})\n}\n" + }, + { + "name": "icon.gno", + "body": "package dice_roller\n\nimport (\n\t\"strconv\"\n)\n\n// diceIcon returns an icon of the dice roll\nfunc diceIcon(roll int) string {\n\tswitch roll {\n\tcase 1:\n\t\treturn \"🎲1\"\n\tcase 2:\n\t\treturn \"🎲2\"\n\tcase 3:\n\t\treturn \"🎲3\"\n\tcase 4:\n\t\treturn \"🎲4\"\n\tcase 5:\n\t\treturn \"🎲5\"\n\tcase 6:\n\t\treturn \"🎲6\"\n\tdefault:\n\t\treturn \"❓\"\n\t}\n}\n\n// resultIcon returns the icon representing the result of a game\nfunc resultIcon(result int) string {\n\tswitch result {\n\tcase ongoing:\n\t\treturn \"🔄\"\n\tcase win:\n\t\treturn \"🏆\"\n\tcase loss:\n\t\treturn \"❌\"\n\tcase draw:\n\t\treturn \"🤝\"\n\tdefault:\n\t\treturn \"❓\"\n\t}\n}\n\n// rankIcon returns the icon for a player's rank\nfunc rankIcon(rank int) string {\n\tswitch rank {\n\tcase 1:\n\t\treturn \"🥇\"\n\tcase 2:\n\t\treturn \"🥈\"\n\tcase 3:\n\t\treturn \"🥉\"\n\tdefault:\n\t\treturn strconv.Itoa(rank)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "sA8tVyn31c04NhoYtKLADkWPwYoJfSt93LL25HEJL28sdp0fjJSxT1dKLZx6174ZlEhqk+cgFCI7JtlyQj0zDg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "shifumi", + "path": "gno.land/r/demo/games/shifumi", + "files": [ + { + "name": "shifumi.gno", + "body": "package shifumi\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nconst (\n\tempty = iota\n\trock\n\tpaper\n\tscissors\n\tlast\n)\n\ntype game struct {\n\tplayer1, player2 std.Address // shifumi is a 2 players game\n\tmove1, move2 int // can be empty, rock, paper, or scissors\n}\n\nvar games avl.Tree\nvar id seqid.ID\n\nfunc (g *game) play(player std.Address, move int) error {\n\tif !(move \u003e empty \u0026\u0026 move \u003c last) {\n\t\treturn errors.New(\"invalid move\")\n\t}\n\tif player != g.player1 \u0026\u0026 player != g.player2 {\n\t\treturn errors.New(\"invalid player\")\n\t}\n\tif player == g.player1 \u0026\u0026 g.move1 == empty {\n\t\tg.move1 = move\n\t\treturn nil\n\t}\n\tif player == g.player2 \u0026\u0026 g.move2 == empty {\n\t\tg.move2 = move\n\t\treturn nil\n\t}\n\treturn errors.New(\"already played\")\n}\n\nfunc (g *game) winner() int {\n\tif g.move1 == empty || g.move2 == empty {\n\t\treturn -1\n\t}\n\tif g.move1 == g.move2 {\n\t\treturn 0\n\t}\n\tif g.move1 == rock \u0026\u0026 g.move2 == scissors ||\n\t\tg.move1 == paper \u0026\u0026 g.move2 == rock ||\n\t\tg.move1 == scissors \u0026\u0026 g.move2 == paper {\n\t\treturn 1\n\t}\n\treturn 2\n}\n\n// NewGame creates a new game where player1 is the caller and player2 the argument.\n// A new game index is returned.\nfunc NewGame(player std.Address) int {\n\tgames.Set(id.Next().String(), \u0026game{player1: std.PreviousRealm().Address(), player2: player})\n\treturn int(id)\n}\n\n// Play executes a move for the game at index idx, where move can be:\n// 1 (rock), 2 (paper), 3 (scissors).\nfunc Play(idx, move int) {\n\tv, ok := games.Get(seqid.ID(idx).String())\n\tif !ok {\n\t\tpanic(\"game not found\")\n\t}\n\tif err := v.(*game).play(std.PreviousRealm().Address(), move); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Render(path string) string {\n\tmov1 := []string{\"\", \" 🤜 \", \" 🫱 \", \" 👉 \"}\n\tmov2 := []string{\"\", \" 🤛 \", \" 🫲 \", \" 👈 \"}\n\twin := []string{\"pending\", \"draw\", \"player1\", \"player2\"}\n\n\toutput := `# 👊 ✋ ✌️ Shifumi\nActions:\n* [NewGame](shifumi$help\u0026func=NewGame) opponentAddress\n* [Play](shifumi$help\u0026func=Play) gameIndex move (1=rock, 2=paper, 3=scissors)\n\n game | player1 | | player2 | | win \n --- | --- | --- | --- | --- | ---\n`\n\t// Output the 100 most recent games.\n\tmaxGames := 100\n\tfor n := int(id); n \u003e 0 \u0026\u0026 int(id)-n \u003c maxGames; n-- {\n\t\tv, ok := games.Get(seqid.ID(n).String())\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tg := v.(*game)\n\t\toutput += strconv.Itoa(n) + \" | \" +\n\t\t\tshortName(g.player1) + \" | \" + mov1[g.move1] + \" | \" +\n\t\t\tshortName(g.player2) + \" | \" + mov2[g.move2] + \" | \" +\n\t\t\twin[g.winner()+1] + \"\\n\"\n\t}\n\treturn output\n}\n\nfunc shortName(addr std.Address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user != nil {\n\t\treturn user.Name()\n\t}\n\tif len(addr) \u003c 10 {\n\t\treturn string(addr)\n\t}\n\treturn string(addr)[:10] + \"...\"\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "3Nh2AMgjXKNBKIbbUAiGnnll+rd5IanhaIKSNfqHzhMsG6uqBjQ8yKCpRJCJ9KgXPUVmGCtXC+felPI7YXFmDg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "groups", + "path": "gno.land/r/demo/groups", + "files": [ + { + "name": "group.gno", + "body": "package groups\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype GroupID uint64\n\nfunc (gid GroupID) String() string {\n\treturn strconv.Itoa(int(gid))\n}\n\ntype Group struct {\n\tid GroupID\n\turl string\n\tname string\n\tlastMemberID MemberID\n\tmembers avl.Tree\n\tcreator std.Address\n\tcreatedAt time.Time\n}\n\nfunc newGroup(url string, name string, creator std.Address) *Group {\n\tif !reName.MatchString(name) {\n\t\tpanic(\"invalid name: \" + name)\n\t}\n\tif gGroupsByName.Has(name) {\n\t\tpanic(\"Group with such name already exists\")\n\t}\n\treturn \u0026Group{\n\t\tid: incGetGroupID(),\n\t\turl: url,\n\t\tname: name,\n\t\tcreator: creator,\n\t\tmembers: avl.Tree{},\n\t\tcreatedAt: time.Now(),\n\t}\n}\n\nfunc (group *Group) newMember(id MemberID, address std.Address, weight int, metadata string) *Member {\n\tif group.members.Has(address.String()) {\n\t\tpanic(\"this member for this group already exists\")\n\t}\n\treturn \u0026Member{\n\t\tid: id,\n\t\taddress: address,\n\t\tweight: weight,\n\t\tmetadata: metadata,\n\t\tcreatedAt: time.Now(),\n\t}\n}\n\nfunc (group *Group) HasPermission(addr std.Address, perm Permission) bool {\n\tif group.creator != addr {\n\t\treturn false\n\t}\n\treturn isValidPermission(perm)\n}\n\nfunc (group *Group) RenderGroup() string {\n\tstr := \"Group ID: \" + groupIDKey(group.id) + \"\\n\\n\" +\n\t\t\"Group Name: \" + group.name + \"\\n\\n\" +\n\t\t\"Group Creator: \" + usernameOf(group.creator) + \"\\n\\n\" +\n\t\t\"Group createdAt: \" + group.createdAt.String() + \"\\n\\n\" +\n\t\t\"Group Last MemberID: \" + memberIDKey(group.lastMemberID) + \"\\n\\n\"\n\n\tstr += \"Group Members: \\n\\n\"\n\tgroup.members.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\tmember := value.(*Member)\n\t\tstr += member.getMemberStr()\n\t\treturn false\n\t})\n\treturn str\n}\n\nfunc (group *Group) deleteGroup() {\n\tgidkey := groupIDKey(group.id)\n\t_, gGroupsRemoved := gGroups.Remove(gidkey)\n\tif !gGroupsRemoved {\n\t\tpanic(\"group does not exist with id \" + group.id.String())\n\t}\n\tgGroupsByName.Remove(group.name)\n}\n\nfunc (group *Group) deleteMember(mid MemberID) {\n\tgidkey := groupIDKey(group.id)\n\tif !gGroups.Has(gidkey) {\n\t\tpanic(\"group does not exist with id \" + group.id.String())\n\t}\n\n\tg := getGroup(group.id)\n\tmidkey := memberIDKey(mid)\n\tg.members.Remove(midkey)\n}\n" + }, + { + "name": "groups.gno", + "body": "package groups\n\nimport (\n\t\"regexp\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n//----------------------------------------\n// Realm (package) state\n\nvar (\n\tgGroups avl.Tree // id -\u003e *Group\n\tgGroupsCtr int // increments Group.id\n\tgGroupsByName avl.Tree // name -\u003e *Group\n)\n\n//----------------------------------------\n// Constants\n\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)\n" + }, + { + "name": "member.gno", + "body": "package groups\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n)\n\ntype MemberID uint64\n\ntype Member struct {\n\tid MemberID\n\taddress std.Address\n\tweight int\n\tmetadata string\n\tcreatedAt time.Time\n}\n\nfunc (mid MemberID) String() string {\n\treturn strconv.Itoa(int(mid))\n}\n\nfunc (member *Member) getMemberStr() string {\n\tmemberDataStr := \"\"\n\tmemberDataStr += \"\\t\\t\\t[\" + memberIDKey(member.id) + \", \" + member.address.String() + \", \" + strconv.Itoa(member.weight) + \", \" + member.metadata + \", \" + member.createdAt.String() + \"],\\n\\n\"\n\treturn memberDataStr\n}\n" + }, + { + "name": "misc.gno", + "body": "package groups\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n//----------------------------------------\n// private utility methods\n// XXX ensure these cannot be called from public.\n\nfunc getGroup(gid GroupID) *Group {\n\tgidkey := groupIDKey(gid)\n\tgroup_, exists := gGroups.Get(gidkey)\n\tif !exists {\n\t\tpanic(\"group id (\" + gid.String() + \") does not exists\")\n\t}\n\tgroup := group_.(*Group)\n\treturn group\n}\n\nfunc incGetGroupID() GroupID {\n\tgGroupsCtr++\n\treturn GroupID(gGroupsCtr)\n}\n\nfunc padLeft(str string, length int) string {\n\tif len(str) \u003e= length {\n\t\treturn str\n\t}\n\treturn strings.Repeat(\" \", length-len(str)) + str\n}\n\nfunc padZero(u64 uint64, length int) string {\n\tstr := strconv.Itoa(int(u64))\n\tif len(str) \u003e= length {\n\t\treturn str\n\t}\n\treturn strings.Repeat(\"0\", length-len(str)) + str\n}\n\nfunc groupIDKey(gid GroupID) string {\n\treturn padZero(uint64(gid), 10)\n}\n\nfunc memberIDKey(mid MemberID) string {\n\treturn padZero(uint64(mid), 10)\n}\n\nfunc indentBody(indent string, body string) string {\n\tlines := strings.Split(body, \"\\n\")\n\tres := \"\"\n\tfor i, line := range lines {\n\t\tif i \u003e 0 {\n\t\t\tres += \"\\n\"\n\t\t}\n\t\tres += indent + line\n\t}\n\treturn res\n}\n\n// NOTE: length must be greater than 3.\nfunc summaryOf(str string, length int) string {\n\tlines := strings.SplitN(str, \"\\n\", 2)\n\tline := lines[0]\n\tif len(line) \u003e length {\n\t\tline = line[:(length-3)] + \"...\"\n\t} else if len(lines) \u003e 1 {\n\t\t// len(line) \u003c= 80\n\t\tline = line + \"...\"\n\t}\n\treturn line\n}\n\nfunc displayAddressMD(addr std.Address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user == nil {\n\t\treturn \"[\" + addr.String() + \"](/u/\" + addr.String() + \")\"\n\t}\n\treturn \"[@\" + user.Name() + \"](/r/gnoland/users/v1:\" + user.Name() + \")\"\n}\n\nfunc usernameOf(addr std.Address) string {\n\tuser := users.ResolveAddress(addr)\n\tif user == nil {\n\t\tpanic(\"user not found\")\n\t}\n\treturn user.Name()\n}\n\nfunc isValidPermission(perm Permission) bool {\n\treturn perm == EditPermission || perm == DeletePermission\n}\n" + }, + { + "name": "public.gno", + "body": "package groups\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n//----------------------------------------\n// Public facing functions\n\nfunc GetGroupIDFromName(name string) (GroupID, bool) {\n\tgroupI, exists := gGroupsByName.Get(name)\n\tif !exists {\n\t\treturn 0, false\n\t}\n\treturn groupI.(*Group).id, true\n}\n\nfunc CreateGroup(name string) GroupID {\n\tstd.AssertOriginCall()\n\tcaller := std.OriginCaller()\n\tusernameOf(caller)\n\turl := \"/r/demo/groups:\" + name\n\tgroup := newGroup(url, name, caller)\n\tgidkey := groupIDKey(group.id)\n\tgGroups.Set(gidkey, group)\n\tgGroupsByName.Set(name, group)\n\treturn group.id\n}\n\nfunc AddMember(gid GroupID, address string, weight int, metadata string) MemberID {\n\tstd.AssertOriginCall()\n\tcaller := std.OriginCaller()\n\tusernameOf(caller)\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, EditPermission) {\n\t\tpanic(\"unauthorized to edit group\")\n\t}\n\tuser := users.ResolveAddress(std.Address(address))\n\tif user == nil {\n\t\tpanic(\"unknown address \" + address)\n\t}\n\tmid := group.lastMemberID\n\tmember := group.newMember(mid, std.Address(address), weight, metadata)\n\tmidkey := memberIDKey(mid)\n\tgroup.members.Set(midkey, member)\n\tmid++\n\tgroup.lastMemberID = mid\n\treturn member.id\n}\n\nfunc DeleteGroup(gid GroupID) {\n\tstd.AssertOriginCall()\n\tcaller := std.OriginCaller()\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, DeletePermission) {\n\t\tpanic(\"unauthorized to delete group\")\n\t}\n\tgroup.deleteGroup()\n}\n\nfunc DeleteMember(gid GroupID, mid MemberID) {\n\tstd.AssertOriginCall()\n\tcaller := std.OriginCaller()\n\tgroup := getGroup(gid)\n\tif !group.HasPermission(caller, DeletePermission) {\n\t\tpanic(\"unauthorized to delete member\")\n\t}\n\tgroup.deleteMember(mid)\n}\n" + }, + { + "name": "render.gno", + "body": "package groups\n\nimport (\n\t\"strings\"\n)\n\n//----------------------------------------\n// Render functions\n\nfunc RenderGroup(gid GroupID) string {\n\tgroup := getGroup(gid)\n\tif group == nil {\n\t\treturn \"missing Group\"\n\t}\n\treturn group.RenderGroup()\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\tstr := \"List of all Groups:\\n\\n\"\n\t\tgGroups.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tgroup := value.(*Group)\n\t\t\tstr += \" * [\" + group.name + \"](\" + group.url + \")\\n\"\n\t\t\treturn false\n\t\t})\n\t\treturn str\n\t}\n\tparts := strings.Split(path, \"/\")\n\tif len(parts) == 1 {\n\t\t// /r/demo/groups:Group_NAME\n\t\tname := parts[0]\n\t\tgroupI, exists := gGroupsByName.Get(name)\n\t\tif !exists {\n\t\t\treturn \"Group does not exist: \" + name\n\t\t}\n\t\treturn groupI.(*Group).RenderGroup()\n\t} else {\n\t\treturn \"unrecognized path \" + path\n\t}\n}\n" + }, + { + "name": "role.gno", + "body": "package groups\n\ntype Permission string\n\nconst (\n\tDeletePermission Permission = \"role:delete\"\n\tEditPermission Permission = \"role:edit\"\n)\n" + }, + { + "name": "z_0_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// user not found\n" + }, + { + "name": "z_0_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/groups\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Output:\n// 1\n// List of all Groups:\n//\n// * [test_group](/r/demo/groups:test_group)\n//\n" + }, + { + "name": "z_1_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar gid groups.GroupID\n\nconst admin = std.Address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\")\n\nfunc main() {\n\tcaller := std.OriginCaller() // main\n\ttesting.SetRealm(std.NewUserRealm(caller))\n\tusers.Register(\"main123\")\n\n\ttest1 := testutils.TestAddress(\"gnouser1\")\n\ttesting.SetOriginCaller(test1)\n\ttesting.SetRealm(std.NewUserRealm(test1))\n\tusers.Register(\"test123\")\n\n\ttest2 := testutils.TestAddress(\"gnouser2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetRealm(std.NewUserRealm(test2))\n\tusers.Register(\"test223\")\n\n\ttest3 := testutils.TestAddress(\"gnouser3\")\n\ttesting.SetOriginCaller(test3)\n\ttesting.SetRealm(std.NewUserRealm(test3))\n\tusers.Register(\"test323\")\n\n\ttesting.SetOriginCaller(caller)\n\ttesting.SetRealm(std.NewUserRealm(caller))\n\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\tgroups.AddMember(gid, test3.String(), 32, \"i am from UAE\")\n\tprintln(groups.Render(\"test_group\"))\n}\n\n// Output:\n// 1\n// Group ID: 0000000001\n//\n// Group Name: test_group\n//\n// Group Creator: main123\n//\n// Group createdAt: 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\n//\n// Group Last MemberID: 0000000001\n//\n// Group Members:\n//\n// \t\t\t[0000000000, g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy, 32, i am from UAE, 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001],\n//\n//\n" + }, + { + "name": "z_1_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/groups\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tcaller := std.OriginCaller()\n\ttesting.SetRealm(std.NewUserRealm(caller))\n\tusers.Register(\"gnouser123\")\n\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.AddMember(2, \"g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy\", 55, \"metadata3\")\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// group id (2) does not exists\n" + }, + { + "name": "z_1_c_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\t// add member via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 9000000}})\n\n\tgroups.AddMember(gid, test2.String(), 42, \"metadata3\")\n}\n\n// Error:\n// user not found\n" + }, + { + "name": "z_2_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar gid groups.GroupID\n\nconst admin = std.Address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\")\n\nfunc main() {\n\tcaller := std.OriginCaller() // main\n\ttesting.SetRealm(std.NewUserRealm(caller))\n\tusers.Register(\"main123\")\n\n\ttest1 := testutils.TestAddress(\"gnouser1\")\n\ttesting.SetOriginCaller(test1)\n\ttesting.SetRealm(std.NewUserRealm(test1))\n\tusers.Register(\"test123\")\n\n\ttest2 := testutils.TestAddress(\"gnouser2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetRealm(std.NewUserRealm(test2))\n\tusers.Register(\"test223\")\n\n\ttest3 := testutils.TestAddress(\"gnouser3\")\n\ttesting.SetOriginCaller(test3)\n\ttesting.SetRealm(std.NewUserRealm(test3))\n\tusers.Register(\"test323\")\n\n\ttesting.SetOriginCaller(caller)\n\ttesting.SetRealm(std.NewUserRealm(caller))\n\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(groups.Render(\"test_group\"))\n\n\tgroups.AddMember(gid, test2.String(), 42, \"metadata3\")\n\n\tgroups.DeleteMember(gid, 0)\n\tprintln(groups.RenderGroup(gid))\n}\n\n// Output:\n// Group ID: 0000000001\n//\n// Group Name: test_group\n//\n// Group Creator: main123\n//\n// Group createdAt: 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\n//\n// Group Last MemberID: 0000000000\n//\n// Group Members:\n//\n//\n// Group ID: 0000000001\n//\n// Group Name: test_group\n//\n// Group Creator: main123\n//\n// Group createdAt: 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\n//\n// Group Last MemberID: 0000000001\n//\n// Group Members:\n//\n//\n" + }, + { + "name": "z_2_b_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"gno.land/r/demo/groups\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"gnouser123\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.DeleteMember(2, 0)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// r/gnoland/users: non-user call\n" + }, + { + "name": "z_2_d_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 200000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tusers.Register(\"gnouser123\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\t// delete member via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 9000000}})\n\n\tgroups.DeleteMember(gid, 0)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// r/gnoland/users: non-user call\n" + }, + { + "name": "z_2_e_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/groups\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\ttesting.SetRealm(std.NewUserRealm(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))) // so that CurrentRealm.Addr() matches OrigCaller\n\tusers.Register(\"gnouser123\")\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\tgroups.DeleteGroup(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Output:\n// 1\n// List of all Groups:\n//\n//\n" + }, + { + "name": "z_2_f_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/demo/groups\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tcaller := std.OriginCaller()\n\ttesting.SetRealm(std.NewUserRealm(caller))\n\tusers.Register(\"gnouser123\")\n\n\tgid = groups.CreateGroup(\"test_group\")\n\n\tprintln(gid)\n\tgroups.DeleteGroup(20)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// group id (20) does not exists\n" + }, + { + "name": "z_2_g_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/groups_test\npackage groups_test\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/groups\"\n\tusers \"gno.land/r/gnoland/users/v1\"\n)\n\nvar gid groups.GroupID\n\nfunc main() {\n\tcaller := std.OriginCaller()\n\ttesting.SetRealm(std.NewUserRealm(caller))\n\tusers.Register(\"gnouser123\")\n\n\tgid = groups.CreateGroup(\"test_group\")\n\tprintln(gid)\n\n\t// delete group via anon user\n\ttest2 := testutils.TestAddress(\"test2\")\n\ttesting.SetOriginCaller(test2)\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 9000000}})\n\n\tgroups.DeleteGroup(gid)\n\tprintln(groups.Render(\"\"))\n}\n\n// Error:\n// unauthorized to delete group\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "T/SXPq+RGyJ+NUbqLYM5x28wkkAaWhGrs3khhHONoZOoQPUHqxqbW/o0vr8D++D5xMq52FcbPdIGShAjuRmtCA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "keystore", + "path": "gno.land/r/demo/keystore", + "files": [ + { + "name": "keystore.gno", + "body": "package keystore\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar data avl.Tree\n\nconst (\n\tBaseURL = \"/r/demo/keystore\"\n\tStatusOK = \"ok\"\n\tStatusNoUser = \"user not found\"\n\tStatusNotFound = \"key not found\"\n\tStatusNoWriteAccess = \"no write access\"\n\tStatusCouldNotExecute = \"could not execute\"\n\tStatusNoDatabases = \"no databases\"\n)\n\nfunc init() {\n\tdata = avl.Tree{} // user -\u003e avl.Tree\n}\n\n// KeyStore stores the owner-specific avl.Tree\ntype KeyStore struct {\n\tOwner std.Address\n\tData avl.Tree\n}\n\n// Set will set a value to a key\n// requires write-access (original caller must be caller)\nfunc Set(k, v string) string {\n\torigOwner := std.OriginCaller()\n\treturn set(origOwner.String(), k, v)\n}\n\n// set (private) will set a key to value\n// requires write-access (original caller must be caller)\nfunc set(owner, k, v string) string {\n\torigOwner := std.OriginCaller()\n\tif origOwner.String() != owner {\n\t\treturn StatusNoWriteAccess\n\t}\n\tvar keystore *KeyStore\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\tkeystore = \u0026KeyStore{\n\t\t\tOwner: origOwner,\n\t\t\tData: avl.Tree{},\n\t\t}\n\t\tdata.Set(owner, keystore)\n\t} else {\n\t\tkeystore = keystoreInterface.(*KeyStore)\n\t}\n\tkeystore.Data.Set(k, v)\n\treturn StatusOK\n}\n\n// Remove removes a key\n// requires write-access (original owner must be caller)\nfunc Remove(k string) string {\n\torigOwner := std.OriginCaller()\n\treturn remove(origOwner.String(), k)\n}\n\n// remove (private) removes a key\n// requires write-access (original owner must be caller)\nfunc remove(owner, k string) string {\n\torigOwner := std.OriginCaller()\n\tif origOwner.String() != owner {\n\t\treturn StatusNoWriteAccess\n\t}\n\tvar keystore *KeyStore\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\tkeystore = \u0026KeyStore{\n\t\t\tOwner: origOwner,\n\t\t\tData: avl.Tree{},\n\t\t}\n\t\tdata.Set(owner, keystore)\n\t} else {\n\t\tkeystore = keystoreInterface.(*KeyStore)\n\t}\n\t_, removed := keystore.Data.Remove(k)\n\tif !removed {\n\t\treturn StatusCouldNotExecute\n\t}\n\treturn StatusOK\n}\n\n// Get returns a value for a key\n// read-only\nfunc Get(k string) string {\n\torigOwner := std.OriginCaller()\n\treturn remove(origOwner.String(), k)\n}\n\n// get (private) returns a value for a key\n// read-only\nfunc get(owner, k string) string {\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\treturn StatusNoUser\n\t}\n\tkeystore := keystoreInterface.(*KeyStore)\n\tval, found := keystore.Data.Get(k)\n\tif !found {\n\t\treturn StatusNotFound\n\t}\n\treturn val.(string)\n}\n\n// Size returns size of database\n// read-only\nfunc Size() string {\n\torigOwner := std.OriginCaller()\n\treturn size(origOwner.String())\n}\n\nfunc size(owner string) string {\n\tkeystoreInterface, exists := data.Get(owner)\n\tif !exists {\n\t\treturn StatusNoUser\n\t}\n\tkeystore := keystoreInterface.(*KeyStore)\n\treturn ufmt.Sprintf(\"%d\", keystore.Data.Size())\n}\n\n// Render provides read-only url access to the functions of the keystore\n// \"\" -\u003e show all keystores listed by owner\n// \"owner\" -\u003e show all keys for that owner's keystore\n// \"owner:size\" -\u003e returns size of owner's keystore\n// \"owner:get:key\" -\u003e show value for that key in owner's keystore\nfunc Render(p string) string {\n\tvar response string\n\targs := strings.Split(p, \":\")\n\tnumArgs := len(args)\n\tif p == \"\" {\n\t\tnumArgs = 0\n\t}\n\tswitch numArgs {\n\tcase 0:\n\t\tif data.Size() == 0 {\n\t\t\treturn StatusNoDatabases\n\t\t}\n\t\tdata.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tks := value.(*KeyStore)\n\t\t\tresponse += ufmt.Sprintf(\"- [%s](%s:%s) (%d keys)\\n\", ks.Owner, BaseURL, ks.Owner, ks.Data.Size())\n\t\t\treturn false\n\t\t})\n\tcase 1:\n\t\towner := args[0]\n\t\tkeystoreInterface, exists := data.Get(owner)\n\t\tif !exists {\n\t\t\treturn StatusNoUser\n\t\t}\n\t\tks := keystoreInterface.(*KeyStore)\n\t\ti := 0\n\t\tresponse += ufmt.Sprintf(\"# %s database\\n\\n\", ks.Owner)\n\t\tks.Data.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tresponse += ufmt.Sprintf(\"- %d [%s](%s:%s:get:%s)\\n\", i, key, BaseURL, ks.Owner, key)\n\t\t\ti++\n\t\t\treturn false\n\t\t})\n\tcase 2:\n\t\towner := args[0]\n\t\tcmd := args[1]\n\t\tif cmd == \"size\" {\n\t\t\treturn size(owner)\n\t\t}\n\tcase 3:\n\t\towner := args[0]\n\t\tcmd := args[1]\n\t\tkey := args[2]\n\t\tif cmd == \"get\" {\n\t\t\treturn get(owner, key)\n\t\t}\n\t}\n\n\treturn response\n}\n" + }, + { + "name": "keystore_test.gno", + "body": "package keystore\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc TestRender(t *testing.T) {\n\tvar (\n\t\tauthor1 std.Address = testutils.TestAddress(\"author1\")\n\t\tauthor2 std.Address = testutils.TestAddress(\"author2\")\n\t)\n\n\ttt := []struct {\n\t\tcaller std.Address\n\t\towner std.Address\n\t\tps []string\n\t\texp string\n\t}{\n\t\t// can set database if the owner is the caller\n\t\t{author1, author1, []string{\"set\", \"hello\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t{author1, author1, []string{\"set\", \"hello\", \"world\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t{author1, author1, []string{\"set\", \"hi\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"2\"},\n\t\t// only owner can remove\n\t\t{author1, author1, []string{\"remove\", \"hi\"}, StatusOK},\n\t\t{author1, author1, []string{\"get\", \"hi\"}, StatusNotFound},\n\t\t{author1, author1, []string{\"size\"}, \"1\"},\n\t\t// add back\n\t\t{author1, author1, []string{\"set\", \"hi\", \"gno\"}, StatusOK},\n\t\t{author1, author1, []string{\"size\"}, \"2\"},\n\n\t\t// different owner has different database\n\t\t{author2, author2, []string{\"set\", \"hello\", \"universe\"}, StatusOK},\n\t\t// either author can get the other info\n\t\t{author1, author2, []string{\"get\", \"hello\"}, \"universe\"},\n\t\t// either author can get the other info\n\t\t{author2, author1, []string{\"get\", \"hello\"}, \"world\"},\n\t\t{author1, author2, []string{\"get\", \"hello\"}, \"universe\"},\n\t\t// anyone can view the databases\n\t\t{author1, author2, []string{}, `- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/keystore:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6) (2 keys)\n- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00) (1 keys)`},\n\t\t// anyone can view the keys in a database\n\t\t{author1, author2, []string{\"\"}, `# g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00 database\n\n- 0 [hello](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00:get:hello)`},\n\t}\n\tfor _, tc := range tt {\n\t\tp := \"\"\n\t\tif len(tc.ps) \u003e 0 {\n\t\t\tp = tc.owner.String()\n\t\t\tfor _, psv := range tc.ps {\n\t\t\t\tp += \":\" + psv\n\t\t\t}\n\t\t}\n\t\tp = strings.TrimSuffix(p, \":\")\n\t\tt.Run(p, func(t *testing.T) {\n\t\t\ttesting.SetOriginCaller(tc.caller)\n\t\t\tvar act string\n\t\t\tif len(tc.ps) \u003e 0 \u0026\u0026 tc.ps[0] == \"set\" {\n\t\t\t\tact = strings.TrimSpace(Set(tc.ps[1], tc.ps[2]))\n\t\t\t} else if len(tc.ps) \u003e 0 \u0026\u0026 tc.ps[0] == \"remove\" {\n\t\t\t\tact = strings.TrimSpace(Remove(tc.ps[1]))\n\t\t\t} else {\n\t\t\t\tact = strings.TrimSpace(Render(p))\n\t\t\t}\n\n\t\t\tuassert.Equal(t, tc.exp, act, ufmt.Sprintf(\"%v -\u003e '%s'\", tc.ps, p))\n\t\t})\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "g8qvBdMc+5qT6OSV/CEy/UGUUod+ZosfiBMFTobcN94QLWLO+bntDZFugCmbRsByMiNHgY6ru781bOtOylNLCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "eval", + "path": "gno.land/r/demo/math_eval", + "files": [ + { + "name": "math_eval.gno", + "body": "// eval realm is capable of evaluating 32-bit integer\n// expressions as they would appear in Go. For example:\n// /r/demo/math_eval:(4+12)/2-1+11*15\npackage eval\n\nimport (\n\tevalint32 \"gno.land/p/demo/math_eval/int32\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc Render(p string) string {\n\tif len(p) == 0 {\n\t\treturn `\nevaluates 32-bit integer expressions. for example:\n\t\t\n[(4+12)/2-1+11*15](/r/demo/math_eval:(4+12)/2-1+11*15)\n\n`\n\t}\n\texpr, err := evalint32.Parse(p)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\tres, err := evalint32.Eval(expr, nil)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\n\treturn ufmt.Sprintf(\"%s = %d\", p, res)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "5njfhLyH0uOIIVyt+Zap1PK1VvjZtZMRAcOUD0BRtZZ677ueQt/68HncrwC0kLabCtisLj30Gw4ruj6A55CjCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "memeland", + "path": "gno.land/r/demo/memeland", + "files": [ + { + "name": "memeland.gno", + "body": "package memeland\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/memeland\"\n)\n\nvar m *memeland.Memeland\n\nfunc init() {\n\tm = memeland.NewMemeland()\n\tm.TransferOwnership(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\")\n}\n\nfunc PostMeme(data string, timestamp int64) string {\n\treturn m.PostMeme(data, timestamp)\n}\n\nfunc Upvote(id string) string {\n\treturn m.Upvote(id)\n}\n\nfunc GetPostsInRange(startTimestamp, endTimestamp int64, page, pageSize int, sortBy string) string {\n\treturn m.GetPostsInRange(startTimestamp, endTimestamp, page, pageSize, sortBy)\n}\n\nfunc RemovePost(id string) string {\n\treturn m.RemovePost(id)\n}\n\nfunc GetOwner() std.Address {\n\treturn m.Owner()\n}\n\nfunc TransferOwnership(newOwner std.Address) {\n\tif err := m.TransferOwnership(newOwner); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc Render(path string) string {\n\tnumOfMemes := int(m.MemeCounter)\n\tif numOfMemes == 0 {\n\t\treturn \"No memes posted yet! :/\"\n\t}\n\n\t// Default render is get Posts since year 2000 to now\n\treturn m.GetPostsInRange(0, time.Now().Unix(), 1, 10, \"DATE_CREATED\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "6CJ/C3RsLQgwtNdThKdoqT7PgwCuX2VtqYiImgftySBMblCDVVpq1ysXqetoVmC6306xgDJNhwgs8HE9mrPnBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "microblog", + "path": "gno.land/r/demo/microblog", + "files": [ + { + "name": "README.md", + "body": "# microblog realm\n\n## Getting started:\n\n(One-time) Add the microblog package:\n\n```\ngnokey maketx addpkg --pkgpath \"gno.land/p/demo/microblog\" --pkgdir \"examples/gno.land/p/demo/microblog\" \\\n --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```\n\n(One-time) Add the microblog realm:\n\n```\ngnokey maketx addpkg --pkgpath \"gno.land/r/demo/microblog\" --pkgdir \"examples/gno.land/r/demo/microblog\" \\\n --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```\n\nAdd a microblog post:\n\n```\ngnokey maketx call --pkgpath \"gno.land/r/demo/microblog\" --func \"NewPost\" --args \"hello, world\" \\\n --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast --chainid dev --remote localhost:26657 \u003cYOURKEY\u003e\n```" + }, + { + "name": "microblog.gno", + "body": "// Microblog is a website with shortform posts from users.\n// The API is simple - \"AddPost\" takes markdown and\n// adds it to the users site.\n// The microblog location is determined by the user address\n// /r/demo/microblog:\u003cYOUR-ADDRESS\u003e\npackage microblog\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/microblog\"\n\t\"gno.land/p/demo/ufmt\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\nvar (\n\ttitle = \"gno-based microblog\"\n\tprefix = \"/r/demo/microblog:\"\n\tm *microblog.Microblog\n)\n\nfunc init() {\n\tm = microblog.NewMicroblog(title, prefix)\n}\n\nfunc renderHome() string {\n\toutput := ufmt.Sprintf(\"# %s\\n\\n\", m.Title)\n\toutput += \"# pages\\n\\n\"\n\n\tfor _, page := range m.GetPages() {\n\t\tif u := susers.ResolveAddress(page.Author); u != nil {\n\t\t\toutput += ufmt.Sprintf(\"- [%s (%s)](%s%s)\\n\", u.Name(), page.Author.String(), m.Prefix, page.Author.String())\n\t\t} else {\n\t\t\toutput += ufmt.Sprintf(\"- [%s](%s%s)\\n\", page.Author.String(), m.Prefix, page.Author.String())\n\t\t}\n\t}\n\n\treturn output\n}\n\nfunc renderUser(user string) string {\n\tsilo, found := m.Pages.Get(user)\n\tif !found {\n\t\treturn \"404\" // StatusNotFound\n\t}\n\n\treturn PageToString((silo.(*microblog.Page)))\n}\n\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\n\tisHome := path == \"\"\n\tisUser := len(parts) == 1\n\n\tswitch {\n\tcase isHome:\n\t\treturn renderHome()\n\n\tcase isUser:\n\t\treturn renderUser(parts[0])\n\t}\n\n\treturn \"404\" // StatusNotFound\n}\n\nfunc PageToString(p *microblog.Page) string {\n\to := \"\"\n\tif u := susers.ResolveAddress(p.Author); u != nil {\n\t\to += ufmt.Sprintf(\"# [%s](/r/gnoland/users/v1:%s)\\n\\n\", u, u)\n\t}\n\to += ufmt.Sprintf(\"## [%s](/r/demo/microblog:%s)\\n\\n\", p.Author, p.Author)\n\n\to += ufmt.Sprintf(\"joined %s, last updated %s\\n\\n\", p.CreatedAt.Format(\"2006-02-01\"), p.LastPosted.Format(\"2006-02-01\"))\n\to += \"## feed\\n\\n\"\n\tfor _, u := range p.GetPosts() {\n\t\to += u.String() + \"\\n\\n\"\n\t}\n\treturn o\n}\n\n// NewPost takes a single argument (post markdown) and\n// adds a post to the address of the caller.\nfunc NewPost(text string) string {\n\tif err := m.NewPost(text); err != nil {\n\t\treturn \"unable to add new post\"\n\t}\n\treturn \"added new post\"\n}\n" + }, + { + "name": "microblog_test.gno", + "body": "package microblog\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestMicroblog(t *testing.T) {\n\tvar (\n\t\tauthor1 std.Address = testutils.TestAddress(\"author1\")\n\t\tauthor2 std.Address = testutils.TestAddress(\"author2\")\n\t)\n\n\ttesting.SetOriginCaller(author1)\n\n\turequire.Equal(t, \"404\", Render(\"/wrongpath\"), \"rendering not giving 404\")\n\turequire.NotEqual(t, \"404\", Render(\"\"), \"rendering / should not give 404\")\n\turequire.NoError(t, m.NewPost(\"goodbyte, web2\"), \"could not create post\")\n\n\t_, err := m.GetPage(author1.String())\n\turequire.NoError(t, err, \"silo should exist\")\n\n\t_, err = m.GetPage(\"no such author\")\n\turequire.Error(t, err, \"silo should not exist\")\n\n\ttesting.SetOriginCaller(author2)\n\n\turequire.NoError(t, m.NewPost(\"hello, web3\"), \"could not create post\")\n\turequire.NoError(t, m.NewPost(\"hello again, web3\"), \"could not create post\")\n\turequire.NoError(t, m.NewPost(\"hi again,\\n web4?\"), \"could not create post\")\n\n\tprintln(\"--- MICROBLOG ---\\n\\n\")\n\n\texpected := `# gno-based microblog\n\n# pages\n\n- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/microblog:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6)\n- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/microblog:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00)\n`\n\turequire.Equal(t, expected, Render(\"\"), \"incorrect rendering\")\n\n\texpected = `## [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/microblog:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6)\n\njoined 2009-13-02, last updated 2009-13-02\n\n## feed\n\n\u003e goodbyte, web2\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*`\n\n\turequire.Equal(t, expected, strings.TrimSpace(Render(author1.String())), \"incorrect rendering\")\n\n\texpected = `## [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/microblog:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00)\n\njoined 2009-13-02, last updated 2009-13-02\n\n## feed\n\n\u003e hi again,\n\u003e\n\u003e web4?\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*\n\n\u003e hello again, web3\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*\n\n\u003e hello, web3\n\u003e\n\u003e *Fri, 13 Feb 2009 23:31:30 UTC*`\n\n\turequire.Equal(t, expected, strings.TrimSpace(Render(author2.String())), \"incorrect rendering\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "OZlkyCoAdplcGtpILwhtgLHPCIfJOu5KUqBLqn9ijvi0ywjC9ujk7UJwR8fRZ76cCZYSnLWtupOEsP56fm7JCQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "mirror", + "path": "gno.land/r/demo/mirror", + "files": [ + { + "name": "doc.gno", + "body": "// Package mirror demonstrates that users can pass realm functions\n// as arguments to other realms.\npackage mirror\n" + }, + { + "name": "mirror.gno", + "body": "package mirror\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\nvar store avl.Tree\n\nfunc Register(pkgpath string, rndr func(string) string) {\n\tif store.Has(pkgpath) {\n\t\treturn\n\t}\n\n\tif rndr == nil {\n\t\treturn\n\t}\n\n\tstore.Set(pkgpath, rndr)\n}\n\nfunc Render(path string) string {\n\tif raw, ok := store.Get(path); ok {\n\t\treturn raw.(func(string) string)(\"\")\n\t}\n\n\tif store.Size() == 0 {\n\t\treturn \"None are fair.\"\n\t}\n\n\treturn \"Mirror, mirror on the wall, which realm's the fairest of them all?\"\n}\n\n// Credits to @jeronimoalbi\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "4cJmtuaVJUMyI9ouu4XF+vOJQ53TWaX3jRFKyQx2LOR5Ug3fyLfkN+bPiF2/DAvpijFEX6a271RYrX0CfFozBA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "nft", + "path": "gno.land/r/demo/nft", + "files": [ + { + "name": "README.md", + "body": "NFT's are all the rage these days, for various reasons.\n\nI read over EIP-721 which appears to be the de-facto NFT standard on Ethereum. Then, made a sample implementation of EIP-721 (let's here called GRC-721). The implementation isn't complete, but it demonstrates the main functionality.\n\n- [EIP-721](https://eips.ethereum.org/EIPS/eip-721)\n- [gno.land/r/demo/nft/nft.go](https://gno.land/r/demo/nft/nft.go)\n- [zrealm_nft3.go test](https://github.com/gnolang/gno/blob/master/tests/files2/zrealm_nft3.gno)\n\nIn short, this demonstrates how to implement Ethereum contract interfaces in gno.land; by using only standard Go language features.\n\nPlease leave a comment ([guide](https://gno.land/r/demo/boards:gnolang/1)).\n" + }, + { + "name": "nft.gno", + "body": "package nft\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/grc/grc721\"\n)\n\ntype token struct {\n\tgrc721.IGRC721 // implements the GRC721 interface\n\n\ttokenCounter int\n\ttokens avl.Tree // grc721.TokenID -\u003e *NFToken{}\n\toperators avl.Tree // owner std.Address -\u003e operator std.Address\n}\n\ntype NFToken struct {\n\towner std.Address\n\tapproved std.Address\n\ttokenID grc721.TokenID\n\tdata string\n}\n\nvar gToken = \u0026token{}\n\nfunc GetToken() *token { return gToken }\n\nfunc (grc *token) nextTokenID() grc721.TokenID {\n\tgrc.tokenCounter++\n\ts := strconv.Itoa(grc.tokenCounter)\n\treturn grc721.TokenID(s)\n}\n\nfunc (grc *token) getToken(tid grc721.TokenID) (*NFToken, bool) {\n\ttoken, ok := grc.tokens.Get(string(tid))\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn token.(*NFToken), true\n}\n\nfunc (grc *token) Mint(to std.Address, data string) grc721.TokenID {\n\ttid := grc.nextTokenID()\n\tgrc.tokens.Set(string(tid), \u0026NFToken{\n\t\towner: to,\n\t\ttokenID: tid,\n\t\tdata: data,\n\t})\n\treturn tid\n}\n\nfunc (grc *token) BalanceOf(owner std.Address) (count int64) {\n\tpanic(\"not yet implemented\")\n}\n\nfunc (grc *token) OwnerOf(tid grc721.TokenID) std.Address {\n\ttoken, ok := grc.getToken(tid)\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\treturn token.owner\n}\n\n// XXX not fully implemented yet.\nfunc (grc *token) SafeTransferFrom(from, to std.Address, tid grc721.TokenID) {\n\tgrc.TransferFrom(from, to, tid)\n\t// When transfer is complete, this function checks if `_to` is a smart\n\t// contract (code size \u003e 0). If so, it calls `onERC721Received` on\n\t// `_to` and throws if the return value is not\n\t// `bytes4(keccak256(\"onERC721Received(address,address,uint256,bytes)\"))`.\n\t// XXX ensure \"to\" is a realm with onERC721Received() signature.\n}\n\nfunc (grc *token) TransferFrom(from, to std.Address, tid grc721.TokenID) {\n\tcaller := std.CallerAt(2)\n\ttoken, ok := grc.getToken(tid)\n\t// Throws if `_tokenId` is not a valid NFT.\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\t// Throws unless `msg.sender` is the current owner, an authorized\n\t// operator, or the approved address for this NFT.\n\tif caller != token.owner \u0026\u0026 caller != token.approved {\n\t\toperator, ok := grc.operators.Get(token.owner.String())\n\t\tif !ok || caller != operator.(std.Address) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t}\n\t// Throws if `_from` is not the current owner.\n\tif from != token.owner {\n\t\tpanic(\"from is not the current owner\")\n\t}\n\t// Throws if `_to` is the zero address.\n\tif to == \"\" {\n\t\tpanic(\"to cannot be empty\")\n\t}\n\t// Good.\n\ttoken.owner = to\n}\n\nfunc (grc *token) Approve(approved std.Address, tid grc721.TokenID) {\n\tcaller := std.CallerAt(2)\n\ttoken, ok := grc.getToken(tid)\n\t// Throws if `_tokenId` is not a valid NFT.\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\t// Throws unless `msg.sender` is the current owner,\n\t// or an authorized operator.\n\tif caller != token.owner {\n\t\toperator, ok := grc.operators.Get(token.owner.String())\n\t\tif !ok || caller != operator.(std.Address) {\n\t\t\tpanic(\"unauthorized\")\n\t\t}\n\t}\n\t// Good.\n\ttoken.approved = approved\n}\n\n// XXX make it work for set of operators.\nfunc (grc *token) SetApprovalForAll(operator std.Address, approved bool) {\n\tcaller := std.CallerAt(2)\n\tgrc.operators.Set(caller.String(), operator)\n}\n\nfunc (grc *token) GetApproved(tid grc721.TokenID) std.Address {\n\ttoken, ok := grc.getToken(tid)\n\t// Throws if `_tokenId` is not a valid NFT.\n\tif !ok {\n\t\tpanic(\"token does not exist\")\n\t}\n\treturn token.approved\n}\n\n// XXX make it work for set of operators\nfunc (grc *token) IsApprovedForAll(owner, operator std.Address) bool {\n\toperator2, ok := grc.operators.Get(owner.String())\n\tif !ok {\n\t\treturn false\n\t}\n\treturn operator == operator2.(std.Address)\n}\n" + }, + { + "name": "z_0_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\taddr1 := testutils.TestAddress(\"addr1\")\n\t// addr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(addr1, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n}\n\n// Output:\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n\n// Realm:\n// switchrealm[\"gno.land/r/demo/nft\"]\n// switchrealm[\"gno.land/r/demo/nft\"]\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:11]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"std.Address\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/grc/grc721.TokenID\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"NFT#1\"\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:11\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:10\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:10]={\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:10\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:9\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/nft.NFToken\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"564a9e78be869bd258fc3c9ad56f5a75ed68818f\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:11\"\n// }\n// }\n// }\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:9]={\n// \"Fields\": [\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"16\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.StringValue\",\n// \"value\": \"1\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/r/demo/nft.NFToken\"\n// }\n// },\n// \"V\": {\n// \"@type\": \"/gno.PointerValue\",\n// \"Base\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b53ffc464e1b5655d19b9d5277f3491717c24aca\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:10\"\n// },\n// \"Index\": \"0\",\n// \"TV\": null\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"64\"\n// }\n// },\n// {\n// \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// },\n// {\n// \"T\": {\n// \"@type\": \"/gno.PointerType\",\n// \"Elt\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:9\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:8\",\n// \"RefCount\": \"1\"\n// }\n// }\n// c[67c479d3d51d4056b2f4111d5352912a00be311e:8]={\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:8\",\n// \"ModTime\": \"0\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:5\",\n// \"RefCount\": \"1\"\n// },\n// \"Value\": {\n// \"T\": {\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// \"Hash\": \"b1d928b3716b147c92730e8d234162bec2f0f2fc\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:9\"\n// }\n// }\n// }\n// u[67c479d3d51d4056b2f4111d5352912a00be311e:5]=\n// @@ -7,12 +7,22 @@\n// \"@type\": \"/gno.RefType\",\n// \"ID\": \"gno.land/p/demo/avl.Node\"\n// }\n// + },\n// + \"V\": {\n// + \"@type\": \"/gno.PointerValue\",\n// + \"Base\": {\n// + \"@type\": \"/gno.RefValue\",\n// + \"Hash\": \"b229b824842ec3e7f2341e33d0fa0ca77af2f480\",\n// + \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:8\"\n// + },\n// + \"Index\": \"0\",\n// + \"TV\": null\n// }\n// }\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:5\",\n// - \"ModTime\": \"0\",\n// + \"ModTime\": \"7\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:4\",\n// \"RefCount\": \"1\"\n// }\n// u[67c479d3d51d4056b2f4111d5352912a00be311e:4]=\n// @@ -2,6 +2,7 @@\n// \"Fields\": [\n// {},\n// {\n// + \"N\": \"AQAAAAAAAAA=\",\n// \"T\": {\n// \"@type\": \"/gno.PrimitiveType\",\n// \"value\": \"32\"\n// @@ -14,7 +15,7 @@\n// },\n// \"V\": {\n// \"@type\": \"/gno.RefValue\",\n// - \"Hash\": \"872ac4fb1c12756da1e64712e7496f1d8db57ee0\",\n// + \"Hash\": \"1e0b9dddb406b4f50500a022266a4cb8a4ea38c6\",\n// \"ObjectID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:5\"\n// }\n// },\n// @@ -32,7 +33,7 @@\n// ],\n// \"ObjectInfo\": {\n// \"ID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:4\",\n// - \"ModTime\": \"0\",\n// + \"ModTime\": \"7\",\n// \"OwnerID\": \"67c479d3d51d4056b2f4111d5352912a00be311e:3\",\n// \"RefCount\": \"1\"\n// }\n// switchrealm[\"gno.land/r/demo/nft\"]\n// switchrealm[\"gno.land/r/demo/nft_test\"]\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\taddr1 := testutils.TestAddress(\"addr1\")\n\taddr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(addr1, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.TransferFrom(addr1, addr2, tid)\n}\n\n// Error:\n// unauthorized\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\tcaller := std.CallerAt(1)\n\taddr1 := testutils.TestAddress(\"addr1\")\n\t// addr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(caller, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.TransferFrom(caller, addr1, tid)\n}\n\n// Output:\n// g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n" + }, + { + "name": "z_3_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\tcaller := std.CallerAt(1)\n\taddr1 := testutils.TestAddress(\"addr1\")\n\taddr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(caller, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.Approve(caller, tid) // approve self.\n\tgrc721.TransferFrom(caller, addr1, tid)\n\tgrc721.TransferFrom(addr1, addr2, tid)\n}\n\n// Output:\n// g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n// g1v9jxgu33ta047h6lta047h6lta047h6l43dqc5\n" + }, + { + "name": "z_4_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/nft_test\npackage nft_test\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/nft\"\n)\n\nfunc main() {\n\tcaller := std.CallerAt(1)\n\taddr1 := testutils.TestAddress(\"addr1\")\n\taddr2 := testutils.TestAddress(\"addr2\")\n\tgrc721 := nft.GetToken()\n\ttid := grc721.Mint(caller, \"NFT#1\")\n\tprintln(grc721.OwnerOf(tid))\n\tprintln(addr1)\n\tgrc721.Approve(caller, tid) // approve self.\n\tgrc721.TransferFrom(caller, addr1, tid)\n\tgrc721.Approve(\"\", tid) // approve addr1.\n\tgrc721.TransferFrom(addr1, addr2, tid)\n}\n\n// Error:\n// unauthorized\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "hHWyXV1XjHSd8yPxwtfMA1N1F6CTcqyEkIAlgXEQoczzsQdgnnI7yA6nUFvx3RKLtKuEMhP+PdCQEW2RPi5uAQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "profile", + "path": "gno.land/r/demo/profile", + "files": [ + { + "name": "profile.gno", + "body": "package profile\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tfields = avl.NewTree()\n\trouter = mux.NewRouter()\n)\n\n// Standard fields\nconst (\n\tDisplayName = \"DisplayName\"\n\tHomepage = \"Homepage\"\n\tBio = \"Bio\"\n\tAge = \"Age\"\n\tLocation = \"Location\"\n\tAvatar = \"Avatar\"\n\tGravatarEmail = \"GravatarEmail\"\n\tAvailableForHiring = \"AvailableForHiring\"\n\tInvalidField = \"InvalidField\"\n)\n\n// Events\nconst (\n\tProfileFieldCreated = \"ProfileFieldCreated\"\n\tProfileFieldUpdated = \"ProfileFieldUpdated\"\n)\n\n// Field types used when emitting event\nconst FieldType = \"FieldType\"\n\nconst (\n\tBoolField = \"BoolField\"\n\tStringField = \"StringField\"\n\tIntField = \"IntField\"\n)\n\nfunc init() {\n\trouter.HandleFunc(\"\", homeHandler)\n\trouter.HandleFunc(\"u/{addr}\", profileHandler)\n\trouter.HandleFunc(\"f/{addr}/{field}\", fieldHandler)\n}\n\n// List of supported string fields\nvar stringFields = map[string]bool{\n\tDisplayName: true,\n\tHomepage: true,\n\tBio: true,\n\tLocation: true,\n\tAvatar: true,\n\tGravatarEmail: true,\n}\n\n// List of support int fields\nvar intFields = map[string]bool{\n\tAge: true,\n}\n\n// List of support bool fields\nvar boolFields = map[string]bool{\n\tAvailableForHiring: true,\n}\n\n// Setters\n\nfunc SetStringField(field, value string) bool {\n\taddr := std.PreviousRealm().Address()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tstd.Emit(event, FieldType, StringField, field, value)\n\n\treturn updated\n}\n\nfunc SetIntField(field string, value int) bool {\n\taddr := std.PreviousRealm().Address()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tstd.Emit(event, FieldType, IntField, field, string(value))\n\n\treturn updated\n}\n\nfunc SetBoolField(field string, value bool) bool {\n\taddr := std.PreviousRealm().Address()\n\tkey := addr.String() + \":\" + field\n\tupdated := fields.Set(key, value)\n\n\tevent := ProfileFieldCreated\n\tif updated {\n\t\tevent = ProfileFieldUpdated\n\t}\n\n\tstd.Emit(event, FieldType, BoolField, field, ufmt.Sprintf(\"%t\", value))\n\n\treturn updated\n}\n\n// Getters\n\nfunc GetStringField(addr std.Address, field, def string) string {\n\tkey := addr.String() + \":\" + field\n\tif value, ok := fields.Get(key); ok {\n\t\treturn value.(string)\n\t}\n\n\treturn def\n}\n\nfunc GetBoolField(addr std.Address, field string, def bool) bool {\n\tkey := addr.String() + \":\" + field\n\tif value, ok := fields.Get(key); ok {\n\t\treturn value.(bool)\n\t}\n\n\treturn def\n}\n\nfunc GetIntField(addr std.Address, field string, def int) int {\n\tkey := addr.String() + \":\" + field\n\tif value, ok := fields.Get(key); ok {\n\t\treturn value.(int)\n\t}\n\n\treturn def\n}\n" + }, + { + "name": "profile_test.gno", + "body": "package profile\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n)\n\n// Global addresses for test users\nvar (\n\talice = testutils.TestAddress(\"alice\")\n\tbob = testutils.TestAddress(\"bob\")\n\tcharlie = testutils.TestAddress(\"charlie\")\n\tdave = testutils.TestAddress(\"dave\")\n\teve = testutils.TestAddress(\"eve\")\n\tfrank = testutils.TestAddress(\"frank\")\n\tuser1 = testutils.TestAddress(\"user1\")\n\tuser2 = testutils.TestAddress(\"user2\")\n)\n\nfunc TestStringFields(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\n\t// Get before setting\n\tname := GetStringField(alice, DisplayName, \"anon\")\n\tuassert.Equal(t, \"anon\", name)\n\n\t// Set new key\n\tupdated := SetStringField(DisplayName, \"Alice foo\")\n\tuassert.Equal(t, updated, false)\n\tupdated = SetStringField(Homepage, \"https://example.com\")\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetStringField(DisplayName, \"Alice foo\")\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\tname = GetStringField(alice, DisplayName, \"anon\")\n\thomepage := GetStringField(alice, Homepage, \"\")\n\tbio := GetStringField(alice, Bio, \"42\")\n\n\tuassert.Equal(t, \"Alice foo\", name)\n\tuassert.Equal(t, \"https://example.com\", homepage)\n\tuassert.Equal(t, \"42\", bio)\n}\n\nfunc TestIntFields(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(bob))\n\n\t// Get before setting\n\tage := GetIntField(bob, Age, 25)\n\tuassert.Equal(t, 25, age)\n\n\t// Set new key\n\tupdated := SetIntField(Age, 30)\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetIntField(Age, 30)\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\tage = GetIntField(bob, Age, 25)\n\tuassert.Equal(t, 30, age)\n}\n\nfunc TestBoolFields(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(charlie))\n\n\t// Get before setting\n\thiring := GetBoolField(charlie, AvailableForHiring, false)\n\tuassert.Equal(t, false, hiring)\n\n\t// Set\n\tupdated := SetBoolField(AvailableForHiring, true)\n\tuassert.Equal(t, updated, false)\n\n\t// Update the key\n\tupdated = SetBoolField(AvailableForHiring, true)\n\tuassert.Equal(t, updated, true)\n\n\t// Get after setting\n\thiring = GetBoolField(charlie, AvailableForHiring, false)\n\tuassert.Equal(t, true, hiring)\n}\n\nfunc TestMultipleProfiles(t *testing.T) {\n\t// Set profile for user1\n\ttesting.SetRealm(std.NewUserRealm(user1))\n\tupdated := SetStringField(DisplayName, \"User One\")\n\tuassert.Equal(t, updated, false)\n\n\t// Set profile for user2\n\ttesting.SetRealm(std.NewUserRealm(user2))\n\tupdated = SetStringField(DisplayName, \"User Two\")\n\tuassert.Equal(t, updated, false)\n\n\t// Get profiles\n\ttesting.SetRealm(std.NewUserRealm(user1)) // Switch back to user1\n\tname1 := GetStringField(user1, DisplayName, \"anon\")\n\ttesting.SetRealm(std.NewUserRealm(user2)) // Switch back to user2\n\tname2 := GetStringField(user2, DisplayName, \"anon\")\n\n\tuassert.Equal(t, \"User One\", name1)\n\tuassert.Equal(t, \"User Two\", name2)\n}\n\nfunc TestArbitraryStringField(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(user1))\n\n\t// Set arbitrary string field\n\tupdated := SetStringField(\"MyEmail\", \"my@email.com\")\n\tuassert.Equal(t, updated, false)\n\n\tval := GetStringField(user1, \"MyEmail\", \"\")\n\tuassert.Equal(t, val, \"my@email.com\")\n}\n\nfunc TestArbitraryIntField(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(user1))\n\n\t// Set arbitrary int field\n\tupdated := SetIntField(\"MyIncome\", 100_000)\n\tuassert.Equal(t, updated, false)\n\n\tval := GetIntField(user1, \"MyIncome\", 0)\n\tuassert.Equal(t, val, 100_000)\n}\n\nfunc TestArbitraryBoolField(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(user1))\n\n\t// Set arbitrary int field\n\tupdated := SetBoolField(\"IsWinner\", true)\n\tuassert.Equal(t, updated, false)\n\n\tval := GetBoolField(user1, \"IsWinner\", false)\n\tuassert.Equal(t, val, true)\n}\n" + }, + { + "name": "render.gno", + "body": "package profile\n\nimport (\n\t\"bytes\"\n\t\"net/url\"\n\t\"std\"\n\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tBaseURL = \"/r/demo/profile\"\n\tSetStringFieldURL = BaseURL + \"$help\u0026func=SetStringField\u0026field=%s\"\n\tSetIntFieldURL = BaseURL + \"$help\u0026func=SetIntField\u0026field=%s\"\n\tSetBoolFieldURL = BaseURL + \"$help\u0026func=SetBoolField\u0026field=%s\"\n\tViewAllFieldsURL = BaseURL + \":u/%s\"\n\tViewFieldURL = BaseURL + \":f/%s/%s\"\n)\n\nfunc homeHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(\"## Setters\\n\")\n\tfor field := range stringFields {\n\t\tlink := ufmt.Sprintf(SetStringFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s](%s)\\n\", field, link))\n\t}\n\n\tfor field := range intFields {\n\t\tlink := ufmt.Sprintf(SetIntFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s](%s)\\n\", field, link))\n\t}\n\n\tfor field := range boolFields {\n\t\tlink := ufmt.Sprintf(SetBoolFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- [Set %s Field](%s)\\n\", field, link))\n\t}\n\n\tb.WriteString(\"\\n---\\n\\n\")\n\n\tres.Write(b.String())\n}\n\nfunc profileHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\taddr := req.GetVar(\"addr\")\n\n\tb.WriteString(ufmt.Sprintf(\"# Profile %s\\n\", addr))\n\n\taddress := std.Address(addr)\n\n\tfor field := range stringFields {\n\t\tvalue := GetStringField(address, field, \"n/a\")\n\t\tlink := ufmt.Sprintf(SetStringFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %s [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tfor field := range intFields {\n\t\tvalue := GetIntField(address, field, 0)\n\t\tlink := ufmt.Sprintf(SetIntFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %d [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tfor field := range boolFields {\n\t\tvalue := GetBoolField(address, field, false)\n\t\tlink := ufmt.Sprintf(SetBoolFieldURL, field)\n\t\tb.WriteString(ufmt.Sprintf(\"- %s: %t [Edit](%s)\\n\", field, value, link))\n\t}\n\n\tres.Write(b.String())\n}\n\nfunc fieldHandler(res *mux.ResponseWriter, req *mux.Request) {\n\tvar b bytes.Buffer\n\taddr := req.GetVar(\"addr\")\n\tfield := req.GetVar(\"field\")\n\n\tb.WriteString(ufmt.Sprintf(\"# Field %s for %s\\n\", field, addr))\n\n\taddress := std.Address(addr)\n\tvalue := \"n/a\"\n\tvar editLink string\n\n\tif _, ok := stringFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%s\", GetStringField(address, field, \"n/a\"))\n\t\teditLink = ufmt.Sprintf(SetStringFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, url.QueryEscape(value))\n\t} else if _, ok := intFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%d\", GetIntField(address, field, 0))\n\t\teditLink = ufmt.Sprintf(SetIntFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, value)\n\t} else if _, ok := boolFields[field]; ok {\n\t\tvalue = ufmt.Sprintf(\"%t\", GetBoolField(address, field, false))\n\t\teditLink = ufmt.Sprintf(SetBoolFieldURL+\"\u0026addr=%s\u0026value=%s\", field, addr, value)\n\t}\n\n\tb.WriteString(ufmt.Sprintf(\"- %s: %s [Edit](%s)\\n\", field, value, editLink))\n\n\tres.Write(b.String())\n}\n\nfunc Render(path string) string {\n\treturn router.Render(path)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "wfgrBANDi/QxRPdo3YLaSO0/llg5LqFjKhHjSi7oTP2JJlL12H+A8gIFaY7Q61ZgkgGzxzDsUE0tq2Sxc9RPDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "releases_example", + "path": "gno.land/r/demo/releases_example", + "files": [ + { + "name": "dummy.gno", + "body": "package releases_example\n\nfunc init() {\n\t// dummy example data\n\tchangelog.NewRelease(\n\t\t\"v1\",\n\t\t\"r/demo/examples_example_v1\",\n\t\t\"initial release\",\n\t)\n\tchangelog.NewRelease(\n\t\t\"v2\",\n\t\t\"r/demo/examples_example_v2\",\n\t\t\"various improvements\",\n\t)\n}\n" + }, + { + "name": "example.gno", + "body": "// this package demonstrates a way to manage contract releases.\npackage releases_example\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/releases\"\n)\n\nvar (\n\tchangelog = releases.NewChangelog(\"example_app\")\n\tadmin = std.Address(\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\") // @administrator\n)\n\nfunc init() {\n\t// FIXME: admin = std.GetCreator()\n}\n\nfunc NewRelease(name, url, notes string) {\n\tcaller := std.OriginCaller()\n\tif caller != admin {\n\t\tpanic(\"restricted area\")\n\t}\n\tchangelog.NewRelease(name, url, notes)\n}\n\nfunc UpdateAdmin(address std.Address) {\n\tcaller := std.OriginCaller()\n\tif caller != admin {\n\t\tpanic(\"restricted area\")\n\t}\n\tadmin = address\n}\n\nfunc Render(path string) string {\n\treturn changelog.Render(path)\n}\n" + }, + { + "name": "releases0_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/p/demo/releases\"\n)\n\nfunc main() {\n\tprintln(\"-----------\")\n\tchangelog := releases.NewChangelog(\"example\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tchangelog.NewRelease(\"v1\", \"r/blahblah\", \"* initial version\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tchangelog.NewRelease(\"v2\", \"r/blahblah2\", \"* various improvements\\n* new shiny logo\")\n\tprint(changelog.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tprint(changelog.Latest().Render())\n}\n\n// Output:\n// -----------\n// # example\n//\n// -----------\n// # example\n//\n// ## [example v1 (latest)](r/blahblah)\n//\n// * initial version\n//\n// -----------\n// # example\n//\n// ## [example v2 (latest)](r/blahblah2)\n//\n// * various improvements\n// * new shiny logo\n//\n// ## [example v1](r/blahblah)\n//\n// * initial version\n//\n// -----------\n// ## [example v2 (latest)](r/blahblah2)\n//\n// * various improvements\n// * new shiny logo\n//\n" + }, + { + "name": "releases1_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/r/demo/releases_example\"\n)\n\nfunc main() {\n\tprintln(\"-----------\")\n\tprint(releases_example.Render(\"\"))\n\n\tprintln(\"-----------\")\n\tprint(releases_example.Render(\"v1\"))\n\n\tprintln(\"-----------\")\n\tprint(releases_example.Render(\"v42\"))\n}\n\n// Output:\n// -----------\n// # example_app\n//\n// ## [example_app v2 (latest)](r/demo/examples_example_v2)\n//\n// various improvements\n//\n// ## [example_app v1](r/demo/examples_example_v1)\n//\n// initial release\n//\n// -----------\n// ## [example_app v1](r/demo/examples_example_v1)\n//\n// initial release\n//\n// -----------\n// no such release\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "RFkp276qPStmOTfrfbGNUzwGrDESDQA64xRcY2+yNd+I6vSHqNYQihDIB5iuRqW3jjPyPlB/lercJ0oKy8j7Bg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "tamagotchi", + "path": "gno.land/r/demo/tamagotchi", + "files": [ + { + "name": "realm.gno", + "body": "package tamagotchi\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/tamagotchi\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar t *tamagotchi.Tamagotchi\n\nfunc init() {\n\tReset(\"gnome#0\")\n}\n\nfunc Reset(optionalName string) string {\n\tname := optionalName\n\tif name == \"\" {\n\t\theight := std.ChainHeight()\n\t\tname = ufmt.Sprintf(\"gnome#%d\", height)\n\t}\n\n\tt = tamagotchi.New(name)\n\n\treturn ufmt.Sprintf(\"A new tamagotchi is born. Their name is %s %s.\", t.Name(), t.Face())\n}\n\nfunc Feed() string {\n\tt.Feed()\n\treturn t.Markdown()\n}\n\nfunc Play() string {\n\tt.Play()\n\treturn t.Markdown()\n}\n\nfunc Heal() string {\n\tt.Heal()\n\treturn t.Markdown()\n}\n\nfunc Render(path string) string {\n\ttama := t.Markdown()\n\tlinks := `Actions:\n* [Feed](/r/demo/tamagotchi$help\u0026func=Feed)\n* [Play](/r/demo/tamagotchi$help\u0026func=Play)\n* [Heal](/r/demo/tamagotchi$help\u0026func=Heal)\n* [Reset](/r/demo/tamagotchi$help\u0026func=Reset)\n`\n\n\treturn tama + \"\\n\\n\" + links\n}\n" + }, + { + "name": "z0_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/r/demo/tamagotchi\"\n)\n\nfunc main() {\n\ttamagotchi.Reset(\"tamagnotchi\")\n\tprintln(tamagotchi.Render(\"\"))\n}\n\n// Output:\n// # tamagnotchi 😃\n//\n// * age: 0\n// * hunger: 50\n// * happiness: 50\n// * health: 50\n// * sleepy: 0\n//\n// Actions:\n// * [Feed](/r/demo/tamagotchi$help\u0026func=Feed)\n// * [Play](/r/demo/tamagotchi$help\u0026func=Play)\n// * [Heal](/r/demo/tamagotchi$help\u0026func=Heal)\n// * [Reset](/r/demo/tamagotchi$help\u0026func=Reset)\n//\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "66U4VzG37c1GgXmT60inukw6cHqZ/D2XKrtjJmv/7oWDONNG5zcuh8oAHyu3zSESyabn6OZ9IlRsFrIiH0UyCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "subtests", + "path": "gno.land/r/demo/tests/subtests", + "files": [ + { + "name": "subtests.gno", + "body": "package subtests\n\nimport (\n\t\"std\"\n)\n\nfunc GetCurrentRealm() std.Realm {\n\treturn std.CurrentRealm()\n}\n\nfunc GetPreviousRealm() std.Realm {\n\treturn std.PreviousRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n\nfunc CallAssertOriginCall() {\n\tstd.AssertOriginCall()\n}\n\nfunc CallIsOriginCall() bool {\n\treturn std.PreviousRealm().IsUser()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "h4Vs8ZHlAI/KZsAMr74dQy04IKceNQCM6Uz/klgbEzoedZbwHy4u1XeN+oc7W2OXELCo1oI7mMRkA+D7fAkKDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "tests", + "path": "gno.land/r/demo/tests", + "files": [ + { + "name": "README.md", + "body": "Modules here are only useful for file realm tests.\nThey can be safely ignored for other purposes.\n" + }, + { + "name": "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(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/demo/avl 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\"std\"\n\t\"testing\"\n)\n\nfunc TestNestedPkg(t *testing.T) {\n\t// direct child\n\tcur := \"gno.land/r/demo/tests/foo\"\n\ttesting.SetRealm(std.NewCodeRealm(cur))\n\tif !IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif !HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should be from the same namespace\")\n\t}\n\n\t// grand-grand-child\n\tcur = \"gno.land/r/demo/tests/foo/bar/baz\"\n\ttesting.SetRealm(std.NewCodeRealm(cur))\n\tif !IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif !HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should be from the same namespace\")\n\t}\n\n\t// direct parent\n\tcur = \"gno.land/r/demo\"\n\ttesting.SetRealm(std.NewCodeRealm(cur))\n\tif IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif !IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should be a parent path\")\n\t}\n\tif !HasCallerSameNamespace() {\n\t\tt.Errorf(cur + \" should be from the same namespace\")\n\t}\n\n\t// fake parent (prefix)\n\tcur = \"gno.land/r/dem\"\n\ttesting.SetRealm(std.NewCodeRealm(cur))\n\tif IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace() {\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(std.NewCodeRealm(cur))\n\tif IsCallerSubPath() {\n\t\tt.Errorf(cur + \" should not be a sub path\")\n\t}\n\tif IsCallerParentPath() {\n\t\tt.Errorf(cur + \" should not be a parent path\")\n\t}\n\tif HasCallerSameNamespace() {\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() nat {\n\tabs = []Word{0}\n\n\treturn abs\n}\n\nfunc AbsAdd() nat {\n\trt := GetAbs().Add()\n\n\treturn rt\n}\n" + }, + { + "name": "tests.gno", + "body": "package tests\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/nestedpkg\"\n\trsubtests \"gno.land/r/demo/tests/subtests\"\n)\n\nvar counter int\n\nfunc IncCounter() {\n\tcounter++\n}\n\nfunc Counter() int {\n\treturn counter\n}\n\nfunc CurrentRealmPath() string {\n\treturn std.CurrentRealm().PkgPath()\n}\n\nvar initOriginCaller = std.OriginCaller()\n\nfunc InitOriginCaller() std.Address {\n\treturn initOriginCaller\n}\n\nfunc CallAssertOriginCall() {\n\tstd.AssertOriginCall()\n}\n\nfunc CallIsOriginCall() bool {\n\treturn std.PreviousRealm().IsUser()\n}\n\nfunc CallSubtestsAssertOriginCall() {\n\trsubtests.CallAssertOriginCall()\n}\n\nfunc CallSubtestsIsOriginCall() bool {\n\treturn rsubtests.CallIsOriginCall()\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(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() {\n\tgTestNode1 = \u0026TestNode{Name: \"first\"}\n\tgTestNode2 = \u0026TestNode{Name: \"second\", Child: \u0026TestNode{Name: \"second's child\"}}\n}\n\nfunc ModTestNodes() {\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() std.Realm {\n\treturn std.PreviousRealm()\n}\n\nfunc GetRSubtestsPreviousRealm() std.Realm {\n\treturn rsubtests.GetPreviousRealm()\n}\n\nfunc Exec(fn func()) {\n\tfn()\n}\n\nfunc IsCallerSubPath() bool {\n\treturn nestedpkg.IsCallerSubPath()\n}\n\nfunc IsCallerParentPath() bool {\n\treturn nestedpkg.IsCallerParentPath()\n}\n\nfunc HasCallerSameNamespace() bool {\n\treturn nestedpkg.IsSameNamespace()\n}\n" + }, + { + "name": "tests_test.gno", + "body": "package tests_test\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/tests\"\n)\n\nfunc TestAssertOriginCall(t *testing.T) {\n\t// CallAssertOriginCall(): no panic\n\tcaller := testutils.TestAddress(\"caller\")\n\ttesting.SetRealm(std.NewUserRealm(caller))\n\ttests.CallAssertOriginCall()\n\tif !tests.CallIsOriginCall() {\n\t\tt.Errorf(\"expected IsOriginCall=true but got false\")\n\t}\n\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/demo/tests\"))\n\t// CallAssertOriginCall() from a block: panic\n\texpectedReason := \"invalid non-origin call\"\n\tfunc() {\n\t\tdefer func() {\n\t\t\tr := recover()\n\t\t\tif r == nil || r.(string) != expectedReason {\n\t\t\t\tt.Errorf(\"expected panic with '%v', got '%v'\", expectedReason, r)\n\t\t\t}\n\t\t}()\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() {\n\t\t\tt.Errorf(\"expected IsOriginCall=false but got true\")\n\t\t}\n\t\ttests.CallAssertOriginCall()\n\t}()\n\n\t// CallSubtestsAssertOriginCall(): panic\n\tdefer func() {\n\t\tr := recover()\n\t\tif r == nil || r.(string) != expectedReason {\n\t\t\tt.Errorf(\"expected panic with '%v', got '%v'\", expectedReason, r)\n\t\t}\n\t}()\n\tif tests.CallSubtestsIsOriginCall() {\n\t\tt.Errorf(\"expected IsOriginCall=false but got true\")\n\t}\n\ttests.CallSubtestsAssertOriginCall()\n}\n\nfunc TestPreviousRealm(t *testing.T) {\n\tvar (\n\t\tfirstRealm = std.DerivePkgAddr(\"gno.land/r/demo/tests_test\")\n\t\trTestsAddr = std.DerivePkgAddr(\"gno.land/r/demo/tests\")\n\t)\n\t// When only one realm in the frames, PreviousRealm returns the same realm\n\tif addr := tests.GetPreviousRealm().Address(); addr != firstRealm {\n\t\tprintln(tests.GetPreviousRealm())\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().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\t\"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tprintln(\"tests.CallIsOriginCall:\", tests.CallIsOriginCall())\n\ttests.CallAssertOriginCall()\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())\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()\n\t}\n}\n\n// Output:\n// tests.CallIsOriginCall: true\n// tests.CallAssertOriginCall doesn't panic when called directly\n// tests.CallIsOriginCall: true\n// tests.AssertOriginCall panics if when called inside a function literal: undefined\n" + }, + { + "name": "z1_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tprintln(tests.Counter())\n\ttests.IncCounter()\n\tprintln(tests.Counter())\n}\n\n// Output:\n// 0\n// 1\n" + }, + { + "name": "z2_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/tests\"\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\trTestsAddr = std.DerivePkgAddr(\"gno.land/r/demo/tests\")\n\t)\n\ttesting.SetOriginCaller(eoa)\n\tprintln(\"tests.GetPreviousRealm().Address(): \", tests.GetPreviousRealm().Address())\n\tprintln(\"tests.GetRSubtestsPreviousRealm().Address(): \", tests.GetRSubtestsPreviousRealm().Address())\n}\n\n// Output:\n// tests.GetPreviousRealm().Address(): g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk\n// tests.GetRSubtestsPreviousRealm().Address(): g1gz4ycmx0s6ln2wdrsh4e00l9fsel2wskqa3snq\n" + }, + { + "name": "z3_filetest.gno", + "body": "// PKGPATH: gno.land/r/demo/test_test\npackage test_test\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/tests\"\n)\n\nfunc main() {\n\tvar (\n\t\teoa = testutils.TestAddress(\"someone\")\n\t\trTestsAddr = std.DerivePkgAddr(\"gno.land/r/demo/tests\")\n\t)\n\ttesting.SetOriginCaller(eoa)\n\t// Contrarily to z2_filetest.gno we EXPECT GetPreviousRealms != eoa (#1704)\n\tif addr := tests.GetPreviousRealm().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().Address(); addr != rTestsAddr {\n\t\tprintln(\"want GetRSubtestsPreviousRealm().Address ==\", rTestsAddr, \"got\", addr)\n\t}\n}\n\n// Output:\n// want tests.GetPreviousRealm().Address == g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk got g1xufrdvnfk6zc9r0nqa23ld3tt2r5gkyvw76q63\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "0JhPfvMHnI4Uq95tgEoNaQ7ewqjfyNwvQRJ08UdmvQZ7+h5JTf+kroaYm3+ravjmy/1wUxf2MOc5pRqMHdT1Dw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "crossrealm", + "path": "gno.land/r/demo/tests/crossrealm", + "files": [ + { + "name": "crossrealm.gno", + "body": "package crossrealm\n\nimport (\n\t\"gno.land/p/demo/tests/p_crossrealm\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype LocalStruct struct {\n\tA int\n}\n\nfunc (ls *LocalStruct) String() string {\n\treturn ufmt.Sprintf(\"LocalStruct{%d}\", ls.A)\n}\n\n// local is saved locally in this realm\nvar local *LocalStruct\n\nfunc init() {\n\tlocal = \u0026LocalStruct{A: 123}\n}\n\n// Make1 returns a local object wrapped by a p struct\nfunc Make1() *p_crossrealm.Container {\n\treturn \u0026p_crossrealm.Container{\n\t\tA: 1,\n\t\tB: local,\n\t}\n}\n\ntype Fooer interface{ Foo() }\n\nvar fooer Fooer\n\nfunc SetFooer(f Fooer) Fooer {\n\tfooer = f\n\treturn fooer\n}\n\nfunc GetFooer() Fooer { return fooer }\n\nfunc CallFooerFoo() { fooer.Foo() }\n\ntype FooerGetter func() Fooer\n\nvar fooerGetter FooerGetter\n\nfunc SetFooerGetter(fg FooerGetter) FooerGetter {\n\tfooerGetter = fg\n\treturn fg\n}\n\nfunc GetFooerGetter() FooerGetter {\n\treturn fooerGetter\n}\n\nfunc CallFooerGetterFoo() { fooerGetter().Foo() }\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "roQXkKn1JCK6pRMwnMMkBT631nm2EunAqhA0k/XRy1vQZm0GBQhy/H0Oh6U7Jb/jkws1BHOEtRTwm4yvDslaBA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "crossrealm_b", + "path": "gno.land/r/demo/tests/crossrealm_b", + "files": [ + { + "name": "crossrealm.gno", + "body": "package crossrealm_b\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/demo/tests/crossrealm\"\n)\n\ntype fooer struct {\n\ts string\n}\n\nfunc (f *fooer) SetS(newVal string) {\n\tf.s = newVal\n}\n\nfunc (f *fooer) Foo() {\n\tprintln(\"hello \" + f.s + \" cur=\" + std.CurrentRealm().PkgPath() + \" prev=\" + std.PreviousRealm().PkgPath())\n}\n\nvar (\n\tFooer = \u0026fooer{s: \"A\"}\n\tFooerGetter = func() crossrealm.Fooer { return Fooer }\n\tFooerGetterBuilder = func() crossrealm.FooerGetter { return func() crossrealm.Fooer { return Fooer } }\n)\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "oqq9gxspNOeXwlZVQ1uSNFWI0hCN7t2dj3XcdW+jaaRV5I1mWYa0zO/7snA86QV5aCk1qNnBx9sTL7MCT7GbAA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "tests_foo", + "path": "gno.land/r/demo/tests_foo", + "files": [ + { + "name": "foo.gno", + "body": "package tests_foo\n\nimport (\n\t\"gno.land/r/demo/tests\"\n)\n\n// for testing gno.land/r/demo/tests/interfaces.go\n\ntype FooStringer struct {\n\tFieldA string\n}\n\nfunc (fs *FooStringer) String() string {\n\treturn \"\u0026FooStringer{\" + fs.FieldA + \"}\"\n}\n\nfunc AddFooStringer(fa string) {\n\ttests.AddStringer(\u0026FooStringer{fa})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "sboGmfHnqSSruL8RViUq27qWdM40uqZ692zMhxqov3xC5YlFkbJ7326k83MlBJDlZHyz4TC7P8qmHKORzbfsBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "todolistrealm", + "path": "gno.land/r/demo/todolist", + "files": [ + { + "name": "todolist.gno", + "body": "package todolistrealm\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/todolist\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// State variables\nvar (\n\ttodolistTree *avl.Tree\n\ttlid seqid.ID\n)\n\n// Constructor\nfunc init() {\n\ttodolistTree = avl.NewTree()\n}\n\nfunc NewTodoList(title string) (int, string) {\n\t// Create new Todolist\n\ttl := todolist.NewTodoList(title)\n\t// Update AVL tree with new state\n\ttlid.Next()\n\ttodolistTree.Set(strconv.Itoa(int(tlid)), tl)\n\treturn int(tlid), \"created successfully\"\n}\n\nfunc AddTask(todolistID int, title string) string {\n\t// Get Todolist from AVL tree\n\ttl, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// get the number of tasks in the todolist\n\tid := tl.(*todolist.TodoList).Tasks.Size()\n\n\t// create the task\n\ttask := todolist.NewTask(title)\n\n\t// Cast raw data from tree into Todolist struct\n\ttl.(*todolist.TodoList).AddTask(id, task)\n\n\treturn \"task added successfully\"\n}\n\nfunc ToggleTaskStatus(todolistID int, taskID int) string {\n\t// Get Todolist from AVL tree\n\ttl, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Get the task from the todolist\n\ttask, found := tl.(*todolist.TodoList).Tasks.Get(strconv.Itoa(taskID))\n\tif !found {\n\t\tpanic(\"Task not found\")\n\t}\n\n\t// Change the status of the task\n\ttodolist.ToggleTaskStatus(task.(*todolist.Task))\n\n\treturn \"task status changed successfully\"\n}\n\nfunc RemoveTask(todolistID int, taskID int) string {\n\t// Get Todolist from AVL tree\n\ttl, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Get the task from the todolist\n\t_, ok = tl.(*todolist.TodoList).Tasks.Get(strconv.Itoa(taskID))\n\tif !ok {\n\t\tpanic(\"Task not found\")\n\t}\n\n\t// Change the status of the task\n\ttl.(*todolist.TodoList).RemoveTask(strconv.Itoa(taskID))\n\n\treturn \"task status changed successfully\"\n}\n\nfunc RemoveTodoList(todolistID int) string {\n\t// Get Todolist from AVL tree\n\t_, ok := todolistTree.Get(strconv.Itoa(todolistID))\n\tif !ok {\n\t\tpanic(\"Todolist not found\")\n\t}\n\n\t// Remove the todolist\n\ttodolistTree.Remove(strconv.Itoa(todolistID))\n\n\treturn \"Todolist removed successfully\"\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn renderHomepage()\n\t}\n\n\treturn \"unknown page\"\n}\n\nfunc renderHomepage() string {\n\t// Define empty buffer\n\tvar b bytes.Buffer\n\n\tb.WriteString(\"# Welcome to ToDolist\\n\\n\")\n\n\t// If no todolists have been created\n\tif todolistTree.Size() == 0 {\n\t\tb.WriteString(\"### No todolists available currently!\")\n\t\treturn b.String()\n\t}\n\n\t// Iterate through AVL tree\n\ttodolistTree.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t// cast raw data from tree into Todolist struct\n\t\ttl := value.(*todolist.TodoList)\n\n\t\t// Add Todolist name\n\t\tb.WriteString(\n\t\t\tufmt.Sprintf(\n\t\t\t\t\"## Todolist #%s: %s\\n\",\n\t\t\t\tkey, // Todolist ID\n\t\t\t\ttl.GetTodolistTitle(),\n\t\t\t),\n\t\t)\n\n\t\t// Add Todolist owner\n\t\tb.WriteString(\n\t\t\tufmt.Sprintf(\n\t\t\t\t\"#### Todolist owner : %s\\n\",\n\t\t\t\ttl.GetTodolistOwner(),\n\t\t\t),\n\t\t)\n\n\t\t// List all todos that are currently Todolisted\n\t\tif todos := tl.GetTasks(); len(todos) \u003e 0 {\n\t\t\tb.WriteString(\n\t\t\t\tufmt.Sprintf(\"Currently Todo tasks: %d\\n\\n\", len(todos)),\n\t\t\t)\n\n\t\t\tfor index, todo := range todos {\n\t\t\t\tb.WriteString(\n\t\t\t\t\tufmt.Sprintf(\"#%d - %s \", index, todo.Title),\n\t\t\t\t)\n\t\t\t\t// displays a checked box if task is marked as done, an empty box if not\n\t\t\t\tif todo.Done {\n\t\t\t\t\tb.WriteString(\n\t\t\t\t\t\t\"☑\\n\\n\",\n\t\t\t\t\t)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tb.WriteString(\n\t\t\t\t\t\"☐\\n\\n\",\n\t\t\t\t)\n\t\t\t}\n\t\t} else {\n\t\t\tb.WriteString(\"No tasks in this list currently\\n\")\n\t\t}\n\n\t\tb.WriteString(\"\\n\")\n\t\treturn false\n\t})\n\n\treturn b.String()\n}\n" + }, + { + "name": "todolist_test.gno", + "body": "package todolistrealm\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/todolist\"\n\t\"gno.land/p/demo/uassert\"\n)\n\nvar (\n\tnode any\n\ttdl *todolist.TodoList\n)\n\nfunc TestNewTodoList(t *testing.T) {\n\ttitle := \"My Todo List\"\n\ttlid, _ := NewTodoList(title)\n\tuassert.Equal(t, 1, tlid, \"tlid does not match\")\n\n\t// get the todolist node from the tree\n\tnode, _ = todolistTree.Get(strconv.Itoa(tlid))\n\t// convert the node to a TodoList struct\n\ttdl = node.(*todolist.TodoList)\n\n\tuassert.Equal(t, title, tdl.Title, \"title does not match\")\n\tuassert.Equal(t, 1, tlid, \"tlid does not match\")\n\tuassert.Equal(t, tdl.Owner.String(), std.OriginCaller().String(), \"owner does not match\")\n\tuassert.Equal(t, 0, len(tdl.GetTasks()), \"Expected no tasks in the todo list\")\n}\n\nfunc TestAddTask(t *testing.T) {\n\tAddTask(1, \"Task 1\")\n\n\ttasks := tdl.GetTasks()\n\tuassert.Equal(t, 1, len(tasks), \"total task does not match\")\n\tuassert.Equal(t, \"Task 1\", tasks[0].Title, \"task title does not match\")\n\tuassert.False(t, tasks[0].Done, \"Expected task to be not done\")\n}\n\nfunc TestToggleTaskStatus(t *testing.T) {\n\tToggleTaskStatus(1, 0)\n\ttask := tdl.GetTasks()[0]\n\tuassert.True(t, task.Done, \"Expected task to be done, but it is not marked as done\")\n\n\tToggleTaskStatus(1, 0)\n\tuassert.False(t, task.Done, \"Expected task to be not done, but it is marked as done\")\n}\n\nfunc TestRemoveTask(t *testing.T) {\n\tRemoveTask(1, 0)\n\ttasks := tdl.GetTasks()\n\tuassert.Equal(t, 0, len(tasks), \"Expected no tasks in the todo list\")\n}\n\nfunc TestRemoveTodoList(t *testing.T) {\n\tRemoveTodoList(1)\n\tuassert.Equal(t, 0, todolistTree.Size(), \"Expected no tasks in the todo list\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "syMsrnryQ9VEiWQ4PlbeG3ZFXQSTNfDiER22T4f4s+eWDKGi0FYM/Vu21YNyqSuNpy7D4USliTbPvb6NeH2+BA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "types", + "path": "gno.land/r/demo/types", + "files": [ + { + "name": "types.gno", + "body": "// package to test types behavior in various conditions (TXs, imports).\npackage types\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nvar (\n\tgInt int = -42\n\tgUint uint = 42\n\tgString string = \"a string\"\n\tgStringSlice []string = []string{\"a\", \"string\", \"slice\"}\n\tgError error = errors.New(\"an error\")\n\tgIntSlice []int = []int{-42, 0, 42}\n\tgUintSlice []uint = []uint{0, 42, 84}\n\tgTree avl.Tree\n\t// gInterface = any{-42, \"a string\", uint(42)}\n)\n\nfunc init() {\n\tgTree.Set(\"a\", \"content of A\")\n\tgTree.Set(\"b\", \"content of B\")\n}\n\nfunc Noop() {}\nfunc RetTimeNow() time.Time { return time.Now() }\nfunc RetString() string { return gString }\nfunc RetStringPointer() *string { return \u0026gString }\nfunc RetUint() uint { return gUint }\nfunc RetInt() int { return gInt }\nfunc RetUintPointer() *uint { return \u0026gUint }\nfunc RetIntPointer() *int { return \u0026gInt }\nfunc RetTree() avl.Tree { return gTree }\nfunc RetIntSlice() []int { return gIntSlice }\nfunc RetUintSlice() []uint { return gUintSlice }\nfunc RetStringSlice() []string { return gStringSlice }\nfunc RetError() error { return gError }\nfunc Panic() { panic(\"PANIC!\") }\n\n// TODO: floats\n// TODO: typed errors\n// TODO: ret interface\n// TODO: recover\n// TODO: take types as input\n\nfunc Render(path string) string {\n\treturn \"package to test data types.\"\n}\n" + }, + { + "name": "types_test.gno", + "body": "package types\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "CSmb6cic6XmpvSNsYbNYRL8s8Jaq6j0IykDxwA6vuyNRpaQvkqq0jAcaIw81JVzNGVpGUZDM/EArpyT0rcaLCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "ui", + "path": "gno.land/r/demo/ui", + "files": [ + { + "name": "ui.gno", + "body": "package ui\n\nimport \"gno.land/p/demo/ui\"\n\nfunc Render(path string) string {\n\t// TODO: build this realm as a demo one with one page per feature.\n\n\t// TODO: pagination\n\t// TODO: non-standard markdown\n\t// TODO: error, warn\n\t// TODO: header\n\t// TODO: HTML\n\t// TODO: toc\n\t// TODO: forms\n\t// TODO: comments\n\n\tdom := ui.DOM{\n\t\tPrefix: \"r/demo/ui:\",\n\t}\n\n\tdom.Title = \"UI Demo\"\n\n\tdom.Header.Append(ui.Breadcrumb{\n\t\tui.Link{Text: \"foo\", Path: \"foo\"},\n\t\tui.Link{Text: \"bar\", Path: \"foo/bar\"},\n\t})\n\n\tdom.Body.Append(\n\t\tui.Paragraph(\"Simple UI demonstration.\"),\n\t\tui.BulletList{\n\t\t\tui.Text(\"a text\"),\n\t\t\tui.Link{Text: \"a relative link\", Path: \"foobar\"},\n\t\t\tui.Text(\"another text\"),\n\t\t\t// ui.H1(\"a H1 text\"),\n\t\t\tui.Bold(\"a bold text\"),\n\t\t\tui.Italic(\"italic text\"),\n\t\t\tui.Text(\"raw markdown with **bold** text in the middle.\"),\n\t\t\tui.Code(\"some inline code\"),\n\t\t\tui.Link{Text: \"a remote link\", URL: \"https://gno.land\"},\n\t\t},\n\t)\n\n\tdom.Footer.Append(ui.Text(\"I'm the footer.\"))\n\tdom.Body.Append(ui.Text(\"another string.\"))\n\tdom.Body.Append(ui.Paragraph(\"a paragraph.\"), ui.HR{})\n\n\treturn dom.String()\n}\n" + }, + { + "name": "ui_test.gno", + "body": "package ui\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestRender(t *testing.T) {\n\tgot := Render(\"\")\n\texpected := \"# UI Demo\\n\\n[foo](r/demo/ui:foo) / [bar](r/demo/ui:foo/bar)\\n\\n\\nSimple UI demonstration.\\n\\n- a text\\n- [a relative link](r/demo/ui:foobar)\\n- another text\\n- **a bold text**\\n- _italic text_\\n- raw markdown with **bold** text in the middle.\\n- `some inline code`\\n- [a remote link](https://gno.land)\\n\\nanother string.\\n\\na paragraph.\\n\\n\\n---\\n\\n\\nI'm the footer.\\n\\n\"\n\tuassert.Equal(t, expected, got)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "DqBetqLc+vq1hKsWp/NuUu47M8zATaN5N2re+zOtvF+a8QOIF7r48jx0pLU64aHwJgeCMBv37bRCD68saj4GBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "userbook", + "path": "gno.land/r/demo/userbook", + "files": [ + { + "name": "render.gno", + "body": "// Package userbook demonstrates a small userbook system working with gnoweb\npackage userbook\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/r/sys/users\"\n\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/txlink\"\n)\n\nconst usersLink = \"/r/gnoland/users/v1\"\n\nfunc Render(path string) string {\n\tp := pager.NewPager(signupsTree, 20, true)\n\tpage := p.MustGetPageByPath(path)\n\n\tout := \"# Welcome to UserBook!\\n\\n\"\n\n\tout += ufmt.Sprintf(\"## [Click here to sign up!](%s)\\n\\n\", txlink.Call(\"SignUp\"))\n\tout += \"---\\n\\n\"\n\n\tfor _, item := range page.Items {\n\t\tsignup := item.Value.(*Signup)\n\t\tuser := signup.address.String()\n\n\t\tif data := users.ResolveAddress(signup.address); data != nil {\n\t\t\tuser = ufmt.Sprintf(\"[%s](%s:%s)\", data.Name(), usersLink, data.Name())\n\t\t}\n\n\t\tout += ufmt.Sprintf(\"- **User #%d - %s - signed up on %s**\\n\\n\", signup.ordinal, user, signup.timestamp.Format(\"January 2 2006, 03:04:04 PM\"))\n\t}\n\n\tout += \"---\\n\\n\"\n\tout += \"**Page \" + strconv.Itoa(page.PageNumber) + \" of \" + strconv.Itoa(page.TotalPages) + \"**\\n\\n\"\n\tout += page.Picker(path)\n\treturn out\n}\n" + }, + { + "name": "userbook.gno", + "body": "// Package userbook demonstrates a small userbook system working with gnoweb\npackage userbook\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Signup struct {\n\taddress std.Address\n\tordinal int\n\ttimestamp time.Time\n}\n\nvar (\n\tsignupsTree = avl.NewTree()\n\ttracker = avl.NewTree()\n\tidCounter seqid.ID\n)\n\nconst signUpEvent = \"SignUp\"\n\nfunc init() {\n\tSignUp() // Sign up the deployer\n}\n\nfunc SignUp() string {\n\t// Get transaction caller\n\tcaller := std.PreviousRealm().Address()\n\n\t// Check if the user is already signed up\n\tif _, exists := tracker.Get(caller.String()); exists {\n\t\tpanic(caller.String() + \" is already signed up!\")\n\t}\n\n\tnow := time.Now()\n\n\t// Sign up the user\n\tsignupsTree.Set(idCounter.Next().String(), \u0026Signup{\n\t\tcaller,\n\t\tsignupsTree.Size(),\n\t\tnow,\n\t})\n\n\ttracker.Set(caller.String(), struct{}{})\n\n\tstd.Emit(signUpEvent, \"account\", caller.String())\n\n\treturn ufmt.Sprintf(\"%s added to userbook! Timestamp: %s\", caller.String(), now.Format(time.RFC822Z))\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "OHjo1EPYV2FdDIdad0hilxvJpbhALbOUObLbXRAFBcTHkaJD19/OFJSaiDcwd6CexO1xyKIQzaTzaIzeUzYkBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "wugnot", + "path": "gno.land/r/demo/wugnot", + "files": [ + { + "name": "wugnot.gno", + "body": "package wugnot\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/grc20reg\"\n)\n\nvar Token, adm = grc20.NewToken(\"wrapped GNOT\", \"wugnot\", 0)\n\nconst (\n\tugnotMinDeposit uint64 = 1000\n\twugnotMinDeposit uint64 = 1\n)\n\nfunc init() {\n\tgrc20reg.Register(Token.Getter(), \"\")\n}\n\nfunc Deposit() {\n\tcaller := std.PreviousRealm().Address()\n\tsent := std.OriginSend()\n\tamount := sent.AmountOf(\"ugnot\")\n\n\trequire(uint64(amount) \u003e= ugnotMinDeposit, ufmt.Sprintf(\"Deposit below minimum: %d/%d ugnot.\", amount, ugnotMinDeposit))\n\n\tcheckErr(adm.Mint(caller, uint64(amount)))\n}\n\nfunc Withdraw(amount uint64) {\n\trequire(amount \u003e= wugnotMinDeposit, ufmt.Sprintf(\"Deposit below minimum: %d/%d wugnot.\", amount, wugnotMinDeposit))\n\n\tcaller := std.PreviousRealm().Address()\n\tpkgaddr := std.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 := std.NewBanker(std.BankerTypeRealmSend)\n\tsend := std.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 := std.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() uint64 { return Token.TotalSupply() }\n\nfunc BalanceOf(owner std.Address) uint64 {\n\treturn Token.BalanceOf(owner)\n}\n\nfunc Allowance(owner, spender std.Address) uint64 {\n\treturn Token.Allowance(owner, spender)\n}\n\nfunc Transfer(to std.Address, amount uint64) {\n\tuserTeller := Token.CallerTeller()\n\tcheckErr(userTeller.Transfer(to, amount))\n}\n\nfunc Approve(spender std.Address, amount uint64) {\n\tuserTeller := Token.CallerTeller()\n\tcheckErr(userTeller.Approve(spender, amount))\n}\n\nfunc TransferFrom(from, to std.Address, amount uint64) {\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/demo/wugnot_test\npackage wugnot_test\n\nimport (\n\t\"fmt\"\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/wugnot\"\n)\n\nvar (\n\taddr1 = testutils.TestAddress(\"test1\")\n\taddrc = std.DerivePkgAddr(\"gno.land/r/demo/wugnot\")\n\taddrt = std.DerivePkgAddr(\"gno.land/r/demo/wugnot_test\")\n)\n\nfunc main() {\n\ttesting.IssueCoins(addrc, std.Coins{{\"ugnot\", 100000001}}) // TODO: remove this\n\n\t// issue ugnots\n\ttesting.IssueCoins(addr1, std.Coins{{\"ugnot\", 100000001}})\n\n\t// print initial state\n\tprintBalances()\n\t// println(wugnot.Render(\"queues\"))\n\t// println(\"A -\", wugnot.Render(\"\"))\n\n\ttesting.SetOriginCaller(addr1)\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", 123_400}})\n\twugnot.Deposit()\n\tprintBalances()\n\twugnot.Withdraw(4242)\n\tprintBalances()\n}\n\nfunc printBalances() {\n\tprintSingleBalance := func(name string, addr std.Address) {\n\t\twugnotBal := wugnot.BalanceOf(addr)\n\t\ttesting.SetOriginCaller(addr)\n\t\trobanker := std.NewBanker(std.BankerTypeReadonly)\n\t\tcoins := robanker.GetCoins(addr).AmountOf(\"ugnot\")\n\t\tfmt.Printf(\"| %-13s | addr=%s | wugnot=%-5d | ugnot=%-9d |\\n\",\n\t\t\tname, addr, wugnotBal, coins)\n\t}\n\tprintln(\"-----------\")\n\tprintSingleBalance(\"wugnot_test\", addrt)\n\tprintSingleBalance(\"wugnot\", addrc)\n\tprintSingleBalance(\"addr1\", addr1)\n\tprintln(\"-----------\")\n}\n\n// Output:\n// -----------\n// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=0 | ugnot=0 |\n// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 |\n// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |\n// -----------\n// -----------\n// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=123400 | ugnot=0 |\n// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 |\n// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |\n// -----------\n// -----------\n// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=119158 | ugnot=4242 |\n// | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=99995759 |\n// | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 |\n// -----------\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "LrKLy7BoddfDRcYlMTtxJ10josLpbqY+KVfYOECZOcMXb496AbfB8CvjLs/7Kxad9WCju5KYSEGIXZUEIJSBCQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "docs", + "path": "gno.land/r/docs", + "files": [ + { + "name": "docs.gno", + "body": "package docs\n\nfunc Render(_ string) string {\n\treturn `# Gno Examples Documentation\n\nWelcome to the Gno examples documentation index.\nExplore various examples to learn more about Gno functionality and usage.\n\n## Examples\n\n- [Hello World](/r/docs/hello) - A simple introductory example.\n- [Adder](/r/docs/adder) - An interactive example to update a number with transactions.\n- [Source](/r/docs/source) - View realm source code.\n- [Buttons](/r/docs/buttons) - Add buttons to your realm's render.\n- [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. \n- [AVL Pager + Render paths](/r/docs/avl_pager_params) - Handle render arguments with pagination.\n- [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image.\n- [Optional Render](/r/docs/optional_render) - Render() is optional in realms.\n- [MiniSocial](/r/docs/minisocial) - Minimalistic social media app for learning purposes.\n- [Markdown](/r/docs/markdown) - Documentation for Gno Flavored Markdown syntax and features.\n- [Resolving usernames and addresses](/r/docs/users) - How to resolve usernames and addresses via the r/sys/users realm.\n- ...\n\u003c!-- meta issue with suggestions: https://github.com/gnolang/gno/issues/3292 --\u003e\n\n## Other resources\n\n- [Official documentation](https://github.com/gnolang/gno/tree/master/docs) \u003c!-- should be /docs with gnoweb embedding the docs/ folder. --\u003e\n`\n}\n" + }, + { + "name": "docs_test.gno", + "body": "package docs\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRenderHome(t *testing.T) {\n\toutput := Render(\"\")\n\n\t// Check for the presence of key sections\n\tif !contains(output, \"# Gno Examples Documentation\") {\n\t\tt.Errorf(\"Render output is missing the title.\")\n\t}\n\tif !contains(output, \"Official documentation\") {\n\t\tt.Errorf(\"Render output is missing the official documentation link.\")\n\t}\n}\n\nfunc contains(s, substr string) bool {\n\treturn strings.Index(s, substr) \u003e= 0\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "MaDm72ve66DQGc/NYliSDMerPEUVC/fvHW0t/jqJdBSCZNvFKHQim14vcgF3W/Wm+DEQRhCZQ3XYAhAwdbYSBQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "adder", + "path": "gno.land/r/docs/adder", + "files": [ + { + "name": "adder.gno", + "body": "package adder\n\nimport (\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/moul/txlink\"\n)\n\n// Global variables to store the current number and last update timestamp\nvar (\n\tnumber int\n\tlastUpdate time.Time\n)\n\n// Add function to update the number and timestamp\nfunc Add(n int) {\n\tnumber += n\n\tlastUpdate = time.Now()\n}\n\n// Render displays the current number value, last update timestamp, and a link to call Add with 42\nfunc Render(path string) string {\n\t// Display the current number and formatted last update time\n\tresult := \"# Add Example\\n\\n\"\n\tresult += \"Current Number: \" + strconv.Itoa(number) + \"\\n\\n\"\n\tresult += \"Last Updated: \" + formatTimestamp(lastUpdate) + \"\\n\\n\"\n\n\t// Generate a transaction link to call Add with 42 as the default parameter\n\ttxLink := txlink.Call(\"Add\", \"n\", \"42\")\n\tresult += \"[Increase Number](\" + txLink + \")\\n\"\n\n\treturn result\n}\n\n// Helper function to format the timestamp for readability\nfunc formatTimestamp(timestamp time.Time) string {\n\tif timestamp.IsZero() {\n\t\treturn \"Never\"\n\t}\n\treturn timestamp.Format(\"2006-01-02 15:04:05\")\n}\n" + }, + { + "name": "adder_test.gno", + "body": "package adder\n\nimport (\n\t\"testing\"\n)\n\nfunc TestRenderAndAdd(t *testing.T) {\n\t// Initial Render output\n\toutput := Render(\"\")\n\texpected := `# Add Example\n\nCurrent Number: 0\n\nLast Updated: Never\n\n[Increase Number](/r/docs/adder$help\u0026func=Add\u0026n=42)\n`\n\tif output != expected {\n\t\tt.Errorf(\"Initial Render failed, got:\\n%s\", output)\n\t}\n\n\t// Call Add with a value of 10\n\tAdd(10)\n\n\t// Call Add again with a value of -5\n\tAdd(-5)\n\n\t// Render after two Add calls\n\tfinalOutput := Render(\"\")\n\n\t// Initial Render output\n\toutput = Render(\"\")\n\texpected = `# Add Example\n\nCurrent Number: 5\n\nLast Updated: 2009-02-13 23:31:30\n\n[Increase Number](/r/docs/adder$help\u0026func=Add\u0026n=42)\n`\n\tif output != expected {\n\t\tt.Errorf(\"Final Render failed, got:\\n%s\\nexpected:\\n%s\", output, finalOutput)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "6rb8IYlaGPNJdmMe1G2xVTt06L5NQnxroAX1yOcOfNckLDsWr32mb0N8t0FwojpuWNMt1QSaeaiQynZz+CE2Dg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "avl_pager", + "path": "gno.land/r/docs/avl_pager", + "files": [ + { + "name": "avl_pager.gno", + "body": "package avl_pager\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n)\n\n// Tree instance for 100 items\nvar tree *avl.Tree\n\n// Initialize a tree with 100 items.\nfunc init() {\n\ttree = avl.NewTree()\n\tfor i := 1; i \u003c= 100; i++ {\n\t\tkey := \"Item\" + strconv.Itoa(i)\n\t\ttree.Set(key, \"Value of \"+key)\n\t}\n}\n\n// Render paginated content based on the given URL path.\n// URL format: `...?page=\u003cpage\u003e\u0026size=\u003csize\u003e` (default is page 1 and size 10).\nfunc Render(path string) string {\n\tp := pager.NewPager(tree, 10, false) // Default page size is 10\n\tpage := p.MustGetPageByPath(path)\n\n\t// Header and pagination info\n\tresult := \"# Paginated Items\\n\"\n\tresult += \"Page \" + strconv.Itoa(page.PageNumber) + \" of \" + strconv.Itoa(page.TotalPages) + \"\\n\\n\"\n\tresult += page.Picker(path) + \"\\n\\n\"\n\n\t// Display items on the current page\n\tfor _, item := range page.Items {\n\t\tresult += \"- \" + item.Key + \": \" + item.Value.(string) + \"\\n\"\n\t}\n\n\tresult += \"\\n\" + page.Picker(path) // Repeat page picker for ease of navigation\n\treturn result\n}\n" + }, + { + "name": "avl_pager_test.gno", + "body": "package avl_pager\n\nimport (\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\treq := \"?sort=name\u0026order=asc\"\n\toutput := Render(req)\n\texpected := `# Paginated Items\nPage 1 of 10\n\n**1** | [2](?page=2\u0026order=asc\u0026sort=name) | [3](?page=3\u0026order=asc\u0026sort=name) | … | [10](?page=10\u0026order=asc\u0026sort=name)\n\n- Item1: Value of Item1\n- Item10: Value of Item10\n- Item100: Value of Item100\n- Item11: Value of Item11\n- Item12: Value of Item12\n- Item13: Value of Item13\n- Item14: Value of Item14\n- Item15: Value of Item15\n- Item16: Value of Item16\n- Item17: Value of Item17\n\n**1** | [2](?page=2\u0026order=asc\u0026sort=name) | [3](?page=3\u0026order=asc\u0026sort=name) | … | [10](?page=10\u0026order=asc\u0026sort=name)`\n\tif output != expected {\n\t\tt.Errorf(\"Render(%q) failed, got:\\n%s\\nwant:\\n%s\", req, output, expected)\n\t}\n}\n\nfunc TestRender_page2(t *testing.T) {\n\treq := \"?page=2\u0026size=10\u0026sort=name\u0026order=asc\"\n\toutput := Render(req)\n\texpected := `# Paginated Items\nPage 2 of 10\n\n[1](?page=1\u0026order=asc\u0026size=10\u0026sort=name) | **2** | [3](?page=3\u0026order=asc\u0026size=10\u0026sort=name) | [4](?page=4\u0026order=asc\u0026size=10\u0026sort=name) | … | [10](?page=10\u0026order=asc\u0026size=10\u0026sort=name)\n\n- Item18: Value of Item18\n- Item19: Value of Item19\n- Item2: Value of Item2\n- Item20: Value of Item20\n- Item21: Value of Item21\n- Item22: Value of Item22\n- Item23: Value of Item23\n- Item24: Value of Item24\n- Item25: Value of Item25\n- Item26: Value of Item26\n\n[1](?page=1\u0026order=asc\u0026size=10\u0026sort=name) | **2** | [3](?page=3\u0026order=asc\u0026size=10\u0026sort=name) | [4](?page=4\u0026order=asc\u0026size=10\u0026sort=name) | … | [10](?page=10\u0026order=asc\u0026size=10\u0026sort=name)`\n\tif output != expected {\n\t\tt.Errorf(\"Render(%q) failed, got:\\n%s\\nwant:\\n%s\", req, output, expected)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "+Bgv1jmxApnBhR/Jl1DMlDsxPe0tU4gvkj+r/2cRSgxpquI03eF+yNawJxauGa/51MxkzaCd6McOwEidjIVjBA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "avl_pager_params", + "path": "gno.land/r/docs/avl_pager_params", + "files": [ + { + "name": "render.gno", + "body": "package avl_pager_params\n\nimport (\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/realmpath\"\n)\n\n// We'll keep some demo data in an AVL tree to showcase pagination.\nvar (\n\titems *avl.Tree\n\tidCounter seqid.ID\n)\n\nfunc init() {\n\titems = avl.NewTree()\n\t// Populate the tree with 15 sample items for demonstration.\n\tfor i := 1; i \u003c= 15; i++ {\n\t\tid := idCounter.Next().String()\n\t\titems.Set(id, \"Some item value: \"+id)\n\t}\n}\n\nfunc Render(path string) string {\n\t// 1) Parse the incoming path to split route vs. query.\n\treq := realmpath.Parse(path)\n\t// - req.Path contains everything *before* ? or $ (? - query params, $ - gnoweb params)\n\t// - The remaining part (page=2, size=5, etc.) is not in req.Path.\n\n\t// 2) If no specific route is provided (req.Path == \"\"), we’ll show a “home” page\n\t// that displays a list of configs in paginated form.\n\tif req.Path == \"\" {\n\t\treturn renderHome(path)\n\t}\n\n\t// 3) If a route *is* provided (e.g. :SomeKey),\n\t// we will interpret it as a request for a specific page.\n\treturn renderConfigItem(req.Path)\n}\n\n// renderHome shows a paginated list of config items if route == \"\".\nfunc renderHome(fullPath string) string {\n\t// Create a Pager for our config tree, with a default page size of 5.\n\tp := pager.NewPager(items, 5, false)\n\n\t// MustGetPageByPath uses the *entire* path (including query parts: ?page=2, etc.)\n\tpage := p.MustGetPageByPath(fullPath)\n\n\t// Start building the output (plain text or markdown).\n\tout := \"# AVL Pager + Render paths\\n\\n\"\n\tout += `This realm showcases how to maintain a paginated list while properly parsing render paths. \nYou can see how a single page can include a paginated element (like the example below), and how clicking \nan item can take you to a dedicated page for that specific item.\n\nNo matter how you browse through the paginated list, the introductory text (this section) remains the same.\n\n`\n\n\tout += ufmt.Sprintf(\"Showing page %d of %d\\n\\n\", page.PageNumber, page.TotalPages)\n\n\t// List items for this page.\n\tfor _, item := range page.Items {\n\t\t// Link each item to a details page: e.g. \":Config01\"\n\t\tout += ufmt.Sprintf(\"- [Item %s](/r/docs/avl_pager_params:%s)\\n\", item.Key, item.Key)\n\t}\n\n\t// Insert pagination controls (previous/next links, etc.).\n\tout += \"\\n\" + page.Picker(fullPath) + \"\\n\\n\"\n\tout += \"### [Go back to r/docs](/r/docs)\"\n\n\treturn out\n}\n\n// renderConfigItem shows details for a single item, e.g. \":item001\".\nfunc renderConfigItem(itemName string) string {\n\tvalue, ok := items.Get(itemName)\n\tif !ok {\n\t\treturn ufmt.Sprintf(\"**No item found** for key: %s\", itemName)\n\t}\n\n\tout := ufmt.Sprintf(\"# Item %s\\n\\n%s\\n\\n\", itemName, value.(string))\n\tout += \"[Go back](/r/docs/avl_pager_params)\"\n\treturn out\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "dRNwOUDRiEuBS6YfHq+Pq0AeUM1u6qDTQcch6Q9FMhFMaOF3RgcQSHP7OqZKYC65wAGLo+RiCbjKzaxyodbXCw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "buttons", + "path": "gno.land/r/docs/buttons", + "files": [ + { + "name": "buttons.gno", + "body": "package buttons\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/txlink\"\n)\n\nvar (\n\tmotd = \"The Initial Message\\n\\n\"\n\tlastCaller std.Address\n)\n\nfunc UpdateMOTD(newmotd string) {\n\tmotd = newmotd\n\tlastCaller = std.PreviousRealm().Address()\n}\n\nfunc Render(path string) string {\n\tif path == \"motd\" {\n\t\tout := \"# Message of the Day:\\n\\n\"\n\t\tout += \"---\\n\\n\"\n\t\tout += \"# \" + motd + \"\\n\\n\"\n\t\tout += \"---\\n\\n\"\n\t\tlink := txlink.Call(\"UpdateMOTD\", \"newmotd\", \"Message!\") // \"/r/docs/buttons$help\u0026func=UpdateMOTD\u0026newmotd=Message!\"\n\t\tout += ufmt.Sprintf(\"Click **[here](%s)** to update the Message of The Day!\\n\\n\", link)\n\t\tout += \"[Go back to home page](/r/docs/buttons)\\n\\n\"\n\t\tout += \"Last updated by \" + lastCaller.String()\n\n\t\treturn out\n\t}\n\n\tout := `# Buttons\n\nUsers can create simple hyperlink buttons to view specific realm pages and\ndo specific realm actions, such as calling a specific function with some arguments.\n\nThe foundation for this functionality are markdown links; for example, you can\nclick...\n` + \"\\n## [here](/r/docs/buttons:motd)\\n\" + `...to view this realm's message of the day.`\n\n\treturn out\n}\n" + }, + { + "name": "buttons_test.gno", + "body": "package buttons\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRenderMotdLink(t *testing.T) {\n\tres := Render(\"motd\")\n\tconst wantLink = \"/r/docs/buttons$help\u0026func=UpdateMOTD\u0026newmotd=Message%21\"\n\tif !strings.Contains(res, wantLink) {\n\t\tt.Fatalf(\"%s\\ndoes not contain correct help page link: %s\", res, wantLink)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "o6qha2t1lRRKKL4m0w0RR0t9l7KM+IcFR1AV0Mm7gCiZAN4EM3i3zuegg7STwhjcB1Dz7QqtiURS2kVpMg8hBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "hello", + "path": "gno.land/r/docs/hello", + "files": [ + { + "name": "hello.gno", + "body": "// Package hello_world demonstrates basic usage of Render().\n// Try adding `:World` at the end of the URL, like `.../hello:World`.\npackage hello\n\n// Render outputs a greeting. It customizes the message based on the provided path.\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn \"# Hello, 世界!\"\n\t}\n\treturn \"# Hello, \" + path + \"!\"\n}\n" + }, + { + "name": "hello_test.gno", + "body": "package hello\n\nimport (\n\t\"testing\"\n)\n\nfunc TestHello(t *testing.T) {\n\texpected := \"# Hello, 世界!\"\n\tgot := Render(\"\")\n\tif got != expected {\n\t\tt.Fatalf(\"Expected %s, got %s\", expected, got)\n\t}\n\n\tgot = Render(\"world\")\n\texpected = \"# Hello, world!\"\n\tif got != expected {\n\t\tt.Fatalf(\"Expected %s, got %s\", expected, got)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "GNdgAs85J+GuhMX4H3R+hmUrgue8F4pi5eQDafC7Wg2ZGitG8zw5QA21akkkDEqKHxDRWXkdqaCUHooYdzDeDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "image_embed", + "path": "gno.land/r/docs/img_embed", + "files": [ + { + "name": "img_embed.gno", + "body": "package image_embed\n\n// Render displays a title and an embedded image from Imgur\nfunc Render(path string) string {\n\treturn `# Image Embed Example\n\nHere’s an example of embedding an image in a Gno realm:\n\n![Example Image](https://i.imgur.com/So4rBPB.jpeg)`\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "daTgxdWvGT7INgK6XN8ocbgBmjFtijK827+mXEHPrtm6K3Nl/K1Dnixy3+dbcCpZCaPyg23DLukKBXgngfbrBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "markdown", + "path": "gno.land/r/docs/markdown", + "files": [ + { + "name": "markdown.gno", + "body": "package markdown\n\nimport \"strings\"\n\n// this package can be used to test markdown rendering engines.\n\nfunc Render(path string) string {\n\toutput := `# Markdown on Gno\n\n## Introduction\n\nMarkdown on Gno is based on standard markdown, but also has some unique features, making it the Gno Flavored Markdown. This document describes the current markdown support in Gno, demonstrating both the syntax and its rendered output.\n\n\u003e [!NOTE]\n\u003e Markdown support in Gno is still evolving. New features and improvements will be added in future releases.\n\n## Basic Syntax\n\n### Headings\n\nHeadings are created using hash symbols (#). The number of hash symbols indicates the heading level.\n\n±±±markdown\n# Heading 1\n## Heading 2\n### Heading 3\n#### Heading 4\n##### Heading 5\n###### Heading 6\n±±±\n\n# Heading 1\n## Heading 2\n### Heading 3\n#### Heading 4\n##### Heading 5\n###### Heading 6\n\n### Text Formatting\n\nYou can format text using the following syntax:\n\n±±±markdown\n**Bold text**\n*Italic text*\n~~Strikethrough text~~\n**Bold and _nested italic_**\n***All bold and italic***\n±±±\n\n**Bold text**\n*Italic text*\n~~Strikethrough text~~\n**Bold and _nested italic_**\n***All bold and italic***\n\n### Links\n\nLinks can be created using the following syntax:\n\n±±±markdown\n[Link text](https://example.com)\n[Link with title](https://example.com \"Link title\")\n±±±\n\n[Link text](https://example.com)\n[Link with title](https://example.com \"Link title\")\n\nXXX: custom CSS for internal/external links and if possible for \"in the same realm/namespace\".\n\n### Lists\n\nUnordered lists use asterisks, plus signs, or hyphens:\n\n±±±markdown\n* Item 1\n* Item 2\n * Nested item 1\n * Nested item 2\n±±±\n\n* Item 1\n* Item 2\n * Nested item 1\n * Nested item 2\n\nOrdered lists use numbers:\n\n±±±markdown\n1. First item\n2. Second item\n 1. Nested item 1\n 2. Nested item 2\n±±±\n\n1. First item\n2. Second item\n 1. Nested item 1\n 2. Nested item 2\n\n### Blockquotes\n\nBlockquotes are created using the \u003e character:\n\n±±±markdown\n\u003e This is a blockquote\n\u003e \n\u003e It can span multiple lines\n±±±\n\n\u003e This is a blockquote\n\u003e \n\u003e It can span multiple lines\n\n### Code\n\nInline code uses single backticks:\n\n±±±markdown\nUse ±func main()± to define the entry point.\n±±±\n\nUse ±func main()± to define the entry point.\n\nCode blocks use triple backticks with an optional language identifier:\n\n\u003c!-- XXX: make the example with 'gno' instead of 'go' --\u003e\n\n±±±markdown\n±±±go\npackage main\n\nfunc main() {\n println(\"Hello, Gno!\")\n}\n±±±\n\n±±±go\npackage main\n\nfunc main() {\n println(\"Hello, Gno!\")\n}\n±±±\n\n### Horizontal Rules\n\nHorizontal rules are created using three or more asterisks, hyphens, or underscores:\n\n±±±markdown\n---\n±±±\n\n---\n\n\u003c!-- XXX: add again this feature that was removed --\u003e\n\u003c!--\n### Task Lists\n\nGno supports task lists for tracking to-do items:\n\n±±±markdown\n- [x] Completed task\n- [ ] Pending task\n±±±\n\n- [x] Completed task\n- [ ] Pending task\n--\u003e\n\n## Tables\n\nTables are created using pipes and hyphens:\n\n±±±markdown\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |\n| Cell 3 | Cell 4 |\n±±±\n\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Cell 1 | Cell 2 |\n| Cell 3 | Cell 4 |\n\n## Images\n\nImages can be included using the following syntax:\n\n±±±markdown\n![Alt text](/public/imgs/gnoland.svg \"Optional title\")\n±±±\n\n![Alt text](/public/imgs/gnoland.svg \"Optional title\")\n\n## Gno-Specific Features\n\n### HTML Support\n\nBy design, most typical HTML support is disabled in Gno's markdown implementation. This is an intentional decision for both security and ecosystem cohesion.\n\nWhile traditional markdown often allows arbitrary HTML tags, Gno Flavored Markdown takes a more controlled approach:\n\n- We may progressively whitelist certain HTML components or add custom ones over time\n- Our priority is to enhance our flavored markdown to natively support all essential components\n- We aim to eventually support all the initially HTML-supported features, but with syntax that is:\n - More readable when viewing the source directly\n - More integrable with custom browsers such as gnobro in CLI\n\nThis approach allows for a more consistent rendering experience across different Gno interfaces while maintaining security and readability as core principles.\n\n### Columns\n\nGno Flavored Markdown introduces a column layout system using special HTML-like tags. This system allows content to be organized into multiple vertical columns using heading elements as separators.\nOn GnoWeb, up to four columns can be displayed in a single row; exceeding this limit will transfer additional columns to another row, and so on.\n\n#### Basic Syntax\nWrap your column content in ±\u003cgno-columns\u003e± tags and use standard markdown headings (from h1 ±#± to h6 ±######±) to define column breaks:\n\n±±±markdown\n\u003cgno-columns\u003e\n## Column 1 Title\n\nColumn 1 content\n\n## Column 2 Title\n\nColumn 2 content\n\u003c/gno-columns\u003e\n±±±\n\nThis will render as:\n\n\u003cgno-columns\u003e\n## Column 1 Title\n\nColumn 1 content\n\n## Column 2 Title\n\nColumn 2 content\n\u003c/gno-columns\u003e\n---\n\n#### Key Features\n\n1. **Heading Levels**: Any heading level from ±#± (h1) to ±######± (h6) can be used as column separators. The first one will be the reference for subsequent separator.\n\n±±±markdown\n\u003cgno-columns\u003e\n# Main Section\n\nContent\n\n## Subsection\n\nMore content\n\n# Second section\n\nContent\n\n## Subsection\n\nMore content\n\u003c/gno-columns\u003e\n±±±\n\n\u003cgno-columns\u003e\n## Main Section\nContent\n### Subsection\nMore content\n## Second section\nContent\n### Subsection\nMore content\n\u003c/gno-columns\u003e\n\n---\n\n2. **Empty Headings**: Use empty headings to create columns without titles:\n\n±±±markdown\n\u003cgno-columns\u003e\n###\n\n![Alt text](/public/imgs/gnoland.svg \"Optional title\")\nContent without title\n\n### Second Column\n\nAnother column\n\u003c/gno-columns\u003e\n±±±\n\n\u003cgno-columns\u003e\n###\n\n![Alt text](/public/imgs/gnoland.svg \"Optional title\")\nContent without title\n\n### Second Column\n\nAnother column\n\u003c/gno-columns\u003e\n\n\n#### Validation Rules\n\nThe column system will ignore invalid structures and generate errors in the form of comments in the following cases:\n\n1. Unclosed Tags\n±±±markdown\n\u003cgno-columns\u003e\n## Title\nContent\n\u003c!-- Missing closing tag --\u003e\n±±±\n\n2. Nested Columns\n\nNested columns tag will be ignored, e.g.\n\n±±±markdown\n\u003cgno-columns\u003e\n## Parent\n\u003cgno-columns\u003e \u003c!-- this tag will be ignored --\u003e\n ## Child\n\u003c/gno-columns\u003e\n\u003c/gno-columns\u003e \u003c!-- this tag will be ignored --\u003e\n±±±\n\n3. Invalid Headings.\n\nInvalid stating heading will generate an error, e.g.\n\n - Headings in list:\n±±±markdown\n\u003cgno-columns\u003e\n- ## List Heading\n\u003c/gno-columns\u003e\n±±±\n\n - Headings beyond h6:\n±±±markdown\n\u003cgno-columns\u003e\n####### Invalid\n\u003c/gno-columns\u003e\n±±±\n\n4. Content Before First Heading\n\nSetting content before first heading, is considered as invalid and will generate an error.\n\n±±±markdown\n\u003cgno-columns\u003e\nInvalid content\n## Title\n\u003c/gno-columns\u003e\n±±±\n\n#### Best Pratices\n- Always start column content with a heading\n- Maintain consistent heading levels within a columns block\n- Close tags immediately after final column content\n- Prefer simple markdown structures within columns\n- Use empty headings (##) for image-focused columns\n\n### Usernames\n\nXXX: TODO (add again this feature that was removed)\n\n\u003c!--\n±±±markdown\n@username\n@dev123\n±±±\n\n@username\n@dev123\n--\u003e\n\n### Bech32 Addresses\n\nXXX: TODO\n\nGno markdown can automatically recognize and render Bech32 addresses.\n\n±±±markdown\ng1jg955hm9a6f0yen878c2hur6q7stqz7rzpyrwpe\n±±±\n\ng1jg955hm9a6f0yen878c2hur6q7stqz7rzpyrwpe\n\n### Smart Contract Integration\n\nXXX: TODO\n\n±±±markdown\ngno.land/r/boards\ngno.land/r/boards:foo/bar\ngno.land/r/docs/markdown$source\n±±±\n\ngno.land/r/boards\ngno.land/r/boards:foo/bar\ngno.land/r/docs/markdown$source\n\n### And more...\n\nXXX: TODO\n\n## Future Enhancements\n\nThe markdown support in Gno is being actively developed. Future enhancements may include:\n\n- Extended support for custom components\n- Interactive elements specific to blockchain functions\n- Rich rendering of on-chain data\n- Better integration with Gno's package system\n\n[Read more](https://github.com/gnolang/gno/issues/3255)\n\n## Conclusion\n\nMarkdown on Gno provides a familiar syntax for developers who have experience with GitHub Flavored Markdown, while adding blockchain-specific extensions that make it more powerful in the context of the Gno platform.\n\nAs the Gno ecosystem grows, expect the markdown capabilities to expand accordingly, providing even richer formatting and interactive elements for documentation and user interfaces.\n`\n\toutput = strings.ReplaceAll(output, \"±\", \"`\")\n\treturn output\n}\n" + }, + { + "name": "markdown_test.gno", + "body": "package markdown\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\toutput := Render(\"\")\n\tif !strings.Contains(output, \"Gno Flavored Markdown\") {\n\t\tt.Errorf(\"invalid output\")\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "15gKJhK+f4AAv+WaUrvl/QVyDeiBzyxI6moNO9j4vYCOg8eHJYaM0msK5hg2vcCr9N7/sbC7U9VNIp9A5tLvAA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "minisocial", + "path": "gno.land/r/docs/minisocial", + "files": [ + { + "name": "minisocial.gno", + "body": "package minisocial\n\nfunc Render(_ string) string {\n\treturn `# MiniSocial\nMiniSocial is a minimalistic social media platform made for example purposes. \n\nThere are two versions of this app:\n- [V1](/r/docs/minisocial/v1) - handles simple post creation and stores posts in a slice\n- [V2](/r/docs/minisocial/v2) - handles post creation, updating, and deletion, \nand manages storage more efficiently with an AVL tree. V2 also utilizes different p/ packages to handle pagination, \neasier Markdown formatting, etc.`\n\n}\n\n// Original work \u0026 inspiration here:\n// https://gno.land/r/leon/fosdem25/microposts\n// https://gno.land/r/moul/microposts\n// Find the full tutorial on the official gno.land docs at docs.gno.land\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "msKC0PnZDbi2Vp6yqp+C9ZmkSVOm6mNJSgjq3DWxw0uJaLeAFf++WBP78SkIcpuQrriUU0nA9273OIWpocNyCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "minisocial", + "path": "gno.land/r/docs/minisocial/v1", + "files": [ + { + "name": "admin.gno", + "body": "package minisocial\n\nimport \"gno.land/p/demo/ownable\"\n\nvar Ownable = ownable.NewWithAddress(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\") // @leohhhn\n\n// ResetPosts allows admin deletion of the posts\nfunc ResetPosts() {\n\tOwnable.AssertCallerIsOwner()\n\tposts = nil\n}\n" + }, + { + "name": "posts.gno", + "body": "package minisocial\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar posts []*Post // inefficient for large amounts of posts; see v2\n\n// CreatePost creates a new post\nfunc CreatePost(text string) error {\n\t// If the body of the post is empty, return an error\n\tif text == \"\" {\n\t\treturn errors.New(\"empty post text\")\n\t}\n\n\t// Append the new post to the list\n\tposts = append(posts, \u0026Post{\n\t\ttext: text, // Set the input text\n\t\tauthor: std.PreviousRealm().Address(), // The author of the address is the previous realm, the realm that called this one\n\t\tcreatedAt: time.Now(), // Capture the time of the transaction, in this case the block timestamp\n\t})\n\n\treturn nil\n}\n\nfunc Render(_ string) string {\n\toutput := \"# MiniSocial\\n\\n\" // \\n is needed just like in standard Markdown\n\n\t// Handle the edge case\n\tif len(posts) == 0 {\n\t\toutput += \"No posts.\\n\"\n\t\treturn output\n\t}\n\n\t// Let's append the text of each post to the output\n\tfor i, post := range posts {\n\t\t// Let's append some post metadata\n\t\toutput += ufmt.Sprintf(\"#### Post #%d\\n\\n\", i)\n\t\t// Add the stringified post\n\t\toutput += post.String()\n\t\t// Add a line break for cleaner UI\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\treturn output\n}\n" + }, + { + "name": "posts_test.gno", + "body": "package minisocial\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\" // Provides testing utilities\n)\n\nfunc TestCreatePostSingle(t *testing.T) {\n\t// Get a test address for alice\n\taliceAddr := testutils.TestAddress(\"alice\")\n\t// TestSetRealm sets the realm caller, in this case Alice\n\ttesting.SetRealm(std.NewUserRealm(aliceAddr))\n\n\ttext1 := \"Hello World!\"\n\terr := CreatePost(text1)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\n\t// Get the rendered page\n\tgot := Render(\"\")\n\n\t// Content should have the text and alice's address in it\n\tif !(strings.Contains(got, text1) \u0026\u0026 strings.Contains(got, aliceAddr.String())) {\n\t\tt.Fatal(\"expected render to contain text \u0026 alice's address\")\n\t}\n}\n\nfunc TestCreatePostMultiple(t *testing.T) {\n\t// Initialize a slice to hold the test posts and their authors\n\tposts := []struct {\n\t\ttext string\n\t\tauthor string\n\t}{\n\t\t{\"Hello World!\", \"alice\"},\n\t\t{\"This is some new text!\", \"bob\"},\n\t\t{\"Another post by alice\", \"alice\"},\n\t\t{\"A post by charlie!\", \"charlie\"},\n\t}\n\n\tfor _, p := range posts {\n\t\t// Set the appropriate caller realm based on the author\n\t\tauthorAddr := testutils.TestAddress(p.author)\n\t\ttesting.SetRealm(std.NewUserRealm(authorAddr))\n\n\t\t// Create the post\n\t\terr := CreatePost(p.text)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error for post '%s', got %v\", p.text, err)\n\t\t}\n\t}\n\n\t// Get the rendered page\n\tgot := Render(\"\")\n\n\t// Check that all posts and their authors are present in the rendered output\n\tfor _, p := range posts {\n\t\texpectedText := p.text\n\t\texpectedAuthor := testutils.TestAddress(p.author).String() // Get the address for the author\n\t\tif !(strings.Contains(got, expectedText) \u0026\u0026 strings.Contains(got, expectedAuthor)) {\n\t\t\tt.Fatalf(\"expected render to contain text '%s' and address '%s'\", expectedText, expectedAuthor)\n\t\t}\n\t}\n}\n" + }, + { + "name": "types.gno", + "body": "package minisocial\n\nimport (\n\t\"std\" // The standard Gno package\n\t\"time\" // For handling time operations\n\n\t\"gno.land/p/demo/ufmt\" // For string formatting, like `fmt`\n)\n\n// Post defines the main data we keep about each post\ntype Post struct {\n\ttext string // Main text body\n\tauthor std.Address // Address of the post author, provided by the execution context\n\tcreatedAt time.Time // When the post was created\n}\n\n// String stringifies a Post\nfunc (p Post) String() string {\n\tout := p.text\n\tout += \"\\n\\n\"\n\tout += ufmt.Sprintf(\"_by %s_, \", p.author)\n\t// We can use `ufmt` to format strings, and the built-in time library formatting function\n\tout += ufmt.Sprintf(\"_on %s_\", p.createdAt.Format(\"02 Jan 2006, 15:04\"))\n\n\tout += \"\\n\\n\"\n\treturn out\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "4Qt82p8K3l7D6OkEYCtnfPZvtFrMWKMQxwXBMYY++0EKFWMg9UVrMiWKdlENmwIUh/XJo6F/HRnEJlS4sjZaAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "minisocial", + "path": "gno.land/r/docs/minisocial/v2", + "files": [ + { + "name": "admin.gno", + "body": "package minisocial\n\nimport (\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/seqid\"\n)\n\nvar Ownable = ownable.NewWithAddress(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\") // @leohhhn\n\n// ResetPosts allows admin deletion of the posts\nfunc ResetPosts() {\n\tOwnable.AssertCallerIsOwner()\n\tposts = avl.NewTree()\n\tpostID = seqid.ID(0)\n\tpag = pager.NewPager(posts, 5, true)\n}\n" + }, + { + "name": "posts.gno", + "body": "package minisocial\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/md\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nvar (\n\tpostID seqid.ID // counter for post IDs\n\tposts = avl.NewTree() // seqid.ID.String() \u003e *Post\n\tpag = pager.NewPager(posts, 5, true) // To help with pagination in rendering\n\n\t// Errors\n\tErrEmptyPost = errors.New(\"empty post text\")\n\tErrPostNotFound = errors.New(\"post not found\")\n\tErrUpdateWindowExpired = errors.New(\"update window expired\")\n\tErrUnauthorized = errors.New(\"you're not authorized to update this post\")\n)\n\n// CreatePost creates a new post\nfunc CreatePost(text string) error {\n\tif text == \"\" {\n\t\treturn ErrEmptyPost\n\t}\n\n\t// Get the next ID\n\t// seqid.IDs are sequentially stored in the AVL tree\n\t// This provides chronological order when iterating\n\tid := postID.Next()\n\n\t// Set the key:value pair into the AVL tree:\n\t// avl.Tree.Set takes a string for a key, and anything as a value.\n\t// Stringify the key, and set the pointer to a new Post struct\n\tposts.Set(id.String(), \u0026Post{\n\t\tid: id, // Set the ID, used later for editing or deletion\n\t\ttext: text, // Set the input text\n\t\tauthor: std.PreviousRealm().Address(), // The author of the address is the previous realm, the realm that called this one\n\t\tcreatedAt: time.Now(), // Capture the time of the transaction, in this case the block timestamp\n\t\tupdatedAt: time.Now(),\n\t})\n\n\treturn nil\n}\n\n// UpdatePost allows the author to update a post\n// The post can only be updated up to 10 minutes after posting\nfunc UpdatePost(id string, text string) error {\n\t// Try to get the post\n\traw, ok := posts.Get(id)\n\tif !ok {\n\t\treturn ErrPostNotFound\n\t}\n\n\t// Cast post from AVL tree\n\tpost := raw.(*Post)\n\tif std.PreviousRealm().Address() != post.author {\n\t\treturn ErrUnauthorized\n\t}\n\n\t// Can only update 10 mins after it was posted\n\tif post.updatedAt.After(post.createdAt.Add(time.Minute * 10)) {\n\t\treturn ErrUpdateWindowExpired\n\t}\n\n\tpost.text = text\n\tpost.updatedAt = time.Now()\n\n\treturn nil\n}\n\n// DeletePost deletes a post with a specific id\n// Only the creator of a post can delete the post\nfunc DeletePost(id string) error {\n\t// Try to get the post\n\traw, ok := posts.Get(id)\n\tif !ok {\n\t\treturn ErrPostNotFound\n\t}\n\n\t// Cast post from AVL tree\n\tpost := raw.(*Post)\n\tif std.PreviousRealm().Address() != post.author {\n\t\treturn ErrUnauthorized\n\t}\n\n\t// Use avl.Tree.Remove\n\t_, removed := posts.Remove(id)\n\tif !removed {\n\t\t// This shouldn't happen after all checks above\n\t\t// If it does, discard any possible state changes\n\t\tpanic(\"failed to remove post\")\n\t}\n\n\treturn nil\n}\n\n// Render renders the main page of threads\nfunc Render(path string) string {\n\tout := md.H1(\"MiniSocial\")\n\n\tif posts.Size() == 0 {\n\t\tout += \"No posts yet!\\n\\n\"\n\t\treturn out\n\t}\n\n\t// Get the page from the path\n\tpage := pag.MustGetPageByPath(path)\n\n\t// Iterate over items in the page\n\tfor _, item := range page.Items {\n\t\tpost := item.Value.(*Post)\n\n\t\t// Try resolving the address for a username\n\t\ttext := post.author.String()\n\t\tuser := users.ResolveAddress(post.author)\n\t\tif user != nil {\n\t\t\ttext = user.RenderLink(\"\")\n\t\t}\n\n\t\tout += md.H4(ufmt.Sprintf(\"Post #%d - %s\\n\\n\", int(post.id), text))\n\n\t\tout += post.String()\n\t\tout += md.HorizontalRule()\n\t}\n\n\tout += page.Picker(path)\n\tout += \"\\n\\n\"\n\tout += \"Page \" + strconv.Itoa(page.PageNumber) + \" of \" + strconv.Itoa(page.TotalPages) + \"\\n\\n\"\n\n\treturn out\n}\n" + }, + { + "name": "posts_test.gno", + "body": "package minisocial\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\" // Provides testing utilities\n)\n\nfunc TestCreatePostSingle(t *testing.T) {\n\t// Get a test address for alice\n\taliceAddr := testutils.TestAddress(\"alice\")\n\t// TestSetRealm sets the realm caller, in this case Alice\n\ttesting.SetRealm(std.NewUserRealm(aliceAddr))\n\n\ttext1 := \"Hello World!\"\n\terr := CreatePost(text1)\n\tif err != nil {\n\t\tt.Fatalf(\"expected no error, got %v\", err)\n\t}\n\n\t// Get the rendered page\n\tgot := Render(\"\")\n\n\t// Content should have the text and alice's address in it\n\tif !(strings.Contains(got, text1) \u0026\u0026 strings.Contains(got, aliceAddr.String())) {\n\t\tt.Fatal(\"expected render to contain text \u0026 alice's address\")\n\t}\n}\n\nfunc TestCreatePostMultiple(t *testing.T) {\n\t// Initialize a slice to hold the test posts and their authors\n\tposts := []struct {\n\t\ttext string\n\t\tauthor string\n\t}{\n\t\t{\"Hello World!\", \"alice\"},\n\t\t{\"This is some new text!\", \"bob\"},\n\t\t{\"Another post by alice\", \"alice\"},\n\t\t{\"A post by charlie!\", \"charlie\"},\n\t}\n\n\tfor _, p := range posts {\n\t\t// Set the appropriate caller realm based on the author\n\t\tauthorAddr := testutils.TestAddress(p.author)\n\t\ttesting.SetRealm(std.NewUserRealm(authorAddr))\n\n\t\t// Create the post\n\t\terr := CreatePost(p.text)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected no error for post '%s', got %v\", p.text, err)\n\t\t}\n\t}\n\n\t// Get the rendered page\n\tgot := Render(\"\")\n\n\t// Check that all posts and their authors are present in the rendered output\n\tfor _, p := range posts {\n\t\texpectedText := p.text\n\t\texpectedAuthor := testutils.TestAddress(p.author).String() // Get the address for the author\n\t\tif !(strings.Contains(got, expectedText) \u0026\u0026 strings.Contains(got, expectedAuthor)) {\n\t\t\tt.Fatalf(\"expected render to contain text '%s' and address '%s'\", expectedText, expectedAuthor)\n\t\t}\n\t}\n}\n\nfunc TestReset(t *testing.T) {\n\taliceAddr := testutils.TestAddress(\"alice\")\n\ttesting.SetRealm(std.NewUserRealm(aliceAddr))\n\n\ttext1 := \"Hello World!\"\n\t_ = CreatePost(text1)\n\n\tgot := Render(\"\")\n\tif !strings.Contains(got, text1) {\n\t\tt.Fatal(\"expected render to contain text1\")\n\t}\n\n\t// Set admin\n\ttesting.SetRealm(std.NewUserRealm(Ownable.Owner()))\n\tResetPosts()\n\n\tgot = Render(\"\")\n\tif strings.Contains(got, text1) {\n\t\tt.Fatal(\"expected render to not contain text1\")\n\t}\n\n\ttext2 := \"Some other Text!!\"\n\t_ = CreatePost(text2)\n\n\tgot = Render(\"\")\n\tif strings.Contains(got, text1) {\n\t\tt.Fatal(\"expected render to not contain text1\")\n\t}\n\n\tif !strings.Contains(got, text2) {\n\t\tt.Fatal(\"expected render to contain text2\")\n\t}\n}\n\n// TODO: Add tests for Update \u0026 Delete\n" + }, + { + "name": "types.gno", + "body": "package minisocial\n\nimport (\n\t\"std\" // The standard Gno package\n\t\"time\" // For handling time operations\n\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n// Post defines the main data we keep about each post\ntype Post struct {\n\tid seqid.ID\n\ttext string\n\tauthor std.Address\n\tcreatedAt time.Time\n\tupdatedAt time.Time\n}\n\nfunc (p Post) String() string {\n\tout := p.text + \"\\n\\n\"\n\n\tauthor := p.author.String()\n\t// We can import and use the r/sys/users package to resolve addresses\n\tuser := users.ResolveAddress(p.author)\n\tif user != nil {\n\t\t// RenderLink provides a link that is clickable\n\t\t// The link goes to the user's profile page\n\t\tauthor = user.RenderLink(\"\")\n\t}\n\n\tout += ufmt.Sprintf(\"_by %s on %s_\\n\\n\", author, p.createdAt.Format(\"02 Jan 2006, 15:04\"))\n\treturn out\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "T0AyJ7CCTyajQwghOdeAmhsSmZkgDdZFbTDlHo7FS3KLI9N6s95P7BmnVml+RRcATh8cv04iAqaKVxXnZLa2CQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "optional_render", + "path": "gno.land/r/docs/optional_render", + "files": [ + { + "name": "optional_render.gno", + "body": "package optional_render\n\nfunc Info() string {\n\treturn `Having a Render() function in your realm is optional!\nIf you do decide to have a Render() function, it must have the following signature:\nfunc Render(path string) string { ... }`\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "syMpsGKuvSrEpgffJiR07oPk1zayT4Xwb/fwm2lMToMCANNQqUJZwIq1f2n9L2tgd3q9/AMFZmT5SMKKxhEmCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "source", + "path": "gno.land/r/docs/source", + "files": [ + { + "name": "source.gno", + "body": "package source\n\n// Welcome to the source code of this realm!\n\nfunc Render(_ string) string {\n\treturn `# Viewing source code \ngno.land makes it easy to view the source code of any pure\npackage or realm, by using ABCI queries.\n\ngno.land's web frontend, ` + \"`gnoweb`, \" + ` makes this easy by\nproviding a intuitive UI that fetches the source of the\nrealm, that you can inspect anywhere by simply clicking\non the [source] button.\n\nCheck it out in the top right corner!\n`\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Bi0JW/HEalCsRToq/2V+samtZ0qurVVVL1UglJZ5f5VWoVmmGOIublv04jtVHVULfPSpT8U1BNpZfGitAB6wAA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "namechecker", + "path": "gno.land/r/docs/users", + "files": [ + { + "name": "resolve.gno", + "body": "package namechecker\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/sys/users\" // Import the user registry\n)\n\nfunc Render(path string) string {\n\tout := \"# Username checker\\n\\n\"\n\n\t// Default render\n\tif path == \"\" {\n\t\tout += \"Add `:{name OR address}` to the search bar to check for a name or address!\\n\\n\"\n\t\tout += \"Here are some examples:\\n\\n\"\n\t\tout += \"- [@test1](/r/docs/users:test1)\\n\"\n\t\tout += \"- [g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5](/r/docs/users:g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5)\\n\"\n\n\t\treturn out\n\t}\n\n\t// If the user inputted an address\n\tif std.Address(path).IsValid() {\n\t\t// Try resolving an address\n\t\tdata := users.ResolveAddress(std.Address(path))\n\t\tif data != nil {\n\t\t\tout += \"## Found the user you're looking for: \"\n\t\t\t// RenderLink will return a clickable gnoweb link leading to the user's page\n\t\t\tout += data.RenderLink(\"\")\n\t\t\treturn out\n\t\t}\n\t}\n\n\t// Else, try resolving a name to get user data\n\tdata, _ := users.ResolveName(path)\n\tif data != nil {\n\t\tout += \"## Found the user you're looking for: \"\n\t\tout += data.RenderLink(\"\")\n\t\treturn out\n\t}\n\n\tout += \"## Didn't find that user :/\"\n\n\treturn out\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "A18jboWEL1NEpCBGYe6TF2blXCORtyhIfGEqCNXzYWDvLJc0on6UTHUdh1/Xz681jrc/dnO6wtIZslwzWvcyAQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "gnoblog", + "path": "gno.land/r/gnoland/blog", + "files": [ + { + "name": "admin.gno", + "body": "package gnoblog\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\nvar (\n\tadminAddr std.Address\n\tmoderatorList avl.Tree\n\tcommenterList avl.Tree\n\tinPause bool\n)\n\nfunc init() {\n\t// adminAddr = std.OriginCaller() // FIXME: find a way to use this from the main's genesis.\n\tadminAddr = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\" // @moul\n}\n\nfunc AdminSetAdminAddr(addr std.Address) {\n\tassertIsAdmin()\n\tadminAddr = addr\n}\n\nfunc AdminSetInPause(state bool) {\n\tassertIsAdmin()\n\tinPause = state\n}\n\nfunc AdminAddModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), true)\n}\n\nfunc AdminRemoveModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), false) // FIXME: delete instead?\n}\n\nfunc NewPostExecutor(slug, title, body, publicationDate, authors, tags string) dao.Executor {\n\tcallback := func() error {\n\t\taddPost(std.PreviousRealm().Address(), slug, title, body, publicationDate, authors, tags)\n\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(callback)\n}\n\nfunc ModAddPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\tcaller := std.OriginCaller()\n\taddPost(caller, slug, title, body, publicationDate, authors, tags)\n}\n\nfunc addPost(caller std.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(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc ModRemovePost(slug string) {\n\tassertIsModerator()\n\n\tb.RemovePost(slug)\n}\n\nfunc ModAddCommenter(addr std.Address) {\n\tassertIsModerator()\n\tcommenterList.Set(addr.String(), true)\n}\n\nfunc ModDelCommenter(addr std.Address) {\n\tassertIsModerator()\n\tcommenterList.Set(addr.String(), false) // FIXME: delete instead?\n}\n\nfunc ModDelComment(slug string, index int) {\n\tassertIsModerator()\n\n\terr := b.GetPost(slug).DeleteComment(index)\n\tcheckErr(err)\n}\n\nfunc isAdmin(addr std.Address) bool {\n\treturn addr == adminAddr\n}\n\nfunc isModerator(addr std.Address) bool {\n\t_, found := moderatorList.Get(addr.String())\n\treturn found\n}\n\nfunc isCommenter(addr std.Address) bool {\n\t_, found := commenterList.Get(addr.String())\n\treturn found\n}\n\nfunc assertIsAdmin() {\n\tcaller := std.OriginCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(\"access restricted.\")\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := std.OriginCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertIsCommenter() {\n\tcaller := std.OriginCaller()\n\tif isAdmin(caller) || isModerator(caller) || isCommenter(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertNotInPause() {\n\tif inPause {\n\t\tpanic(\"access restricted (pause)\")\n\t}\n}\n" + }, + { + "name": "gnoblog.gno", + "body": "package gnoblog\n\nimport (\n\t\"std\"\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(postSlug, comment string) {\n\tassertIsCommenter()\n\tassertNotInPause()\n\n\tcaller := std.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\"std\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestPackage(t *testing.T) {\n\ttesting.SetOriginCaller(std.Address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"))\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(\"slug1\", \"title1\", \"body1\", \"2022-05-20T13:17:22Z\", \"moul\", \"tag1,tag2\")\n\t\tModAddPost(\"slug2\", \"title2\", \"body2\", \"2022-05-20T13:17:23Z\", \"moul\", \"tag1,tag3\")\n\t\tgot := Render(\"\")\n\t\texpected := `\n\t# gno.land's blog\n\n\u003cdiv class='columns-3'\u003e\u003cdiv\u003e\n\n### [title2](/r/gnoland/blog:p/slug2)\n 20 May 2022\n\u003c/div\u003e\u003cdiv\u003e\n\n### [title1](/r/gnoland/blog:p/slug1)\n 20 May 2022\n\u003c/div\u003e\u003c/div\u003e\n\t`\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\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 g1manfred47kzduec920z88wfr64ylksmdcedlf5 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\n\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\u003cdiv\u003e\n\n### [title1](/r/gnoland/blog:p/slug1)\n 20 May 2022\n\u003c/div\u003e\n\t`\n\t\tassertMDEquals(t, got, expected)\n\t}\n\n\t// add comments.\n\t{\n\t\tAddComment(\"slug1\", \"comment1\")\n\t\tAddComment(\"slug2\", \"comment2\")\n\t\tAddComment(\"slug1\", \"comment3\")\n\t\tAddComment(\"slug2\", \"comment4\")\n\t\tAddComment(\"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 g1manfred47kzduec920z88wfr64ylksmdcedlf5 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 g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003ch5\u003ecomment2\n\n\u003c/h5\u003e\u003ch6\u003eby g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003c/details\u003e\n\u003c/main\u003e\n\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(\"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 g1manfred47kzduec920z88wfr64ylksmdcedlf5 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 g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003ch5\u003ecomment2\n\n\u003c/h5\u003e\u003ch6\u003eby g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC\u003c/h6\u003e\n\n---\n\n\u003c/details\u003e\n\u003c/main\u003e\n\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\n\t{ // Test remove functionality\n\t\ttitle := \"example title\"\n\t\tslug := \"testSlug1\"\n\t\tModAddPost(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(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\n\t// TODO: pagination.\n\t// TODO: ?format=...\n\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": "util.gno", + "body": "package gnoblog\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "SU30ZGM2U2rE9g6fFJaR5Y2gDKXU6fM6szhhkDihVQDzy1kGsOrVgXRzFUYsM9RmhMd73/D8MWsk6P598zPFDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "events", + "path": "gno.land/r/gnoland/events", + "files": [ + { + "name": "errors.gno", + "body": "package events\n\nimport (\n\t\"errors\"\n\t\"strconv\"\n)\n\nvar (\n\tErrEmptyName = errors.New(\"event name cannot be empty\")\n\tErrNoSuchID = errors.New(\"event with specified ID does not exist\")\n\tErrMinWidgetSize = errors.New(\"you need to request at least 1 event to render\")\n\tErrMaxWidgetSize = errors.New(\"maximum number of events in widget is\" + strconv.Itoa(MaxWidgetSize))\n\tErrDescriptionTooLong = errors.New(\"event description is too long\")\n\tErrInvalidStartTime = errors.New(\"invalid start time format\")\n\tErrInvalidEndTime = errors.New(\"invalid end time format\")\n\tErrEndBeforeStart = errors.New(\"end time cannot be before start time\")\n\tErrStartEndTimezonemMismatch = errors.New(\"start and end timezones are not the same\")\n)\n" + }, + { + "name": "events.gno", + "body": "// Package events allows you to upload data about specific IRL/online events\n// It includes dynamic support for updating rendering events based on their\n// status, ie if they are upcoming, in progress, or in the past.\npackage events\n\nimport (\n\t\"sort\"\n\t\"std\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/ownable/exts/authorizable\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype (\n\tEvent struct {\n\t\tid string\n\t\tname string // name of event\n\t\tdescription string // short description of event\n\t\tlink string // link to auth corresponding web2 page, ie eventbrite/luma or conference page\n\t\tlocation string // location of the event\n\t\tstartTime time.Time // given in RFC3339\n\t\tendTime time.Time // end time of the event, given in RFC3339\n\t}\n\n\teventsSlice []*Event\n)\n\nvar (\n\tsu = std.Address(\"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5\") // @leohhhn\n\tAuth = authorizable.NewAuthorizableWithAddress(su)\n\n\tevents = make(eventsSlice, 0) // sorted\n\tidCounter seqid.ID\n)\n\nconst (\n\tmaxDescLength = 100\n\tEventAdded = \"EventAdded\"\n\tEventDeleted = \"EventDeleted\"\n\tEventEdited = \"EventEdited\"\n)\n\n// AddEvent adds auth new event\n// Start time \u0026 end time need to be specified in RFC3339, ie 2024-08-08T12:00:00+02:00\nfunc AddEvent(name, description, link, location, startTime, endTime string) (string, error) {\n\tAuth.AssertOnAuthList()\n\n\tif strings.TrimSpace(name) == \"\" {\n\t\treturn \"\", ErrEmptyName\n\t}\n\n\tif len(description) \u003e maxDescLength {\n\t\treturn \"\", ufmt.Errorf(\"%s: provided length is %d, maximum is %d\", ErrDescriptionTooLong, len(description), maxDescLength)\n\t}\n\n\t// Parse times\n\tst, et, err := parseTimes(startTime, endTime)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tid := idCounter.Next().String()\n\te := \u0026Event{\n\t\tid: id,\n\t\tname: name,\n\t\tdescription: description,\n\t\tlink: link,\n\t\tlocation: location,\n\t\tstartTime: st,\n\t\tendTime: et,\n\t}\n\n\tevents = append(events, e)\n\tsort.Sort(events)\n\n\tstd.Emit(EventAdded,\n\t\t\"id\", e.id,\n\t)\n\n\treturn id, nil\n}\n\n// DeleteEvent deletes an event with auth given ID\nfunc DeleteEvent(id string) {\n\tAuth.AssertOnAuthList()\n\n\te, idx, err := GetEventByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tevents = append(events[:idx], events[idx+1:]...)\n\n\tstd.Emit(EventDeleted,\n\t\t\"id\", e.id,\n\t)\n}\n\n// EditEvent edits an event with auth given ID\n// It only updates values corresponding to non-empty arguments sent with the call\n// Note: if you need to update the start time or end time, you need to provide both every time\nfunc EditEvent(id string, name, description, link, location, startTime, endTime string) {\n\tAuth.AssertOnAuthList()\n\n\te, _, err := GetEventByID(id)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Set only valid values\n\tif strings.TrimSpace(name) != \"\" {\n\t\te.name = name\n\t}\n\n\tif strings.TrimSpace(description) != \"\" {\n\t\te.description = description\n\t}\n\n\tif strings.TrimSpace(link) != \"\" {\n\t\te.link = link\n\t}\n\n\tif strings.TrimSpace(location) != \"\" {\n\t\te.location = location\n\t}\n\n\tif strings.TrimSpace(startTime) != \"\" || strings.TrimSpace(endTime) != \"\" {\n\t\tst, et, err := parseTimes(startTime, endTime)\n\t\tif err != nil {\n\t\t\tpanic(err) // need to also revert other state changes\n\t\t}\n\n\t\toldStartTime := e.startTime\n\t\te.startTime = st\n\t\te.endTime = et\n\n\t\t// If sort order was disrupted, sort again\n\t\tif oldStartTime != e.startTime {\n\t\t\tsort.Sort(events)\n\t\t}\n\t}\n\n\tstd.Emit(EventEdited,\n\t\t\"id\", e.id,\n\t)\n}\n\nfunc GetEventByID(id string) (*Event, int, error) {\n\tfor i, event := range events {\n\t\tif event.id == id {\n\t\t\treturn event, i, nil\n\t\t}\n\t}\n\n\treturn nil, -1, ErrNoSuchID\n}\n\n// Len returns the length of the slice\nfunc (m eventsSlice) Len() int {\n\treturn len(m)\n}\n\n// Less compares the startTime fields of two elements\n// In this case, events will be sorted by largest startTime first (upcoming \u003e past)\nfunc (m eventsSlice) Less(i, j int) bool {\n\treturn m[i].startTime.After(m[j].startTime)\n}\n\n// Swap swaps two elements in the slice\nfunc (m eventsSlice) Swap(i, j int) {\n\tm[i], m[j] = m[j], m[i]\n}\n\n// parseTimes parses the start and end time for an event and checks for possible errors\nfunc parseTimes(startTime, endTime string) (time.Time, time.Time, error) {\n\tst, err := time.Parse(time.RFC3339, startTime)\n\tif err != nil {\n\t\treturn time.Time{}, time.Time{}, ufmt.Errorf(\"%s: %s\", ErrInvalidStartTime, err.Error())\n\t}\n\n\tet, err := time.Parse(time.RFC3339, endTime)\n\tif err != nil {\n\t\treturn time.Time{}, time.Time{}, ufmt.Errorf(\"%s: %s\", ErrInvalidEndTime, err.Error())\n\t}\n\n\tif et.Before(st) {\n\t\treturn time.Time{}, time.Time{}, ErrEndBeforeStart\n\t}\n\n\t_, stOffset := st.Zone()\n\t_, etOffset := et.Zone()\n\tif stOffset != etOffset {\n\t\treturn time.Time{}, time.Time{}, ErrStartEndTimezonemMismatch\n\t}\n\n\treturn st, et, nil\n}\n" + }, + { + "name": "events_test.gno", + "body": "package events\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\tsuRealm = std.NewUserRealm(su)\n\n\tnow = \"2009-02-13T23:31:30Z\" // time.Now() is hardcoded to this value in the gno test machine currently\n\tparsedTimeNow, _ = time.Parse(time.RFC3339, now)\n)\n\nfunc TestAddEvent(t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\tAddEvent(\"Event 1\", \"this event is upcoming\", \"gno.land\", \"gnome land\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\n\tgot := renderHome(false)\n\n\tif !strings.Contains(got, \"Event 1\") {\n\t\tt.Fatalf(\"Expected to find Event 1 in render\")\n\t}\n\n\te2Start := parsedTimeNow.Add(-time.Hour * 24 * 5)\n\te2End := e2Start.Add(time.Hour * 4)\n\n\tAddEvent(\"Event 2\", \"this event is in the past\", \"gno.land\", \"gnome land\", e2Start.Format(time.RFC3339), e2End.Format(time.RFC3339))\n\n\tgot = renderHome(false)\n\n\tupcomingPos := strings.Index(got, \"## Upcoming events\")\n\tpastPos := strings.Index(got, \"## Past events\")\n\n\te1Pos := strings.Index(got, \"Event 1\")\n\te2Pos := strings.Index(got, \"Event 2\")\n\n\t// expected index ordering: upcoming \u003c e1 \u003c past \u003c e2\n\tif e1Pos \u003c upcomingPos || e1Pos \u003e pastPos {\n\t\tt.Fatalf(\"Expected to find Event 1 in Upcoming events\")\n\t}\n\n\tif e2Pos \u003c upcomingPos || e2Pos \u003c pastPos || e2Pos \u003c e1Pos {\n\t\tt.Fatalf(\"Expected to find Event 2 on auth different pos\")\n\t}\n\n\t// larger index =\u003e smaller startTime (future =\u003e past)\n\tif events[0].startTime.Unix() \u003c events[1].startTime.Unix() {\n\t\tt.Fatalf(\"expected ordering to be different\")\n\t}\n}\n\nfunc TestAddEventErrors(t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\t_, err := AddEvent(\"\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorIs(t, err, ErrEmptyName)\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorContains(t, err, ErrInvalidStartTime.Error())\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidEndTime.Error())\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:30:31Z\")\n\tuassert.ErrorIs(t, err, ErrEndBeforeStart)\n\n\t_, err = AddEvent(\"sample name\", \"sample desc\", \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31+06:00\", \"2009-02-13T23:33:31+02:00\")\n\tuassert.ErrorIs(t, err, ErrStartEndTimezonemMismatch)\n\n\ttooLongDesc := `Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean ma`\n\t_, err = AddEvent(\"sample name\", tooLongDesc, \"gno.land\", \"gnome land\", \"2009-02-13T23:31:31Z\", \"2009-02-13T23:33:31Z\")\n\tuassert.ErrorContains(t, err, ErrDescriptionTooLong.Error())\n}\n\nfunc TestDeleteEvent(t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\tid, _ := AddEvent(\"ToDelete\", \"description\", \"gno.land\", \"gnome land\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\n\tgot := renderHome(false)\n\n\tif !strings.Contains(got, \"ToDelete\") {\n\t\tt.Fatalf(\"Expected to find ToDelete event in render\")\n\t}\n\n\tDeleteEvent(id)\n\tgot = renderHome(false)\n\n\tif strings.Contains(got, \"ToDelete\") {\n\t\tt.Fatalf(\"Did not expect to find ToDelete event in render\")\n\t}\n}\n\nfunc TestEditEvent(t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\tloc := \"gnome land\"\n\n\tid, _ := AddEvent(\"ToDelete\", \"description\", \"gno.land\", loc, e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\n\tnewName := \"New Name\"\n\tnewDesc := \"Normal description\"\n\tnewLink := \"new Link\"\n\tnewST := e1Start.Add(time.Hour)\n\tnewET := newST.Add(time.Hour)\n\n\tEditEvent(id, newName, newDesc, newLink, \"\", newST.Format(time.RFC3339), newET.Format(time.RFC3339))\n\tedited, _, _ := GetEventByID(id)\n\n\t// Check updated values\n\tuassert.Equal(t, edited.name, newName)\n\tuassert.Equal(t, edited.description, newDesc)\n\tuassert.Equal(t, edited.link, newLink)\n\tuassert.True(t, edited.startTime.Equal(newST))\n\tuassert.True(t, edited.endTime.Equal(newET))\n\n\t// Check if the old values are the same\n\tuassert.Equal(t, edited.location, loc)\n}\n\nfunc TestInvalidEdit(t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\tuassert.PanicsWithMessage(t, ErrNoSuchID.Error(), func() {\n\t\tEditEvent(\"123123\", \"\", \"\", \"\", \"\", \"\", \"\")\n\t})\n}\n\nfunc TestParseTimes(t *testing.T) {\n\t// times not provided\n\t// end time before start time\n\t// timezone Missmatch\n\n\t_, _, err := parseTimes(\"\", \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidStartTime.Error())\n\n\t_, _, err = parseTimes(now, \"\")\n\tuassert.ErrorContains(t, err, ErrInvalidEndTime.Error())\n\n\t_, _, err = parseTimes(\"2009-02-13T23:30:30Z\", \"2009-02-13T21:30:30Z\")\n\tuassert.ErrorContains(t, err, ErrEndBeforeStart.Error())\n\n\t_, _, err = parseTimes(\"2009-02-10T23:30:30+02:00\", \"2009-02-13T21:30:33+05:00\")\n\tuassert.ErrorContains(t, err, ErrStartEndTimezonemMismatch.Error())\n}\n\nfunc TestRenderEventWidget(t *testing.T) {\n\ttesting.SetRealm(suRealm)\n\n\t// No events yet\n\tevents = nil\n\tout, err := RenderEventWidget(1)\n\tuassert.NoError(t, err)\n\tuassert.Equal(t, out, \"No events.\")\n\n\t// Too many events\n\tout, err = RenderEventWidget(MaxWidgetSize + 1)\n\tuassert.ErrorIs(t, err, ErrMaxWidgetSize)\n\n\t// Too little events\n\tout, err = RenderEventWidget(0)\n\tuassert.ErrorIs(t, err, ErrMinWidgetSize)\n\n\t// Ordering \u0026 if requested amt is larger than the num of events that exist\n\te1Start := parsedTimeNow.Add(time.Hour * 24 * 5)\n\te1End := e1Start.Add(time.Hour * 4)\n\n\te2Start := parsedTimeNow.Add(time.Hour * 24 * 10) // event 2 is after event 1\n\te2End := e2Start.Add(time.Hour * 4)\n\n\t_, err = AddEvent(\"Event 1\", \"description\", \"gno.land\", \"loc\", e1Start.Format(time.RFC3339), e1End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\t_, err = AddEvent(\"Event 2\", \"description\", \"gno.land\", \"loc\", e2Start.Format(time.RFC3339), e2End.Format(time.RFC3339))\n\turequire.NoError(t, err)\n\n\tout, err = RenderEventWidget(MaxWidgetSize)\n\turequire.NoError(t, err)\n\n\tuniqueSequence := \"- [\" // sequence that is displayed once per each event as per the RenderEventWidget function\n\tuassert.Equal(t, 2, strings.Count(out, uniqueSequence))\n\n\tuassert.True(t, strings.Index(out, \"Event 1\") \u003e strings.Index(out, \"Event 2\"))\n}\n" + }, + { + "name": "render.gno", + "body": "package events\n\nimport (\n\t\"bytes\"\n\t\"time\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nconst (\n\tMaxWidgetSize = 5\n)\n\n// RenderEventWidget shows up to eventsToRender of the latest events to a caller\nfunc RenderEventWidget(eventsToRender int) (string, error) {\n\tnumOfEvents := len(events)\n\tif numOfEvents == 0 {\n\t\treturn \"No events.\", nil\n\t}\n\n\tif eventsToRender \u003e MaxWidgetSize {\n\t\treturn \"\", ErrMaxWidgetSize\n\t}\n\n\tif eventsToRender \u003c 1 {\n\t\treturn \"\", ErrMinWidgetSize\n\t}\n\n\tif eventsToRender \u003e numOfEvents {\n\t\teventsToRender = numOfEvents\n\t}\n\n\toutput := \"\"\n\n\tfor _, event := range events[:eventsToRender] {\n\t\toutput += ufmt.Sprintf(\"- [%s](%s)\\n\", event.name, event.link)\n\t}\n\n\treturn output, nil\n}\n\n// renderHome renders the home page of the events realm\nfunc renderHome(admin bool) string {\n\toutput := \"# gno.land events\\n\\n\"\n\n\tif len(events) == 0 {\n\t\toutput += \"No upcoming or past events.\"\n\t\treturn output\n\t}\n\n\toutput += \"Below is a list of all gno.land events, including in progress, upcoming, and past ones.\\n\\n\"\n\toutput += \"---\\n\\n\"\n\n\tvar (\n\t\tinProgress = \"\"\n\t\tupcoming = \"\"\n\t\tpast = \"\"\n\t\tnow = time.Now()\n\t)\n\n\tfor _, e := range events {\n\t\tif now.Before(e.startTime) {\n\t\t\tupcoming += e.Render(admin)\n\t\t} else if now.After(e.endTime) {\n\t\t\tpast += e.Render(admin)\n\t\t} else {\n\t\t\tinProgress += e.Render(admin)\n\t\t}\n\t}\n\n\tif upcoming != \"\" {\n\t\t// Add upcoming events\n\t\toutput += \"## Upcoming events\\n\\n\"\n\t\toutput += \"\u003cdiv class='columns-3'\u003e\"\n\n\t\toutput += upcoming\n\n\t\toutput += \"\u003c/div\u003e\\n\\n\"\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\tif inProgress != \"\" {\n\t\toutput += \"## Currently in progress\\n\\n\"\n\t\toutput += \"\u003cdiv class='columns-3'\u003e\"\n\n\t\toutput += inProgress\n\n\t\toutput += \"\u003c/div\u003e\\n\\n\"\n\t\toutput += \"---\\n\\n\"\n\t}\n\n\tif past != \"\" {\n\t\t// Add past events\n\t\toutput += \"## Past events\\n\\n\"\n\t\toutput += \"\u003cdiv class='columns-3'\u003e\"\n\n\t\toutput += past\n\n\t\toutput += \"\u003c/div\u003e\\n\\n\"\n\t}\n\n\treturn output\n}\n\n// Render returns the markdown representation of a single event instance\nfunc (e Event) Render(admin bool) string {\n\tvar buf bytes.Buffer\n\n\tbuf.WriteString(\"\u003cdiv\u003e\\n\\n\")\n\tbuf.WriteString(ufmt.Sprintf(\"### %s\\n\\n\", e.name))\n\tbuf.WriteString(ufmt.Sprintf(\"%s\\n\\n\", e.description))\n\tbuf.WriteString(ufmt.Sprintf(\"**Location:** %s\\n\\n\", e.location))\n\n\t_, offset := e.startTime.Zone() // offset is in seconds\n\thoursOffset := offset / (60 * 60)\n\tsign := \"\"\n\tif offset \u003e= 0 {\n\t\tsign = \"+\"\n\t}\n\n\tbuf.WriteString(ufmt.Sprintf(\"**Starts:** %s UTC%s%d\\n\\n\", e.startTime.Format(\"02 Jan 2006, 03:04 PM\"), sign, hoursOffset))\n\tbuf.WriteString(ufmt.Sprintf(\"**Ends:** %s UTC%s%d\\n\\n\", e.endTime.Format(\"02 Jan 2006, 03:04 PM\"), sign, hoursOffset))\n\n\tif admin {\n\t\tbuf.WriteString(ufmt.Sprintf(\"[EDIT](/r/gnoland/events$help\u0026func=EditEvent\u0026id=%s)\\n\\n\", e.id))\n\t\tbuf.WriteString(ufmt.Sprintf(\"[DELETE](/r/gnoland/events$help\u0026func=DeleteEvent\u0026id=%s)\\n\\n\", e.id))\n\t}\n\n\tif e.link != \"\" {\n\t\tbuf.WriteString(ufmt.Sprintf(\"[See more](%s)\\n\\n\", e.link))\n\t}\n\n\tbuf.WriteString(\"\u003c/div\u003e\")\n\n\treturn buf.String()\n}\n\n// Render is the main rendering entry point\nfunc Render(path string) string {\n\tif path == \"admin\" {\n\t\treturn renderHome(true)\n\t}\n\n\treturn renderHome(false)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "uGwLpT1E0oo8w7E0jkpnQpCjOHQjjyKpqz3rilidqv8Py6V7mpmiUlxt1Vomo2dJo+R+INRMuirK6zoHwQY5BQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "faucet", + "path": "gno.land/r/gnoland/faucet", + "files": [ + { + "name": "admin.gno", + "body": "package faucet\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nfunc AdminSetInPause(inPause bool) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgInPause = inPause\n\treturn \"\"\n}\n\nfunc AdminSetMessage(message string) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgMessage = message\n\treturn \"\"\n}\n\nfunc AdminSetTransferLimit(amount int64) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgLimit = std.NewCoin(\"ugnot\", amount)\n\treturn \"\"\n}\n\nfunc AdminSetAdminAddr(addr std.Address) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\tgAdminAddr = addr\n\treturn \"\"\n}\n\nfunc AdminAddController(addr std.Address) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tsize := gControllers.Size()\n\n\tif size \u003e= gControllersMaxSize {\n\t\treturn \"can not add more controllers than allowed\"\n\t}\n\n\tif gControllers.Has(addr.String()) {\n\t\treturn addr.String() + \" exists, no need to add.\"\n\t}\n\n\tgControllers.Set(addr.String(), addr)\n\n\treturn \"\"\n}\n\nfunc AdminRemoveController(addr std.Address) string {\n\tif err := assertIsAdmin(); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tif !gControllers.Has(addr.String()) {\n\t\treturn addr.String() + \" is not on the controller list\"\n\t}\n\n\t_, ok := gControllers.Remove(addr.String())\n\n\t// it not should happen.\n\t// we will check anyway to prevent issues in the underline implementation.\n\n\tif !ok {\n\t\treturn addr.String() + \" is not on the controller list\"\n\t}\n\n\treturn \"\"\n}\n\nfunc assertIsAdmin() error {\n\tcaller := std.OriginCaller()\n\tif caller != gAdminAddr {\n\t\treturn errors.New(\"restricted for admin\")\n\t}\n\treturn nil\n}\n" + }, + { + "name": "faucet.gno", + "body": "package faucet\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\t// configurable by admin.\n\tgAdminAddr std.Address = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\tgControllers = avl.NewTree()\n\tgControllersMaxSize = 10 // limit it to 10\n\tgInPause = false\n\tgMessage = \"# Community Faucet.\\n\\n\"\n\n\t// internal vars, for stats.\n\tgTotalTransferred std.Coins\n\tgTotalTransfers = uint(0)\n\n\t// per request limit, 350 gnot\n\tgLimit std.Coin = std.NewCoin(\"ugnot\", 350000000)\n)\n\nfunc Transfer(to std.Address, send int64) string {\n\tif err := assertIsController(); err != nil {\n\t\treturn err.Error()\n\t}\n\n\tif gInPause {\n\t\treturn errors.New(\"faucet in pause\").Error()\n\t}\n\n\t// limit the per request\n\tif send \u003e gLimit.Amount {\n\t\treturn errors.New(\"Per request limit \" + gLimit.String() + \" exceed\").Error()\n\t}\n\tsendCoins := std.Coins{std.NewCoin(\"ugnot\", send)}\n\n\tgTotalTransferred = gTotalTransferred.Add(sendCoins)\n\tgTotalTransfers++\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\tpkgaddr := std.CurrentRealm().Address()\n\tbanker.SendCoins(pkgaddr, to, sendCoins)\n\treturn \"\"\n}\n\nfunc GetPerTransferLimit() int64 {\n\treturn gLimit.Amount\n}\n\nfunc Render(_ string) string {\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\tbalance := banker.GetCoins(std.CurrentRealm().Address())\n\n\toutput := gMessage\n\tif gInPause {\n\t\toutput += \"Status: inactive.\\n\"\n\t} else {\n\t\toutput += \"Status: active.\\n\"\n\t}\n\toutput += ufmt.Sprintf(\"Balance: %s.\\n\", balance.String())\n\toutput += ufmt.Sprintf(\"Total transfers: %s (in %d times).\\n\\n\", gTotalTransferred.String(), gTotalTransfers)\n\n\toutput += \"Package address: \" + std.CurrentRealm().Address().String() + \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Admin: %s\\n\\n \", gAdminAddr.String())\n\toutput += ufmt.Sprintf(\"Controllers:\\n\\n \")\n\n\tfor i := 0; i \u003c gControllers.Size(); i++ {\n\t\t_, v := gControllers.GetByIndex(i)\n\t\toutput += ufmt.Sprintf(\"%s \", v.(std.Address))\n\t}\n\n\toutput += \"\\n\\n\"\n\toutput += ufmt.Sprintf(\"Per request limit: %s\\n\\n\", gLimit.String())\n\n\treturn output\n}\n\nfunc assertIsController() error {\n\tcaller := std.OriginCaller()\n\n\tok := gControllers.Has(caller.String())\n\tif !ok {\n\t\treturn errors.New(caller.String() + \" is not on the controller list\")\n\t}\n\treturn nil\n}\n" + }, + { + "name": "faucet_test.gno", + "body": "package faucet_test\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\nfunc TestPackage(t *testing.T) {\n\tvar (\n\t\tadminaddr = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\tfaucetaddr = std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t\tcontrolleraddr3 = testutils.TestAddress(\"controller3\")\n\t\tcontrolleraddr4 = testutils.TestAddress(\"controller4\")\n\t\tcontrolleraddr5 = testutils.TestAddress(\"controller5\")\n\t\tcontrolleraddr6 = testutils.TestAddress(\"controller6\")\n\t\tcontrolleraddr7 = testutils.TestAddress(\"controller7\")\n\t\tcontrolleraddr8 = testutils.TestAddress(\"controller8\")\n\t\tcontrolleraddr9 = testutils.TestAddress(\"controller9\")\n\t\tcontrolleraddr10 = testutils.TestAddress(\"controller10\")\n\t\tcontrolleraddr11 = testutils.TestAddress(\"controller11\")\n\n\t\ttest1addr = testutils.TestAddress(\"test1\")\n\t)\n\t// deposit 1000gnot to faucet contract\n\ttesting.IssueCoins(faucetaddr, std.Coins{{\"ugnot\", 1000000000}})\n\tassertBalance(t, faucetaddr, 1000000000)\n\n\t// by default, balance is empty, and as a user I cannot call Transfer, or Admin commands.\n\n\tassertBalance(t, test1addr, 0)\n\ttesting.SetOriginCaller(test1addr)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n\n\tassertErr(t, faucet.AdminAddController(controlleraddr1))\n\ttesting.SetOriginCaller(controlleraddr1)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n\n\t// as an admin, add the controller to contract and deposit more 2000gnot to contract\n\ttesting.SetOriginCaller(adminaddr)\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr1))\n\tassertBalance(t, faucetaddr, 1000000000)\n\n\t// now, send some tokens as controller.\n\ttesting.SetOriginCaller(controlleraddr1)\n\tassertNoErr(t, faucet.Transfer(test1addr, 1000000))\n\tassertBalance(t, test1addr, 1000000)\n\tassertNoErr(t, faucet.Transfer(test1addr, 1000000))\n\tassertBalance(t, test1addr, 2000000)\n\tassertBalance(t, faucetaddr, 998000000)\n\n\t// remove controller\n\t// as an admin, remove controller\n\ttesting.SetOriginCaller(adminaddr)\n\tassertNoErr(t, faucet.AdminRemoveController(controlleraddr1))\n\ttesting.SetOriginCaller(controlleraddr1)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n\n\t// duplicate controller\n\ttesting.SetOriginCaller(adminaddr)\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr1))\n\tassertErr(t, faucet.AdminAddController(controlleraddr1))\n\t// add more than more than allowed controllers\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr2))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr3))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr4))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr5))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr6))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr7))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr8))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr9))\n\tassertNoErr(t, faucet.AdminAddController(controlleraddr10))\n\tassertErr(t, faucet.AdminAddController(controlleraddr11))\n\n\t// send more than per transfer limit\n\ttesting.SetOriginCaller(adminaddr)\n\tfaucet.AdminSetTransferLimit(300000000)\n\ttesting.SetOriginCaller(controlleraddr1)\n\tassertErr(t, faucet.Transfer(test1addr, 301000000))\n\n\t// block transefer from the address not on the controllers list.\n\ttesting.SetOriginCaller(controlleraddr11)\n\tassertErr(t, faucet.Transfer(test1addr, 1000000))\n}\n\nfunc assertErr(t *testing.T, err string) {\n\tt.Helper()\n\n\tif err == \"\" {\n\t\tt.Logf(\"info: got err: %v\", err)\n\t\tt.Errorf(\"expected an error, got nil.\")\n\t}\n}\n\nfunc assertNoErr(t *testing.T, err string) {\n\tt.Helper()\n\tif err != \"\" {\n\t\tt.Errorf(\"got err: %v.\", err)\n\t}\n}\n\nfunc assertBalance(t *testing.T, addr std.Address, expectedBal int64) {\n\tt.Helper()\n\tbanker := std.NewBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(addr)\n\tgot := coins.AmountOf(\"ugnot\")\n\n\tif expectedBal != got {\n\t\tt.Errorf(\"invalid balance: expected %d, got %d.\", expectedBal, got)\n\t}\n}\n" + }, + { + "name": "z0_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\ttesting.IssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with empty path and no controllers\nfunc main() {\n\tprintln(faucet.Render(\"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers: (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n//\n//\n// Per request limit: 350000000ugnot\n//\n//\n" + }, + { + "name": "z1_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\ttesting.IssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with a path and no controllers\nfunc main() {\n\tprintln(faucet.Render(\"path\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers: (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n//\n//\n// Per request limit: 350000000ugnot\n//\n//\n" + }, + { + "name": "z2_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints ugnot to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\ttesting.IssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with empty path and 2 controllers\nfunc main() {\n\tvar (\n\t\tadminaddr = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t)\n\ttesting.SetOriginCaller(adminaddr)\n\terr := faucet.AdminAddController(controlleraddr1)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\terr = faucet.AdminAddController(controlleraddr2)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tprintln(faucet.Render(\"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 200000000ugnot.\n// Total transfers: (in 0 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n// g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v\n//\n// Per request limit: 350000000ugnot\n//\n//\n" + }, + { + "name": "z3_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/faucet\"\n)\n\n// mints coints to current realm\nfunc init() {\n\tfacuetaddr := std.DerivePkgAddr(\"gno.land/r/gnoland/faucet\")\n\ttesting.IssueCoins(facuetaddr, std.Coins{{\"ugnot\", 200000000}})\n}\n\n// assert render with 2 controllers and 2 transfers\nfunc main() {\n\tvar (\n\t\tadminaddr = std.Address(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")\n\t\tcontrolleraddr1 = testutils.TestAddress(\"controller1\")\n\t\tcontrolleraddr2 = testutils.TestAddress(\"controller2\")\n\t\ttestaddr1 = testutils.TestAddress(\"test1\")\n\t\ttestaddr2 = testutils.TestAddress(\"test2\")\n\t)\n\ttesting.SetOriginCaller(adminaddr)\n\terr := faucet.AdminAddController(controlleraddr1)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\terr = faucet.AdminAddController(controlleraddr2)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\ttesting.SetOriginCaller(controlleraddr1)\n\terr = faucet.Transfer(testaddr1, 1000000)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\ttesting.SetOriginCaller(controlleraddr2)\n\terr = faucet.Transfer(testaddr1, 2000000)\n\tif err != \"\" {\n\t\tpanic(err)\n\t}\n\tprintln(faucet.Render(\"\"))\n}\n\n// Output:\n// # Community Faucet.\n//\n// Status: active.\n// Balance: 197000000ugnot.\n// Total transfers: 3000000ugnot (in 2 times).\n//\n// Package address: g1ttrq7mp4zy6dssnmgyyktnn4hcj3ys8xhju0n7\n//\n// Admin: g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n//\n// Controllers:\n//\n// g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v\n//\n// Per request limit: 350000000ugnot\n//\n//\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "BBYxnY1rev5oYzde0nAifz9x3I+fogzw29zqRl+1XIiQhvnNSuA/g6VWm1EVHiPzZDSLL6XikDrUbiJHchfLAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "ghverify", + "path": "gno.land/r/gnoland/ghverify", + "files": [ + { + "name": "README.md", + "body": "# ghverify\n\nThis realm is intended to enable off chain gno address to github handle verification.\nThe steps are as follows:\n- A user calls `RequestVerification` and provides a github handle. This creates a new static oracle feed.\n- An off-chain agent controlled by the owner of this realm requests current feeds using the `GnorkleEntrypoint` function and provides a message of `\"request\"`\n- The agent receives the task information that includes the github handle and the gno address. It performs the verification step by checking whether this github user has the address in a github repository it controls.\n- The agent publishes the result of the verification by calling `GnorkleEntrypoint` with a message structured like: `\"ingest,\u003ctask id\u003e,\u003cverification status\u003e\"`. The verification status is `OK` if verification succeeded and any other value if it failed.\n- The oracle feed's ingester processes the verification and the handle to address mapping is written to the avl trees that exist as ghverify realm variables." + }, + { + "name": "contract.gno", + "body": "package ghverify\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/gnorkle/feeds/static\"\n\t\"gno.land/p/demo/gnorkle/gnorkle\"\n\t\"gno.land/p/demo/gnorkle/message\"\n)\n\nconst (\n\t// The agent should send this value if it has verified the github handle.\n\tverifiedResult = \"OK\"\n)\n\nvar (\n\townerAddress = std.OriginCaller()\n\toracle *gnorkle.Instance\n\tpostHandler postGnorkleMessageHandler\n\n\thandleToAddressMap = avl.NewTree()\n\taddressToHandleMap = avl.NewTree()\n)\n\nfunc init() {\n\toracle = gnorkle.NewInstance()\n\toracle.AddToWhitelist(\"\", []string{string(ownerAddress)})\n}\n\ntype postGnorkleMessageHandler struct{}\n\n// Handle does post processing after a message is ingested by the oracle feed. It extracts the value to realm\n// storage and removes the feed from the oracle.\nfunc (h postGnorkleMessageHandler) Handle(i *gnorkle.Instance, funcType message.FuncType, feed gnorkle.Feed) error {\n\tif funcType != message.FuncTypeIngest {\n\t\treturn nil\n\t}\n\n\tresult, _, consumable := feed.Value()\n\tif !consumable {\n\t\treturn nil\n\t}\n\n\t// The value is consumable, meaning the ingestion occurred, so we can remove the feed from the oracle\n\t// after saving it to realm storage.\n\tdefer oracle.RemoveFeed(feed.ID())\n\n\t// Couldn't verify; nothing to do.\n\tif result.String != verifiedResult {\n\t\treturn nil\n\t}\n\n\tfeedTasks := feed.Tasks()\n\tif len(feedTasks) != 1 {\n\t\treturn errors.New(\"expected feed to have exactly one task\")\n\t}\n\n\ttask, ok := feedTasks[0].(*verificationTask)\n\tif !ok {\n\t\treturn errors.New(\"expected ghverify task\")\n\t}\n\n\thandleToAddressMap.Set(task.githubHandle, task.gnoAddress)\n\taddressToHandleMap.Set(task.gnoAddress, task.githubHandle)\n\treturn nil\n}\n\n// RequestVerification creates a new static feed with a single task that will\n// instruct an agent to verify the github handle / gno address pair.\nfunc RequestVerification(githubHandle string) {\n\tgnoAddress := string(std.OriginCaller())\n\tif err := oracle.AddFeeds(\n\t\tstatic.NewSingleValueFeed(\n\t\t\tgnoAddress,\n\t\t\t\"string\",\n\t\t\t\u0026verificationTask{\n\t\t\t\tgnoAddress: gnoAddress,\n\t\t\t\tgithubHandle: githubHandle,\n\t\t\t},\n\t\t),\n\t); err != nil {\n\t\tpanic(err)\n\t}\n\tstd.Emit(\n\t\t\"verification_requested\",\n\t\t\"from\", gnoAddress,\n\t\t\"handle\", githubHandle,\n\t)\n}\n\n// GnorkleEntrypoint is the entrypoint to the gnorkle oracle handler.\nfunc GnorkleEntrypoint(message string) string {\n\tresult, err := oracle.HandleMessage(message, postHandler)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn result\n}\n\n// SetOwner transfers ownership of the contract to the given address.\nfunc SetOwner(owner std.Address) {\n\tif ownerAddress != std.OriginCaller() {\n\t\tpanic(\"only the owner can set a new owner\")\n\t}\n\n\townerAddress = owner\n\n\t// In the context of this contract, the owner is the only one that can\n\t// add new feeds to the oracle.\n\toracle.ClearWhitelist(\"\")\n\toracle.AddToWhitelist(\"\", []string{string(ownerAddress)})\n}\n\n// GetHandleByAddress returns the github handle associated with the given gno address.\nfunc GetHandleByAddress(address string) string {\n\tif value, ok := addressToHandleMap.Get(address); ok {\n\t\treturn value.(string)\n\t}\n\n\treturn \"\"\n}\n\n// GetAddressByHandle returns the gno address associated with the given github handle.\nfunc GetAddressByHandle(handle string) string {\n\tif value, ok := handleToAddressMap.Get(handle); ok {\n\t\treturn value.(string)\n\t}\n\n\treturn \"\"\n}\n\n// Render returns a json object string will all verified handle -\u003e address mappings.\nfunc Render(_ string) string {\n\tresult := \"{\"\n\tvar appendComma bool\n\thandleToAddressMap.Iterate(\"\", \"\", func(handle string, address any) bool {\n\t\tif appendComma {\n\t\t\tresult += \",\"\n\t\t}\n\n\t\tresult += `\"` + handle + `\": \"` + address.(string) + `\"`\n\t\tappendComma = true\n\n\t\treturn false\n\t})\n\n\treturn result + \"}\"\n}\n" + }, + { + "name": "contract_test.gno", + "body": "package ghverify\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n)\n\nfunc TestVerificationLifecycle(t *testing.T) {\n\tdefaultAddress := std.OriginCaller()\n\tuser1Address := std.Address(testutils.TestAddress(\"user 1\"))\n\tuser2Address := std.Address(testutils.TestAddress(\"user 2\"))\n\n\t// Verify request returns no feeds.\n\tresult := GnorkleEntrypoint(\"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Make a verification request with the created user.\n\ttesting.SetOriginCaller(user1Address)\n\tRequestVerification(\"deelawn\")\n\n\t// A subsequent request from the same address should panic because there is\n\t// already a feed with an ID of this user's address.\n\tvar errMsg string\n\tfunc() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\terrMsg = r.(error).Error()\n\t\t\t}\n\t\t}()\n\t\tRequestVerification(\"deelawn\")\n\t}()\n\tif errMsg != \"feed already exists\" {\n\t\tt.Fatalf(\"expected feed already exists, got %s\", errMsg)\n\t}\n\n\t// Verify the request returns no feeds for this non-whitelisted user.\n\tresult = GnorkleEntrypoint(\"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Make a verification request with the created user.\n\ttesting.SetOriginCaller(user2Address)\n\tRequestVerification(\"omarsy\")\n\n\t// Set the caller back to the whitelisted user and verify that the feed data\n\t// returned matches what should have been created by the `RequestVerification`\n\t// invocation.\n\ttesting.SetOriginCaller(defaultAddress)\n\tresult = GnorkleEntrypoint(\"request\")\n\texpResult := `[{\"id\":\"` + string(user1Address) + `\",\"type\":\"0\",\"value_type\":\"string\",\"tasks\":[{\"gno_address\":\"` +\n\t\tstring(user1Address) + `\",\"github_handle\":\"deelawn\"}]},` +\n\t\t`{\"id\":\"` + string(user2Address) + `\",\"type\":\"0\",\"value_type\":\"string\",\"tasks\":[{\"gno_address\":\"` +\n\t\tstring(user2Address) + `\",\"github_handle\":\"omarsy\"}]}]`\n\tif result != expResult {\n\t\tt.Fatalf(\"expected request result %s, got %s\", expResult, result)\n\t}\n\n\t// Try to trigger feed ingestion from the non-authorized user.\n\ttesting.SetOriginCaller(user1Address)\n\tfunc() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\terrMsg = r.(error).Error()\n\t\t\t}\n\t\t}()\n\t\tGnorkleEntrypoint(\"ingest,\" + string(user1Address) + \",OK\")\n\t}()\n\tif errMsg != \"caller not whitelisted\" {\n\t\tt.Fatalf(\"expected caller not whitelisted, got %s\", errMsg)\n\t}\n\n\t// Set the caller back to the whitelisted user and transfer contract ownership.\n\ttesting.SetOriginCaller(defaultAddress)\n\tSetOwner(defaultAddress)\n\n\t// Now trigger the feed ingestion from the user and new owner and only whitelisted address.\n\tGnorkleEntrypoint(\"ingest,\" + string(user1Address) + \",OK\")\n\tGnorkleEntrypoint(\"ingest,\" + string(user2Address) + \",OK\")\n\n\t// Verify the ingestion autocommitted the value and triggered the post handler.\n\tdata := Render(\"\")\n\texpResult = `{\"deelawn\": \"` + string(user1Address) + `\",\"omarsy\": \"` + string(user2Address) + `\"}`\n\tif data != expResult {\n\t\tt.Fatalf(\"expected render data %s, got %s\", expResult, data)\n\t}\n\n\t// Finally make sure the feed was cleaned up after the data was committed.\n\tresult = GnorkleEntrypoint(\"request\")\n\tif result != \"[]\" {\n\t\tt.Fatalf(\"expected empty request result, got %s\", result)\n\t}\n\n\t// Check that the accessor functions are working as expected.\n\tif handle := GetHandleByAddress(string(user1Address)); handle != \"deelawn\" {\n\t\tt.Fatalf(\"expected deelawn, got %s\", handle)\n\t}\n\tif address := GetAddressByHandle(\"deelawn\"); address != string(user1Address) {\n\t\tt.Fatalf(\"expected %s, got %s\", string(user1Address), address)\n\t}\n}\n" + }, + { + "name": "task.gno", + "body": "package ghverify\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n)\n\ntype verificationTask struct {\n\tgnoAddress string\n\tgithubHandle string\n}\n\n// MarshalJSON marshals the task contents to JSON.\nfunc (t *verificationTask) MarshalJSON() ([]byte, error) {\n\tbuf := new(bytes.Buffer)\n\tw := bufio.NewWriter(buf)\n\n\tw.Write(\n\t\t[]byte(`{\"gno_address\":\"` + t.gnoAddress + `\",\"github_handle\":\"` + t.githubHandle + `\"}`),\n\t)\n\n\tw.Flush()\n\treturn buf.Bytes(), nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "egOlYTZ4lWj6mb+3aaBwuRdVG5f6uIq8TdnDggUZNT0vwS4qJ+2Mzkt8obdrdDikzPip9sn15nkZ3KyDTjQ3Ag==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/gnoland/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ui\"\n\t\"gno.land/p/moul/dynreplacer\"\n\tblog \"gno.land/r/gnoland/blog\"\n\t\"gno.land/r/gnoland/events\"\n\t\"gno.land/r/leon/hof\"\n)\n\nvar (\n\toverride string\n\tAdmin = ownable.NewWithAddress(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n)\n\nfunc Render(_ string) string {\n\tr := dynreplacer.New()\n\tr.RegisterCallback(\":latest-blogposts:\", func() string {\n\t\treturn blog.RenderLastPostsWidget(4)\n\t})\n\tr.RegisterCallback(\":upcoming-events:\", func() string {\n\t\tout, _ := events.RenderEventWidget(events.MaxWidgetSize)\n\t\treturn out\n\t})\n\tr.RegisterCallback(\":latest-hof:\", func() string {\n\t\treturn hof.RenderExhibWidget(5)\n\t})\n\tr.RegisterCallback(\":qotb:\", quoteOfTheBlock)\n\tr.RegisterCallback(\":chain-height:\", func() string {\n\t\treturn strconv.Itoa(int(std.ChainHeight()))\n\t})\n\n\ttemplate := `# Welcome to gno.land\n\nWe’re building gno.land, set to become the leading open-source smart contract\nplatform, using Gno, an interpreted and fully deterministic variation of the\nGo programming language for succinct and composable smart contracts.\n\nWith transparent and timeless code, gno.land is the next generation of smart\ncontract platforms, serving as the “GitHub” of the ecosystem, with realms built\nusing fully transparent, auditable code that anyone can inspect and reuse.\n\nIntuitive and easy to use, gno.land lowers the barrier to web3 and makes\ncensorship-resistant platforms accessible to everyone. If you want to help lay\nthe foundations of a fairer and freer world, join us today. \n\n## Learn about gno.land\n\n- [About](/about)\n- [GitHub](https://github.com/gnolang)\n- [Blog](/blog)\n- [Events](/events)\n- Tokenomics (soon)\n- [Partners, Fund, Grants](/partners)\n- [Explore the Ecosystem](/ecosystem)\n- [Careers](https://jobs.ashbyhq.com/allinbits)\n\n## Build with Gno\n\n- [Write Gno in the browser](https://play.gno.land)\n- [Read about the Gno Language](/gnolang)\n- [Visit the official documentation](https://docs.gno.land)\n- [Gno by Example](https://gno-by-example.com/)\n- [Efficient local development for Gno](https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev)\n- [Get testnet GNOTs](https://faucet.gno.land)\n\n## Explore the universe\n\n- [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples)\n- [Gnoscan](https://gnoscan.io)\n- [Portal Loop](https://docs.gno.land/concepts/portal-loop)\n- Testnet 6 (upcoming)\n- [Testnet 5](https://test5.gno.land/)\n- [Faucet Hub](https://faucet.gno.land)\n\n## [Latest Blogposts](/r/gnoland/blog)\n\n:latest-blogposts:\n\n## [Latest Events](/r/gnoland/events)\n\n:upcoming-events:\n\n## [Hall of Fame](/r/leon/hof)\n\n:latest-hof:\n\n---\n\n## [Gno Playground](https://play.gno.land)\n\n\nGno Playground is a web application designed for building, running, testing, and\ninteracting with your Gno code, enhancing your understanding of the Gno\nlanguage. With Gno Playground, you can share your code, execute tests, deploy\nyour realms and packages to gno.land, and explore a multitude of other features.\n\nExperience the convenience of code sharing and rapid experimentation with\n[Gno Playground](https://play.gno.land).\n\n## Explore New Packages and Realms\n\n### [r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland)\n\n- [r/gnoland/blog](/r/gnoland/blog)\n- [r/gnoland/users](/r/gnoland/users)\n- [r/gnoland/home](/r/gnoland/home)\n- [r/gnoland/pages](/r/gnoland/pages)\n\n### [r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)\n\n- [r/sys/names](/r/sys/names)\n- [r/sys/users](/r/sys/users)\n- [r/sys/rewards](/r/sys/rewards)\n- [/r/sys/validators/v2](/r/sys/validators/v2)\n\n### [r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)\n\n- [r/demo/boards](/r/demo/boards)\n- [r/demo/banktest](/r/demo/banktest)\n- [r/demo/foo20](/r/demo/foo20)\n- [r/demo/foo721](/r/demo/foo721)\n- [r/demo/microblog](/r/demo/microblog)\n- [r/demo/nft](/r/demo/nft)\n- [r/demo/types](/r/demo/types)\n- [r/demo/art/gnoface](/r/demo/art/gnoface)\n- [r/demo/art/millipede](/r/demo/art/millipede)\n- [r/demo/groups](/r/demo/groups)\n- ...\n\n### [p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo)\n\n- [p/demo/avl](/p/demo/avl)\n- [p/demo/blog](/p/demo/blog)\n- [p/demo/ui](/p/demo/ui)\n- [p/demo/ufmt](/p/demo/ufmt)\n- [p/demo/merkle](/p/demo/merkle)\n- [p/demo/bf](/p/demo/bf)\n- [p/demo/flow](/p/demo/flow)\n- [p/demo/gnode](/p/demo/gnode)\n- [p/demo/grc/grc20](/p/demo/grc/grc20)\n- [p/demo/grc/grc721](/p/demo/grc/grc721)\n- ...\n\n---\n\n## Socials\n\n- Check out our [community projects](https://github.com/gnolang/awesome-gno)\n- [Discord](https://discord.gg/S8nKUqwkPn)\n- [Twitter](https://twitter.com/_gnoland)\n- [Youtube](https://www.youtube.com/@_gnoland)\n- [Telegram](https://t.me/gnoland)\n\n## Quote of the ~Day~ Block#:chain-height:\n\n\u003e :qotb:\n\n---\n\n**This is a testnet.**\nPackage names are not guaranteed to be available for production.\n`\n\n\tif override != \"\" {\n\t\ttemplate = override\n\t}\n\tresult := r.Replace(template)\n\treturn result\n}\n\nfunc latestHOFItems(num int) ui.Element {\n\tsubmissions := hof.RenderExhibWidget(num)\n\n\treturn ui.Element{\n\t\tui.H2(\"[Hall of Fame](/r/leon/hof)\"),\n\t\tui.Text(submissions),\n\t}\n}\n\nfunc quoteOfTheBlock() string {\n\tquotes := []string{\n\t\t\"Gno is for Truth.\",\n\t\t\"Gno is for Social Coordination.\",\n\t\t\"Gno is _not only_ for DeFi.\",\n\t\t\"Now, you Gno.\",\n\t\t\"Come for the Go, Stay for the Gno.\",\n\t}\n\theight := std.ChainHeight()\n\tidx := int(height) % len(quotes)\n\tqotb := quotes[idx]\n\treturn qotb\n}\n\nfunc AdminSetOverride(content string) {\n\tAdmin.AssertCallerIsOwner()\n\toverride = content\n}\n" + }, + { + "name": "home_filetest.gno", + "body": "package main\n\nimport \"gno.land/r/gnoland/home\"\n\nfunc main() {\n\tprintln(home.Render(\"\"))\n}\n\n// Output:\n// # Welcome to gno.land\n//\n// We’re building gno.land, set to become the leading open-source smart contract\n// platform, using Gno, an interpreted and fully deterministic variation of the\n// Go programming language for succinct and composable smart contracts.\n//\n// With transparent and timeless code, gno.land is the next generation of smart\n// contract platforms, serving as the “GitHub” of the ecosystem, with realms built\n// using fully transparent, auditable code that anyone can inspect and reuse.\n//\n// Intuitive and easy to use, gno.land lowers the barrier to web3 and makes\n// censorship-resistant platforms accessible to everyone. If you want to help lay\n// the foundations of a fairer and freer world, join us today.\n//\n// ## Learn about gno.land\n//\n// - [About](/about)\n// - [GitHub](https://github.com/gnolang)\n// - [Blog](/blog)\n// - [Events](/events)\n// - Tokenomics (soon)\n// - [Partners, Fund, Grants](/partners)\n// - [Explore the Ecosystem](/ecosystem)\n// - [Careers](https://jobs.ashbyhq.com/allinbits)\n//\n// ## Build with Gno\n//\n// - [Write Gno in the browser](https://play.gno.land)\n// - [Read about the Gno Language](/gnolang)\n// - [Visit the official documentation](https://docs.gno.land)\n// - [Gno by Example](https://gno-by-example.com/)\n// - [Efficient local development for Gno](https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev)\n// - [Get testnet GNOTs](https://faucet.gno.land)\n//\n// ## Explore the universe\n//\n// - [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples)\n// - [Gnoscan](https://gnoscan.io)\n// - [Portal Loop](https://docs.gno.land/concepts/portal-loop)\n// - Testnet 6 (upcoming)\n// - [Testnet 5](https://test5.gno.land/)\n// - [Faucet Hub](https://faucet.gno.land)\n//\n// ## [Latest Blogposts](/r/gnoland/blog)\n//\n// No posts.\n//\n// ## [Latest Events](/r/gnoland/events)\n//\n// No events.\n//\n// ## [Hall of Fame](/r/leon/hof)\n//\n//\n//\n// ---\n//\n// ## [Gno Playground](https://play.gno.land)\n//\n//\n// Gno Playground is a web application designed for building, running, testing, and\n// interacting with your Gno code, enhancing your understanding of the Gno\n// language. With Gno Playground, you can share your code, execute tests, deploy\n// your realms and packages to gno.land, and explore a multitude of other features.\n//\n// Experience the convenience of code sharing and rapid experimentation with\n// [Gno Playground](https://play.gno.land).\n//\n// ## Explore New Packages and Realms\n//\n// ### [r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland)\n//\n// - [r/gnoland/blog](/r/gnoland/blog)\n// - [r/gnoland/users](/r/gnoland/users)\n// - [r/gnoland/home](/r/gnoland/home)\n// - [r/gnoland/pages](/r/gnoland/pages)\n//\n// ### [r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)\n//\n// - [r/sys/names](/r/sys/names)\n// - [r/sys/users](/r/sys/users)\n// - [r/sys/rewards](/r/sys/rewards)\n// - [/r/sys/validators/v2](/r/sys/validators/v2)\n//\n// ### [r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)\n//\n// - [r/demo/boards](/r/demo/boards)\n// - [r/demo/banktest](/r/demo/banktest)\n// - [r/demo/foo20](/r/demo/foo20)\n// - [r/demo/foo721](/r/demo/foo721)\n// - [r/demo/microblog](/r/demo/microblog)\n// - [r/demo/nft](/r/demo/nft)\n// - [r/demo/types](/r/demo/types)\n// - [r/demo/art/gnoface](/r/demo/art/gnoface)\n// - [r/demo/art/millipede](/r/demo/art/millipede)\n// - [r/demo/groups](/r/demo/groups)\n// - ...\n//\n// ### [p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo)\n//\n// - [p/demo/avl](/p/demo/avl)\n// - [p/demo/blog](/p/demo/blog)\n// - [p/demo/ui](/p/demo/ui)\n// - [p/demo/ufmt](/p/demo/ufmt)\n// - [p/demo/merkle](/p/demo/merkle)\n// - [p/demo/bf](/p/demo/bf)\n// - [p/demo/flow](/p/demo/flow)\n// - [p/demo/gnode](/p/demo/gnode)\n// - [p/demo/grc/grc20](/p/demo/grc/grc20)\n// - [p/demo/grc/grc721](/p/demo/grc/grc721)\n// - ...\n//\n// ---\n//\n// ## Socials\n//\n// - Check out our [community projects](https://github.com/gnolang/awesome-gno)\n// - [Discord](https://discord.gg/S8nKUqwkPn)\n// - [Twitter](https://twitter.com/_gnoland)\n// - [Youtube](https://www.youtube.com/@_gnoland)\n// - [Telegram](https://t.me/gnoland)\n//\n// ## Quote of the ~Day~ Block#123\n//\n// \u003e Now, you Gno.\n//\n// ---\n//\n// **This is a testnet.**\n// Package names are not guaranteed to be available for production.\n//\n" + }, + { + "name": "override_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/gnoland/home\"\n)\n\nfunc main() {\n\tvar admin = std.Address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\")\n\ttesting.SetOriginCaller(admin)\n\thome.AdminSetOverride(\"Hello World!\")\n\tprintln(\"---\")\n\tprintln(home.Render(\"\"))\n\thome.Admin.TransferOwnership(testutils.TestAddress(\"newAdmin\"))\n\tdefer func() {\n\t\tr := recover()\n\t\tprintln(\"r: \", r)\n\t}()\n\thome.AdminSetOverride(\"Not admin anymore\")\n}\n\n// Output:\n// ---\n// Hello World!\n// r: ownable: caller is not owner\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "w7o0Pnd8XXDszNqOabjmFiEt35D1sW6/xby6Fkz2VmfZRBWMcbACGUJWfSkO7wRniSM5/BdS6HgOjg4dn5IzBQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "monit", + "path": "gno.land/r/gnoland/monit", + "files": [ + { + "name": "monit.gno", + "body": "// Package monit links a monitoring system with the chain in both directions.\n//\n// The agent will periodically call Incr() and verify that the value is always\n// higher than the previously known one. The contract will store the last update\n// time and use it to detect whether or not the monitoring agent is functioning\n// correctly.\npackage monit\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/demo/watchdog\"\n)\n\nvar (\n\tcounter int\n\tlastUpdate time.Time\n\tlastCaller std.Address\n\twd = watchdog.Watchdog{Duration: 5 * time.Minute}\n\tOwnable = ownable.New() // TODO: replace with -\u003e ownable.NewWithAddress...\n\twatchdogDuration = 5 * time.Minute\n)\n\n// Incr increments the counter and informs the watchdog that we're alive.\n// This function can be called by anyone.\nfunc Incr() int {\n\tcounter++\n\tlastUpdate = time.Now()\n\tlastCaller = std.PreviousRealm().Address()\n\twd.Alive()\n\treturn counter\n}\n\n// Reset resets the realm state.\n// This function can only be called by the admin.\nfunc Reset() {\n\tOwnable.AssertCallerIsOwner()\n\n\tcounter = 0\n\tlastCaller = std.PreviousRealm().Address()\n\tlastUpdate = time.Now()\n\twd = watchdog.Watchdog{Duration: 5 * time.Minute}\n}\n\nfunc Render(_ string) string {\n\tstatus := wd.Status()\n\treturn ufmt.Sprintf(\n\t\t\"counter=%d\\nlast update=%s\\nlast caller=%s\\nstatus=%s\",\n\t\tcounter, lastUpdate, lastCaller, status,\n\t)\n}\n" + }, + { + "name": "monit_test.gno", + "body": "package monit\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n)\n\nfunc TestPackage(t *testing.T) {\n\t// initial state, watchdog is KO.\n\t{\n\t\texpected := `counter=0\nlast update=0001-01-01 00:00:00 +0000 UTC\nlast caller=\nstatus=KO`\n\t\tgot := Render(\"\")\n\t\tuassert.Equal(t, expected, got)\n\t}\n\n\t// call Incr(), watchdog is OK.\n\tIncr()\n\tIncr()\n\tIncr()\n\t{\n\t\texpected := `counter=3\nlast update=2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\nlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\nstatus=OK`\n\t\tgot := Render(\"\")\n\t\tuassert.Equal(t, expected, got)\n\t}\n\n\t/* XXX: improve tests once we've the missing std.TestSkipTime feature\n\t\t// wait 1h, watchdog is KO.\n\t\tuse std.TestSkipTime(time.Hour)\n\t\t{\n\t\t\texpected := `counter=3\n\tlast update=2009-02-13 22:31:30 +0000 UTC m=+1234564290.000000001\n\tlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n\tstatus=KO`\n\t\t\tgot := Render(\"\")\n\t\t\tuassert.Equal(t, expected, got)\n\t\t}\n\n\t\t// call Incr(), watchdog is OK.\n\t\tIncr()\n\t\t{\n\t\t\texpected := `counter=4\n\tlast update=2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001\n\tlast caller=g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\n\tstatus=OK`\n\t\t\tgot := Render(\"\")\n\t\t\tuassert.Equal(t, expected, got)\n\t\t}\n\t*/\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "wcgU+E0Xj6rHqmEcltPZ2JMIQqxDLBKeeyYsRC6v3izuj8vS6smkz+jSSrCWWKSoZgflAtRe5fPrLs8JR6KEBA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "gnopages", + "path": "gno.land/r/gnoland/pages", + "files": [ + { + "name": "admin.gno", + "body": "package gnopages\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nvar (\n\tadminAddr std.Address\n\tmoderatorList avl.Tree\n\tinPause bool\n)\n\nfunc init() {\n\t// adminAddr = std.OriginCaller() // FIXME: find a way to use this from the main's genesis.\n\tadminAddr = \"g1manfred47kzduec920z88wfr64ylksmdcedlf5\" // @moul\n}\n\nfunc AdminSetAdminAddr(addr std.Address) {\n\tassertIsAdmin()\n\tadminAddr = addr\n}\n\nfunc AdminSetInPause(state bool) {\n\tassertIsAdmin()\n\tinPause = state\n}\n\nfunc AdminAddModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), true)\n}\n\nfunc AdminRemoveModerator(addr std.Address) {\n\tassertIsAdmin()\n\tmoderatorList.Set(addr.String(), false) // XXX: delete instead?\n}\n\nfunc ModAddPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\tcaller := std.OriginCaller()\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc ModEditPost(slug, title, body, publicationDate, authors, tags string) {\n\tassertIsModerator()\n\n\ttagList := strings.Split(tags, \",\")\n\tauthorList := strings.Split(authors, \",\")\n\n\terr := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList)\n\tcheckErr(err)\n}\n\nfunc isAdmin(addr std.Address) bool {\n\treturn addr == adminAddr\n}\n\nfunc isModerator(addr std.Address) bool {\n\t_, found := moderatorList.Get(addr.String())\n\treturn found\n}\n\nfunc assertIsAdmin() {\n\tcaller := std.OriginCaller()\n\tif !isAdmin(caller) {\n\t\tpanic(\"access restricted.\")\n\t}\n}\n\nfunc assertIsModerator() {\n\tcaller := std.OriginCaller()\n\tif isAdmin(caller) || isModerator(caller) {\n\t\treturn\n\t}\n\tpanic(\"access restricted\")\n}\n\nfunc assertNotInPause() {\n\tif inPause {\n\t\tpanic(\"access restricted (pause)\")\n\t}\n}\n" + }, + { + "name": "page_about.gno", + "body": "package gnopages\n\nfunc init() {\n\tpath := \"about\"\n\ttitle := \"gno.land Is A Platform To Write Smart Contracts In Gno\"\n\t// XXX: description := \"On gno.land, developers write smart contracts and other blockchain apps using Gno without learning a language that’s exclusive to a single ecosystem.\"\n\tbody := `\ngno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\nprogramming language. On gno.land, smart contracts can be uploaded on-chain only by publishing their full source code,\nmaking it trivial to verify the contract or fork it into an improved version. With a system to publish reusable code\nlibraries on-chain, gno.land serves as the “GitHub” of the ecosystem, with realms built using fully transparent,\nauditable code that anyone can inspect and reuse.\n\ngno.land addresses many pressing issues in the blockchain space, starting with the ease of use and intuitiveness of\nsmart contract platforms. Developers can write smart contracts without having to learn a new language that’s exclusive\nto a single ecosystem or limited by design. Go developers can easily port their existing web apps to gno.land or build\nnew ones from scratch, making web3 vastly more accessible.\n\nSecured by Proof of Contribution (PoC), a DAO-managed Proof-of-Authority consensus mechanism, gno.land prioritizes\nfairness and merit, rewarding the people most active on the platform. PoC restructures the financial incentives that\noften corrupt blockchain projects, opting instead to reward contributors for their work based on expertise, commitment, and\nalignment.\n\nOne of our inspirations for gno.land is the gospels, which built a system of moral code that lasted thousands of years.\nBy observing a minimal production implementation, gno.land’s design will endure over time and serve as a reference for\nfuture generations with censorship-resistant tools that improve their understanding of the world.\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:22Z\", nil, nil)\n}\n" + }, + { + "name": "page_contribute.gno", + "body": "package gnopages\n\nfunc init() {\n\tpath := \"contribute\"\n\ttitle := \"Contributor Ecosystem: Call for Contributions\"\n\tbody := `\n\ngno.land puts at the center of its identity the contributors that help to create and shape the project into what it is; incentivizing those who contribute the most and help advance its vision. Eventually, contributions will be incentivized directly on-chain; in the meantime, this page serves to illustrate our current off-chain initiatives.\n\ngno.land is still in full-steam development. For now, we're looking for the earliest of adopters; curious to explore a new way to build smart contracts and eager to make an impact. Joining gno.land's development now means you can help to shape the base of its development ecosystem, which will pave the way for the next generation of blockchain programming.\n\nAs an open-source project, we welcome all contributions. On this page you can find some pointers on where to get started; as well as some incentives for the most valuable and important contributions.\n\n## Where to get started\n\nIf you are interested in contributing to gno.land, you can jump on in on our [GitHub monorepo](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md) - where most development happens.\n\nA good place where to start are the issues tagged [\"good first issue\"](https://github.com/gnolang/gno/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). They should allow you to make some impact on the Gno repository while you're still exploring the details of how everything works.\n\n## Gno Bounties\n\nAdditionally, you can look out to help on specific issues labeled as bounties. The Gno bounty program is a good way to find interesting challenges in Gno, and get rewarded for helping us advance the project. We will maintain open and rewardable bounties in the gnolang/gno repository, and you can search all available bounties by using the [\"bounty\" label](https://github.com/gnolang/gno/labels/bounty). For more detals on the categories and types of bounties, we have a [bounties README](https://github.com/gnolang/bounties). \n\nRecommendations on participating in the gno.land Bounty Program:\n\n- Identify the bounty you want to work on, and join in the discussion on the issue for anything that is unclear; or where you want to more clearly define the work to be done. At this stage, you can also start working on an initial implementation in your local enviornment.\n- Once you have spent time on the code related to the bounty, we recommend submitting a 'draft' PR as soon as possible.\n - The draft PR doesn't indicate that the bounty has been assigned to you, others are free to work on other draft PRs for the bounty.\n - Make sure to reference the bounty issue on the PR description you're writing.\n - After submitting the 'draft' PR, continue working until you are ready to mark the PR as \"ready for review\".\n - The core team will review the bounty PR submission after the work on the bounty has been completed, and determine if it qualifies for the bounty reward.\n- Ask for clarification early if an element on the requirements or implementation design is unclear.\n - Aside from publishing the PR early, keeping regular updates with the core team on the bounty issue is key to being on the right track.\n - As part of the requirements, you must adhere to the [contributing guidelines](https://github.com/gnolang/gno/blob/master/CONTRIBUTING.md); additionally, it is expected that any newly added code or functionality is properly documented, tested and covered, at least in 80% of added code.\n - You're welcome to propose additional features and work on an issue should you envision a plausible expansion or change in scope. The core team may assign a bounty to the additional work, or change the bounty with respect to the changed scope.\n\nYou may make your submission at any time; however we invite you to publish your draft PR very early in the development process. This will make your work public, so you can easily get help by the core team and other community members. Additionally, your work can be continued by other people should you get stuck or no longer be willing to work on the bounty. Likewise, you can continue the abandoned or stuck work that someone else worked on.\n\nDon't fear your work being \"stolen\": if a submission is the result of multiple people's efforts, we will look to split the bounty in a way that is fair and recognises each participant in creating the final outcome. Here are some examples of how that can happen:\n\n- If Alice does most of the work and abandons it; then Bob comes around and finishes the job, then Bob's PR will be merged. But the core team will propose a split like 70% for Alice and 30% for Bob (depending, of course, on the relative effort undertaken by both).\n- If Alice makes a PR that does only 50% of the work outlined in the requirements for the original issue, she will get 50%. Someone can still come up and finish the job; and claim the remaining part.\n\t- If you, for instance, cannot complete the entirety of the task or, as a non-developer, can only contribute a part of the specification/implementation, you may still be awarded a bounty for your input in the contribution.\n- If Alice makes a PR that aside from implementing what's required, also undertakes creating useful tools among the way, she may qualify for an \"outstanding contribution\"; and may be awarded up to 25% more of the original bounty's value. Or she may also ask if the team would be willing to offer a different bounty for the implementation of the tools.\n\nParticipants in the gno.land Bounty Program must meet the legal Terms and Conditions referenced [here](https://docs.google.com/document/d/e/2PACX-1vSUF-JwIXGscrNsc5QBD7Pa6i83mXUGogAEIf1wkeb_w42UgL3Lj6jFKMlNTdwEMUnhsLkjRlhe25K4/pub).\n\n### Bounty sizes\n\nEach bounty is associated with a size, to which corresponds the maximum compensation for the work involved on the bounty. A bounty size may under rare occasion be revisited to a bigger or smaller size; hence why it's important to talk about your proposed solution with the core team ahead of time.\n\nIn some cases, the work associated with a bounty may be outstanding. When that happens, the core team can decide to award up to 25% of the bounty's value to the recipient.\n\nThe value of the bounty, aside from the material completion of the task, considers the involved time in managing the created pull request and iterating on feedback.\n\n\nt-shirt size | expected compensation\n-------------|-----------------------\n[XS] | $ 500\n[S] | $ 1000\n[M] | $ 2000\n[L] | $ 4000\n[XL] | $ 8000\n_[XXL]_ \\* | $ 16000\n_[3XL]_ \\* | $ 32000\n\n[XS]: https://github.com/gnolang/gno/labels/bounty%2FXS\n[S]: https://github.com/gnolang/gno/labels/bounty%2FS\n[M]: https://github.com/gnolang/gno/labels/bounty%2FM\n[L]: https://github.com/gnolang/gno/labels/bounty%2FL\n[XL]: https://github.com/gnolang/gno/labels/bounty%2FXL\n[XXL]: https://github.com/gnolang/gno/labels/bounty%2FXXL\n[3XL]: https://github.com/gnolang/gno/labels/bounty%2F3XL\n\n\\*: XXL and 3XL bounties are exceptional. Almost no issues will have these sizes; most will be broken down into smaller bounties.\n\n## gno.land Grants\n\nThe gno.land grants program is to encourage and support the growth of the gno.land contributor community, and build out the usability of the platform and smart contract library. The program provides financial resources to contributors to explore the Gno tech stack, and build dApps, tooling, infrastructure, products, and smart contract libraries in gno.land.\n\nFor more details on gno.land grants, suggested topics, and how to apply, visit our grants [repository](https://github.com/gnolang/grants). \n\n## Join Game of Realms\n\nGame of Realms is the overarching contributor network of gnomes, currently running off-chain, and will eventually transition on-chain. At this stage, a Game of Realms contribution is comprised of high-impact contributions identified as ['notable contributions'](https://github.com/gnolang/game-of-realms/tree/main/contributors).\n\nThese contributions are not linked to immediate financial rewards, but are notable in nature, in the sense they are a challenge, make a significant addition to the project, and require persistence, with minimal feedback loops from the core team.\n\nThe selection of a notable contribution or the sum of contributions that equal 'notable' is based on the impact it has on the development of the project. For now, it is focused on code contributions, and will evolve over time. The Gno development teams will initially qualify and evaluate notable contributions, and vote off-chain on adding them to the 'notable contributions' folder on GitHub.\n\nYou can always contribute to the project, and all contributions will be noticed. Contributing now is a way to build your personal contributor profile in gno.land early on in the ecosystem, and signal your commitment to the project, the community, and its future.\n\nThere are a variety of ways to make your contributions count:\n\n- Core code contributions\n- Realm and pure package development\n- Validator tooling\n- Developer tooling\n- Tutorials and documentation\n\nTo start, we recommend you create a PR in the Game of Realms [repository](https://github.com/gnolang/game-of-realms) to create your profile page for all your contributions.`\n\n\t_ = b.NewPost(\"\", path, title, body, \"2024-09-05T00:00:00Z\", nil, nil)\n}\n" + }, + { + "name": "page_ecosystem.gno", + "body": "package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"ecosystem\"\n\t\ttitle = \"Discover gno.land Ecosystem Projects \u0026 Initiatives\"\n\t\t// XXX: description = \"Dive further into the gno.land ecosystem and discover the core infrastructure, projects, smart contracts, and tooling we’re building.\"\n\t\tbody = `\n### [Gno Playground](https://play.gno.land)\n\nGno Playground is a simple web interface that lets you write, test, and experiment with your Gno code to improve your\nunderstanding of the Gno language. You can share your code, run unit tests, deploy your realms and packages, and execute\nfunctions in your code using the repo.\n\nVisit the playground at [play.gno.land](https://play.gno.land)!\n\n### [Gno Studio Connect](https://gno.studio/connect)\n\nGno Studio Connect provides seamless access to realms, making it simple to explore, interact, and engage\nwith gno.land’s smart contracts through function calls. Connect focuses on function calls, enabling users to interact\nwith any realm’s exposed function(s) on gno.land.\n\nSee your realm interactions in [Gno Studio Connect](https://gno.studio/connect)\n\n### [Gnoscan](https://gnoscan.io)\n\nDeveloped by the Onbloc team, Gnoscan is gno.land’s blockchain explorer. Anyone can use Gnoscan to easily find\ninformation that resides on the gno.land blockchain, such as wallet addresses, TX hashes, blocks, and contracts.\nGnoscan makes our on-chain data easy to read and intuitive to discover.\n\nExplore the gno.land blockchain at [gnoscan.io](https://gnoscan.io)!\n\n### Adena\n\nAdena is a user-friendly non-custodial wallet for gno.land. Open-source and developed by Onbloc, Adena allows gnomes to\ninteract easily with the chain. With an emphasis on UX, Adena is built to handle millions of realms and tokens with a\nhigh-quality interface, support for NFTs and custom tokens, and seamless integration. Install Adena via the [official website](https://www.adena.app/)\n\n### Gnoswap\n\nGnoswap is currently under development and led by the Onbloc team. Gnoswap will be the first DEX on gno.land and is an\nautomated market maker (AMM) protocol written in Gno that allows for permissionless token exchanges on the platform.\n\n### Flippando\n\nFlippando is a simple on-chain memory game, ported from Solidity to Gno, which starts with an empty matrix to flip tiles\non to see what’s underneath. If the tiles match, they remain uncovered; if not, they are briefly shown, and the player\nmust memorize their colors until the entire matrix is uncovered. The end result can be minted as an NFT, which can later\nbe assembled into bigger, more complex NFTs, creating a digital “painting” with the uncovered tiles. Play the game at [Flippando](https://gno.flippando.xyz/flip)\n\n### Gno Native Kit\n\n[Gno Native Kit](https://github.com/gnolang/gnonative) is a framework that allows developers to build and port gno.land (d)apps written in the (d)app's native language.\n\n\n`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:23Z\", nil, nil)\n}\n" + }, + { + "name": "page_gnolang.gno", + "body": "package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"gnolang\"\n\t\ttitle = \"About the Gno, the Language for gno.land\"\n\t\t// TODO fix broken images\n\t\tbody = `\n\n[Gno](https://github.com/gnolang/gno) is an interpretation of the widely-used Go (Golang) programming language for blockchain created by Cosmos co-founder Jae Kwon in 2022 to mark a new era in smart contracting. Gno is ~99% identical to Go, so Go programmers can start coding in Gno right away, with a minimal learning curve. For example, Gno comes with blockchain-specific standard libraries, but any code that doesn’t use blockchain-specific logic can run in Go with minimal processing. Libraries that don’t make sense in the blockchain context, such as network or operating-system access, are not available in Gno. Otherwise, Gno loads and uses many standard libraries that power Go, so most of the parsing of the source code is the same.\n\nUnder the hood, the Gno code is parsed into an abstract syntax tree (AST) and the AST itself is used in the interpreter, rather than bytecode as in many virtual machines such as Java, Python, or Wasm. This makes even the GnoVM accessible to any Go programmer. The novel design of the intuitive GnoVM interpreter allows Gno to freeze and resume the program by persisting and loading the entire memory state. Gno is deterministic, auto-persisted, and auto-Merkle-ized, allowing (smart contract) programs to be succinct, as the programmer doesn’t have to serialize and deserialize objects to persist them into a database (unlike programming applications with the Cosmos SDK).\n\n## How Gno Differs from Go\n\n![Gno and Go differences](static/img/gno-language/go-and-gno.jpg)\n\nThe composable nature of Go/Gno allows for type-checked interactions between contracts, making gno.land safer and more powerful, as well as operationally cheaper and faster. Smart contracts on gno.land are light, simple, more focused, and easily interoperable—a network of interconnected contracts rather than siloed monoliths that limit interactions with other contracts.\n\n![Example of Gno code](static/img/gno-language/code-example.jpg)\n\n## Gno Inherits Go’s Built-in Security Features\n\nGo supports secure programming through exported/non-exported fields, enabling a “least-authority” design. It is easy to create objects and APIs that expose only what should be accessible to callers while hiding what should not be simply by the capitalization of letters, thus allowing a succinct representation of secure logic that can be called by multiple users.\n\nAnother major advantage of Go is that the language comes with an ecosystem of great tooling, like the compiler and third-party tools that statically analyze code. Gno inherits these advantages from Go directly to create a smart contract programming language that provides embedding, composability, type-check safety, and garbage collection, helping developers to write secure code relying on the compiler, parser, and interpreter to give warning alerts for common mistakes.\n\n## Gno vs Solidity\n\nThe most widely-adopted smart contract language today is Ethereum’s EVM-compatible Solidity. With bytecode built from the ground up and Turing complete, Solidity opened up a world of possibilities for decentralized applications (dApps) and there are currently more than 10 million contracts deployed on Ethereum. However, Solidity provides limited tooling and its EVM has a stack limit and computational inefficiencies.\n\nSolidity is designed for one purpose only (writing smart contracts) and is bound by the limitations of the EVM. In addition, developers have to learn several languages if they want to understand the whole stack or work across different ecosystems. Gno aspires to exceed Solidity on multiple fronts (and other smart contract languages like CosmWasm or Substrate) as every part of the stack is written in Gno. It’s easy for developers to understand the entire system just by studying a relatively small code base.\n\n## Gno Is Essential for the Wider Adoption of Web3\n\nGno makes imports as easy as they are in web2 with runtime-based imports for seamless dependency flow comprehension, and support for complex structs, beyond primitive types. Gno is ultimately cost-effective as dependencies are loaded once, enabling remote function calls as local, and providing automatic and independent per-realm state persistence.\n\nUsing Gno, developers can rapidly accelerate application development and adopt a modular structure by reusing and reassembling existing modules without building from scratch. They can embed one structure inside another in an intuitive way while preserving localism, and the language specification is simple, successfully balancing practicality and minimalism.\n\nThe Go language is so well designed that the Gno smart contract system will become the new gold standard for smart contract development and other blockchain applications. As a programming language that is universally adopted, secure, composable, and complete, Gno is essential for the broader adoption of web3 and its sustainable growth.`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:25Z\", nil, nil)\n}\n" + }, + { + "name": "page_license.gno", + "body": "package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"license\"\n\t\ttitle = \"Gno Network General Public License\"\n\t\tbody = `Copyright (C) 2024 NewTendermint, LLC\n\nThis program is free software: you can redistribute it and/or modify it under\nthe terms of the GNO Network General Public License as published by\nNewTendermint, LLC, either version 4 of the License, or (at your option) any\nlater version published by NewTendermint, LLC.\n\nThis program is distributed in the hope that it will be useful, but is provided\nas-is and WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNO Network\nGeneral Public License for more details.\n\nYou should have received a copy of the GNO Network General Public License along\nwith this program. If not, see \u003chttps://gno.land/license\u003e.\n\nAttached below are the terms of the GNO Network General Public License, Version\n4 (a fork of the GNU Affero General Public License 3).\n\n## Additional Terms\n\n### Strong Attribution\n\nIf any of your user interfaces, such as websites and mobile applications, serve\nas the primary point of entry to a platform or blockchain that 1) offers users\nthe ability to upload their own smart contracts to the platform or blockchain,\nand 2) leverages any Covered Work (including the GNO virtual machine) to run\nthose smart contracts on the platform or blockchain (\"Applicable Work\"), then\nthe Applicable Work must prominently link to (1) gno.land or (2) any other URL\ndesignated by NewTendermint, LLC that has not been rejected by the governance of\nthe first chain known as gno.land, provided that the identity of the first chain\nis not ambiguous. In the event the identity of the first chain is ambiguous,\nthen NewTendermint, LLC's designation shall control. Such link must appear\nconspicuously in the header or footer of the Applicable Work, such that all\nusers may learn of gno.land or the URL designated by NewTendermint, LLC.\n\nThis additional attribution requirement shall remain in effect for (1) 7\nyears from the date of publication of the Applicable Work, or (2) 7 years from\nthe date of publication of the Covered Work (including republication of new\nversions), whichever is later, but no later than 12 years after the application\nof this strong attribution requirement to the publication of the Applicable\nWork. For purposes of this Strong Attribution requirement, Covered Work shall\nmean any work that is licensed under the GNO Network General Public License,\nVersion 4 or later, by NewTendermint, LLC.\n\n\n# GNO NETWORK GENERAL PUBLIC LICENSE\n\nVersion 4, 7 May 2024\n\nModified from the GNU AFFERO GENERAL PUBLIC LICENSE.\nGNU is not affiliated with GNO or NewTendermint, LLC.\nCopyright (C) 2022 NewTendermint, LLC.\n\n## Preamble\n\nThe GNO Network General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\nThe licenses for most software and other practical works are designed\nto take away your freedom to share and change the works. By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\nWhen we speak of free software, we are referring to freedom, not\nprice. Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\nDevelopers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\nA secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate. Many developers of free software are heartened and\nencouraged by the resulting cooperation. However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\nThe GNO Network General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community. It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server. Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\nThe precise terms and conditions for copying, distribution and\nmodification follow.\n\n## TERMS AND CONDITIONS\n\n### 0. Definitions.\n\n\"This License\" refers to version 4 of the GNO Network General Public License.\n\n\"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n\"The Program\" refers to any copyrightable work licensed under this\nLicense. Each licensee is addressed as \"you\". \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\nTo \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy. The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\nA \"covered work\" means either the unmodified Program or a work based\non the Program.\n\nTo \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy. Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\nTo \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies. Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\nAn interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License. If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n### 1. Source Code.\n\nThe \"source code\" for a work means the preferred form of the work\nfor making modifications to it. \"Object code\" means any non-source\nform of a work.\n\nA \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\nThe \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form. A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\nThe \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities. However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work. For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\nThe Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\nThe Corresponding Source for a work in source code form is that\nsame work.\n\n### 2. Basic Permissions.\n\nAll rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met. This License explicitly affirms your unlimited\npermission to run the unmodified Program. The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work. This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\nYou may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force. You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright. Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\nConveying under any other circumstances is permitted solely under\nthe conditions stated below. Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\nNo covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\nWhen you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n### 4. Conveying Verbatim Copies.\n\nYou may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\nYou may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n### 5. Conveying Modified Source Versions.\n\nYou may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n- a) The work must carry prominent notices stating that you modified\n it, and giving a relevant date.\n- b) The work must carry prominent notices stating that it is\n released under this License and any conditions added under section\n 7. This requirement modifies the requirement in section 4 to\n \"keep intact all notices\".\n- c) You must license the entire work, as a whole, under this\n License to anyone who comes into possession of a copy. This\n License will therefore apply, along with any applicable section 7\n additional terms, to the whole of the work, and all its parts,\n regardless of how they are packaged. This License gives no\n permission to license the work in any other way, but it does not\n invalidate such permission if you have separately received it.\n- d) If the work has interactive user interfaces, each must display\n Appropriate Legal Notices; however, if the Program has interactive\n interfaces that do not display Appropriate Legal Notices, your\n work need not make them do so.\n\nA compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit. Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n### 6. Conveying Non-Source Forms.\n\n You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n- a) Convey the object code in, or embodied in, a physical product\n (including a physical distribution medium), accompanied by the\n Corresponding Source fixed on a durable physical medium\n customarily used for software interchange.\n- b) Convey the object code in, or embodied in, a physical product\n (including a physical distribution medium), accompanied by a\n written offer, valid for at least three years and valid for as\n long as you offer spare parts or customer support for that product\n model, to give anyone who possesses the object code either (1) a\n copy of the Corresponding Source for all the software in the\n product that is covered by this License, on a durable physical\n medium customarily used for software interchange, for a price no\n more than your reasonable cost of physically performing this\n conveying of source, or (2) access to copy the\n Corresponding Source from a network server at no charge.\n- c) Convey individual copies of the object code with a copy of the\n written offer to provide the Corresponding Source. This\n alternative is allowed only occasionally and noncommercially, and\n only if you received the object code with such an offer, in accord\n with subsection 6b.\n- d) Convey the object code by offering access from a designated\n place (gratis or for a charge), and offer equivalent access to the\n Corresponding Source in the same way through the same place at no\n further charge. You need not require recipients to copy the\n Corresponding Source along with the object code. If the place to\n copy the object code is a network server, the Corresponding Source\n may be on a different server (operated by you or a third party)\n that supports equivalent copying facilities, provided you maintain\n clear directions next to the object code saying where to find the\n Corresponding Source. Regardless of what server hosts the\n Corresponding Source, you remain obligated to ensure that it is\n available for as long as needed to satisfy these requirements.\n- e) Convey the object code using peer-to-peer transmission, provided\n you inform other peers where the object code and Corresponding\n Source of the work are being offered to the general public at no\n charge under subsection 6d.\n\nA separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\nA \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling. In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage. For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product. A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n\"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source. The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\nIf you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information. But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\nThe requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed. Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\nCorresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n### 7. Additional Terms.\n\n\"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law. If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\nWhen you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit. (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.) You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\nNotwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n- a) Disclaiming warranty or limiting liability differently from the\n terms of sections 15 and 16 of this License; or\n- b) Requiring preservation of specified reasonable legal notices or\n author attributions in that material or in the Appropriate Legal\n Notices displayed by works containing it; or\n- c) Prohibiting misrepresentation of the origin of that material, or\n requiring that modified versions of such material be marked in\n reasonable ways as different from the original version; or\n- d) Limiting the use for publicity purposes of names of licensors or\n authors of the material; or\n- e) Declining to grant rights under trademark law for use of some\n trade names, trademarks, or service marks; or\n- f) Requiring indemnification of licensors and authors of that\n material by anyone who conveys the material (or modified versions of\n it) with contractual assumptions of liability to the recipient, for\n any liability that these contractual assumptions directly impose on\n those licensors and authors; or\n- g) Requiring strong attribution such as notices on any user interfaces\n that run or convey any covered work, such as a prominent link to a URL\n on the header of a website, such that all users of the covered work may\n become aware of the notice, for a period no longer than 20 years.\n\nAll other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10. If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term. If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\nIf you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\nAdditional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n### 8. Termination.\n\nYou may not propagate or modify a covered work except as expressly\nprovided under this License. Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\nHowever, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\nMoreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\nTermination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License. If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n### 9. Acceptance Not Required for Having Copies.\n\nYou are not required to accept this License in order to receive or\nrun a copy of the Program. Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance. However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work. These actions infringe copyright if you do\nnot accept this License. Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n### 10. Automatic Licensing of Downstream Recipients.\n\nEach time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License. You are not responsible\nfor enforcing compliance by third parties with this License.\n\nAn \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations. If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\nYou may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License. For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n### 11. Patents.\n\nA \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based. The\nwork thus licensed is called the contributor's \"contributor version\".\n\nA contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version. For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\nEach contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\nIn the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement). To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\nIf you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients. \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\nIf, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\nA patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License. You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\nNothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n### 12. No Surrender of Others' Freedom.\n\nIf conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License. If you cannot convey a\ncovered work so as to simultaneously satisfy your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all. For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n### 13. Remote Network Interaction; Use with the GNU General Public License.\n\nNotwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software. This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\nNotwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work. The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n### 14. Revised Versions of this License.\n\nNewTendermint LLC may publish revised and/or new versions of\nthe GNO Network General Public License from time to time. Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number. If the\nProgram specifies that a certain numbered version of the GNO Network General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Gno Software\nFoundation. If the Program does not specify a version number of the\nGNO Network General Public License, you may choose any version ever published\nby NewTendermint LLC.\n\nIf the Program specifies that a proxy can decide which future\nversions of the GNO Network General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\nLater license versions may give you additional or different\npermissions. However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n### 15. Disclaimer of Warranty.\n\nTHERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n### 16. Limitation of Liability.\n\nIN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n### 17. Interpretation of Sections 15 and 16.\n\nIf the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\nEND OF TERMS AND CONDITIONS\n\n## How to Apply These Terms to Your New Programs\n\nIf you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\nTo do so, attach the following notices to the program. It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n \u003cone line to give the program's name and a brief idea of what it does.\u003e\n Copyright (C) \u003cyear\u003e \u003cname of author\u003e\n\n This program is free software: you can redistribute it and/or modify\n it under the terms of the GNO Network General Public License as published by\n NewTendermint LLC, either version 4 of the License, or\n (at your option) any later version.\n\n This program is distributed in the hope that it will be useful,\n but WITHOUT ANY WARRANTY; without even the implied warranty of\n MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n GNO Network General Public License for more details.\n\n You should have received a copy of the GNO Network General Public License\n along with this program. If not, see \u003chttps://gno.land/license\u003e.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source. For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code. There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2024-04-22T00:00:00Z\", nil, nil)\n}\n" + }, + { + "name": "page_partners.gno", + "body": "package gnopages\n\nfunc init() {\n\tpath := \"partners\"\n\ttitle := \"Partnerships\"\n\tbody := `### Fund and Grants Program\n\nAre you a builder, tinkerer, or researcher? If you’re looking to create awesome dApps, tooling, infrastructure, \nor smart contract libraries on gno.land, you can apply for a grant. The gno.land Ecosystem Fund and Grants program \nprovides financial contributions for individuals and teams to innovate on the platform.\n\nRead more about our Funds and Grants program [here](https://github.com/gnolang/ecosystem-fund-grants).\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:27Z\", nil, nil)\n}\n" + }, + { + "name": "page_start.gno", + "body": "package gnopages\n\nfunc init() {\n\tpath := \"start\"\n\ttitle := \"Getting Started with Gno\"\n\t// XXX: description := \"\"\n\n\t// TODO: codegen to use README files here\n\n\t/* TODO: port previous message: This is a demo of Gno smart contract programming. This document was\n\tconstructed by Gno onto a smart contract hosted on the data Realm\n\tname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n\t([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\t*/\n\tbody := `## Getting Started with Gno\n\n- [Install Gno Key](/r/demo/boards:testboard/5)\n- TODO: add more links\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:28Z\", nil, nil)\n}\n" + }, + { + "name": "page_testnets.gno", + "body": "package gnopages\n\nfunc init() {\n\tpath := \"testnets\"\n\ttitle := \"gno.land Testnet List\"\n\tbody := `\n- [Portal Loop](https://docs.gno.land/concepts/portal-loop) - a rolling testnet\n- [staging.gno.land](https://staging.gno.land) - wiped every commit to monorepo master\n- _[test4.gno.land](https://test4.gno.land) (latest)_\n\nFor a list of RPC endpoints, see the [reference documentation](https://docs.gno.land/reference/rpc-endpoints).\n\n## Local development\n\nSee the \"Getting started\" section in the [official documentation](https://docs.gno.land/getting-started/local-setup).\n`\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:29Z\", nil, nil)\n}\n" + }, + { + "name": "page_tokenomics.gno", + "body": "package gnopages\n\nfunc init() {\n\tvar (\n\t\tpath = \"tokenomics\"\n\t\ttitle = \"gno.land Tokenomics\"\n\t\t// XXX: description = \"\"\"\n\t\tbody = `Lorem Ipsum`\n\t)\n\t_ = b.NewPost(\"\", path, title, body, \"2022-05-20T13:17:30Z\", nil, nil)\n}\n" + }, + { + "name": "pages.gno", + "body": "package gnopages\n\nimport (\n\t\"gno.land/p/demo/blog\"\n)\n\n// TODO: switch from p/blog to p/pages\n\nvar b = \u0026blog.Blog{\n\tTitle: \"Gnoland's Pages\",\n\tPrefix: \"/r/gnoland/pages:\",\n\tNoBreadcrumb: true,\n}\n\nfunc Render(path string) string {\n\treturn b.Render(path)\n}\n" + }, + { + "name": "pages_test.gno", + "body": "package gnopages\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestHome(t *testing.T) {\n\tprintedOnce := false\n\tgot := Render(\"\")\n\texpectedSubtrings := []string{\n\t\t\"/r/gnoland/pages:p/tokenomics\",\n\t\t\"/r/gnoland/pages:p/start\",\n\t\t\"/r/gnoland/pages:p/contribute\",\n\t\t\"/r/gnoland/pages:p/about\",\n\t\t\"/r/gnoland/pages:p/gnolang\",\n\t}\n\tfor _, substring := range expectedSubtrings {\n\t\tif !strings.Contains(got, substring) {\n\t\t\tif !printedOnce {\n\t\t\t\tprintln(got)\n\t\t\t\tprintedOnce = true\n\t\t\t}\n\t\t\tt.Errorf(\"expected %q, but not found.\", substring)\n\t\t}\n\t}\n}\n\nfunc TestAbout(t *testing.T) {\n\tprintedOnce := false\n\tgot := Render(\"p/about\")\n\texpectedSubtrings := []string{\n\t\t\"gno.land Is A Platform To Write Smart Contracts In Gno\",\n\t\t\"gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\\nprogramming language.\",\n\t}\n\tfor _, substring := range expectedSubtrings {\n\t\tif !strings.Contains(got, substring) {\n\t\t\tif !printedOnce {\n\t\t\t\tprintln(got)\n\t\t\t\tprintedOnce = true\n\t\t\t}\n\t\t\tt.Errorf(\"expected %q, but not found.\", substring)\n\t\t}\n\t}\n}\n" + }, + { + "name": "util.gno", + "body": "package gnopages\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "KsMbsb/HBUqQo3wGCJxc9neZdofRz2r1gFfTZIGoRFNs8CSNxhhyTxM26JPCqN6CS8/yvOwvhgPeQEnepWrBBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "users", + "path": "gno.land/r/gnoland/users", + "files": [ + { + "name": "users.gno", + "body": "package users\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/releases\"\n\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\nvar (\n\tcd = std.ChainDomain()\n\tchangelog = releases.NewChangelog(\"r/gnoland/users\")\n)\n\nconst usersPrefix = \"gno.land/r/gnoland/users/\"\n\nfunc init() {\n\tchangelog.NewRelease(\"v1\", \"/r/gnoland/users/v1\", \"[Original PR](https://github.com/gnolang/gno/pull/3166)\")\n}\n\nfunc Render(_ string) string {\n\treturn changelog.RenderAsTable(10)\n}\n\nfunc LatestRelease() string {\n\treturn cd + changelog.Latest().URL()\n}\n\n// ProposeNewRelease allows a GovDAO proposal to add a release to the changelog\nfunc ProposeNewRelease(newVerPkgPath, note string) dao.Executor {\n\tver := strings.TrimPrefix(newVerPkgPath, usersPrefix)\n\tif ver == newVerPkgPath || // TrimPrefix returns unchanged newVerPkgPath if !HasPrefix\n\t\tstrings.Contains(ver, \"/\") { // if has prefix, has to be first child under\n\t\tpanic(\"r/gnoland/users: invalid version pkgpath\")\n\t}\n\n\tcb := func() error {\n\t\tchangelog.NewRelease(ver, strings.TrimPrefix(newVerPkgPath, \"gno.land\"), note)\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(cb)\n}\n" + }, + { + "name": "z_0_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/p/demo/dao\"\n\n\t\"gno.land/r/gnoland/users\"\n\t_ \"gno.land/r/gov/dao/init\" // so that the govdao.GovDAO initializer is executed\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\nfunc init() {\n\tex := users.ProposeNewRelease(\"gno.land/r/gnoland/users/v2\", \"This is a note!\")\n\n\t// Create a proposal\n\tprop := dao.ProposalRequest{\n\t\tTitle: \"Propose users registry v2\",\n\t\tDescription: \"\",\n\t\tExecutor: ex,\n\t}\n\n\tgovdao.GovDAO.Propose(prop)\n}\n\nfunc main() {\n\tgovdao.GovDAO.VoteOnProposal(0, \"YES\")\n\tgovdao.GovDAO.ExecuteProposal(0)\n\tprintln(users.Render(\"\"))\n}\n\n// Output:\n// # r/gnoland/users\n// See the r/gnoland/users changelog below.\n//\n// | Version | Link | Notes |\n// | --- | --- | --- |\n// | v2 | [r/gnoland/users v2 (latest)](/r/gnoland/users/v2) | This is a note! |\n// | v1 | [r/gnoland/users v1](/r/gnoland/users/v1) | [Original PR](https://github.com/gnolang/gno/pull/3166) |\n//\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "/Ku/QrzDi+iM0jEhSACsSCefZuLpifDitxeWLlcrHlBvRu0pllKANmKwf1bF8eSnII9ycNkDBk3izYb74nKZBA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "users", + "path": "gno.land/r/gnoland/users/v1", + "files": [ + { + "name": "admin.gno", + "body": "package users\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\n\t\"gno.land/r/gov/dao/bridge\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\nvar paused = false // XXX: replace with p/moul/authz\n\n// ProposeNewPausedValue allows GovDAO to pause or unpause this realm\nfunc ProposeNewPausedValue(newPausedValue bool) dao.Executor {\n\tcb := func() error {\n\t\tpaused = newPausedValue\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(cb)\n}\n\n// ProposeNewName allows GovDAO to propose a new name for an existing user\n// The associated address and all previous names of a user that changes a name\n// are preserved, and all resolve to the new name.\nfunc ProposeNewName(addr std.Address, newName string) dao.Executor {\n\tif matched := reUsername.MatchString(newName); !matched {\n\t\tpanic(ErrInvalidUsername)\n\t}\n\n\tuserData := susers.ResolveAddress(addr)\n\tif userData == nil {\n\t\tpanic(susers.ErrUserNotExistOrDeleted)\n\t}\n\n\tcb := func() error {\n\t\tif err := userData.UpdateName(newName); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(cb)\n}\n\n// ProposeDeleteUser allows GovDAO to propose deletion of a user\n// This will make the associated address and names unresolvable.\n// WARN: After deletion, the same address WILL NOT be able to register a new name.\nfunc ProposeDeleteUser(addr std.Address) dao.Executor {\n\tuserData := susers.ResolveAddress(addr)\n\tif userData == nil {\n\t\tpanic(susers.ErrUserNotExistOrDeleted)\n\t}\n\n\tcb := func() error {\n\t\tif err := userData.Delete(); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(cb)\n}\n\n// ProposeNewRegisterPrice allows GovDAO to update the price of registration\nfunc ProposeNewRegisterPrice(newPrice int64) dao.Executor {\n\tif newPrice \u003c 0 {\n\t\tpanic(\"invalid price\")\n\t}\n\n\tcb := func() error {\n\t\tregisterPrice = newPrice\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(cb)\n}\n" + }, + { + "name": "errors.gno", + "body": "package users\n\nimport (\n\t\"errors\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\tErrNonUserCall = errors.New(\"r/gnoland/users: non-user call\")\n\tErrPaused = errors.New(\"r/gnoland/users: paused\")\n\tErrInvalidPayment = ufmt.Errorf(\"r/gnoland/users: you need to send exactly %d ugnot\", registerPrice)\n\tErrInvalidUsername = errors.New(\"r/gnoland/users: invalid username\")\n)\n" + }, + { + "name": "preregister.gno", + "body": "package users\n\nimport (\n\t\"std\"\n\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// pre-registered users\nvar preRegisteredUsers = []struct {\n\tName string\n\tAddress std.Address\n}{\n\t// system name\n\t{\"archives\", \"g1xlnyjrnf03ju82v0f98ruhpgnquk28knmjfe5k\"}, // -\u003e @r_archives\n\t{\"demo\", \"g13ek2zz9qurzynzvssyc4sthwppnruhnp0gdz8n\"}, // -\u003e @r_demo\n\t{\"gno\", \"g19602kd9tfxrfd60sgreadt9zvdyyuudcyxsz8a\"}, // -\u003e @r_gno\n\t{\"gnoland\", \"g1g3lsfxhvaqgdv4ccemwpnms4fv6t3aq3p5z6u7\"}, // -\u003e @r_gnoland\n\t{\"gnolang\", \"g1yjlnm3z2630gg5mryjd79907e0zx658wxs9hnd\"}, // -\u003e @r_gnolang\n\t{\"gov\", \"g1g73v2anukg4ej7axwqpthsatzrxjsh0wk797da\"}, // -\u003e @r_gov\n\t{\"nt\", \"g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l\"}, // -\u003e @r_nt\n\t{\"sys\", \"g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l\"}, // -\u003e @r_sys\n\t{\"x\", \"g164sdpew3c2t3rvxj3kmfv7c7ujlvcw2punzzuz\"}, // -\u003e @r_x\n\n\t// test1 user\n\t{\"test1\", \"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"}, // -\u003e @test1\n}\n\nfunc init() {\n\t// add pre-registered users\n\tfor _, res := range preRegisteredUsers {\n\t\t// Try registering, skip if it fails\n\t\t_ = susers.RegisterUser(res.Name, res.Address)\n\t}\n}\n" + }, + { + "name": "render.gno", + "body": "package users\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/realmpath\"\n\t\"gno.land/p/moul/txlink\"\n\n\t\"gno.land/r/demo/profile\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\nfunc Render(path string) string {\n\treq := realmpath.Parse(path)\n\n\tif req.Path == \"\" {\n\t\treturn renderHomePage()\n\t}\n\n\t// Otherwise, render the user page\n\treturn renderUserPage(req.Path)\n}\n\nfunc renderHomePage() string {\n\tvar out string\n\n\tout += \"# gno.land user registry\\n\"\n\n\tif paused {\n\t\tout += md.HorizontalRule()\n\t\tout += md.H2(\"This realm is paused.\")\n\t\tout += md.Paragraph(\"Check out [`gno.land/r/gnoland/users`](/r/gnoland/users) for newer versions of the registry.\")\n\t\tout += md.HorizontalRule()\n\t}\n\n\tout += renderIntroParagraph()\n\n\tout += md.H2(\"Latest registrations\")\n\tentries := latestUsers.Entries()\n\tif len(entries) == 0 {\n\t\tout += \"No registered users.\"\n\t}\n\n\tfor i := len(entries) - 1; i \u003e= 0; i-- {\n\t\tuser := entries[i].(string)\n\t\tout += ufmt.Sprintf(\"- User [%s](/r/gnoland/users/v1:%s)\\n\", md.Bold(user), user)\n\t}\n\n\treturn out\n}\n\nfunc renderIntroParagraph() string {\n\tout := md.Paragraph(\"Welcome to the gno.land user registry (v1). Please register a username.\")\n\tout += md.Paragraph(`Registering a username grants the registering address the right to deploy packages and realms\nunder that username’s namespace. For example, if an address registers the username ` + md.InlineCode(\"gnome123\") + `, it \nwill gain permission to deploy packages and realms to package paths with the pattern ` + md.InlineCode(\"gno.land/{p,r}/gnome123/*\") + `.`)\n\n\tout += md.Paragraph(\"In V1, usernames must follow these rules, in order to prevent username squatting:\")\n\titems := []string{\n\t\t\"Must start with 3 characters\",\n\t\t\"Must end with 3 numbers\",\n\t\t\"Have a maximum length of 20 characters\",\n\t\t\"With the only special character allowed being `_`\",\n\t}\n\tout += md.BulletList(items)\n\n\tout += \"\\n\\n\"\n\tout += md.Paragraph(\"In later versions of the registry, vanity usernames will be allowed through specific mechanisms.\")\n\n\tif !paused {\n\t\tout += md.H3(ufmt.Sprintf(\" [[Click here to register]](%s)\", txlink.Call(\"Register\")))\n\t\tout += ufmt.Sprintf(\"Registration price: %f GNOT (%dugnot)\\n\\n\", float64(registerPrice)/1_000_000, registerPrice)\n\t}\n\n\tout += md.HorizontalRule()\n\tout += \"\\n\\n\"\n\n\treturn out\n}\n\n// resolveUser resolves the user based on the path, determining if it's a name or address\nfunc resolveUser(path string) (*susers.UserData, bool, bool) {\n\tif std.Address(path).IsValid() {\n\t\treturn susers.ResolveAddress(std.Address(path)), false, false\n\t}\n\n\tdata, isLatest := susers.ResolveName(path)\n\treturn data, isLatest, true\n}\n\n// renderUserPage generates the user page based on user data and path\nfunc renderUserPage(path string) string {\n\tvar out string\n\n\t// Render single user page\n\tdata, isLatest, isName := resolveUser(path)\n\tif data == nil {\n\t\tout += md.H1(\"User not found.\")\n\t\tout += \"This user does not exist or has been deleted.\\n\"\n\t\treturn out\n\t}\n\n\tout += md.H1(\"User - \" + md.InlineCode(data.Name()))\n\n\tif isName \u0026\u0026 !isLatest {\n\t\tout += md.Paragraph(ufmt.Sprintf(\n\t\t\t\"Note: You searched for `%s`, which is a previous name of [`%s`](/r/gnoland/users/v1:%s).\",\n\t\t\tpath, data.Name(), data.Name()))\n\t} else {\n\t\tout += ufmt.Sprintf(\"Address: %s\\n\\n\", data.Addr().String())\n\n\t\tout += md.H2(\"Bio\")\n\t\tout += profile.GetStringField(data.Addr(), \"Bio\", \"No bio defined.\")\n\t\tout += \"\\n\\n\"\n\t\tout += ufmt.Sprintf(\"[Update bio](%s)\", txlink.Realm(\"gno.land/r/demo/profile\").Call(\"SetStringField\", \"field\", \"Bio\"))\n\t\tout += \"\\n\\n\"\n\t}\n\n\treturn out\n}\n" + }, + { + "name": "users.gno", + "body": "package users\n\nimport (\n\t\"regexp\"\n\t\"std\"\n\n\t\"gno.land/p/moul/fifo\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\nconst (\n\treValidUsername = \"^[a-z]{3}[_a-z0-9]{0,14}[0-9]{3}$\"\n)\n\nvar (\n\tregisterPrice = int64(1_000_000) // 1 GNOT\n\tlatestUsers = fifo.New(10) // Save the latest 10 users for rendering purposes\n\treUsername = regexp.MustCompile(reValidUsername)\n)\n\n// Register registers a new username for the caller.\n// A valid username must start with a minimum of 3 letters,\n// end with a minimum of 3 numbers, and be less than 20 chars long.\n// All letters must be lowercase, and the only valid special char is `_`.\n// Only calls from EOAs are supported.\nfunc Register(username string) {\n\tif !std.PreviousRealm().IsUser() {\n\t\tpanic(ErrNonUserCall)\n\t}\n\n\tif paused {\n\t\tpanic(ErrPaused)\n\t}\n\n\tif std.OriginSend().AmountOf(\"ugnot\") != registerPrice {\n\t\tpanic(ErrInvalidPayment)\n\t}\n\n\tif matched := reUsername.MatchString(username); !matched {\n\t\tpanic(ErrInvalidUsername)\n\t}\n\n\tregistrant := std.PreviousRealm().Address()\n\tif err := susers.RegisterUser(username, registrant); err != nil {\n\t\tpanic(err)\n\t}\n\n\tlatestUsers.Append(username)\n\tstd.Emit(\"Registeration\", \"address\", registrant.String(), \"name\", username)\n}\n" + }, + { + "name": "users_test.gno", + "body": "package users\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n\n\tsusers \"gno.land/r/sys/users\"\n)\n\nvar (\n\talice = \"alice123\"\n\tbob = \"bob123\"\n\taliceAddr = testutils.TestAddress(alice)\n\tbobAddr = testutils.TestAddress(bob)\n)\n\nfunc TestRegister_Valid(t *testing.T) {\n\ttesting.SetOriginSend(std.NewCoins(std.NewCoin(\"ugnot\", 1_000_000)))\n\ttesting.SetRealm(std.NewUserRealm(aliceAddr))\n\ttesting.SetOriginCaller(aliceAddr)\n\n\tuassert.NotPanics(t, func() {\n\t\tRegister(alice)\n\t})\n\n\tres, latest := susers.ResolveName(alice)\n\n\tuassert.NotEqual(t, nil, res)\n\tuassert.Equal(t, alice, res.Name())\n\tuassert.Equal(t, aliceAddr, res.Addr())\n\tuassert.False(t, res.IsDeleted())\n\tuassert.True(t, latest)\n}\n\nfunc TestRegister_Invalid(t *testing.T) {\n\ttesting.SetOriginSend(std.NewCoins(std.NewCoin(\"ugnot\", 1_000_000)))\n\ttesting.SetRealm(std.NewUserRealm(bobAddr))\n\ttesting.SetOriginCaller(bobAddr)\n\n\t// Invalid usernames\n\tuassert.PanicsWithMessage(t, ErrInvalidUsername.Error(), func() {\n\t\tRegister(\"alice\") // vanity\n\t})\n\n\tuassert.PanicsWithMessage(t, ErrInvalidUsername.Error(), func() {\n\t\tRegister(\"\") // empty\n\t})\n\n\tuassert.PanicsWithMessage(t, ErrInvalidUsername.Error(), func() {\n\t\tRegister(\" \") // empty\n\t})\n\n\tuassert.PanicsWithMessage(t, ErrInvalidUsername.Error(), func() {\n\t\tRegister(\"123\") // empty\n\t})\n\n\tuassert.PanicsWithMessage(t, ErrInvalidUsername.Error(), func() {\n\t\tRegister(\"123\") // only numbers\n\t})\n\n\tuassert.PanicsWithMessage(t, ErrInvalidUsername.Error(), func() {\n\t\tRegister(\"alice\u0026#($)\") // non-allowed chars\n\t})\n\n\tuassert.PanicsWithMessage(t, ErrInvalidUsername.Error(), func() {\n\t\tRegister(\"Alice123\") // upper-case\n\t})\n\n\tuassert.PanicsWithMessage(t, ErrInvalidUsername.Error(), func() {\n\t\tRegister(\"toolongusernametoolongusernametoolongusername123\") // too long\n\t})\n\n\t// Name taken\n\turequire.NotPanics(t, func() {\n\t\tRegister(bob)\n\t})\n\n\tuassert.PanicsWithMessage(t, susers.ErrNameTaken.Error(), func() {\n\t\tRegister(bob) // already registered\n\t})\n}\n\nfunc TestRegister_InvalidPayment(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(bobAddr))\n\ttesting.SetOriginCaller(bobAddr)\n\n\ttesting.SetOriginSend(std.NewCoins(std.NewCoin(\"ugnot\", 12))) // invalid payment amount\n\n\tuassert.PanicsWithMessage(t, ErrInvalidPayment.Error(), func() {\n\t\tRegister(alice)\n\t})\n}\n" + }, + { + "name": "z_0_prop1_filetest.gno", + "body": "package main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/testutils\"\n\n\tusers \"gno.land/r/gnoland/users/v1\"\n\t_ \"gno.land/r/gov/dao/init\" // so that the govdao.GovDAO initializer is executed\n\tgovdao \"gno.land/r/gov/dao/v2\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// Test updating a name via GovDAO\n\nfunc init() {\n\tc := std.OriginCaller()\n\talice := testutils.TestAddress(\"alice\")\n\n\t// Register alice\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\tusers.Register(\"alice123\")\n\n\t// Prop to change name\n\ttesting.SetOriginCaller(c)\n\ttesting.SetRealm(std.NewUserRealm(c))\n\tex := users.ProposeNewName(alice, \"alice_new123\")\n\n\t// Create a proposal\n\tprop := dao.ProposalRequest{\n\t\tTitle: \"Change alice's name!\",\n\t\tDescription: \"\",\n\t\tExecutor: ex,\n\t}\n\n\tgovdao.GovDAO.Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.GovDAO.VoteOnProposal(0, \"YES\")\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.GovDAO.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\n\tdata, _ := susers.ResolveName(\"alice_new123\")\n\tprintln(data.Addr())\n}\n\n// Output:\n// --\n// # GovDAO Proposals\n//\n// ## [Prop #0 - Change alice's name!](/r/gov/dao/v2:0)\n//\n// **Status: ACTIVE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n//\n// --\n// # Proposal #0 - Change alice's name!\n//\n// ## Description\n//\n// No description provided.\n//\n// ## Proposal information\n//\n// **Status: ACTIVE**\n//\n// **Voting stats:**\n// - YES 0 (0%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 10 (100%)\n//\n//\n// **Threshold met: FALSE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// #### [[Vote YES](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=YES)] - [[Vote NO](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=ABSTAIN)]\n//\n//\n// --\n// --\n// # Proposal #0 - Change alice's name!\n//\n// ## Description\n//\n// No description provided.\n//\n// ## Proposal information\n//\n// **Status: ACCEPTED**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// --\n// --\n// # Proposal #0 - Change alice's name!\n//\n// ## Description\n//\n// No description provided.\n//\n// ## Proposal information\n//\n// **Status: EXECUTION SUCCESSFUL**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh\n\n// Events:\n// [\n// {\n// \"type\": \"Registered\",\n// \"attrs\": [\n// {\n// \"key\": \"name\",\n// \"value\": \"alice123\"\n// },\n// {\n// \"key\": \"address\",\n// \"value\": \"g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/sys/users\",\n// \"func\": \"RegisterUser\"\n// },\n// {\n// \"type\": \"Registeration\",\n// \"attrs\": [\n// {\n// \"key\": \"address\",\n// \"value\": \"g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh\"\n// },\n// {\n// \"key\": \"name\",\n// \"value\": \"alice123\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gnoland/users/v1\",\n// \"func\": \"Register\"\n// },\n// {\n// \"type\": \"ProposalAdded\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"proposal-author\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitProposalAdded\"\n// },\n// {\n// \"type\": \"VoteAdded\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"author\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// },\n// {\n// \"key\": \"option\",\n// \"value\": \"YES\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitVoteAdded\"\n// },\n// {\n// \"type\": \"ProposalAccepted\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitProposalAccepted\"\n// },\n// {\n// \"type\": \"Updated\",\n// \"attrs\": [\n// {\n// \"key\": \"alias\",\n// \"value\": \"alice_new123\"\n// },\n// {\n// \"key\": \"address\",\n// \"value\": \"g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/sys/users\",\n// \"func\": \"UpdateName\"\n// },\n// {\n// \"type\": \"ProposalExecuted\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"exec-status\",\n// \"value\": \"accepted\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"ExecuteProposal\"\n// }\n// ]\n" + }, + { + "name": "z_1_prop2_filetest.gno", + "body": "package main\n\n// SEND: 1000000ugnot\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/testutils\"\n\n\tusers \"gno.land/r/gnoland/users/v1\"\n\t_ \"gno.land/r/gov/dao/init\" // so that the govdao.GovDAO initializer is executed\n\tgovdao \"gno.land/r/gov/dao/v2\"\n\tsusers \"gno.land/r/sys/users\"\n)\n\n// Test updating a name via GovDAO\n\nfunc init() {\n\tc := std.OriginCaller()\n\talice := testutils.TestAddress(\"alice\")\n\n\t// Register alice\n\ttesting.SetOriginCaller(alice)\n\ttesting.SetRealm(std.NewUserRealm(alice))\n\tusers.Register(\"alice123\")\n\n\t// Prop to change name\n\ttesting.SetOriginCaller(c)\n\ttesting.SetRealm(std.NewUserRealm(c))\n\tex := users.ProposeDeleteUser(alice)\n\n\t// Create a proposal\n\tprop := dao.ProposalRequest{\n\t\tTitle: \"Change alice's name!\",\n\t\tDescription: \"\",\n\t\tExecutor: ex,\n\t}\n\n\tgovdao.GovDAO.Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.GovDAO.VoteOnProposal(0, \"YES\")\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.GovDAO.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\n\tdata, _ := susers.ResolveName(\"alice123\")\n\tif data == nil {\n\t\tprintln(\"Successfully deleted alice\")\n\t}\n}\n\n// Output:\n// --\n// # GovDAO Proposals\n//\n// ## [Prop #0 - Change alice's name!](/r/gov/dao/v2:0)\n//\n// **Status: ACTIVE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n//\n// --\n// # Proposal #0 - Change alice's name!\n//\n// ## Description\n//\n// No description provided.\n//\n// ## Proposal information\n//\n// **Status: ACTIVE**\n//\n// **Voting stats:**\n// - YES 0 (0%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 10 (100%)\n//\n//\n// **Threshold met: FALSE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// #### [[Vote YES](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=YES)] - [[Vote NO](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=ABSTAIN)]\n//\n//\n// --\n// --\n// # Proposal #0 - Change alice's name!\n//\n// ## Description\n//\n// No description provided.\n//\n// ## Proposal information\n//\n// **Status: ACCEPTED**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// --\n// --\n// # Proposal #0 - Change alice's name!\n//\n// ## Description\n//\n// No description provided.\n//\n// ## Proposal information\n//\n// **Status: EXECUTION SUCCESSFUL**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// Successfully deleted alice\n\n// Events:\n// [\n// {\n// \"type\": \"Registered\",\n// \"attrs\": [\n// {\n// \"key\": \"name\",\n// \"value\": \"alice123\"\n// },\n// {\n// \"key\": \"address\",\n// \"value\": \"g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/sys/users\",\n// \"func\": \"RegisterUser\"\n// },\n// {\n// \"type\": \"Registeration\",\n// \"attrs\": [\n// {\n// \"key\": \"address\",\n// \"value\": \"g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh\"\n// },\n// {\n// \"key\": \"name\",\n// \"value\": \"alice123\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gnoland/users/v1\",\n// \"func\": \"Register\"\n// },\n// {\n// \"type\": \"ProposalAdded\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"proposal-author\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitProposalAdded\"\n// },\n// {\n// \"type\": \"VoteAdded\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"author\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// },\n// {\n// \"key\": \"option\",\n// \"value\": \"YES\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitVoteAdded\"\n// },\n// {\n// \"type\": \"ProposalAccepted\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitProposalAccepted\"\n// },\n// {\n// \"type\": \"Deleted\",\n// \"attrs\": [\n// {\n// \"key\": \"address\",\n// \"value\": \"g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/sys/users\",\n// \"func\": \"Delete\"\n// },\n// {\n// \"type\": \"ProposalExecuted\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"exec-status\",\n// \"value\": \"accepted\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"ExecuteProposal\"\n// }\n// ]\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Ms2LDd/WdQRRBXjW1dA2K4zV9nYGo4/0J2xT1Yjk4PvJxFpaOChrYZr4Poa39Zm7FyoyRbpkXt8CFC3wqFFvDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "valopers", + "path": "gno.land/r/gnoland/valopers", + "files": [ + { + "name": "admin.gno", + "body": "package valopers\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/moul/authz\"\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\nvar auth *authz.Authorizer\n\nfunc Auth() *authz.Authorizer {\n\treturn auth\n}\n\nfunc updateInstructions(newInstructions string) {\n\terr := auth.Do(\"update-instructions\", func() error {\n\t\tinstructions = newInstructions\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc updateMinFee(newMinFee int64) {\n\terr := auth.Do(\"update-min-fee\", func() error {\n\t\tminFee = std.NewCoin(\"ugnot\", newMinFee)\n\t\treturn nil\n\t})\n\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc NewInstructionsExecutor(newInstructions string) dao.Executor {\n\tcb := func() error {\n\t\tupdateInstructions(newInstructions)\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(cb)\n}\n\nfunc NewMinFeeExecutor(newMinFee int64) dao.Executor {\n\tcb := func() error {\n\t\tupdateMinFee(newMinFee)\n\t\treturn nil\n\t}\n\n\treturn bridge.GovDAO().NewGovDAOExecutor(cb)\n}\n" + }, + { + "name": "admin_test.gno", + "body": "package valopers\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/moul/authz\"\n)\n\nfunc TestUpdateInstructions(t *testing.T) {\n\tauth = authz.NewWithAuthority(\n\t\tauthz.NewContractAuthority(\n\t\t\t\"gno.land/r/gov/dao/v2\",\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(std.DerivePkgAddr(\"gno.land/r/gov/dao/v2\"))\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/v2\",\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(std.DerivePkgAddr(\"gno.land/r/gov/dao/v2\"))\n\n\tuassert.NotPanics(t, func() {\n\t\tupdateMinFee(newMinFee)\n\t})\n\n\tuassert.Equal(t, newMinFee, minFee.Amount)\n}\n" + }, + { + "name": "init.gno", + "body": "package valopers\n\nimport (\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/moul/authz\"\n\t\"gno.land/p/moul/txlink\"\n)\n\nfunc init() {\n\tvalopers = avl.NewTree()\n\n\tauth = authz.NewWithAuthority(\n\t\tauthz.NewContractAuthority(\n\t\t\t\"gno.land/r/gov/dao/v2\",\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\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- **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/gnoland/valopers_proposal.ProposeNewValidator**](` + txlink.Realm(\"gno.land/r/gnoland/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\"crypto/bech32\"\n\t\"errors\"\n\t\"regexp\"\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/combinederr\"\n\t\"gno.land/p/demo/ownable/exts/authorizable\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/realmpath\"\n)\n\nconst (\n\tMonikerMaxLength = 32\n\tDescriptionMaxLength = 2048\n)\n\nvar (\n\tErrValoperExists = errors.New(\"valoper already exists\")\n\tErrValoperMissing = errors.New(\"valoper does not exist\")\n\tErrAddressExists = errors.New(\"valoper updated address exists\")\n\tErrSameAddress = errors.New(\"valoper updated address is the same\")\n\tErrInvalidAddress = errors.New(\"invalid address\")\n\tErrOriginCaller = errors.New(\"origin call is not the same\")\n\tErrInvalidMoniker = errors.New(\"moniker is not valid\")\n\tErrInvalidDescription = errors.New(\"description is not valid\")\n\tErrInvalidPubKey = errors.New(\"invalid public key\")\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 = std.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\tvalidateListeningAddressRe = regexp.MustCompile(`^(?:\\w+://)?[\\w\\.-]+:\\d+$`) // \u003cprotocol\u003e://\u003chostname\u003e:\u003cport\u003e where \u003cprotocol\u003e is optional\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\n\tAddress std.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(address std.Address, member std.Address) {\n\tv := GetByAddr(address)\n\n\tif err := v.Auth().AddToAuthList(member); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc DeleteFromAuthList(address std.Address, member std.Address) {\n\tv := GetByAddr(address)\n\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(moniker string, description string, address std.Address, pubKey string) {\n\tif err := validateSentCoins(); err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Check if the valoper is already registered\n\tif isValoper(address) {\n\t\tpanic(ErrValoperExists)\n\t}\n\n\tv := Valoper{\n\t\tMoniker: moniker,\n\t\tDescription: description,\n\t\tAddress: address,\n\t\tPubKey: pubKey,\n\t\tKeepRunning: true,\n\t\tauth: authorizable.NewAuthorizable(),\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(address std.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)\n\n\t// Check that the caller has permissions\n\tv.Auth().AssertOnAuthList()\n\n\t// Update the moniker\n\tv.Moniker = moniker\n\n\t// Save the valoper info\n\tvalopers.Set(address.String(), v)\n}\n\n// UpdateDescription updates an existing valoper's description\nfunc UpdateDescription(address std.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)\n\n\t// Check that the caller has permissions\n\tv.Auth().AssertOnAuthList()\n\n\t// Update the description\n\tv.Description = description\n\n\t// Save the valoper info\n\tvalopers.Set(address.String(), v)\n}\n\n// UpdateKeepRunning updates an existing valoper's active status\nfunc UpdateKeepRunning(address std.Address, keepRunning bool) {\n\tv := GetByAddr(address)\n\n\t// Check that the caller has permissions\n\tv.Auth().AssertOnAuthList()\n\n\t// Update status\n\tv.KeepRunning = keepRunning\n\n\t// Save the valoper info\n\tvalopers.Set(address.String(), v)\n}\n\n// GetByAddr fetches the valoper using the address, if present\nfunc GetByAddr(address std.Address) Valoper {\n\tvaloperRaw, exists := valopers.Get(address.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/gnoland/valopers\" lists all valopers, paginated.\n// \"/r/gnoland/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/gnoland/valopers:%s) - [profile](/r/demo/profile:u/%s)\\n\",\n\t\t\tv.Moniker, v.Address, v.Auth().Owner())\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(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\\n\", v.PubKey)\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 std.Address) bool {\n\t_, exists := valopers.Get(address.String())\n\n\treturn exists\n}\n\nfunc validateSentCoins() error {\n\tsentCoins := std.OriginSend()\n\n\tif len(sentCoins) == 1 \u0026\u0026 sentCoins[0].IsGTE(minFee) {\n\t\treturn nil\n\t}\n\treturn errors.New(\"payment must not be less than \" + strconv.Itoa(int(minFee.Amount)))\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 std.Address) error {\n\tif !std.Address.IsValid(address) {\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" + }, + { + "name": "valopers_test.gno", + "body": "package valopers\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable/exts/authorizable\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nfunc validValidatorInfo(t *testing.T) struct {\n\tMoniker string\n\tDescription string\n\tAddress std.Address\n\tPubKey string\n} {\n\tt.Helper()\n\n\treturn struct {\n\t\tMoniker string\n\t\tDescription string\n\t\tAddress std.Address\n\t\tPubKey string\n\t}{\n\t\tMoniker: \"test-1\",\n\t\tDescription: \"test-1's description\",\n\t\tAddress: std.Address(\"g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\"),\n\t\tPubKey: \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\",\n\t}\n}\n\nfunc TestValopers_Register(t *testing.T) {\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\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(std.Coins{minFee})\n\n\t\tuassert.PanicsWithMessage(t, ErrValoperExists.Error(), func() {\n\t\t\tRegister(info.Moniker, info.Description, 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(std.Coins{std.NewCoin(\"ugnot\", 0)})\n\n\t\tuassert.PanicsWithMessage(t, ufmt.Sprintf(\"payment must not be less than %d\", minFee.Amount), func() {\n\t\t\tRegister(info.Moniker, info.Description, 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(std.Coins{std.NewCoin(\"ugnot\", minFee.Amount-1)})\n\n\t\tuassert.PanicsWithMessage(t, ufmt.Sprintf(\"payment must not be less than %d\", minFee.Amount), func() {\n\t\t\tRegister(info.Moniker, info.Description, 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(std.Coins{std.NewCoin(\"gnogno\", minFee.Amount)})\n\n\t\tuassert.PanicsWithMessage(t, \"incompatible coin denominations: gnogno, ugnot\", func() {\n\t\t\tRegister(info.Moniker, info.Description, 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(std.Coins{minFee})\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, 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.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(std.Coins{minFee})\n\n\t\ttesting.SetRealm(std.NewUserRealm(test1Address))\n\t\ttesting.SetOriginCaller(test1Address) // TODO(bug, issue #2371): should not be needed\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, info.Address, info.PubKey)\n\t\t})\n\n\t\ttesting.SetRealm(std.NewUserRealm(info.Address))\n\t\ttesting.SetOriginCaller(info.Address) // TODO(bug, issue #2371): should not be needed\n\n\t\t// try to add member without being authorized\n\t\tuassert.PanicsWithMessage(t, authorizable.ErrNotSuperuser.Error(), func() {\n\t\t\tAddToAuthList(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(std.Coins{minFee})\n\n\t\ttesting.SetRealm(std.NewUserRealm(test1Address))\n\t\ttesting.SetOriginCaller(test1Address) // TODO(bug, issue #2371): should not be needed\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tAddToAuthList(info.Address, test2Address)\n\t\t})\n\n\t\ttesting.SetRealm(std.NewUserRealm(info.Address))\n\t\ttesting.SetOriginCaller(info.Address) // TODO(bug, issue #2371): should not be needed\n\n\t\t// try to add member without being authorized\n\t\tuassert.PanicsWithMessage(t, authorizable.ErrNotSuperuser.Error(), func() {\n\t\t\tDeleteFromAuthList(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(std.Coins{minFee})\n\n\t\ttesting.SetRealm(std.NewUserRealm(test1Address))\n\t\ttesting.SetOriginCaller(test1Address) // TODO(bug, issue #2371): should not be needed\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, info.Address, info.PubKey)\n\t\t})\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tAddToAuthList(info.Address, test2Address)\n\t\t})\n\n\t\ttesting.SetRealm(std.NewUserRealm(test2Address))\n\t\ttesting.SetOriginCaller(test2Address) // TODO(bug, issue #2371): should not be needed\n\n\t\tnewMoniker := \"new moniker\"\n\t\t// Update the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tUpdateMoniker(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.PanicsWithMessage(t, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateMoniker(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(std.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, 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.PanicsWithMessage(t, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateMoniker(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// Send coins\n\t\ttesting.SetOriginSend(std.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, 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.PanicsWithMessage(t, ErrInvalidMoniker.Error(), func() {\n\t\t\t\tUpdateMoniker(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// Send coins\n\t\ttesting.SetOriginSend(std.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Update the valoper\n\t\tuassert.PanicsWithMessage(t, ErrInvalidMoniker.Error(), func() {\n\t\t\tUpdateMoniker(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// Send coins\n\t\ttesting.SetOriginSend(std.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, 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(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.PanicsWithMessage(t, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateDescription(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(std.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, 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.PanicsWithMessage(t, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateDescription(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// Send coins\n\t\ttesting.SetOriginSend(std.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, info.Address, info.PubKey)\n\t\t})\n\n\t\temptyDescription := \"\"\n\t\t// Update the valoper\n\t\tuassert.PanicsWithMessage(t, ErrInvalidDescription.Error(), func() {\n\t\t\tUpdateDescription(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// Send coins\n\t\ttesting.SetOriginSend(std.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Update the valoper\n\t\tuassert.PanicsWithMessage(t, ErrInvalidDescription.Error(), func() {\n\t\t\tUpdateDescription(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// Send coins\n\t\ttesting.SetOriginSend(std.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, 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(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.PanicsWithMessage(t, ErrValoperMissing.Error(), func() {\n\t\t\tUpdateKeepRunning(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(std.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, 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.PanicsWithMessage(t, authorizable.ErrNotInAuthList.Error(), func() {\n\t\t\tUpdateKeepRunning(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// Send coins\n\t\ttesting.SetOriginSend(std.Coins{minFee})\n\n\t\t// Add the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tRegister(info.Moniker, info.Description, info.Address, info.PubKey)\n\t\t})\n\n\t\t// Update the valoper\n\t\tuassert.NotPanics(t, func() {\n\t\t\tUpdateKeepRunning(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" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/gnoland/valopers_test\npackage valopers_test\n\n// SEND: 20000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/gnoland/valopers\"\n)\n\nconst (\n\tvalidMoniker = \"test-1\"\n\tvalidDescription = \"test-1's description\"\n\tvalidAddress = std.Address(\"g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\")\n\tvalidPubKey = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n)\n\nfunc init() {\n\t// Register a validator and add the proposal\n\tvalopers.Register(validMoniker, validDescription, 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/gnoland/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// - **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/gnoland/valopers_proposal.ProposeNewValidator**](/r/gnoland/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/gnoland/valopers:g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h) - [profile](/r/demo/profile:u/g1p9elmfxvctlkypargf7wruch5vchuysqr2xg2q)\n//\n//\n" + }, + { + "name": "z_2_filetest.gno", + "body": "// PKGPATH: gno.land/r/gnoland/valopers_test\npackage valopers_test\n\n// SEND: 20000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/gnoland/valopers\"\n)\n\nconst (\n\tvalidMoniker = \"test-1\"\n\tvalidDescription = \"test-1's description\"\n\tvalidAddress = std.Address(\"g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\")\n\tvalidPubKey = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n)\n\nfunc init() {\n\t// Register a validator and add the proposal\n\tvalopers.Register(validMoniker, validDescription, 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//\n// [Profile link](/r/demo/profile:u/g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h)\n//\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "lG3bMMv/v63wbRDCaKBLikEKa3pLNg8O66f3SqVGfWiTH4QemHwkW4KOEJWWiG0yfjbT8fcAiRUOQp42M0YNAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "govdao", + "path": "gno.land/r/gov/dao/v2", + "files": [ + { + "name": "dao.gno", + "body": "package govdao\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/demo/simpledao\"\n)\n\nvar (\n\td *simpledao.SimpleDAO // the current active DAO implementation\n\tmembers membstore.MemberStore // the member store\n\n\t// GovDAO exposes all functions of this contract as methods\n\tGovDAO = \u0026DAO{}\n)\n\n// DAO is an empty struct that allows all\n// functions of this realm to be methods instead of functions\n// This allows a registry, such as r/gov/dao/bridge\n// to take this object and match it to a required interface\ntype DAO struct{}\n\nconst daoPkgPath = \"gno.land/r/gov/dao/v2\"\n\nfunc init() {\n\t// Example initial member set (just test addresses)\n\tset := []membstore.Member{\n\t\t{\n\t\t\tAddress: std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"),\n\t\t\tVotingPower: 10,\n\t\t},\n\t}\n\n\t// Set the member store\n\tmembers = membstore.NewMembStore(membstore.WithInitialMembers(set), membstore.WithDAOPkgPath(daoPkgPath))\n\n\t// Set the DAO implementation\n\td = simpledao.New(members)\n}\n\n// Propose is designed to be called by another contract or with\n// `maketx run`, not by a `maketx call`.\nfunc (_ DAO) Propose(request dao.ProposalRequest) uint64 {\n\tidx, err := d.Propose(request)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn idx\n}\n\n// VoteOnProposal casts a vote for the given proposal\nfunc (_ DAO) VoteOnProposal(id uint64, option dao.VoteOption) {\n\tif err := d.VoteOnProposal(id, option); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// ExecuteProposal executes the proposal\nfunc (_ DAO) ExecuteProposal(id uint64) {\n\tif err := d.ExecuteProposal(id); err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// GetPropStore returns the active proposal store\nfunc (_ DAO) GetPropStore() dao.PropStore {\n\treturn d\n}\n\n// GetMembStore returns the active member store\nfunc (_ DAO) GetMembStore() membstore.MemberStore {\n\treturn members\n}\n" + }, + { + "name": "poc.gno", + "body": "package govdao\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/combinederr\"\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/p/gov/executor\"\n)\n\nvar errNoChangesProposed = errors.New(\"no set changes proposed\")\n\n// NewGovDAOExecutor creates the govdao wrapped callback executor\nfunc (_ DAO) NewGovDAOExecutor(cb func() error) dao.Executor {\n\tif cb == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\treturn executor.NewCallbackExecutor(\n\t\tcb,\n\t\tstd.CurrentRealm().PkgPath(),\n\t)\n}\n\n// NewMemberPropExecutor returns the GOVDAO member change executor\nfunc (_ DAO) NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor {\n\tif changesFn == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\tcallback := func() error {\n\t\terrs := \u0026combinederr.CombinedError{}\n\t\tcbMembers := changesFn()\n\n\t\tfor _, member := range cbMembers {\n\t\t\tswitch {\n\t\t\tcase !members.IsMember(member.Address):\n\t\t\t\t// Addition request\n\t\t\t\terr := members.AddMember(member)\n\n\t\t\t\terrs.Add(err)\n\t\t\tcase member.VotingPower == 0:\n\t\t\t\t// Remove request\n\t\t\t\terr := members.UpdateMember(member.Address, membstore.Member{\n\t\t\t\t\tAddress: member.Address,\n\t\t\t\t\tVotingPower: 0, // 0 indicated removal\n\t\t\t\t})\n\n\t\t\t\terrs.Add(err)\n\t\t\tdefault:\n\t\t\t\t// Update request\n\t\t\t\terr := members.UpdateMember(member.Address, member)\n\n\t\t\t\terrs.Add(err)\n\t\t\t}\n\t\t}\n\n\t\t// Check if there were any execution errors\n\t\tif errs.Size() == 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn errs\n\t}\n\n\treturn GovDAO.NewGovDAOExecutor(callback)\n}\n\nfunc (_ DAO) NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor {\n\tif changeFn == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\tcallback := func() error {\n\t\tsetMembStoreImpl(changeFn())\n\n\t\treturn nil\n\t}\n\n\treturn GovDAO.NewGovDAOExecutor(callback)\n}\n\n// setMembStoreImpl sets a new dao.MembStore implementation\nfunc setMembStoreImpl(impl membstore.MemberStore) {\n\tif impl == nil {\n\t\tpanic(\"invalid member store\")\n\t}\n\n\tmembers = impl\n}\n" + }, + { + "name": "prop1_filetest.gno", + "body": "// Please note that this package is intended for demonstration purposes only.\n// You could execute this code (the init part) by running a `maketx run` command\n// or by uploading a similar package to a personal namespace.\n//\n// For the specific case of validators, a `r/gnoland/valopers` will be used to\n// organize the lifecycle of validators (register, etc), and this more complex\n// contract will be responsible to generate proposals.\npackage main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\tpVals \"gno.land/p/sys/validators\"\n\t_ \"gno.land/r/gov/dao/init\" // so that the govdao initializer is executed\n\tgovdao \"gno.land/r/gov/dao/v2\"\n\tvalidators \"gno.land/r/sys/validators/v2\"\n)\n\nfunc init() {\n\n\tchangesFn := func() []pVals.Validator {\n\t\treturn []pVals.Validator{\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g12345678\"),\n\t\t\t\tPubKey: \"pubkey\",\n\t\t\t\tVotingPower: 10, // add a new validator\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g000000000\"),\n\t\t\t\tPubKey: \"pubkey\",\n\t\t\t\tVotingPower: 10, // add a new validator\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g000000000\"),\n\t\t\t\tPubKey: \"pubkey\",\n\t\t\t\tVotingPower: 0, // remove an existing validator\n\t\t\t},\n\t\t}\n\t}\n\n\t// Wraps changesFn to emit a certified event only if executed from a\n\t// complete governance proposal process.\n\texecutor := validators.NewPropExecutor(changesFn)\n\n\t// Create a proposal\n\ttitle := \"Valset change\"\n\tdescription := \"manual valset changes proposal example\"\n\n\tprop := dao.ProposalRequest{\n\t\tTitle: title,\n\t\tDescription: description,\n\t\tExecutor: executor,\n\t}\n\n\tgovdao.GovDAO.Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.GovDAO.VoteOnProposal(0, dao.YesVote)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(validators.Render(\"\"))\n\tprintln(\"--\")\n\tgovdao.GovDAO.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(validators.Render(\"\"))\n}\n\n// Output:\n// --\n// # GovDAO Proposals\n//\n// ## [Prop #0 - Valset change](/r/gov/dao/v2:0)\n//\n// **Status: ACTIVE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n//\n// --\n// # Proposal #0 - Valset change\n//\n// ## Description\n//\n// manual valset changes proposal example\n//\n// ## Proposal information\n//\n// **Status: ACTIVE**\n//\n// **Voting stats:**\n// - YES 0 (0%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 10 (100%)\n//\n//\n// **Threshold met: FALSE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// #### [[Vote YES](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=YES)] - [[Vote NO](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=ABSTAIN)]\n//\n//\n// --\n// --\n// # Proposal #0 - Valset change\n//\n// ## Description\n//\n// manual valset changes proposal example\n//\n// ## Proposal information\n//\n// **Status: ACCEPTED**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// --\n// No valset changes to apply.\n// --\n// --\n// # Proposal #0 - Valset change\n//\n// ## Description\n//\n// manual valset changes proposal example\n//\n// ## Proposal information\n//\n// **Status: EXECUTION SUCCESSFUL**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// --\n// Valset changes:\n// - #123: g12345678 (10)\n// - #123: g000000000 (10)\n// - #123: g000000000 (0)\n//\n\n// Events:\n// [\n// {\n// \"type\": \"ProposalAdded\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"proposal-author\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitProposalAdded\"\n// },\n// {\n// \"type\": \"VoteAdded\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"author\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// },\n// {\n// \"key\": \"option\",\n// \"value\": \"YES\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitVoteAdded\"\n// },\n// {\n// \"type\": \"ProposalAccepted\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitProposalAccepted\"\n// },\n// {\n// \"type\": \"ValidatorAdded\",\n// \"attrs\": [],\n// \"pkg_path\": \"gno.land/r/sys/validators/v2\",\n// \"func\": \"addValidator\"\n// },\n// {\n// \"type\": \"ValidatorAdded\",\n// \"attrs\": [],\n// \"pkg_path\": \"gno.land/r/sys/validators/v2\",\n// \"func\": \"addValidator\"\n// },\n// {\n// \"type\": \"ValidatorRemoved\",\n// \"attrs\": [],\n// \"pkg_path\": \"gno.land/r/sys/validators/v2\",\n// \"func\": \"removeValidator\"\n// },\n// {\n// \"type\": \"ProposalExecuted\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"exec-status\",\n// \"value\": \"accepted\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"ExecuteProposal\"\n// }\n// ]\n" + }, + { + "name": "prop2_filetest.gno", + "body": "package main\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/dao\"\n\tgnoblog \"gno.land/r/gnoland/blog\"\n\t_ \"gno.land/r/gov/dao/init\" // so that the govdao initializer is executed\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\nfunc init() {\n\tex := gnoblog.NewPostExecutor(\n\t\t\"hello-from-govdao\", // slug\n\t\t\"Hello from GovDAO!\", // title\n\t\t\"This post was published by a GovDAO proposal.\", // body\n\t\ttime.Now().Format(time.RFC3339), // publication date\n\t\t\"moul\", // authors\n\t\t\"govdao,example\", // tags\n\t)\n\n\t// Create a proposal\n\ttitle := \"govdao blog post title\"\n\tdescription := \"post a new blogpost about govdao\"\n\n\tprop := dao.ProposalRequest{\n\t\tTitle: title,\n\t\tDescription: description,\n\t\tExecutor: ex,\n\t}\n\n\tgovdao.GovDAO.Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.GovDAO.VoteOnProposal(0, \"YES\")\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(gnoblog.Render(\"\"))\n\tprintln(\"--\")\n\tgovdao.GovDAO.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(gnoblog.Render(\"\"))\n}\n\n// Output:\n// --\n// # GovDAO Proposals\n//\n// ## [Prop #0 - govdao blog post title](/r/gov/dao/v2:0)\n//\n// **Status: ACTIVE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n//\n// --\n// # Proposal #0 - govdao blog post title\n//\n// ## Description\n//\n// post a new blogpost about govdao\n//\n// ## Proposal information\n//\n// **Status: ACTIVE**\n//\n// **Voting stats:**\n// - YES 0 (0%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 10 (100%)\n//\n//\n// **Threshold met: FALSE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// #### [[Vote YES](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=YES)] - [[Vote NO](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=ABSTAIN)]\n//\n//\n// --\n// --\n// # Proposal #0 - govdao blog post title\n//\n// ## Description\n//\n// post a new blogpost about govdao\n//\n// ## Proposal information\n//\n// **Status: ACCEPTED**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// --\n// # gno.land's blog\n//\n// No posts.\n// --\n// --\n// # Proposal #0 - govdao blog post title\n//\n// ## Description\n//\n// post a new blogpost about govdao\n//\n// ## Proposal information\n//\n// **Status: EXECUTION SUCCESSFUL**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// --\n// # gno.land's blog\n//\n// \u003cdiv class='columns-3'\u003e\u003cdiv\u003e\n//\n// ### [Hello from GovDAO!](/r/gnoland/blog:p/hello-from-govdao)\n// 13 Feb 2009\n// \u003c/div\u003e\u003c/div\u003e\n\n// Events:\n// [\n// {\n// \"type\": \"ProposalAdded\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"proposal-author\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitProposalAdded\"\n// },\n// {\n// \"type\": \"VoteAdded\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"author\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// },\n// {\n// \"key\": \"option\",\n// \"value\": \"YES\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitVoteAdded\"\n// },\n// {\n// \"type\": \"ProposalAccepted\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitProposalAccepted\"\n// },\n// {\n// \"type\": \"ProposalExecuted\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"exec-status\",\n// \"value\": \"accepted\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"ExecuteProposal\"\n// }\n// ]\n" + }, + { + "name": "prop3_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/membstore\"\n\t\"gno.land/r/gov/dao/bridge\"\n\t_ \"gno.land/r/gov/dao/init\" // so that the govdao initializer is executed\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\nfunc init() {\n\tmemberFn := func() []membstore.Member {\n\t\treturn []membstore.Member{\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g123\"),\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g456\"),\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAddress: std.Address(\"g789\"),\n\t\t\t\tVotingPower: 10,\n\t\t\t},\n\t\t}\n\t}\n\n\t// Create a proposal\n\ttitle := \"new govdao member addition\"\n\tdescription := \"add new members to the govdao\"\n\n\tprop := dao.ProposalRequest{\n\t\tTitle: title,\n\t\tDescription: description,\n\t\tExecutor: govdao.GovDAO.NewMemberPropExecutor(memberFn),\n\t}\n\n\tbridge.GovDAO().Propose(prop)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdao.GovDAO.GetMembStore().Size())\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tgovdao.GovDAO.VoteOnProposal(0, \"YES\")\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tgovdao.GovDAO.ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"0\"))\n\tprintln(\"--\")\n\tprintln(govdao.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdao.GovDAO.GetMembStore().Size())\n}\n\n// Output:\n// --\n// 1\n// --\n// # GovDAO Proposals\n//\n// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)\n//\n// **Status: ACTIVE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n//\n// --\n// # Proposal #0 - new govdao member addition\n//\n// ## Description\n//\n// add new members to the govdao\n//\n// ## Proposal information\n//\n// **Status: ACTIVE**\n//\n// **Voting stats:**\n// - YES 0 (0%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 10 (100%)\n//\n//\n// **Threshold met: FALSE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// #### [[Vote YES](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=YES)] - [[Vote NO](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=ABSTAIN)]\n//\n//\n// --\n// --\n// # Proposal #0 - new govdao member addition\n//\n// ## Description\n//\n// add new members to the govdao\n//\n// ## Proposal information\n//\n// **Status: ACCEPTED**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// --\n// # GovDAO Proposals\n//\n// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)\n//\n// **Status: ACCEPTED**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n//\n// --\n// --\n// # Proposal #0 - new govdao member addition\n//\n// ## Description\n//\n// add new members to the govdao\n//\n// ## Proposal information\n//\n// **Status: EXECUTION SUCCESSFUL**\n//\n// **Voting stats:**\n// - YES 10 (25%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 30 (75%)\n//\n//\n// **Threshold met: FALSE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// --\n// # GovDAO Proposals\n//\n// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0)\n//\n// **Status: EXECUTION SUCCESSFUL**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n//\n// --\n// 4\n\n// Events:\n// [\n// {\n// \"type\": \"ProposalAdded\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"proposal-author\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitProposalAdded\"\n// },\n// {\n// \"type\": \"VoteAdded\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"author\",\n// \"value\": \"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"\n// },\n// {\n// \"key\": \"option\",\n// \"value\": \"YES\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitVoteAdded\"\n// },\n// {\n// \"type\": \"ProposalAccepted\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"EmitProposalAccepted\"\n// },\n// {\n// \"type\": \"ProposalExecuted\",\n// \"attrs\": [\n// {\n// \"key\": \"proposal-id\",\n// \"value\": \"0\"\n// },\n// {\n// \"key\": \"exec-status\",\n// \"value\": \"accepted\"\n// }\n// ],\n// \"pkg_path\": \"gno.land/r/gov/dao/v2\",\n// \"func\": \"ExecuteProposal\"\n// }\n// ]\n" + }, + { + "name": "prop4_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/r/gov/dao/bridge\"\n\t_ \"gno.land/r/gov/dao/init\" // so that the govdao initializer is executed\n\tgovdaov2 \"gno.land/r/gov/dao/v2\"\n\t\"gno.land/r/sys/params\"\n)\n\nfunc init() {\n\tmExec := params.NewSysParamStringPropExecutor(\"foo\", \"bar\", \"baz\", \"quz\")\n\ttitle := \"Setting foo:bar:baz param\"\n\tcomment := \"new value will be quz\"\n\tprop := dao.ProposalRequest{\n\t\tTitle: title,\n\t\tDescription: comment,\n\t\tExecutor: mExec,\n\t}\n\tid := bridge.GovDAO().Propose(prop)\n\tprintln(\"new prop\", id)\n}\n\nfunc main() {\n\tprintln(\"--\")\n\tprintln(govdaov2.Render(\"\"))\n\tprintln(\"--\")\n\tprintln(govdaov2.Render(\"0\"))\n\tprintln(\"--\")\n\tbridge.GovDAO().VoteOnProposal(0, \"YES\")\n\tprintln(\"--\")\n\tprintln(govdaov2.Render(\"0\"))\n\tprintln(\"--\")\n\tbridge.GovDAO().ExecuteProposal(0)\n\tprintln(\"--\")\n\tprintln(govdaov2.Render(\"0\"))\n}\n\n// Output:\n// new prop 0\n// --\n// # GovDAO Proposals\n//\n// ## [Prop #0 - Setting foo:bar:baz param](/r/gov/dao/v2:0)\n//\n// **Status: ACTIVE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n//\n// --\n// # Proposal #0 - Setting foo:bar:baz param\n//\n// ## Description\n//\n// new value will be quz\n//\n// ## Proposal information\n//\n// **Status: ACTIVE**\n//\n// **Voting stats:**\n// - YES 0 (0%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 10 (100%)\n//\n//\n// **Threshold met: FALSE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// #### [[Vote YES](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=YES)] - [[Vote NO](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help\u0026func=VoteOnProposal\u0026id=0\u0026option=ABSTAIN)]\n//\n//\n// --\n// --\n// # Proposal #0 - Setting foo:bar:baz param\n//\n// ## Description\n//\n// new value will be quz\n//\n// ## Proposal information\n//\n// **Status: ACCEPTED**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n// --\n// --\n// # Proposal #0 - Setting foo:bar:baz param\n//\n// ## Description\n//\n// new value will be quz\n//\n// ## Proposal information\n//\n// **Status: EXECUTION SUCCESSFUL**\n//\n// **Voting stats:**\n// - YES 10 (100%)\n// - NO 0 (0%)\n// - ABSTAIN 0 (0%)\n// - MISSING VOTES 0 (0%)\n//\n//\n// **Threshold met: TRUE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n// ### Actions\n//\n// The voting period for this proposal is over.\n//\n//\n" + }, + { + "name": "render.gno", + "body": "package govdao\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/txlink\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nfunc Render(path string) string {\n\tvar out string\n\n\tif path == \"\" {\n\t\tout += \"# GovDAO Proposals\\n\\n\"\n\t\tnumProposals := d.Size()\n\n\t\tif numProposals == 0 {\n\t\t\tout += \"No proposals found :(\" // corner case\n\t\t\treturn out\n\t\t}\n\n\t\toffset := uint64(0)\n\t\tif numProposals \u003e= 10 {\n\t\t\toffset = uint64(numProposals) - 10\n\t\t}\n\n\t\t// Fetch the last 10 proposals\n\t\tproposals := d.Proposals(offset, uint64(10))\n\t\tfor i := len(proposals) - 1; i \u003e= 0; i-- {\n\t\t\tprop := proposals[i]\n\n\t\t\ttitle := prop.Title()\n\t\t\tif len(title) \u003e 40 {\n\t\t\t\ttitle = title[:40] + \"...\"\n\t\t\t}\n\n\t\t\tpropID := offset + uint64(i)\n\t\t\tout += ufmt.Sprintf(\"## [Prop #%d - %s](/r/gov/dao/v2:%d)\\n\\n\", propID, title, propID)\n\t\t\tout += ufmt.Sprintf(\"**Status: %s**\\n\\n\", strings.ToUpper(prop.Status().String()))\n\n\t\t\tuser := users.ResolveAddress(prop.Author())\n\t\t\tauthorDisplayText := prop.Author().String()\n\t\t\tif user != nil {\n\t\t\t\tauthorDisplayText = user.RenderLink(\"\")\n\t\t\t}\n\n\t\t\tout += ufmt.Sprintf(\"**Author: %s**\\n\\n\", authorDisplayText)\n\n\t\t\tif i != 0 {\n\t\t\t\tout += \"---\\n\\n\"\n\t\t\t}\n\t\t}\n\n\t\treturn out\n\t}\n\n\t// Display the detailed proposal\n\tidx, err := strconv.Atoi(path)\n\tif err != nil {\n\t\treturn \"404: Invalid proposal ID\"\n\t}\n\n\t// Fetch the proposal\n\tprop, err := d.ProposalByID(uint64(idx))\n\tif err != nil {\n\t\treturn ufmt.Sprintf(\"unable to fetch proposal, %s\", err.Error())\n\t}\n\n\t// Render the proposal page\n\tout += renderPropPage(prop, idx)\n\n\treturn out\n}\n\nfunc renderPropPage(prop dao.Proposal, idx int) string {\n\tvar out string\n\n\tout += ufmt.Sprintf(\"# Proposal #%d - %s\\n\\n\", idx, prop.Title())\n\tout += prop.Render()\n\tout += renderAuthor(prop)\n\tout += renderActionBar(prop, idx)\n\tout += \"\\n\\n\"\n\n\treturn out\n}\n\nfunc renderAuthor(p dao.Proposal) string {\n\tvar out string\n\n\tauthorUsername := \"\"\n\tuser := users.ResolveAddress(p.Author())\n\tif user != nil {\n\t\tauthorUsername = user.Name()\n\t}\n\n\tif authorUsername != \"\" {\n\t\tout += ufmt.Sprintf(\"**Author: [%s](/r/gnoland/users/v1:%s)**\\n\\n\", authorUsername, authorUsername)\n\t} else {\n\t\tout += ufmt.Sprintf(\"**Author: %s**\\n\\n\", p.Author().String())\n\t}\n\n\treturn out\n}\n\nfunc renderActionBar(p dao.Proposal, idx int) string {\n\tvar out string\n\n\tout += \"### Actions\\n\\n\"\n\tif p.Status() == dao.Active {\n\t\tout += ufmt.Sprintf(\"#### [[Vote YES](%s)] - [[Vote NO](%s)] - [[Vote ABSTAIN](%s)]\",\n\t\t\ttxlink.Call(\"VoteOnProposal\", \"id\", strconv.Itoa(idx), \"option\", \"YES\"),\n\t\t\ttxlink.Call(\"VoteOnProposal\", \"id\", strconv.Itoa(idx), \"option\", \"NO\"),\n\t\t\ttxlink.Call(\"VoteOnProposal\", \"id\", strconv.Itoa(idx), \"option\", \"ABSTAIN\"),\n\t\t)\n\t} else {\n\t\tout += \"The voting period for this proposal is over.\"\n\t}\n\n\treturn out\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "zBykaSWNiPfNIXZd1FUflqi/A8tGmLH23LAxRBJ09BURjMQzHXx6IIKJQ+Agyo9mV6cRcX0i8G8dZQdWj9PJCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "init", + "path": "gno.land/r/gov/dao/init", + "files": [ + { + "name": "init.gno", + "body": "// Package init's only task is to load the initial GovDAO version into the bridge.\n// This is done to avoid gov/dao/v2 as a bridge dependency,\n// As this can often lead to cyclic dependency errors.\npackage init\n\nimport (\n\t\"gno.land/r/gov/dao/bridge\"\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\nfunc init() {\n\tbridge.LoadGovDAO(govdao.GovDAO)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "4mmLDHAF0Wy2x3ewbbDTBsfkRlCpxuCF52UM3ohX/yHTf+NS6pfvx4ZN/7V4C/a0kn7vcAzM9+K2xtHlX5WKDw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "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": "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/demo/avl\"\n\t\"gno.land/p/nt/poa\"\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\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/sys/validators\"\n\t\"gno.land/r/gov/dao/bridge\"\n)\n\nconst errNoChangesProposed = \"no set changes proposed\"\n\n// NewPropExecutor creates a new executor that wraps a changes closure\n// proposal. This wrapper is required to ensure the GovDAO Realm actually\n// executed the callback.\n//\n// Concept adapted from:\n// https://github.com/gnolang/gno/pull/1945\nfunc NewPropExecutor(changesFn func() []validators.Validator) dao.Executor {\n\tif changesFn == nil {\n\t\tpanic(errNoChangesProposed)\n\t}\n\n\tcallback := func() error {\n\t\tfor _, change := range changesFn() {\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\treturn bridge.GovDAO().NewGovDAOExecutor(callback)\n}\n\n// IsValidator returns a flag indicating if the given bech32 address\n// is part of the validator set\nfunc IsValidator(addr std.Address) bool {\n\treturn vp.IsValidator(addr)\n}\n\n// GetValidator returns the typed validator\nfunc GetValidator(addr std.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\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\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: std.ChainHeight(),\n\t\tvalidator: val,\n\t}\n\n\tsaveChange(ch)\n\n\t// Emit the validator set change\n\tstd.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 std.Address) {\n\tval, err := vp.RemoveValidator(address)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// Validator removed, note the change\n\tch := change{\n\t\tblockNum: std.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\tstd.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\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\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 := std.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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "IRVd0N3e/lbDAVPcO03onKgBks2fpPtMetp2CKgcGw+b7yVwjWKammP8+9WgRV8Qm0/H8coSDBo8jNZRUIp4CQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "valopers_proposal", + "path": "gno.land/r/gnoland/valopers_proposal", + "files": [ + { + "name": "proposal.gno", + "body": "package valopers_proposal\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/ufmt\"\n\tpVals \"gno.land/p/sys/validators\"\n\tvalopers \"gno.land/r/gnoland/valopers\"\n\t\"gno.land/r/gov/dao/bridge\"\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// ProposeNewValidator creates a proposal to the GovDAO\n// for adding the given valoper to the validator set.\nfunc ProposeNewValidator(address std.Address) {\n\tvar (\n\t\tvaloper = valopers.GetByAddr(address)\n\t\tvotingPower = uint64(1)\n\t)\n\n\texist := validators.IsValidator(address)\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)\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// Create the executor\n\texecutor := validators.NewPropExecutor(changesFn)\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/gnoland/valopers:%s)\\n\\n%s\",\n\t\tvaloper.Moniker,\n\t\tvaloper.Address,\n\t\tvaloper.Render(),\n\t)\n\n\tprop := dao.ProposalRequest{\n\t\tTitle: title,\n\t\tDescription: description,\n\t\tExecutor: executor,\n\t}\n\n\t// Create the govdao proposal\n\tbridge.GovDAO().Propose(prop)\n}\n\n// ProposeNewInstructions creates a proposal to the GovDAO\n// for updating the realm instructions.\nfunc ProposeNewInstructions(newInstructions string) {\n\texecutor := valopers.NewInstructionsExecutor(newInstructions)\n\n\t// Create a proposal\n\ttitle := \"/p/gnoland/valopers: Update instructions\"\n\tdescription := ufmt.Sprintf(\"Update the instructions to: \\n\\n%s\", newInstructions)\n\n\tprop := dao.ProposalRequest{\n\t\tTitle: title,\n\t\tDescription: description,\n\t\tExecutor: executor,\n\t}\n\n\tbridge.GovDAO().Propose(prop)\n}\n\n// ProposeNewMinFee creates a proposal to the GovDAO\n// for updating the minimum fee to register a new valoper.\nfunc ProposeNewMinFee(newMinFee int64) {\n\texecutor := valopers.NewMinFeeExecutor(newMinFee)\n\n\t// Create a proposal\n\ttitle := \"/p/gnoland/valopers: Update minFee\"\n\tdescription := ufmt.Sprintf(\"Update the minimum register fee to: %d ugnot\", newMinFee)\n\n\tprop := dao.ProposalRequest{\n\t\tTitle: title,\n\t\tDescription: description,\n\t\tExecutor: executor,\n\t}\n\n\tbridge.GovDAO().Propose(prop)\n}\n" + }, + { + "name": "proposal_test.gno", + "body": "package valopers_proposal\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/gnoland/valopers\"\n\t\"gno.land/r/gov/dao/bridge\"\n\t_ \"gno.land/r/gov/dao/init\" // so that the govdao initializer is executed\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\tpubKey = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n\t)\n\n\ttest1Address := testutils.TestAddress(\"test1\")\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(std.Coins{std.NewCoin(\"ugnot\", registerMinFee)})\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvalopers.Register(moniker, description, test1Address, pubKey)\n\t\t\tvalopers.UpdateKeepRunning(test1Address, false)\n\t\t})\n\n\t\tvar valoper valopers.Valoper\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper = valopers.GetByAddr(test1Address)\n\t\t})\n\n\t\t// Send coins to be able to make a proposal\n\t\ttesting.SetOriginSend(std.Coins{std.NewCoin(\"ugnot\", proposalMinFee)})\n\n\t\tuassert.PanicsWithMessage(t, ErrValidatorMissing.Error(), func() {\n\t\t\tProposeNewValidator(test1Address)\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(std.Coins{std.NewCoin(\"ugnot\", registerMinFee)})\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvalopers.UpdateKeepRunning(test1Address, true)\n\t\t})\n\n\t\tvar valoper valopers.Valoper\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper = valopers.GetByAddr(test1Address)\n\t\t})\n\n\t\tdao := bridge.GovDAO()\n\t\tpropStore := dao.GetPropStore()\n\t\tcurrentSize := propStore.Size()\n\n\t\t// Send coins to be able to make a proposal\n\t\ttesting.SetOriginSend(std.Coins{std.NewCoin(\"ugnot\", proposalMinFee)})\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tProposeNewValidator(test1Address)\n\t\t})\n\n\t\tuassert.Equal(t, currentSize+1, propStore.Size())\n\n\t\tproposal, err := propStore.ProposalByID(uint64(currentSize)) // index starts from 0\n\t\tuassert.NoError(t, err, \"proposal not found\")\n\n\t\tdescription := ufmt.Sprintf(\"Valoper profile: [%s](/r/gnoland/valopers:%s)\\n\\n%s\",\n\t\t\tvaloper.Moniker,\n\t\t\tvaloper.Address,\n\t\t\tvaloper.Render(),\n\t\t)\n\n\t\t// Check that the proposal is correct\n\t\tuassert.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(std.Coins{std.NewCoin(\"ugnot\", registerMinFee)})\n\n\t\tvar valoper valopers.Valoper\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\tvaloper = valopers.GetByAddr(test1Address)\n\t\t})\n\n\t\tdao := bridge.GovDAO()\n\t\tpropStore := dao.GetPropStore()\n\t\tcurrentSize := propStore.Size()\n\n\t\t// Set a GovDAO member as the caller\n\t\ttesting.SetOriginCaller(std.Address(\"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm\"))\n\n\t\tuassert.NotPanics(t, func() {\n\t\t\t// Vote the proposal created in the previous test\n\t\t\tdao.VoteOnProposal(uint64(currentSize-1), \"YES\")\n\n\t\t\t// Execute the proposal\n\t\t\tdao.ExecuteProposal(uint64(currentSize - 1))\n\t\t})\n\n\t\t// Send coins to be able to make a proposal\n\t\ttesting.SetOriginSend(std.Coins{std.NewCoin(\"ugnot\", proposalMinFee)})\n\n\t\tuassert.PanicsWithMessage(t, ErrSameValues.Error(), func() {\n\t\t\tProposeNewValidator(test1Address)\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\tdao := bridge.GovDAO()\n\tpropStore := dao.GetPropStore()\n\tcurrentSize := propStore.Size()\n\n\t// Send coins to be able to make a proposal\n\ttesting.SetOriginSend(std.Coins{std.NewCoin(\"ugnot\", proposalMinFee)})\n\n\tuassert.NotPanics(t, func() {\n\t\tProposeNewInstructions(newInstructions)\n\t})\n\n\tuassert.Equal(t, currentSize+1, propStore.Size())\n\n\tproposal, err := propStore.ProposalByID(uint64(currentSize)) // index starts from 0\n\tuassert.NoError(t, err, \"proposal not found\")\n\n\t// Check that the proposal is correct\n\tuassert.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\tdao := bridge.GovDAO()\n\tpropStore := dao.GetPropStore()\n\tcurrentSize := propStore.Size()\n\n\t// Send coins to be able to make a proposal\n\ttesting.SetOriginSend(std.Coins{std.NewCoin(\"ugnot\", proposalMinFee)})\n\n\tuassert.NotPanics(t, func() {\n\t\tProposeNewMinFee(newMinFee)\n\t})\n\n\tuassert.Equal(t, currentSize+1, propStore.Size())\n\n\tproposal, err := propStore.ProposalByID(uint64(currentSize)) // index starts from 0\n\tuassert.NoError(t, err, \"proposal not found\")\n\n\t// Check that the proposal is correct\n\tuassert.Equal(t, description, proposal.Description())\n}\n" + }, + { + "name": "z_0_a_filetest.gno", + "body": "// PKGPATH: gno.land/r/gnoland/valopers_proposal_test\npackage valopers_proposal_test\n\n// SEND: 20000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/gnoland/valopers\"\n\t\"gno.land/r/gnoland/valopers_proposal\"\n\t_ \"gno.land/r/gov/dao/init\" // so that the govdao initializer is executed\n)\n\nconst (\n\tvalidMoniker = \"test-1\"\n\tvalidDescription = \"test-1's description\"\n\tvalidAddress = std.Address(\"g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\")\n\totherAddress = std.Address(\"g1juz2yxmdsa6audkp6ep9vfv80c8p5u76e03vvh\")\n\tvalidPubKey = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n)\n\nfunc init() {\n}\n\nfunc main() {\n\t// Register a validator\n\tvalopers.Register(validMoniker, validDescription, validAddress, validPubKey)\n\t// Try to make a proposal for a non-existing validator\n\tdefer func() {\n\t\tr := recover()\n\t\tprintln(\"r: \", r)\n\t}()\n\tvalopers_proposal.ProposeNewValidator(otherAddress)\n}\n\n// Output:\n// r: valoper does not exist\n" + }, + { + "name": "z_1_filetest.gno", + "body": "// PKGPATH: gno.land/r/gnoland/valopers_proposal_test\npackage valopers_proposal_test\n\n// SEND: 100000000ugnot\n\nimport (\n\t\"std\"\n\n\t\"gno.land/r/gnoland/valopers\"\n\t\"gno.land/r/gnoland/valopers_proposal\"\n\t_ \"gno.land/r/gov/dao/init\" // so that the govdao initializer is executed\n\tgovdao \"gno.land/r/gov/dao/v2\"\n)\n\nconst (\n\tvalidMoniker = \"test-1\"\n\tvalidDescription = \"test-1's description\"\n\tvalidAddress = std.Address(\"g1sp8v98h2gadm5jggtzz9w5ksexqn68ympsd68h\")\n\tvalidPubKey = \"gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqwpdwpd0f9fvqla089ndw5g9hcsufad77fml2vlu73fk8q8sh8v72cza5p\"\n)\n\nfunc init() {\n\t// Register a validator and add the proposal\n\tvalopers.Register(validMoniker, validDescription, validAddress, validPubKey)\n\tvalopers_proposal.ProposeNewValidator(validAddress)\n}\n\nfunc main() {\n\tprintln(govdao.Render(\"\"))\n}\n\n// Output:\n// # GovDAO Proposals\n//\n// ## [Prop #0 - Add valoper test-1 to the valset](/r/gov/dao/v2:0)\n//\n// **Status: ACTIVE**\n//\n// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm**\n//\n//\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "ZDVfrYwK3PFnJiwDdjAv4DZphFZxpi2SGY6uX5xIKp8+340MGY/xOpOMvSFjxIYIS1uEdraSvK4+KeJm+ygyAQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/grepsuzette/home", + "files": [ + { + "name": "README.md", + "body": "Dwilgelindildong, traveler.\n\n" + }, + { + "name": "contribs.gno", + "body": "package home\n\nfunc r3() string {\n\treturn `# Greps' notable contributions\n\nMy main contributions until the gno.land beta launch are listed below; most aren't in the monorepo, note.\n\n### Port Joeson from coffeescript to golang\n\nWorked on this from june 2022 until january 2023. Bounty [applied for](https://github.com/gnolang/bounties-old/issues/33) on Feb 2, 2023. \n\nHere is the port I did in Go: [grepsuzette/joeson](https://github.com/grepsuzette/joeson/).\n\n 4. Port JOESON to Go\n github.com/jaekwon/joescript\n The intent is to create an independent left-recursive PEG parser for Gno.\n Optional: port Joescript or Javascript.\n 1000 ATOMs from @jaekwon\n More GNOTs than from #3.\n\nThere have been many examples posted, including a minimal [LISP REPL](https://github.com/grepsuzette/joeson/tree/master/examples/lisp-repl) and a theorical [study on precedence](https://github.com/grepsuzette/joeson/blob/master/examples/precedence/precedence_test.go) (precedence is often problematic with PEG parsers, this allowed to find a solution, used in the next part). \n\n### GNO grammar - partial\n\nIn summer 2023, started to port the GNO grammar using Joeson (since there was no news about joeson, so this was an attempt to demonstrate it worked). Grammar was posted in [PR 1156](https://github.com/gnolang/gno/pull/1156). There are only 3 files, they are quite dense:\n\n1. [joeson_test.go](https://github.com/grepsuzette/gno/blob/joeson/gnovm/pkg/gnolang/joeson_test.go)\n1. [joeson_rules.go](https://github.com/grepsuzette/gno/blob/joeson/gnovm/pkg/gnolang/joeson_rules.go)\n1. [joeson_f.go](https://github.com/grepsuzette/gno/blob/joeson/gnovm/pkg/gnolang/joeson_f.go)\n\n### gnAsteroid\n\n![asteroid](https://raw.githubusercontent.com/grepsuzette/gfx/master/asteroid160.png)\n\n**gnAsteroid** is an asteroid creation-kit, it was started around the time the joeson port was published, but didn't have a name back then. \n\nAsteroids orbit gno.land, it's the same blockchain, but different frontend,\nthemable, working with wiki-like markdown files (enabling realms from gno.land\nto be rendered there).\n\n* [asteroid 0](https://gnAsteroid.com) - asteroid explaining what it is, containing instructions, to use, deploy on [Akash](https://gnasteroid.com/publishing/akash.md), [Vercel](https://gnasteroid.com/publishing/vercel.md).\n* [greps' asteroid](https://greps.gnAsteroid.com)\n* [gnAsteroid](https://github.com/gnAsteroid/gnAsteroid) - The github for gnAsteroid.\n\n### Research with markdown and gnoweb, mini-games, experiments (summer-oct 2024)\n\nA series of experiments with gnoweb 1.0 lead from the summer 2024, to try to advocate for keeping html\nand css enabled in gnoweb, or at least to try to determine what we could\npotentially miss without. Gnoweb1.0, markdown, html, css, js-less.\n\nNote those still work with [gnAsteroid](https://gnAsteroid.com), or with gnoweb\nrunning with the -web-html switch. As of now they are rendered through an\nasteroid.\n\n| 1 | 2 |\n| :-------------------: | :-------------------------: |\n| ![parrot](https://raw.githubusercontent.com/grepsuzette/gfx/master/parrot160.png) | ![octopus](https://raw.githubusercontent.com/grepsuzette/gfx/master/octopus160.png) |\n| [tic-tac-toe](https://greps.gnAsteroid.com/r/grepsuzette/pr2554/v6/games/tictactoe) | [minesweeper](https://greps.gnAsteroid.com/r/grepsuzette/pr2554/v6/games/minesweeper) |\n\nCheck the [other experiments here](/conjects/gnoweb.md).\n\n![octopus](https://raw.githubusercontent.com/grepsuzette/gfx/master/screen-minesweeper390.png)\n\n### Tendermint vuln retrospective (2023)\n\nAlso worked on an anthology of publicly knowned vulnerabilities that affected Tendermint. \n\n* [Cosmos-sdk vulnerability retrospective](https://github.com/gnolang/gno/issues/587)\n* found most vulns were not affecting our Tendermint version, however:\n* [demonstrated vulnerability to BSC 2022-10-07 hack](https://github.com/gnolang/gno/pull/583)\n* [proposed fix to vuln to BSC 2022-10-07 hack (merged)](https://github.com/gnolang/gno/pull/584)\n* not all of them were tested, as I was hoping some more feedback before to continue.\n\nThere is also a small [GNO mail](https://github.com/gnolang/gno/pull/641) which got no UI is discussed in [one of my articles](https://greps.gnasteroid.com/articles/encryptedmail.md).\n\nThanks for reading!\n`\n}\n" + }, + { + "name": "render.gno", + "body": "package home\n\nfunc Render(path string) string {\n\tswitch path {\n\tcase \"3\":\n\t\treturn r3()\n\tcase \"2\":\n\t\treturn r2()\n\tdefault:\n\t\treturn r1()\n\t}\n}\n\nconst tripleBackquote = \"```\"\nconst art = `\n ( )\n ( )\n ) )\n ( ( /\\\n (_) / \\ /\\\n ________[_]________ /\\/ \\/ \\\n /\\ /\\ ______ \\ / /\\/\\ /\\/\\\n / \\ //_\\ \\ /\\ \\ /\\/\\/ \\/ \\\n /\\ / /\\/\\ //___\\ \\__/ \\ \\/ + ' \n / \\ /\\/ \\//_____\\ \\ |[]| \\ . t .\n /\\/\\/\\/ //_______\\ \\|__| \\ p e \n/ \\ /XXXXXXXXXX\\ \\ o l \n \\ /_I_II I__I_\\__________________\\ + r e\n I_I| I__I_____[]_|_[]_____I . t \n I_II I__I_____[]_|_[]_____I + '\n I II__I I XXXXXXX I \n ~~~~~\" \"~~~~~~~~~~~~~~~~~~~~~~~~ :*:*:*:*:*\n`\n\nfunc r1() string {\n\treturn \"# greps' (gn)home\" +\n\t\t`\nYou've reached the terrestrial realms of Grepsuzette on gno.land. \n\n` + tripleBackquote + art + tripleBackquote + `\n\nI am often on my [GNO asteroid](https://greps.gnAsteroid.com) too.\n\n* Public address: g1fjh9y7ausp27dqsdq0qrcsnmgvwm6829v2au7d\n* Contributor since summer 2022 ([notable contributions](/r/grepsuzette/home:3))\n* You can try my games in GNO (they use gnoweb -html):\n * [tic-tac-toe](https://greps.gnasteroid.com/r/grepsuzette/pr2554/v6/games/tictactoe)\n * [minesweeper](https://greps.gnasteroid.com/r/grepsuzette/pr2554/v6/games/minesweeper)\n`\n}\n\nfunc r2() string {\n\treturn `A manual index, until there's an automated way:\n\n* [home](home/): greps' home on gno.land\n* [games](games/): series of games\n\nI'm often on my [GNO asteroid][1] too.\n\n[1]: https://greps.gnAsteroid.com\n`\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "bOPleLmmCde/joXuKNOd3vWWgkyu/n7MPwPi8UaXVygz78AQYUjGVQAxpUW9rjnGqlPKQIL64fadnHK0zaq6BQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/jjoptimist/home", + "files": [ + { + "name": "config.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n)\n\ntype Config struct {\n\tTitle string\n\tDescription string\n\tGithub string\n}\n\nvar config = Config{\n\tTitle: \"JJOptimist's Home Realm 🏠\",\n\tDescription: \"Exploring Gno and building on-chain\",\n\tGithub: \"jjoptimist\",\n}\n\nvar Ownable = ownable.NewWithAddress(std.Address(\"g16vfw3r7zuz43fhky3xfsuc2hdv9tnhvlkyn0nj\"))\n\nfunc GetConfig() Config {\n\treturn config\n}\n\nfunc UpdateConfig(newTitle, newDescription, newGithub string) {\n\tOwnable.AssertCallerIsOwner()\n\tconfig.Title = newTitle\n\tconfig.Description = newDescription\n\tconfig.Github = newGithub\n}\n" + }, + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/r/leon/hof\"\n)\n\nconst (\n\tgnomeArt1 = ` /\\\n / \\\n ,,,,,\n(o.o)\n(\\_/)\n-\"-\"-`\n\n\tgnomeArt2 = ` /\\\n / \\\n ,,,,,\n(^.^)\n(\\_/)\n -\"-`\n\n\tgnomeArt3 = ` /\\\n / \\\n ,,,,,\n(*.*)\n(\\_/)\n\"-\"-\"`\n\n\tgnomeArt4 = ` /\\\n / \\\n ,,,,,\n(o.~)\n(\\_/)\n -\"-`\n)\n\nvar creation time.Time\n\nfunc getGnomeArt(height int64) string {\n\tvar art string\n\tswitch {\n\tcase height%7 == 0:\n\t\tart = gnomeArt4 // winking gnome\n\tcase height%5 == 0:\n\t\tart = gnomeArt3 // starry-eyed gnome\n\tcase height%3 == 0:\n\t\tart = gnomeArt2 // happy gnome\n\tdefault:\n\t\tart = gnomeArt1 // regular gnome\n\t}\n\treturn \"```\\n\" + art + \"\\n```\\n\"\n}\n\nfunc init() {\n\tcreation = time.Now()\n\thof.Register(\"JJoptimist's Home Realm\", \"\")\n}\n\nfunc Render(path string) string {\n\theight := std.ChainHeight()\n\n\toutput := \"# \" + config.Title + \"\\n\\n\"\n\n\toutput += \"## About Me\\n\"\n\toutput += \"- 👋 Hi, I'm JJOptimist\\n\"\n\toutput += getGnomeArt(height)\n\toutput += \"- 🌱 \" + config.Description + \"\\n\"\n\n\toutput += \"## Contact\\n\"\n\toutput += \"- 📫 GitHub: [\" + config.Github + \"](https://github.com/\" + config.Github + \")\\n\"\n\n\toutput += \"\\n---\\n\"\n\toutput += \"_Realm created: \" + creation.Format(\"2006-01-02 15:04:05 UTC\") + \"_\\n\"\n\toutput += \"_Owner: \" + Ownable.Owner().String() + \"_\\n\"\n\toutput += \"_Current Block Height: \" + strconv.Itoa(int(height)) + \"_\"\n\n\treturn output\n}\n" + }, + { + "name": "home_test.gno", + "body": "package home\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestConfig(t *testing.T) {\n\tcfg := GetConfig()\n\n\tif cfg.Title != \"JJOptimist's Home Realm 🏠\" {\n\t\tt.Errorf(\"Expected title to be 'JJOptimist's Home Realm 🏠', got %s\", cfg.Title)\n\t}\n\tif cfg.Description != \"Exploring Gno and building on-chain\" {\n\t\tt.Errorf(\"Expected description to be 'Exploring Gno and building on-chain', got %s\", cfg.Description)\n\t}\n\tif cfg.Github != \"jjoptimist\" {\n\t\tt.Errorf(\"Expected github to be 'jjoptimist', got %s\", cfg.Github)\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\toutput := Render(\"\")\n\n\t// Test that required sections are present\n\tif !strings.Contains(output, \"# \"+config.Title) {\n\t\tt.Error(\"Rendered output missing title\")\n\t}\n\tif !strings.Contains(output, \"## About Me\") {\n\t\tt.Error(\"Rendered output missing About Me section\")\n\t}\n\tif !strings.Contains(output, \"## Contact\") {\n\t\tt.Error(\"Rendered output missing Contact section\")\n\t}\n\tif !strings.Contains(output, config.Description) {\n\t\tt.Error(\"Rendered output missing description\")\n\t}\n\tif !strings.Contains(output, config.Github) {\n\t\tt.Error(\"Rendered output missing github link\")\n\t}\n}\n\nfunc TestGetGnomeArt(t *testing.T) {\n\ttests := []struct {\n\t\theight int64\n\t\texpected string\n\t}{\n\t\t{7, gnomeArt4}, // height divisible by 7\n\t\t{5, gnomeArt3}, // height divisible by 5\n\t\t{3, gnomeArt2}, // height divisible by 3\n\t\t{2, gnomeArt1}, // default case\n\t}\n\n\tfor _, tt := range tests {\n\t\tart := getGnomeArt(tt.height)\n\t\tif !strings.Contains(art, tt.expected) {\n\t\t\tt.Errorf(\"For height %d, expected art containing %s, got %s\", tt.height, tt.expected, art)\n\t\t}\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "N9i9uNMrtoVjNeQdCcZ3g22Pzj3VSEr82Fwo7T+hqjjtkfg4H0xEMQphuQeuvAx1xU6SxMQfBpvk4XcjjLY+Ag==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/leon/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/ufmt\"\n\n\t\"gno.land/r/demo/art/gnoface\"\n\t\"gno.land/r/demo/art/millipede\"\n\t\"gno.land/r/demo/mirror\"\n\t\"gno.land/r/leon/config\"\n\t\"gno.land/r/leon/hof\"\n)\n\nvar (\n\tpfp string // link to profile picture\n\tpfpCaption string // profile picture caption\n\tabtMe [2]string\n)\n\nfunc Render(path string) string {\n\tout := \"# Leon's Homepage\\n\\n\"\n\n\tout += renderAboutMe()\n\tout += renderBlogPosts()\n\tout += \"\\n\\n\"\n\tout += renderArt()\n\tout += \"\\n\\n\"\n\tout += config.Banner()\n\tout += \"\\n\\n\"\n\n\treturn out\n}\n\nfunc init() {\n\thof.Register(\"Leon's Home Realm\", \"\")\n\tmirror.Register(std.CurrentRealm().PkgPath(), Render)\n\n\tpfp = \"https://i.imgflip.com/91vskx.jpg\"\n\tpfpCaption = \"[My favourite painting \u0026 pfp](https://en.wikipedia.org/wiki/Wanderer_above_the_Sea_of_Fog)\"\n\tabtMe = [2]string{\n\t\t`### About me\nHi, I'm Leon, a DevRel Engineer at gno.land. I am a tech enthusiast, \nlife-long learner, and sharer of knowledge.`,\n\t\t`### Contributions\nMy contributions to gno.land can mainly be found \n[here](https://github.com/gnolang/gno/issues?q=sort:updated-desc+author:leohhhn).\n\nTODO import r/gh`,\n\t}\n}\n\nfunc UpdatePFP(url, caption string) {\n\tif !config.IsAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tpfp = url\n\tpfpCaption = caption\n}\n\nfunc UpdateAboutMe(col1, col2 string) {\n\tif !config.IsAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tabtMe[0] = col1\n\tabtMe[1] = col2\n}\n\nfunc renderBlogPosts() string {\n\tout := \"\"\n\t// out += \"## Leon's Blog Posts\"\n\n\t// todo fetch blog posts authored by @leohhhn\n\t// and render them\n\treturn out\n}\n\nfunc renderAboutMe() string {\n\tout := \"\u003cdiv class='columns-3'\u003e\"\n\n\tout += \"\u003cdiv\u003e\\n\\n\"\n\tout += ufmt.Sprintf(\"![my profile pic](%s)\\n\\n%s\\n\\n\", pfp, pfpCaption)\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tout += \"\u003cdiv\u003e\\n\\n\"\n\tout += abtMe[0] + \"\\n\\n\"\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tout += \"\u003cdiv\u003e\\n\\n\"\n\tout += abtMe[1] + \"\\n\\n\"\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\tout += \"\u003c/div\u003e\u003c!-- /columns-3 --\u003e\\n\\n\"\n\n\treturn out\n}\n\nfunc renderArt() string {\n\tout := `\u003cdiv class=\"jumbotron\"\u003e` + \"\\n\\n\"\n\tout += \"# Gno Art\\n\\n\"\n\n\tout += \"\u003cdiv class='columns-3'\u003e\"\n\n\tout += renderGnoFace()\n\tout += renderMillipede()\n\tout += \"Empty spot :/\"\n\n\tout += \"\u003c/div\u003e\u003c!-- /columns-3 --\u003e\\n\\n\"\n\n\tout += \"This art is dynamic; it will change with every new block.\\n\\n\"\n\tout += `\u003c/div\u003e\u003c!-- /jumbotron --\u003e` + \"\\n\"\n\n\treturn out\n}\n\nfunc renderGnoFace() string {\n\tout := \"\u003cdiv\u003e\\n\\n\"\n\tout += gnoface.Render(strconv.Itoa(int(std.ChainHeight())))\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\treturn out\n}\n\nfunc renderMillipede() string {\n\tout := \"\u003cdiv\u003e\\n\\n\"\n\tout += \"Millipede\\n\\n\"\n\tout += \"```\\n\" + millipede.Draw(int(std.ChainHeight())%10+1) + \"```\\n\"\n\tout += \"\u003c/div\u003e\\n\\n\"\n\n\treturn out\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "GEW2+TYDrZwo37eDk1qnjYQiLQoPZo5WtwBlKJjme3sJFCOwUHnUvIdoAzEDQEV3rf9BBEU82Oi9H3Xd1vu7Aw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/manfred/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nfunc Render(path string) string {\n\treturn \"Moved to r/moul\"\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "tgF+AQscFqTiJMwNUYZYkHRiXGQPTbakJ3uGLZZTRB1WUCN6Q5tIidz3/iN+c5Ta5s7yrrYB1zGbCK8Jlb5NAA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/mason/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"gno.land/p/mason/md\"\n)\n\nconst (\n\tgnomeArt1 = ` /\\\n / \\\n ,,,,,\n (o.o)\n (\\_/)\n -\"-\"-`\n\tdgnonut = `\n #$$$$$$$$* \n #$$@@@@@@@@@@$$$# \n #$$@@@@@@@@@@@@@$$$$# \n #$$$@@@@$$$$$$$$$$$$$$$$* \n #$$$$$$$$$$$$$$$$$$$$$$$$$#! \n #$$$$$$$$$############$$$$$##* \n !##$$$$$$####**********#####$###* \n =##$$$$$###****!!!!!!!!!***######*! \n *##$$$###***!!!!!!==!!!!!!**######*= \n !*#######***!!!=;;;;;====!!!!**####** \n !*#######**!!!==;;::::::;;==!!!**###**! \n !*######***!==;::~~~~~~:::;;=!!!***#***= \n =**#####**!!==;::~-,,,,,--~:;;=!!!******! \n !**####***!==;:~-,.. ..,,-~:;==!!******!; \n ;!**###***!!=;:~-,. ..-~:;==!!*****!= \n =!*******!!==::-. .,-::==!!*****!= \n =!*******!!=;:~, .-~:;=!!!****!=: \n ~=!*******!==;:-. .,-:;=!!!****!=; \n :=!*******!==;~,. ,-:;==!!!***!=; \n :=!******!!==:~, ,-:;=!!!***!!=; \n :=!!*****!!=;:~, ,~:;=!!****!!=;- \n :=!!!****!!==;~, -~;==!!****!!=;- \n :;=!!*****!!=;:- -:;=!!*****!!=:- \n ~;=!!!****!!==;~ :;=!!*****!!!;:- \n ~;==!!****!!!==: ;=!!******!!=;:, \n ~:==!!!****!!!=;~ :=!********!!=;:. \n -:;==!!*****!!!!; =!*********!==;: \n ,~;==!!*******!!== =**#####****!==:~ \n ,~:;=!!!!*********! **#######***!!=;~- \n -~;;=!!!!**********! *##$$$$$###***!!=:~. \n ,~:;==!!!****##########$$$$$$$$###****!=;:~ \n -~:;==!!!***####$$$$$$@@@@@$$$###**!!=;:~, \n ,-~:;=!!!***####$$$$@@@@@@$$$$##**!!!=;:-. \n -~:;;=!!!***###$$$$$@@@@$$$$##***!!=;:-. \n .-~:;;=!!!***###$$$$$$$$$$$##***!==;:~- \n .-~:;==!!!!**####$$$$$$$###**!!==;:~- \n ,-~::;==!!!!***########****!!==;:~-. \n ,-~:;;==!!!!!***********!!!==;:~,. \n ,,~~::;====!!!!!!!!!!!!!==;::~,. \n .,-~::;;;===!!!!!!!!===;::~-,. \n ,--~~:;;;;========;;::~--. \n .,,-~~:::::::::::~~~-,,. \n ..,---~~~~~~~~~--,.. \n ..,,,,,,,,,... \n ...`\n)\n\nfunc Render(path string) string {\n\thome := md.New()\n\thome.H1(\"Mason's Realm\")\n\n\thome.Im(\"https://cdn.esawebb.org/archives/images/screen/weic2428a.jpg\", \"Placeholder\")\n\thome.P(\"Welcome to my realm. \" + md.Link(\"github\", \"https://github.com/masonmcbride\"))\n\n\thome.H3(\"Dgnonut\")\n\thome.Code(dgnonut)\n\n\thome.H3(\"More\")\n\thome.Code(gnomeArt1)\n\thome.Bullet(\"Credit to \" + md.Link(\"JJOptimist\", \"https://gno.land/r/jjoptimist/home\") + \" for this gnome art.\")\n\thome.Bullet(\"I'm testing out my markdown system.\")\n\treturn home.Render()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "y6SK0L+HYwt9fEF6dh8xEQKGgsRxh8PpTRgE2ly//kqKsSuQUbMBAh5Y3zBb99/M5MDCy+cWnpNdOg2vxIDADw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/matijamarjanovic/home", + "files": [ + { + "name": "config.gno", + "body": "package home\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nvar (\n\tmainAddr = std.Address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\") // matija's main address\n\tbackupAddr std.Address // backup address\n\n\terrorInvalidAddr = errors.New(\"config: invalid address\")\n\terrorUnauthorized = errors.New(\"config: unauthorized\")\n)\n\nfunc Address() std.Address {\n\treturn mainAddr\n}\n\nfunc Backup() std.Address {\n\treturn backupAddr\n}\n\nfunc SetAddress(newAddress std.Address) error {\n\tif !newAddress.IsValid() {\n\t\treturn errorInvalidAddr\n\t}\n\n\tif err := checkAuthorized(); err != nil {\n\t\treturn err\n\t}\n\n\tmainAddr = newAddress\n\treturn nil\n}\n\nfunc SetBackup(newAddress std.Address) error {\n\tif !newAddress.IsValid() {\n\t\treturn errorInvalidAddr\n\t}\n\n\tif err := checkAuthorized(); err != nil {\n\t\treturn err\n\t}\n\n\tbackupAddr = newAddress\n\treturn nil\n}\n\nfunc checkAuthorized() error {\n\tcaller := std.OriginCaller()\n\tif caller != mainAddr \u0026\u0026 caller != backupAddr {\n\t\treturn errorUnauthorized\n\t}\n\n\treturn nil\n}\n\nfunc AssertAuthorized() {\n\tcaller := std.OriginCaller()\n\tif caller != mainAddr \u0026\u0026 caller != backupAddr {\n\t\tpanic(errorUnauthorized)\n\t}\n}\n" + }, + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/r/leon/hof\"\n)\n\nvar (\n\tpfp string // link to profile picture\n\tpfpCaption string // profile picture caption\n\tabtMe string\n\n\tmodernVotes int64\n\tclassicVotes int64\n\tminimalVotes int64\n\tcurrentTheme string\n\n\tmodernLink string\n\tclassicLink string\n\tminimalLink string\n)\n\nfunc init() {\n\tpfp = \"https://static.artzone.ai/media/38734/conversions/IPF9dR7ro7n05CmMLLrXIojycr1qdLFxgutaaanG-w768.webp\"\n\tpfpCaption = \"My profile picture - Tarantula Nebula\"\n\tabtMe = `Motivated Computer Science student with strong\n analytical and problem-solving skills. Proficient in\n programming and version control, with a high level of\n focus and attention to detail. Eager to apply academic\n knowledge to real-world projects and contribute to\n innovative technology solutions.\n In addition to my academic pursuits,\n I enjoy traveling and staying active through weightlifting.\n I have a keen interest in electronic music and often explore various genres.\n I believe in maintaining a balanced lifestyle that complements my professional development.`\n\n\tmodernVotes = 0\n\tclassicVotes = 0\n\tminimalVotes = 0\n\tcurrentTheme = \"classic\"\n\tmodernLink = \"https://www.google.com\"\n\tclassicLink = \"https://www.google.com\"\n\tminimalLink = \"https://www.google.com\"\n\thof.Register(\"Matija Marijanovic's Home Realm\", \"\")\n}\n\nfunc UpdatePFP(url, caption string) {\n\tAssertAuthorized()\n\tpfp = url\n\tpfpCaption = caption\n}\n\nfunc UpdateAboutMe(col1 string) {\n\tAssertAuthorized()\n\tabtMe = col1\n}\n\nfunc maxOfThree(a, b, c int64) int64 {\n\tmax := a\n\tif b \u003e max {\n\t\tmax = b\n\t}\n\tif c \u003e max {\n\t\tmax = c\n\t}\n\treturn max\n}\n\nfunc VoteModern() {\n\tugnotAmount := std.OriginSend().AmountOf(\"ugnot\")\n\tvotes := ugnotAmount\n\tmodernVotes += votes\n\tupdateCurrentTheme()\n}\n\nfunc VoteClassic() {\n\tugnotAmount := std.OriginSend().AmountOf(\"ugnot\")\n\tvotes := ugnotAmount\n\tclassicVotes += votes\n\tupdateCurrentTheme()\n}\n\nfunc VoteMinimal() {\n\tugnotAmount := std.OriginSend().AmountOf(\"ugnot\")\n\tvotes := ugnotAmount\n\tminimalVotes += votes\n\tupdateCurrentTheme()\n}\n\nfunc updateCurrentTheme() {\n\tmaxVotes := maxOfThree(modernVotes, classicVotes, minimalVotes)\n\n\tif maxVotes == modernVotes {\n\t\tcurrentTheme = \"modern\"\n\t} else if maxVotes == classicVotes {\n\t\tcurrentTheme = \"classic\"\n\t} else {\n\t\tcurrentTheme = \"minimal\"\n\t}\n}\n\nfunc CollectBalance() {\n\tAssertAuthorized()\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\townerAddr := Address()\n\n\tbanker.SendCoins(std.CurrentRealm().Address(), ownerAddr, banker.GetCoins(std.CurrentRealm().Address()))\n}\n\nfunc Render(path string) string {\n\tvar sb strings.Builder\n\n\t// Theme-specific header styling\n\tswitch currentTheme {\n\tcase \"modern\":\n\t\t// Modern theme - Clean and minimalist with emojis\n\t\tsb.WriteString(md.H1(\"🚀 Matija's Space\"))\n\t\tsb.WriteString(md.Image(pfpCaption, pfp))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.Italic(pfpCaption))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(abtMe)\n\t\tsb.WriteString(\"\\n\")\n\n\tcase \"minimal\":\n\t\t// Minimal theme - No emojis, minimal formatting\n\t\tsb.WriteString(md.H1(\"Matija Marjanovic\"))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(abtMe)\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.Image(pfpCaption, pfp))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(pfpCaption)\n\t\tsb.WriteString(\"\\n\")\n\n\tdefault: // classic\n\t\t// Classic theme - Traditional blog style with decorative elements\n\t\tsb.WriteString(md.H1(\"✨ Welcome to Matija's Homepage ✨\"))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.Image(pfpCaption, pfp))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(pfpCaption)\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(md.H2(\"About me\"))\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(abtMe)\n\t\tsb.WriteString(\"\\n\")\n\t}\n\n\t// Theme-specific voting section\n\tswitch currentTheme {\n\tcase \"modern\":\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(md.H2(\"🎨 Theme Selector\"))\n\t\tsb.WriteString(\"Choose your preferred viewing experience:\\n\")\n\t\titems := []string{\n\t\t\tmd.Link(ufmt.Sprintf(\"Modern Design (%d votes)\", modernVotes), modernLink),\n\t\t\tmd.Link(ufmt.Sprintf(\"Classic Style (%d votes)\", classicVotes), classicLink),\n\t\t\tmd.Link(ufmt.Sprintf(\"Minimal Look (%d votes)\", minimalVotes), minimalLink),\n\t\t}\n\t\tsb.WriteString(md.BulletList(items))\n\n\tcase \"minimal\":\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.H3(\"Theme Selection\"))\n\t\tsb.WriteString(ufmt.Sprintf(\"Current theme: %s\\n\", currentTheme))\n\t\tsb.WriteString(ufmt.Sprintf(\"Votes - Modern: %d | Classic: %d | Minimal: %d\\n\",\n\t\t\tmodernVotes, classicVotes, minimalVotes))\n\t\tsb.WriteString(md.Link(\"Modern\", modernLink))\n\t\tsb.WriteString(\" | \")\n\t\tsb.WriteString(md.Link(\"Classic\", classicLink))\n\t\tsb.WriteString(\" | \")\n\t\tsb.WriteString(md.Link(\"Minimal\", minimalLink))\n\t\tsb.WriteString(\"\\n\")\n\n\tdefault: // classic\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(md.H2(\"✨ Theme Customization ✨\"))\n\t\tsb.WriteString(md.Bold(\"Choose Your Preferred Theme:\"))\n\t\tsb.WriteString(\"\\n\\n\")\n\t\titems := []string{\n\t\t\tufmt.Sprintf(\"Modern 🚀 (%d votes) - %s\", modernVotes, md.Link(\"Vote\", modernLink)),\n\t\t\tufmt.Sprintf(\"Classic ✨ (%d votes) - %s\", classicVotes, md.Link(\"Vote\", classicLink)),\n\t\t\tufmt.Sprintf(\"Minimal ⚡ (%d votes) - %s\", minimalVotes, md.Link(\"Vote\", minimalLink)),\n\t\t}\n\t\tsb.WriteString(md.BulletList(items))\n\t}\n\n\t// Theme-specific footer/links section\n\tswitch currentTheme {\n\tcase \"modern\":\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(md.Link(\"GitHub\", \"https://github.com/matijamarjanovic\"))\n\t\tsb.WriteString(\" | \")\n\t\tsb.WriteString(md.Link(\"LinkedIn\", \"https://www.linkedin.com/in/matijamarjanovic\"))\n\t\tsb.WriteString(\"\\n\")\n\n\tcase \"minimal\":\n\t\tsb.WriteString(\"\\n\")\n\t\tsb.WriteString(md.Link(\"GitHub\", \"https://github.com/matijamarjanovic\"))\n\t\tsb.WriteString(\" | \")\n\t\tsb.WriteString(md.Link(\"LinkedIn\", \"https://www.linkedin.com/in/matijamarjanovic\"))\n\t\tsb.WriteString(\"\\n\")\n\n\tdefault: // classic\n\t\tsb.WriteString(md.HorizontalRule())\n\t\tsb.WriteString(md.H3(\"✨ Connect With Me\"))\n\t\titems := []string{\n\t\t\tmd.Link(\"🌟 GitHub\", \"https://github.com/matijamarjanovic\"),\n\t\t\tmd.Link(\"💼 LinkedIn\", \"https://www.linkedin.com/in/matijamarjanovic\"),\n\t\t}\n\t\tsb.WriteString(md.BulletList(items))\n\t}\n\n\treturn sb.String()\n}\n\nfunc UpdateModernLink(link string) {\n\tAssertAuthorized()\n\tmodernLink = link\n}\n\nfunc UpdateClassicLink(link string) {\n\tAssertAuthorized()\n\tclassicLink = link\n}\n\nfunc UpdateMinimalLink(link string) {\n\tAssertAuthorized()\n\tminimalLink = link\n}\n" + }, + { + "name": "home_test.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// Helper function to set up test environment\nfunc setupTest() {\n\ttesting.SetOriginCaller(std.Address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\"))\n}\n\nfunc TestUpdatePFP(t *testing.T) {\n\tsetupTest()\n\tpfp = \"\"\n\tpfpCaption = \"\"\n\n\tUpdatePFP(\"https://example.com/pic.png\", \"New Caption\")\n\n\turequire.Equal(t, pfp, \"https://example.com/pic.png\", \"Profile picture URL should be updated\")\n\turequire.Equal(t, pfpCaption, \"New Caption\", \"Profile picture caption should be updated\")\n}\n\nfunc TestUpdateAboutMe(t *testing.T) {\n\tsetupTest()\n\tabtMe = \"\"\n\n\tUpdateAboutMe(\"This is my new bio.\")\n\n\turequire.Equal(t, abtMe, \"This is my new bio.\", \"About Me should be updated\")\n}\n\nfunc TestVoteModern(t *testing.T) {\n\tsetupTest()\n\tmodernVotes, classicVotes, minimalVotes = 0, 0, 0\n\n\tcoinsSent := std.NewCoins(std.NewCoin(\"ugnot\", 75000000))\n\tcoinsSpent := std.NewCoins(std.NewCoin(\"ugnot\", 1))\n\n\ttesting.SetOriginSend(coinsSent)\n\ttesting.SetOriginSpend(coinsSpent)\n\tVoteModern()\n\n\tuassert.Equal(t, int64(75000000), modernVotes, \"Modern votes should be calculated correctly\")\n\tuassert.Equal(t, \"modern\", currentTheme, \"Theme should be updated to modern\")\n}\n\nfunc TestVoteClassic(t *testing.T) {\n\tsetupTest()\n\tmodernVotes, classicVotes, minimalVotes = 0, 0, 0\n\n\tcoinsSent := std.NewCoins(std.NewCoin(\"ugnot\", 75000000))\n\tcoinsSpent := std.NewCoins(std.NewCoin(\"ugnot\", 1))\n\n\ttesting.SetOriginSend(coinsSent)\n\ttesting.SetOriginSpend(coinsSpent)\n\tVoteClassic()\n\n\tuassert.Equal(t, int64(75000000), classicVotes, \"Classic votes should be calculated correctly\")\n\tuassert.Equal(t, \"classic\", currentTheme, \"Theme should be updated to classic\")\n}\n\nfunc TestVoteMinimal(t *testing.T) {\n\tsetupTest()\n\tmodernVotes, classicVotes, minimalVotes = 0, 0, 0\n\n\tcoinsSent := std.NewCoins(std.NewCoin(\"ugnot\", 75000000))\n\tcoinsSpent := std.NewCoins(std.NewCoin(\"ugnot\", 1))\n\n\ttesting.SetOriginSend(coinsSent)\n\ttesting.SetOriginSpend(coinsSpent)\n\tVoteMinimal()\n\n\tuassert.Equal(t, int64(75000000), minimalVotes, \"Minimal votes should be calculated correctly\")\n\tuassert.Equal(t, \"minimal\", currentTheme, \"Theme should be updated to minimal\")\n}\n\nfunc TestRender(t *testing.T) {\n\tsetupTest()\n\t// Reset the state to known values\n\tmodernVotes, classicVotes, minimalVotes = 0, 0, 0\n\tcurrentTheme = \"classic\"\n\tpfp = \"https://example.com/pic.png\"\n\tpfpCaption = \"Test Caption\"\n\tabtMe = \"Test About Me\"\n\n\tout := Render(\"\")\n\turequire.NotEqual(t, out, \"\", \"Render output should not be empty\")\n\n\t// Test classic theme specific content\n\tuassert.True(t, strings.Contains(out, \"✨ Welcome to Matija's Homepage ✨\"), \"Classic theme should have correct header\")\n\tuassert.True(t, strings.Contains(out, pfp), \"Should contain profile picture URL\")\n\tuassert.True(t, strings.Contains(out, pfpCaption), \"Should contain profile picture caption\")\n\tuassert.True(t, strings.Contains(out, \"About me\"), \"Should contain About me section\")\n\tuassert.True(t, strings.Contains(out, abtMe), \"Should contain about me content\")\n\tuassert.True(t, strings.Contains(out, \"Theme Customization\"), \"Should contain theme customization section\")\n\tuassert.True(t, strings.Contains(out, \"Connect With Me\"), \"Should contain connect section\")\n}\n\nfunc TestRenderModernTheme(t *testing.T) {\n\tsetupTest()\n\tmodernVotes, classicVotes, minimalVotes = 100, 0, 0\n\tcurrentTheme = \"modern\"\n\tupdateCurrentTheme()\n\n\tout := Render(\"\")\n\tuassert.True(t, strings.Contains(out, \"🚀 Matija's Space\"), \"Modern theme should have correct header\")\n}\n\nfunc TestRenderMinimalTheme(t *testing.T) {\n\tsetupTest()\n\tmodernVotes, classicVotes, minimalVotes = 0, 0, 100\n\tcurrentTheme = \"minimal\"\n\tupdateCurrentTheme()\n\n\tout := Render(\"\")\n\tuassert.True(t, strings.Contains(out, \"Matija Marjanovic\"), \"Minimal theme should have correct header\")\n}\n\nfunc TestUpdateLinks(t *testing.T) {\n\tsetupTest()\n\n\tnewLink := \"https://example.com/vote\"\n\n\tUpdateModernLink(newLink)\n\turequire.Equal(t, modernLink, newLink, \"Modern link should be updated\")\n\n\tUpdateClassicLink(newLink)\n\turequire.Equal(t, classicLink, newLink, \"Classic link should be updated\")\n\n\tUpdateMinimalLink(newLink)\n\turequire.Equal(t, minimalLink, newLink, \"Minimal link should be updated\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "fosS/XFv01jL/sCgM6n7vON3ra/YUz/7o/dBrTSYicY4QDleAV633pcPgdknOKdGSHBhSYAAyqYMVjCN76ORAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "tokenhub", + "path": "gno.land/r/matijamarjanovic/tokenhub", + "files": [ + { + "name": "errors.gno", + "body": "package tokenhub\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\tErrNFTAlreadyRegistered = errors.New(\"NFT already registered\")\n\tErrNFTNotFound = errors.New(\"NFT not found\")\n\tErrMTAlreadyRegistered = errors.New(\"multi-token already registered\")\n\tErrMTNotFound = errors.New(\"multi-token not found\")\n\tErrMTInfoNotFound = errors.New(\"multi-token info not found\")\n\tErrNFTtokIDNotExists = errors.New(\"NFT token ID does not exists\")\n\tErrNFTNotMetadata = errors.New(\"NFT must implement IGRC721CollectionMetadata\")\n)\n" + }, + { + "name": "getters.gno", + "body": "package tokenhub\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc1155\"\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/demo/grc20reg\"\n\t\"gno.land/r/sys/users\"\n)\n\n// GetUserTokenBalances returns a string of all the grc20 tokens the user owns\nfunc GetUserTokenBalances(userNameOrAddress string) string {\n\treturn getTokenBalances(userNameOrAddress, false)\n}\n\n// GetUserTokenBalancesNonZero returns a string of all the grc20 tokens the user owns, but only the ones that have a balance greater than 0\nfunc GetUserTokenBalancesNonZero(userNameOrAddress string) string {\n\treturn getTokenBalances(userNameOrAddress, true)\n}\n\n// GetUserNFTBalances returns a string of all the NFTs the user owns\nfunc GetUserNFTBalances(userNameOrAddress string) string {\n\treturn getNFTBalances(userNameOrAddress)\n}\n\n// GetUserMultiTokenBalances returns a string of all the multi-tokens the user owns\nfunc GetUserMultiTokenBalances(userNameOrAddress string) string {\n\treturn getMultiTokenBalances(userNameOrAddress, false)\n}\n\n// GetUserMultiTokenBalancesNonZero returns a string of all the multi-tokens the user owns, but only the ones that have a balance greater than 0\nfunc GetUserMultiTokenBalancesNonZero(userNameOrAddress string) string {\n\treturn getMultiTokenBalances(userNameOrAddress, true)\n}\n\n// GetToken returns a token instance for a given key\nfunc GetToken(key string) *grc20.Token {\n\tgetter := grc20reg.Get(key)\n\treturn getter()\n}\n\n// MustGetToken returns a token instance for a given key, panics if the token is not found\nfunc MustGetToken(key string) *grc20.Token {\n\tgetter := grc20reg.MustGet(key)\n\tif getter == nil {\n\t\tpanic(\"unknown token: \" + key)\n\t}\n\treturn getter()\n}\n\n// GetNFT returns an NFT instance for a given key\nfunc GetNFT(key string) grc721.IGRC721 {\n\tnftGetter, ok := registeredNFTs.Get(key)\n\tif !ok {\n\t\treturn nil\n\t}\n\treturn (nftGetter.(grc721.NFTGetter))()\n}\n\n// MustGetNFT returns an NFT instance for a given key, panics if the NFT is not found\nfunc MustGetNFT(key string) grc721.IGRC721 {\n\tnftGetter := GetNFT(key)\n\tif nftGetter == nil {\n\t\tpanic(\"unknown NFT: \" + key)\n\t}\n\treturn nftGetter\n}\n\n// GetMultiToken returns a multi-token instance for a given key\nfunc GetMultiToken(key string) grc1155.IGRC1155 {\n\tinfo, ok := registeredMTs.Get(key)\n\tif !ok {\n\t\treturn nil\n\t}\n\tmt := info.(GRC1155TokenInfo).Collection\n\treturn mt()\n}\n\n// MustGetMultiToken returns a multi-token instance for a given key, panics if the multi-token is not found\nfunc MustGetMultiToken(key string) grc1155.IGRC1155 {\n\tinfo := GetMultiToken(key)\n\tif info == nil {\n\t\tpanic(\"unknown multi-token: \" + key)\n\t}\n\treturn info\n}\n\n// GetAllNFTs returns a string of all the NFTs registered\nfunc GetAllNFTs() string {\n\tvar out string\n\tregisteredNFTs.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tout += ufmt.Sprintf(\"NFT:%s,\", key)\n\t\treturn false\n\t})\n\treturn out\n}\n\n// GetAllTokens returns a string of all the tokens registered\nfunc GetAllTokens() string {\n\tvar out string\n\tgrc20reg.GetRegistry().Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tout += \"Token:\" + key + \",\"\n\t\treturn false\n\t})\n\treturn out\n}\n\n// GetAllTokenWithDetails returns a string of all the tokens registered with their details\nfunc GetAllTokenWithDetails() string {\n\tvar out string\n\tgrc20reg.GetRegistry().Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttokenGetter := value.(grc20.TokenGetter)\n\t\ttoken := tokenGetter()\n\t\tout += ufmt.Sprintf(\"Token:%s,Name:%s,Symbol:%s,Decimals:%d;\", key, token.GetName(), token.GetSymbol(), token.GetDecimals())\n\t\treturn false\n\t})\n\treturn out\n}\n\n// GetAllMultiTokens returns a string of all the multi-tokens registered\nfunc GetAllMultiTokens() string {\n\tvar out string\n\tregisteredMTs.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tout += \"MultiToken:\" + key + \",\"\n\t\treturn false\n\t})\n\treturn out\n}\n\n// GetAllRegistered returns a string of all the registered tokens, NFTs and multi-tokens\nfunc GetAllRegistered() string {\n\treturn GetAllNFTs() + GetAllTokens() + GetAllMultiTokens()\n}\n\n// getNFTBalances returns a string of all the NFTs the user owns\nfunc getNFTBalances(input string) string {\n\taddr := getAddressForUsername(input)\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid address or username: \" + input)\n\t}\n\tvar out string\n\n\tregisteredNFTs.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tnftGetter := value.(grc721.NFTGetter)\n\t\tnft := nftGetter()\n\t\tkey_parts := strings.Split(key, \".\")\n\t\towner, err := nft.OwnerOf(grc721.TokenID(key_parts[len(key_parts)-1]))\n\t\tif err == nil \u0026\u0026 addr == owner { // show only the nfts owner owns\n\t\t\tout += \"NFT:\" + key + \",\"\n\t\t}\n\t\treturn false\n\t})\n\n\treturn out\n}\n\n// getTokenBalances returns a string of all the tokens the user owns\nfunc getTokenBalances(input string, nonZero bool) string {\n\taddr := getAddressForUsername(input)\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid address or username: \" + input)\n\t}\n\tvar out string\n\tgrc20reg.GetRegistry().Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttokenGetter := value.(grc20.TokenGetter)\n\t\ttoken := tokenGetter()\n\t\tbalance := token.BalanceOf(addr)\n\t\tif !nonZero || balance \u003e 0 {\n\t\t\tout += ufmt.Sprintf(\"Token:%s:%d,\", key, balance)\n\t\t}\n\t\treturn false\n\t})\n\n\treturn out\n}\n\n// getMultiTokenBalances returns a string of all the multi-tokens the user owns\nfunc getMultiTokenBalances(input string, nonZero bool) string {\n\taddr := getAddressForUsername(input)\n\tif !addr.IsValid() {\n\t\tpanic(\"invalid address or username: \" + input)\n\t}\n\tvar out string\n\n\tregisteredMTs.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tinfo := value.(GRC1155TokenInfo)\n\t\tmt := info.Collection()\n\t\tbalance, err := mt.BalanceOf(addr, grc1155.TokenID(info.TokenID))\n\t\tif err == nil {\n\t\t\tif !nonZero || balance \u003e 0 {\n\t\t\t\tout += ufmt.Sprintf(\"MultiToken:%s:%d,\", key, balance)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\treturn out\n}\n\n// getAddressForUsername returns an address for a given username or address\nfunc getAddressForUsername(addrOrName string) std.Address {\n\taddr := std.Address(addrOrName)\n\tif addr.IsValid() {\n\t\treturn addr\n\t}\n\n\tif userData, _ := users.ResolveName(addrOrName); userData != nil {\n\t\treturn userData.Addr()\n\t}\n\n\treturn \"\"\n}\n" + }, + { + "name": "render.gno", + "body": "package tokenhub\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/r/demo/grc20reg\"\n)\n\nconst (\n\ttoken = \"token\" // grc20\n\tnft = \"nft\" // grc721\n\tmt = \"mt\" // grc1155\n)\n\nfunc Render(path string) string {\n\tvar out string\n\n\tswitch {\n\tcase path == \"\":\n\t\tout = renderHome()\n\n\tcase strings.HasPrefix(path, token):\n\t\tout = renderToken(path)\n\n\tcase strings.HasPrefix(path, nft):\n\t\tout = renderNFT(path)\n\n\tcase strings.HasPrefix(path, mt):\n\t\tout = renderMT(path)\n\t}\n\n\treturn out\n}\n\nfunc renderHome() string {\n\tout := md.H1(\"Token Hub\")\n\tout += md.Paragraph(\"A central registry for GRC721 NFTs, GRC20 tokens, and GRC1155 multi-tokens on gno.land\")\n\n\tlinks := []string{\n\t\t\"[GRC20 Tokens](/r/matijamarjanovic/tokenhub:tokens)\",\n\t\t\"[GRC721 NFTs](/r/matijamarjanovic/tokenhub:nfts)\",\n\t\t\"[GRC1155 Multi-Tokens](/r/matijamarjanovic/tokenhub:mts)\",\n\t}\n\tout += md.BulletList(links)\n\n\treturn out\n}\n\nfunc renderToken(path string) string {\n\tout := md.H1(\"GRC20 Tokens\")\n\tvar tokenItems []string\n\n\ttokenPager := pager.NewPager(grc20reg.GetRegistry(), pageSize, false)\n\tpage := tokenPager.MustGetPageByPath(path)\n\n\tfor _, item := range page.Items {\n\t\ttokenGetter := item.Value.(grc20.TokenGetter)\n\t\ttoken := tokenGetter()\n\t\ttokenItems = append(tokenItems, ufmt.Sprintf(\"%s (%s) [%s]\",\n\t\t\tmd.Bold(token.GetName()),\n\t\t\tmd.InlineCode(token.GetSymbol()),\n\t\t\tmd.InlineCode(item.Key)))\n\t}\n\n\tout += renderItemsList(tokenItems, page, \"No tokens registered yet\")\n\tout += renderFooter()\n\treturn out\n}\n\nfunc renderNFT(path string) string {\n\tout := md.H1(\"GRC721 NFTs\")\n\n\tvar nftItems []string\n\tnftPager := pager.NewPager(registeredNFTs, pageSize, false)\n\tpage := nftPager.MustGetPageByPath(path)\n\n\tfor _, item := range page.Items {\n\t\tnftGetter := item.Value.(grc721.NFTGetter)\n\t\tnft := nftGetter()\n\t\tmetadata, ok := nft.(grc721.IGRC721CollectionMetadata)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tnftItems = append(nftItems, ufmt.Sprintf(\"%s (%s) [%s]\",\n\t\t\tmd.Bold(metadata.Name()),\n\t\t\tmd.InlineCode(metadata.Symbol()),\n\t\t\tmd.InlineCode(item.Key)))\n\t}\n\n\tout += renderItemsList(nftItems, page, \"No NFTs registered yet\")\n\tout += renderFooter()\n\treturn out\n}\n\nfunc renderMT(path string) string {\n\tout := md.H1(\"GRC1155 Multi-Tokens\")\n\tvar mtItems []string\n\n\tmtPager := pager.NewPager(registeredMTs, pageSize, false)\n\tpage := mtPager.MustGetPageByPath(path)\n\n\tfor _, item := range page.Items {\n\t\tinfo := item.Value.(GRC1155TokenInfo)\n\t\tmtItems = append(mtItems, ufmt.Sprintf(\"%s: %s [%s]\",\n\t\t\tmd.Bold(\"TokenID\"),\n\t\t\tmd.InlineCode(info.TokenID),\n\t\t\tmd.InlineCode(item.Key)))\n\t}\n\n\tout += renderItemsList(mtItems, page, \"No multi-tokens registered yet\")\n\tout += renderFooter()\n\treturn out\n}\n\nfunc renderItemsList(items []string, page *pager.Page, emptyMessage string) string {\n\tvar out string\n\tif len(items) == 0 {\n\t\tout += md.Italic(emptyMessage)\n\t\tout += \"\\n\"\n\t\treturn out\n\t}\n\n\tout += md.BulletList(items)\n\tout += \"\\n\"\n\tout += md.HorizontalRule()\n\n\tpicker := page.Picker(page.Pager.PageQueryParam)\n\tif picker != \"\" {\n\t\tout += md.Paragraph(picker)\n\t}\n\n\treturn out\n}\n\nfunc renderFooter() string {\n\tout := \"\\n\"\n\tout += md.Link(\"Back to home\", \"/r/matijamarjanovic/tokenhub\")\n\treturn out\n}\n" + }, + { + "name": "tokenhub.gno", + "body": "package tokenhub\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/fqname\"\n\t\"gno.land/p/demo/grc/grc1155\"\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/r/demo/grc20reg\"\n\t\"gno.land/r/leon/hof\"\n)\n\ntype GRC1155TokenInfo struct {\n\tCollection grc1155.MultiTokenGetter\n\tTokenID string\n}\n\nvar (\n\tregisteredTokens = avl.NewTree() // rlmPath[.slug] -\u003e grc20.TokenGetter\n\tregisteredNFTs = avl.NewTree() // rlmPath[.slug] -\u003e grc721.NFTGetter\n\tregisteredMTs = avl.NewTree() // rlmPath[.slug] -\u003e GRC1155TokenInfo\n)\n\nconst pageSize = 10\n\nfunc init() {\n\thof.Register(\"Token Hub\", \"Registry for tokens and NFTs on gno.land\")\n}\n\n// RegisterToken is a function that uses gno.land/r/demo/grc20reg to register a token\n// It uses the slug to construct a key and then registers the token in the registry\n// The logic is the same as in grc20reg, but it's done here so the key path is callers pkgpath and not of this realm\n// After doing so, the token hub realm uses grc20reg's registry as a read-only avl.Tree\n//\n// Note: register token returns the key path that can be used to retrieve the token\nfunc RegisterToken(tokenGetter grc20.TokenGetter, slug string) string {\n\trlmPath := std.PreviousRealm().PkgPath()\n\tkey := fqname.Construct(rlmPath, slug)\n\tgrc20reg.Register(tokenGetter, key)\n\n\treturn fqname.Construct(std.CurrentRealm().PkgPath(), key)\n}\n\n// RegisterNFT is a function that registers an NFT in an avl.Tree\nfunc RegisterNFT(nftGetter grc721.NFTGetter, collection string, tokenId string) error {\n\tnft := nftGetter()\n\t_, ok := nft.(grc721.IGRC721CollectionMetadata)\n\tif !ok {\n\t\treturn ErrNFTNotMetadata\n\t}\n\n\tnftOwner, err := nft.OwnerOf(grc721.TokenID(tokenId))\n\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !nftOwner.IsValid() {\n\t\treturn ErrNFTtokIDNotExists\n\t}\n\n\trlmPath := std.PreviousRealm().PkgPath()\n\tkey := rlmPath + \".\" + collection + \".\" + tokenId\n\n\tif registeredNFTs.Has(key) {\n\t\treturn ErrNFTAlreadyRegistered\n\t}\n\n\tregisteredNFTs.Set(key, nftGetter)\n\treturn nil\n}\n\n// RegisterMultiToken is a function that registers a multi-token in an avl.Tree\n// The avl.Tree value is a struct defined in this realm. It contains not only the getter (like other token types) but also the tokenID\nfunc RegisterMultiToken(mtGetter grc1155.MultiTokenGetter, tokenID string) error {\n\trlmPath := std.PreviousRealm().PkgPath()\n\n\tkey := rlmPath + \".\" + tokenID\n\n\tif registeredMTs.Has(key) {\n\t\treturn ErrMTAlreadyRegistered\n\t}\n\n\tregisteredMTs.Set(key, GRC1155TokenInfo{\n\t\tCollection: mtGetter,\n\t\tTokenID: tokenID,\n\t})\n\treturn nil\n}\n" + }, + { + "name": "tokenhub_test.gno", + "body": "package tokenhub\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/grc/grc1155\"\n\t\"gno.land/p/demo/grc/grc20\"\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nfunc TestTokenRegistration(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/matijamarjanovic/home\"))\n\n\ttoken, _ := grc20.NewToken(\"Test Token\", \"TEST\", 6)\n\tRegisterToken(token.Getter(), \"test_token\")\n\n\tretrievedToken := GetToken(\"gno.land/r/matijamarjanovic/tokenhub.gno.land/r/matijamarjanovic/home.test_token\")\n\turequire.True(t, retrievedToken != nil, \"Should retrieve registered token\")\n\n\tuassert.Equal(t, \"Test Token\", retrievedToken.GetName(), \"Token name should match\")\n\tuassert.Equal(t, \"TEST\", retrievedToken.GetSymbol(), \"Token symbol should match\")\n}\n\nfunc TestNFTRegistration(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/matijamarjanovic/home\"))\n\n\tnft := grc721.NewBasicNFT(\"Test NFT\", \"TNFT\")\n\tnft.Mint(std.CurrentRealm().Address(), grc721.TokenID(\"1\"))\n\terr := RegisterNFT(nft.Getter(), \"test_nft\", \"1\")\n\turequire.NoError(t, err, \"Should register NFT without error\")\n\n\tretrievedNFT := GetNFT(\"gno.land/r/matijamarjanovic/home.test_nft.1\")\n\turequire.True(t, retrievedNFT != nil, \"Should retrieve registered NFT\")\n\n\tmetadata, ok := retrievedNFT.(grc721.IGRC721CollectionMetadata)\n\turequire.True(t, ok, \"NFT should implement IGRC721CollectionMetadata\")\n\tuassert.Equal(t, \"Test NFT\", metadata.Name(), \"NFT name should match\")\n\tuassert.Equal(t, \"TNFT\", metadata.Symbol(), \"NFT symbol should match\")\n}\n\nfunc TestGRC1155Registration(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/matijamarjanovic/home\"))\n\n\tmt := grc1155.NewBasicGRC1155Token(\"test-uri\")\n\terr := RegisterMultiToken(mt.Getter(), \"1\")\n\turequire.NoError(t, err, \"Should register multi-token without error\")\n\n\tmultiToken := GetMultiToken(\"gno.land/r/matijamarjanovic/home.1\")\n\turequire.True(t, multiToken != nil, \"Should retrieve multi-token\")\n\t_, ok := multiToken.(grc1155.IGRC1155)\n\turequire.True(t, ok, \"Retrieved multi-token should implement IGRC1155\")\n\n\terr = RegisterMultiToken(mt.Getter(), \"1\")\n\tuassert.True(t, err != nil, \"Should not allow duplicate registration\")\n}\n\nfunc TestBalanceRetrieval(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/matijamarjanovic/home\"))\n\n\ttoken, ledger := grc20.NewToken(\"Test Token\", \"TEST\", 6)\n\tRegisterToken(token.Getter(), \"test_tokenn\")\n\tledger.Mint(std.CurrentRealm().Address(), 1000)\n\n\tmt := grc1155.NewBasicGRC1155Token(\"test-uri\")\n\tRegisterMultiToken(mt.Getter(), \"11\")\n\tmt.SafeMint(std.CurrentRealm().Address(), grc1155.TokenID(\"11\"), 5)\n\n\tbalances := GetUserTokenBalances(std.CurrentRealm().Address().String())\n\tuassert.True(t, strings.Contains(balances, \"Token:gno.land/r/matijamarjanovic/tokenhub.gno.land/r/matijamarjanovic/home.test_tokenn:1000\"), \"Should show correct GRC20 balance\")\n\n\tbalances = GetUserNFTBalances(std.CurrentRealm().Address().String())\n\tuassert.True(t, strings.Contains(balances, \"NFT:gno.land/r/matijamarjanovic/home.test_nft.1\"), \"Should show correct NFT balance\") //already minted in test register\n\n\tbalances = GetUserMultiTokenBalances(std.CurrentRealm().Address().String())\n\tuassert.True(t, strings.Contains(balances, \"MultiToken:gno.land/r/matijamarjanovic/home.11:5\"), \"Should show multi-token balance\")\n\n\tnonZeroBalances := GetUserTokenBalancesNonZero(std.CurrentRealm().Address().String())\n\tuassert.True(t, strings.Contains(nonZeroBalances, \"Token:gno.land/r/matijamarjanovic/tokenhub.gno.land/r/matijamarjanovic/home.test_tokenn:1000\"), \"Should show non-zero GRC20 balance\")\n}\n\nfunc TestErrorCases(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/matijamarjanovic/home\"))\n\n\tnft := grc721.NewBasicNFT(\"Test NFT\", \"TNFT\")\n\terr := RegisterNFT(nft.Getter(), \"test_nft\", \"1\")\n\tuassert.True(t, err != nil, \"Should not allow duplicate registration\")\n\n\terr = RegisterNFT(nft.Getter(), \"test_nft\", \"1\")\n\tuassert.True(t, err != nil, \"Should not allow duplicate registration\")\n\n\tmt := grc1155.NewBasicGRC1155Token(\"test-uri\")\n\terr = RegisterMultiToken(mt.Getter(), \"1\")\n\tuassert.True(t, err != nil, \"Should not allow duplicate registration\")\n\n\terr = RegisterMultiToken(mt.Getter(), \"1\")\n\tuassert.True(t, err != nil, \"Should not allow duplicate registration\")\n}\n\nfunc TestTokenListingFunctions(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/matijamarjanovic/home\"))\n\n\tgrc20Token, _ := grc20.NewToken(\"Test Token\", \"TEST\", 6)\n\tRegisterToken(grc20Token.Getter(), \"listing_token\")\n\n\tnftToken := grc721.NewBasicNFT(\"Listing NFT\", \"LNFT\")\n\tnftToken.Mint(std.Address(\"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\"), grc721.TokenID(\"1\"))\n\tRegisterNFT(nftToken.Getter(), \"listing_nft\", \"1\")\n\n\tmultiToken := grc1155.NewBasicGRC1155Token(\"test-uri\")\n\tRegisterMultiToken(multiToken.Getter(), \"listing_mt\")\n\n\tnftList := GetAllNFTs()\n\tuassert.True(t, strings.Contains(nftList, \"NFT:gno.land/r/matijamarjanovic/home.listing_nft.1\"),\n\t\t\"GetAllNFTs should list registered NFT\")\n\n\tgrc20List := GetAllTokens()\n\tuassert.True(t, strings.Contains(grc20List, \"Token:gno.land/r/matijamarjanovic/tokenhub.gno.land/r/matijamarjanovic/home.listing_token\"),\n\t\t\"GetAllGRC20Tokens should list registered token\")\n\n\tgrc1155List := GetAllMultiTokens()\n\tuassert.True(t, strings.Contains(grc1155List, \"MultiToken:gno.land/r/matijamarjanovic/home.listing_mt\"),\n\t\t\"GetAllMultiTokens should list registered multi-token\")\n\n\tcompleteList := GetAllRegistered()\n\tuassert.True(t, strings.Contains(completeList, \"NFT:gno.land/r/matijamarjanovic/home.listing_nft.1\"),\n\t\t\"GetAllTokens should list NFTs\")\n\tuassert.True(t, strings.Contains(completeList, \"Token:gno.land/r/matijamarjanovic/tokenhub.gno.land/r/matijamarjanovic/home.listing_token\"),\n\t\t\"GetAllTokens should list GRC20 tokens\")\n\tuassert.True(t, strings.Contains(completeList, \"MultiToken:gno.land/r/matijamarjanovic/home.listing_mt\"),\n\t\t\"GetAllTokens should list multi-tokens\")\n}\n\nfunc TestMustGetFunctions(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/matijamarjanovic/home\"))\n\n\ttoken, _ := grc20.NewToken(\"Must Token\", \"MUST\", 6)\n\tRegisterToken(token.Getter(), \"must_token\")\n\n\tretrievedToken := MustGetToken(\"gno.land/r/matijamarjanovic/tokenhub.gno.land/r/matijamarjanovic/home.must_token\")\n\tuassert.Equal(t, \"Must Token\", retrievedToken.GetName(), \"Token name should match\")\n\n\tdefer func() {\n\t\tr := recover()\n\t\tuassert.True(t, r != nil, \"MustGetToken should panic for non-existent token\")\n\t\tuassert.True(t, strings.Contains(r.(string), \"unknown token\"), \"Panic message should mention unknown token\")\n\t}()\n\tMustGetToken(\"non_existent_token\")\n\n\tnft := grc721.NewBasicNFT(\"Must NFT\", \"MNFT\")\n\tnft.Mint(std.CurrentRealm().Address(), grc721.TokenID(\"1\"))\n\tRegisterNFT(nft.Getter(), \"must_nft\", \"1\")\n\n\tretrievedNFT := MustGetNFT(\"gno.land/r/matijamarjanovic/home.must_nft.1\")\n\tmetadata, ok := retrievedNFT.(grc721.IGRC721CollectionMetadata)\n\turequire.True(t, ok, \"NFT should implement IGRC721CollectionMetadata\")\n\tuassert.Equal(t, \"Must NFT\", metadata.Name(), \"NFT name should match\")\n\n\tdefer func() {\n\t\tr := recover()\n\t\tuassert.True(t, r != nil, \"MustGetNFT should panic for non-existent NFT\")\n\t\tuassert.True(t, strings.Contains(r.(string), \"unknown NFT\"), \"Panic message should mention unknown NFT\")\n\t}()\n\tMustGetNFT(\"non_existent_nft\")\n\n\tmt := grc1155.NewBasicGRC1155Token(\"must-uri\")\n\tRegisterMultiToken(mt.Getter(), \"must_mt\")\n\n\tretrievedMT := MustGetMultiToken(\"gno.land/r/matijamarjanovic/home.must_mt\")\n\t_, ok = retrievedMT.(grc1155.IGRC1155)\n\turequire.True(t, ok, \"Retrieved multi-token should implement IGRC1155\")\n\n\tdefer func() {\n\t\tr := recover()\n\t\tuassert.True(t, r != nil, \"MustGetMultiToken should panic for non-existent multi-token\")\n\t\tuassert.True(t, strings.Contains(r.(string), \"unknown multi-token\"), \"Panic message should mention unknown multi-token\")\n\t}()\n\tMustGetMultiToken(\"non_existent_mt\")\n}\n\nfunc TestGetAddressForUsername(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/matijamarjanovic/home\"))\n\n\tvalidAddr := \"g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y\"\n\taddr := getAddressForUsername(validAddr)\n\tuassert.Equal(t, validAddr, addr.String(), \"Should return same address for valid address input\")\n\n\tinvalidInput := \"invalid_input\"\n\taddr = getAddressForUsername(invalidInput)\n\tuassert.Equal(t, \"\", addr.String(), \"Should return empty address for invalid input\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "cWmcLDUCZN36l2KDkkfsj1nkeyWIAZ8irR7xM/79WxUjMTnp+nZJOlSH3Hr01NtlorhyGO0XphOVEEq6hrj8Dw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "chess", + "path": "gno.land/r/morgan/chess", + "files": [ + { + "name": "chess.gno", + "body": "// Realm chess implements a Gno chess server.\npackage chess\n\nimport (\n\t\"errors\"\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/morgan/chess\"\n)\n\n// realm state\nvar (\n\t// (not \"games\" because that's too useful a variable name)\n\tgameStore avl.Tree // string (game ID) -\u003e *Game\n\tgameIDCounter seqid.ID\n\n\t// Value must be sorted by game ID, descending\n\tuser2Games avl.Tree // std.Address -\u003e []*Game\n)\n\n// Game represents a chess game.\ntype Game struct {\n\tID string `json:\"id\"`\n\n\tWhite std.Address `json:\"white\"`\n\tBlack std.Address `json:\"black\"`\n\tPosition chess.Position `json:\"position\"`\n\tState GameState `json:\"state\"`\n\tWinner Winner `json:\"winner\"`\n\n\tCreator std.Address `json:\"creator\"`\n\tCreatedAt time.Time `json:\"created_at\"`\n\tDrawOfferer *std.Address `json:\"draw_offerer\"` // set on draw offers\n\tConcluder *std.Address `json:\"concluder\"` // set on non-auto draws, and aborts\n\n\tTime *TimeControl `json:\"time\"`\n}\n\nfunc (g Game) json() string {\n\ts, err := g.MarshalJSON()\n\tcheckErr(err)\n\treturn string(s)\n}\n\n// Winner represents the \"direct\" outcome of a game\n// (white, black or draw?)\ntype Winner byte\n\nconst (\n\tWinnerNone Winner = iota\n\tWinnerWhite\n\tWinnerBlack\n\tWinnerDraw\n)\n\nvar winnerString = [...]string{\n\tWinnerNone: \"none\",\n\tWinnerWhite: \"white\",\n\tWinnerBlack: \"black\",\n\tWinnerDraw: \"draw\",\n}\n\n// GameState represents the current game state.\ntype GameState byte\n\nconst (\n\tGameStateInvalid = iota\n\n\tGameStateOpen\n\n\t// \"automatic\" endgames following moves\n\tGameStateCheckmated\n\tGameStateStalemate\n\tGameStateDrawn75Move\n\tGameStateDrawn5Fold\n\n\t// single-party draws\n\tGameStateDrawn50Move\n\tGameStateDrawn3Fold\n\tGameStateDrawnInsufficient\n\n\t// timeout by either player\n\tGameStateTimeout\n\t// aborted within first two moves\n\tGameStateAborted\n\t// resignation by either player\n\tGameStateResigned\n\t// draw by agreement\n\tGameStateDrawnByAgreement\n)\n\nvar gameStatesSnake = [...]string{\n\tGameStateInvalid: \"invalid\",\n\tGameStateOpen: \"open\",\n\tGameStateCheckmated: \"checkmated\",\n\tGameStateStalemate: \"stalemate\",\n\tGameStateDrawn75Move: \"drawn_75_move\",\n\tGameStateDrawn5Fold: \"drawn_5_fold\",\n\tGameStateDrawn50Move: \"drawn_50_move\",\n\tGameStateDrawn3Fold: \"drawn_3_fold\",\n\tGameStateDrawnInsufficient: \"drawn_insufficient\",\n\tGameStateTimeout: \"timeout\",\n\tGameStateAborted: \"aborted\",\n\tGameStateResigned: \"resigned\",\n\tGameStateDrawnByAgreement: \"drawn_by_agreement\",\n}\n\n// IsFinished returns whether the game is in a finished state.\nfunc (g GameState) IsFinished() bool {\n\treturn g != GameStateOpen\n}\n\n// NewGame initialized a new game with the given opponent.\n// opponent may be a bech32 address or \"@user\" (r/demo/users).\n//\n// seconds and increment specifies the time control for the given game.\n// seconds is the amount of time given to play to each player; increment\n// is by how many seconds the player's time should be increased when they make a move.\n// seconds \u003c= 0 means no time control (correspondence).\n//\n// XXX: Disabled for GnoChess production temporarily. (prefixed with x for unexported)\n// Ideally, we'd need this to work either by not forcing users not to have\n// parallel games OR by introducing a \"request\" system, so that a game is not\n// immediately considered \"open\" when calling NewGame.\nfunc xNewGame(opponentRaw string, seconds, increment int) string {\n\tassertOriginCall()\n\n\tif seconds \u003e= 0 \u0026\u0026 increment \u003c 0 {\n\t\tpanic(\"negative increment invalid\")\n\t}\n\n\topponent := parsePlayer(opponentRaw)\n\tcaller := std.PreviousRealm().Address()\n\tassertUserNotInLobby(caller)\n\n\treturn newGame(caller, opponent, seconds, increment).json()\n}\n\nfunc getUserGames(user std.Address) []*Game {\n\tval, exist := user2Games.Get(user.String())\n\tif !exist {\n\t\treturn nil\n\t}\n\treturn val.([]*Game)\n}\n\nfunc assertGamesFinished(games []*Game) {\n\tfor _, g := range games {\n\t\tif g.State.IsFinished() {\n\t\t\tcontinue\n\t\t}\n\t\terr := g.claimTimeout()\n\t\tif err != nil {\n\t\t\tpanic(\"can't start new game: game \" + g.ID + \" is not yet finished\")\n\t\t}\n\t}\n}\n\nfunc newGame(caller, opponent std.Address, seconds, increment int) *Game {\n\tgames := getUserGames(caller)\n\t// Ensure player has no ongoing games.\n\tassertGamesFinished(games)\n\tassertGamesFinished(getUserGames(opponent))\n\n\tif caller == opponent {\n\t\tpanic(\"can't create a game with yourself\")\n\t}\n\n\tisBlack := determineColor(games, caller, opponent)\n\n\t// id is zero-padded to work well with avl's alphabetic order.\n\tid := gameIDCounter.Next().String()\n\tg := \u0026Game{\n\t\tID: id,\n\t\tWhite: caller,\n\t\tBlack: opponent,\n\t\tPosition: chess.NewPosition(),\n\t\tState: GameStateOpen,\n\t\tCreator: caller,\n\t\tCreatedAt: time.Now(),\n\t\tTime: NewTimeControl(seconds, increment),\n\t}\n\tif isBlack {\n\t\tg.White, g.Black = g.Black, g.White\n\t}\n\n\tgameStore.Set(g.ID, g)\n\taddToUser2Games(caller, g)\n\taddToUser2Games(opponent, g)\n\n\treturn g\n}\n\nfunc addToUser2Games(addr std.Address, game *Game) {\n\tvar games []*Game\n\tv, ok := user2Games.Get(string(addr))\n\tif ok {\n\t\tgames = v.([]*Game)\n\t}\n\t// game must be at top, because it is the latest ID\n\tgames = append([]*Game{game}, games...)\n\tuser2Games.Set(string(addr), games)\n}\n\nfunc determineColor(games []*Game, caller, opponent std.Address) (isBlack bool) {\n\t// fast path for no games\n\tif len(games) == 0 {\n\t\treturn false\n\t}\n\n\t// Determine color of player. If the player has already played with\n\t// opponent, invert from last game played among them.\n\t// Otherwise invert from last game played by the player.\n\tisBlack = games[0].White == caller\n\n\t// \"try\" to save gas if the user has really a lot of past games\n\tif len(games) \u003e 256 {\n\t\tgames = games[:256]\n\t}\n\tfor _, game := range games {\n\t\tif game.White == opponent || game.Black == opponent {\n\t\t\treturn game.White == caller\n\t\t}\n\t}\n\treturn\n}\n\n// GetGame returns a game, knowing its ID.\nfunc GetGame(id string) string {\n\treturn getGame(id, false).json()\n}\n\nfunc getGame(id string, wantOpen bool) *Game {\n\tgraw, ok := gameStore.Get(id)\n\tif !ok {\n\t\tpanic(\"game not found\")\n\t}\n\tg := graw.(*Game)\n\tif wantOpen \u0026\u0026 g.State.IsFinished() {\n\t\tpanic(\"game is already finished\")\n\t}\n\treturn g\n}\n\n// MakeMove specifies a move to be done on the given game, specifying in\n// algebraic notation the square where to move the piece.\n// If the piece is a pawn which is moving to the last row, a promotion piece\n// must be specified.\n// Castling is specified by indicating the king's movement.\nfunc MakeMove(gameID, from, to string, promote chess.Piece) string {\n\tassertOriginCall()\n\n\tg := getGame(gameID, true)\n\n\t// determine if this is a black move\n\tisBlack := len(g.Position.Moves)%2 == 1\n\n\tcaller := std.PreviousRealm().Address()\n\tif (isBlack \u0026\u0026 g.Black != caller) ||\n\t\t(!isBlack \u0026\u0026 g.White != caller) {\n\t\t// either not a player involved; or not the caller's turn.\n\t\tpanic(\"you are not allowed to make a move at this time\")\n\t}\n\n\t// game is time controlled? add move to time control\n\tif g.Time != nil {\n\t\tvalid := g.Time.AddMove()\n\t\tif !valid \u0026\u0026 len(g.Position.Moves) \u003c 2 {\n\t\t\tg.State = GameStateAborted\n\t\t\tg.Concluder = \u0026caller\n\t\t\tg.Winner = WinnerNone\n\t\t\treturn g.json()\n\t\t}\n\t\tif !valid {\n\t\t\tg.State = GameStateTimeout\n\t\t\tif caller == g.White {\n\t\t\t\tg.Winner = WinnerBlack\n\t\t\t} else {\n\t\t\t\tg.Winner = WinnerWhite\n\t\t\t}\n\t\t\tg.saveResult()\n\t\t\treturn g.json()\n\t\t}\n\t}\n\n\t// validate move\n\tm := chess.Move{\n\t\tFrom: chess.SquareFromString(from),\n\t\tTo: chess.SquareFromString(to),\n\t}\n\tif m.From == chess.SquareInvalid || m.To == chess.SquareInvalid {\n\t\tpanic(\"invalid from/to square\")\n\t}\n\tif promote \u003e 0 \u0026\u0026 promote \u003c= chess.PieceKing {\n\t\tm.Promotion = promote\n\t}\n\tnewp, ok := g.Position.ValidateMove(m)\n\tif !ok {\n\t\tpanic(\"illegal move\")\n\t}\n\n\t// add move and record new board\n\tg.Position = newp\n\n\to := newp.IsFinished()\n\tif o == chess.NotFinished {\n\t\t// opponent of draw offerer has made a move. take as implicit rejection of draw.\n\t\tif g.DrawOfferer != nil \u0026\u0026 *g.DrawOfferer != caller {\n\t\t\tg.DrawOfferer = nil\n\t\t}\n\n\t\treturn g.json()\n\t}\n\n\tswitch {\n\tcase o == chess.Checkmate \u0026\u0026 isBlack:\n\t\tg.State = GameStateCheckmated\n\t\tg.Winner = WinnerBlack\n\tcase o == chess.Checkmate \u0026\u0026 !isBlack:\n\t\tg.State = GameStateCheckmated\n\t\tg.Winner = WinnerWhite\n\tcase o == chess.Stalemate:\n\t\tg.State = GameStateStalemate\n\t\tg.Winner = WinnerDraw\n\n\tcase o == chess.Drawn75Move:\n\t\tg.State = GameStateDrawn75Move\n\t\tg.Winner = WinnerDraw\n\tcase o == chess.Drawn5Fold:\n\t\tg.State = GameStateDrawn5Fold\n\t\tg.Winner = WinnerDraw\n\t}\n\tg.DrawOfferer = nil\n\tg.saveResult()\n\n\treturn g.json()\n}\n\nfunc (g *Game) claimTimeout() error {\n\t// no assert origin call or caller check: anyone can claim a game to have\n\t// finished in timeout.\n\n\tif g.Time == nil {\n\t\treturn errors.New(\"game is not time controlled\")\n\t}\n\n\t// game is time controlled? add move to time control\n\tto := g.Time.TimedOut()\n\tif !to {\n\t\treturn errors.New(\"game is not timed out\")\n\t}\n\n\tif nmov := len(g.Position.Moves); nmov \u003c 2 {\n\t\tg.State = GameStateAborted\n\t\tif nmov == 1 {\n\t\t\tg.Concluder = \u0026g.Black\n\t\t} else {\n\t\t\tg.Concluder = \u0026g.White\n\t\t}\n\t\tg.Winner = WinnerNone\n\t\treturn nil\n\t}\n\n\tg.State = GameStateTimeout\n\tif len(g.Position.Moves)\u00261 == 0 {\n\t\tg.Winner = WinnerBlack\n\t} else {\n\t\tg.Winner = WinnerWhite\n\t}\n\tg.DrawOfferer = nil\n\tg.saveResult()\n\n\treturn nil\n}\n\n// ClaimTimeout should be called when the caller believes the game has resulted\n// in a timeout.\nfunc ClaimTimeout(gameID string) string {\n\tg := getGame(gameID, true)\n\n\terr := g.claimTimeout()\n\tcheckErr(err)\n\n\treturn g.json()\n}\n\nfunc Abort(gameID string) string {\n\tassertOriginCall()\n\n\tg := getGame(gameID, true)\n\terr := abort(g)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\treturn g.json()\n}\n\nfunc abort(g *Game) error {\n\tif len(g.Position.Moves) \u003e= 2 {\n\t\treturn errors.New(\"game can no longer be aborted; if you wish to quit, resign\")\n\t}\n\n\tcaller := std.PreviousRealm().Address()\n\tif caller != g.White \u0026\u0026 caller != g.Black {\n\t\treturn errors.New(\"you are not involved in this game\")\n\t}\n\tg.State = GameStateAborted\n\tg.Concluder = \u0026caller\n\tg.DrawOfferer = nil\n\tg.Winner = WinnerNone\n\n\treturn nil\n}\n\nfunc Resign(gameID string) string {\n\tassertOriginCall()\n\n\tg := getGame(gameID, true)\n\terr := resign(g)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\treturn g.json()\n}\n\nfunc resign(g *Game) error {\n\tif len(g.Position.Moves) \u003c 2 {\n\t\treturn abort(g)\n\t}\n\tcaller := std.PreviousRealm().Address()\n\tswitch caller {\n\tcase g.Black:\n\t\tg.State = GameStateResigned\n\t\tg.Winner = WinnerWhite\n\tcase g.White:\n\t\tg.State = GameStateResigned\n\t\tg.Winner = WinnerBlack\n\tdefault:\n\t\treturn errors.New(\"you are not involved in this game\")\n\t}\n\tg.DrawOfferer = nil\n\tg.saveResult()\n\n\treturn nil\n}\n\n// DrawOffer creates a draw offer in the current game, if one doesn't already\n// exist.\nfunc DrawOffer(gameID string) string {\n\tassertOriginCall()\n\n\tg := getGame(gameID, true)\n\tcaller := std.PreviousRealm().Address()\n\n\tswitch {\n\tcase caller != g.Black \u0026\u0026 caller != g.White:\n\t\tpanic(\"you are not involved in this game\")\n\tcase g.DrawOfferer != nil:\n\t\tpanic(\"a draw offer in this game already exists\")\n\t}\n\n\tg.DrawOfferer = \u0026caller\n\treturn g.json()\n}\n\n// DrawRefuse refuse a draw offer in the given game.\nfunc DrawRefuse(gameID string) string {\n\tassertOriginCall()\n\n\tg := getGame(gameID, true)\n\tcaller := std.PreviousRealm().Address()\n\n\tswitch {\n\tcase caller != g.Black \u0026\u0026 caller != g.White:\n\t\tpanic(\"you are not involved in this game\")\n\tcase g.DrawOfferer == nil:\n\t\tpanic(\"no draw offer present\")\n\tcase *g.DrawOfferer == caller:\n\t\tpanic(\"can't refuse an offer you sent yourself\")\n\t}\n\n\tg.DrawOfferer = nil\n\treturn g.json()\n}\n\n// Draw implements draw by agreement, as well as \"single-party\" draw:\n// - Threefold repetition (§9.2)\n// - Fifty-move rule (§9.3)\n// - Insufficient material (§9.4)\n// Note: stalemate happens as a consequence of a Move, and thus is handled in that function.\nfunc Draw(gameID string) string {\n\tassertOriginCall()\n\n\tg := getGame(gameID, true)\n\n\tcaller := std.PreviousRealm().Address()\n\tif caller != g.Black \u0026\u0026 caller != g.White {\n\t\tpanic(\"you are not involved in this game\")\n\t}\n\n\t// accepted draw offer (do early to avoid gas for g.Position.IsFinished())\n\tif g.DrawOfferer != nil \u0026\u0026 *g.DrawOfferer != caller {\n\t\tg.State = GameStateDrawnByAgreement\n\t\tg.Winner = WinnerDraw\n\t\tg.Concluder = \u0026caller\n\n\t\tg.saveResult()\n\n\t\treturn g.json()\n\t}\n\n\to := g.Position.IsFinished()\n\tswitch {\n\tcase o\u0026chess.Can50Move != 0:\n\t\tg.State = GameStateDrawn50Move\n\tcase o\u0026chess.Can3Fold != 0:\n\t\tg.State = GameStateDrawn3Fold\n\tcase o\u0026chess.CanInsufficient != 0:\n\t\tg.State = GameStateDrawnInsufficient\n\tdefault:\n\t\tpanic(\"this game can't be automatically drawn\")\n\t}\n\tg.Concluder = \u0026caller\n\tg.Winner = WinnerDraw\n\tg.DrawOfferer = nil\n\n\tg.saveResult()\n\n\treturn g.json()\n}\n\nfunc checkErr(err error) {\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\n// Replacement for OriginCall using std.PreviousRealm().\nfunc assertOriginCall() {\n\tif !std.PreviousRealm().IsUser() {\n\t\tpanic(\"invalid non-origin call\")\n\t}\n}\n" + }, + { + "name": "chess_test.gno", + "body": "package chess\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/morgan/chess\"\n\t\"gno.land/p/morgan/chess/glicko2\"\n)\n\nfunc cleanup() {\n\tgameStore = avl.Tree{}\n\tgameIDCounter = 0\n\tuser2Games = avl.Tree{}\n\tplayerStore = avl.Tree{}\n\tleaderboard = [CategoryMax]leaderboardType{}\n\tlobby = [tcLobbyMax][]lobbyPlayer{}\n\tlobbyPlayer2Game = avl.Tree{}\n\tplayerRatings = [CategoryMax][]*glicko2.PlayerRating{}\n}\n\nfunc TestNewGame(t *testing.T) {\n\tcleanup()\n\n\tg := xNewGame(std.DerivePkgAddr(\"xx\").String(), 0, 0)\n\tprintln(g)\n}\n\nconst (\n\twhite std.Address = \"g1white\"\n\tblack std.Address = \"g1black\"\n)\n\n/*\nsyntax:\n\n\t[\u003ccommand\u003e ][#[!][\u003cbuf\u003e] \u003cchecker\u003e]\n\ncommand is executed; result of command is stored in buffer.\nthe test is split in lines. other white space is ignored (strings.Fields).\n\n\u003cbuf\u003e: all commands below will generally store a string result value in the\nbuffer \"result\", which is the default and thus may be omitted.\nif the command panics, the panic value is stored in the buffer \"panic\".\n(if it doesn't, buffer panic is set to an empty string).\nif following a command there is no checker on the #panic buffer, the line\n\"#!panic empty\" is implicitly added.\nif \u003cbuf\u003e is preceded by ! (e.g. \"#!panic empty\"), then if the checker fails,\nprocessing is stopped on that line.\n\n\u003ccommand\u003e:\n\n\tnewgame [\u003cwhite\u003e \u003cblack\u003e [\u003cseconds\u003e [\u003cincrement\u003e]]]\n\t\tstores game ID in buffer #id.\n\t\t\u003cwhite\u003e and \u003cblack\u003e are two addresses. if they are not passed, \u003cwhite\u003e\n\t\tassumes value \"white\" and \u003cblack\u003e \"black\"\n\tmove \u003cplayer\u003e \u003clan_move\u003e\n\t\tlan_move is in the same format as Move.String.\n\t\tretrieves game id from #id.\n\tdraw \u003cplayer\u003e\n\tdrawoffer \u003cplayer\u003e\n\tabort \u003cplayer\u003e\n\ttimeout \u003cplayer\u003e\n\t\t(ClaimTimeout)\n\tresign \u003cplayer\u003e\n\tgame [\u003cid\u003e]\n\t\tif not given, id is retrieved from buffer #id.\n\tplayer \u003cplayer\u003e\n\tname \u003cpredicate\u003e\n\t\tsets the name of the test to predicate.\n\tcopy \u003cdst\u003e [\u003csrc\u003e]\n\t\tcopies buffer src to buffer dst.\n\t\tif src not specified, assumed result.\n\t\t(don't specify the #; ie: copy oldresult result)\n\tsleep \u003cseconds\u003e\n\t\tsleep for the given amount of seconds (float).\n\nNOTE: for all values of \u003cplayer\u003e, including \u003cwhite\u003e and \u003cblack\u003e in newgame,\nthe addresses are passed prefixed by \"g1\", and the matching checkers should\nexpect this.\n\n\u003cchecker\u003e:\n\n\tempty\n\t\tthe buffer should be empty.\n\tequal \u003cpredicate\u003e\n\t\tpredicate may start with #, which indicates a buffer.\n\tcontains [\u003cpredicate\u003e...]\n\t\tthe buffer should contain all of the given predicates.\n\tcontainssp \u003cpredicate\u003e\n\t\tthe buffer should contain the given predicate, which contains spaces.\n*/\nvar commandTests = [...]string{\n\t`\tname NewGameNegativeIncrement\n\t\tnewgame white black 10 -5 #panic containssp negative increment invalid\n\t`,\n\t`\tname NewGameDouble\n\t\tnewgame\n\t\tnewgame #panic contains is not yet finished\n\t`,\n\t`\tname NewGameWithSelf\n\t\tnewgame white white #panic contains game with yourself\n\t`,\n\t// ColoursInvert within games played by two players\n\t`\tname ColoursInvert\n\t\tnewgame\n\t\tmove white e2e4\n\t\tmove black e7e5\n\t\tmove white f1c4\n\t\tresign white\n\t\tnewgame\n\t\t# contains \"white\":\"g1black\" \"black\":\"g1white\"\n\t\t#id equal 0000002\n\t`,\n\t// Otherwise, invert from p1's history.\n\t`\tname ColoursInvert3p\n\t\tnewgame p1 p2 #! contains \"white\":\"g1p1\" \"black\":\"g1p2\"\n\t\tmove p1 e2e4\n\t\tabort p1\n\t\tnewgame p1 p3 # contains \"white\":\"g1p3\" \"black\":\"g1p1\"\n\t`,\n\t`\tname ScholarsMate\n\t\tnewgame #id equal 0000001\n\t\tmove white e2e4\n\t\tmove black e7e5\n\t\tmove white f1c4\n\t\tmove black b8c6\n\t\tmove white d1f3\n\t\tmove black d7d6\n\t\tmove white f3f7\n\t\tcopy moveres\n\t\tgame # equal #moveres\n\t\t# contains \"state\":\"checkmated\" \"winner\":\"white\"\n\t\t# containssp r1bqkbnr/ppp2Qpp/2np4/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4\n\t\tplayer white\n\t\t# contains \"address\":\"g1white\" \"position\":0 \"wins\":1 \"losses\":0 \"draws\":0\n\t\tplayer black\n\t\t# contains \"address\":\"g1black\" \"position\":1 \"wins\":0 \"losses\":1 \"draws\":0\n\t`,\n\t`\tname DrawByAgreement\n\t\tnewgame\n\t\tmove white e2e4\n\t\tmove black e7e5\n\t\tmove white f1c4\n\t\tmove black b8c6\n\t\tcopy moveres\n\t\tgame # equal #moveres\n\t\t# contains \"open\" \"concluder\":null \"draw_offerer\":null\n\t\tdrawoffer white\n\t\t# contains \"open\" \"concluder\":null \"draw_offerer\":\"g1white\"\n\t\tdraw black\n\t\t# contains \"drawn_by_agreement\" \"concluder\":\"g1black\" \"draw_offerer\":\"g1white\"\n\t`,\n\t`\tname AbortFirstMove\n\t\tnewgame\n\t\tabort white # contains \"winner\":\"none\" \"concluder\":\"g1white\"\n\t`,\n\n\t`\tname ThreefoldRepetition\n\t\tnewgame\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\tdraw black # contains \"winner\":\"draw\" \"concluder\":\"g1black\"\n\t\t# contains \"state\":\"drawn_3_fold\"\n\t`,\n\t`\tname FivefoldRepetition\n\t\tnewgame\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\tmove white g1f3\n\t\tmove black g8f6\n\t\tmove white f3g1\n\t\tmove black f6g8\n\n\t\t# contains \"winner\":\"draw\" \"concluder\":null \"state\":\"drawn_5_fold\"\n\n\t\tmove white g1f3 #panic contains game is already finished\n\t`,\n\t`\tname TimeoutAborted\n\t\tnewgame white black 3\n\t\tmove white e2e4 #! contains \"state\":\"open\"\n\t\tsleep 31\n\t\tmove black e7e5\n\t\tgame\n\t\t# contains e2e4\n\t\t# contains \"aborted\"\n\t\t# contains \"concluder\":\"g1black\"\n\t`,\n\t`\tname TimeoutAbandoned\n\t\tnewgame white black 1\n\t\tmove white e2e4\n\t\tmove black e7e5\n\t\tsleep 61\n\t\ttimeout black\n\t\t# contains \"state\":\"timeout\" \"winner\":\"black\"\n\t`,\n}\n\nfunc TestCommands(t *testing.T) {\n\tfor _, command := range commandTests {\n\t\trunCommandTest(t, command)\n\t}\n}\n\n// testCommandRunner is used to represent the single testCommand types below.\n// the Run function is used to execute the actual command, after parsing.\n//\n// This could have been implemented with simple closures generated within the\n// parser, however it's not because of this issue:\n// https://github.com/gnolang/gno/issues/1135\ntype testCommandRunner interface {\n\tRun(t *testing.T, bufs map[string]string)\n}\n\n// testCommandChecker is a wrapper for a runner which is performing a check.\n// This is marked in order not to wrap the calls to them as a panic.\ntype testCommandChecker struct{ testCommandRunner }\n\nfunc (testCommandChecker) Checker() {}\n\ntype testCommandFunc func(t *testing.T, bufs map[string]string)\n\nfunc (tc testCommandFunc) Run(t *testing.T, bufs map[string]string) { tc(t, bufs) }\n\n// testCommandColorID represents a testCommand, which uses a function of the\n// form func(gameID string) string (hence ID), and that takes as the first\n// parameter a \u003cplayer\u003e which will be the caller.\ntype testCommandColorID struct {\n\tfn func(string) string\n\taddr std.Address\n}\n\nfunc newTestCommandColorID(fn func(string) string, s string, addr string) testCommandRunner {\n\treturn \u0026testCommandColorID{fn, std.Address(\"g1\" + addr)}\n}\n\nfunc (tc *testCommandColorID) Run(t *testing.T, bufs map[string]string) {\n\ttesting.SetRealm(std.NewUserRealm(tc.addr))\n\tbufs[\"result\"] = tc.fn(bufs[\"id\"])\n}\n\ntype testCommandNewGame struct {\n\tw, b std.Address\n\tseconds, incr int\n}\n\nfunc (tc *testCommandNewGame) Run(t *testing.T, bufs map[string]string) {\n\ttesting.SetRealm(std.NewUserRealm(tc.w))\n\tres := xNewGame(string(tc.b), tc.seconds, tc.incr)\n\tbufs[\"result\"] = res\n\n\tconst idMagicString = `\"id\":\"`\n\tidx := strings.Index(res, idMagicString)\n\tif idx \u003c 0 {\n\t\tpanic(\"id not found\")\n\t}\n\tid := res[idx+len(idMagicString):]\n\tid = id[:strings.IndexByte(id, '\"')]\n\tbufs[\"id\"] = id\n}\n\ntype testCommandMove struct {\n\taddr std.Address\n\tfrom, to string\n\tpromotion chess.Piece\n}\n\nfunc (tc *testCommandMove) Run(t *testing.T, bufs map[string]string) {\n\ttesting.SetRealm(std.NewUserRealm(tc.addr))\n\tbufs[\"result\"] = MakeMove(bufs[\"id\"], tc.from, tc.to, tc.promotion)\n}\n\ntype testCommandGame struct {\n\tidWanted string\n}\n\nfunc (tc *testCommandGame) Run(t *testing.T, bufs map[string]string) {\n\tidl := tc.idWanted\n\tif idl == \"\" {\n\t\tidl = bufs[\"id\"]\n\t}\n\tbufs[\"result\"] = GetGame(idl)\n}\n\ntype testCommandPlayer struct {\n\taddr string\n}\n\nfunc (tc *testCommandPlayer) Run(t *testing.T, bufs map[string]string) {\n\tbufs[\"result\"] = GetPlayer(tc.addr)\n}\n\ntype testCommandCopy struct {\n\tdst, src string\n}\n\nfunc (tc *testCommandCopy) Run(t *testing.T, bufs map[string]string) {\n\tbufs[tc.dst] = bufs[tc.src]\n}\n\ntype testCommandSleep struct {\n\tdur time.Duration\n}\n\nfunc (tc *testCommandSleep) Run(t *testing.T, bufs map[string]string) {\n\tos.Sleep(tc.dur)\n}\n\ntype testChecker struct {\n\tfn func(t *testing.T, bufs map[string]string, tc *testChecker)\n\ttf func(*testing.T, string, ...interface{})\n\tbufp string\n\tpreds []string\n}\n\nfunc (*testChecker) Checker() {}\nfunc (tc *testChecker) Run(t *testing.T, bufs map[string]string) {\n\ttc.fn(t, bufs, tc)\n}\n\nfunc parseCommandTest(t *testing.T, command string) (funcs []testCommandRunner, testName string) {\n\tlines := strings.Split(command, \"\\n\")\n\tatoi := func(s string) int {\n\t\tn, err := strconv.Atoi(s)\n\t\tcheckErr(err)\n\t\treturn n\n\t}\n\t// used to detect whether to auto-add a panic checker\n\tvar hasPanicChecker bool\n\tpanicChecker := func(lineNum int, testName string) testCommandRunner {\n\t\treturn testCommandChecker{testCommandFunc(\n\t\t\tfunc(t *testing.T, bufs map[string]string) {\n\t\t\t\tif bufs[\"panic\"] != \"\" {\n\t\t\t\t\tt.Fatalf(\"%s:%d: buffer \\\"panic\\\" is not empty (%q)\", testName, lineNum, bufs[\"panic\"])\n\t\t\t\t}\n\t\t\t},\n\t\t)}\n\t}\n\n\tfor lineNum, line := range lines {\n\t\tflds := strings.Fields(line)\n\t\tif len(flds) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tcommand, checker := flds, ([]string)(nil)\n\t\tfor idx, fld := range flds {\n\t\t\tif strings.HasPrefix(fld, \"#\") {\n\t\t\t\tcommand, checker = flds[:idx], flds[idx:]\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tvar cmd string\n\t\tif len(command) \u003e 0 {\n\t\t\tcmd = command[0]\n\n\t\t\t// there is a new command; if hasPanicChecker == false,\n\t\t\t// it means the previous command did not have a panic checker.\n\t\t\t// add it.\n\t\t\tif !hasPanicChecker \u0026\u0026 len(funcs) \u003e 0 {\n\t\t\t\t// no lineNum+1 because it was the previous line\n\t\t\t\tfuncs = append(funcs, panicChecker(lineNum, testName))\n\t\t\t}\n\t\t}\n\t\tswitch cmd {\n\t\tcase \"\": // move on\n\t\tcase \"newgame\":\n\t\t\tw, b := white, black\n\t\t\tvar seconds, incr int\n\t\t\tswitch len(command) {\n\t\t\tcase 1:\n\t\t\tcase 5:\n\t\t\t\tincr = atoi(command[4])\n\t\t\t\tfallthrough\n\t\t\tcase 4:\n\t\t\t\tseconds = atoi(command[3])\n\t\t\t\tfallthrough\n\t\t\tcase 3:\n\t\t\t\tw, b = std.Address(\"g1\"+command[1]), std.Address(\"g1\"+command[2])\n\t\t\tdefault:\n\t\t\t\tpanic(\"invalid newgame command \" + line)\n\t\t\t}\n\t\t\tfuncs = append(funcs,\n\t\t\t\t\u0026testCommandNewGame{w, b, seconds, incr},\n\t\t\t)\n\t\tcase \"move\":\n\t\t\tif len(command) != 3 {\n\t\t\t\tpanic(\"invalid move command \" + line)\n\t\t\t}\n\t\t\tif len(command[2]) \u003c 4 || len(command[2]) \u003e 5 {\n\t\t\t\tpanic(\"invalid lan move \" + command[2])\n\t\t\t}\n\t\t\tfrom, to := command[2][:2], command[2][2:4]\n\t\t\tvar promotion chess.Piece\n\t\t\tif len(command[2]) == 5 {\n\t\t\t\tpromotion = chess.PieceFromChar(command[2][4])\n\t\t\t\tif promotion == chess.PieceEmpty {\n\t\t\t\t\tpanic(\"invalid piece for promotion: \" + string(command[2][4]))\n\t\t\t\t}\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testCommandMove{\n\t\t\t\taddr: std.Address(\"g1\" + command[1]),\n\t\t\t\tfrom: from,\n\t\t\t\tto: to,\n\t\t\t\tpromotion: promotion,\n\t\t\t})\n\t\tcase \"abort\":\n\t\t\tfuncs = append(funcs, newTestCommandColorID(Abort, \"abort\", command[1]))\n\t\tcase \"draw\":\n\t\t\tfuncs = append(funcs, newTestCommandColorID(Draw, \"draw\", command[1]))\n\t\tcase \"drawoffer\":\n\t\t\tfuncs = append(funcs, newTestCommandColorID(DrawOffer, \"drawoffer\", command[1]))\n\t\tcase \"timeout\":\n\t\t\tfuncs = append(funcs, newTestCommandColorID(ClaimTimeout, \"timeout\", command[1]))\n\t\tcase \"resign\":\n\t\t\tfuncs = append(funcs, newTestCommandColorID(Resign, \"resign\", command[1]))\n\t\tcase \"game\":\n\t\t\tif len(command) \u003e 2 {\n\t\t\t\tpanic(\"invalid game command \" + line)\n\t\t\t}\n\t\t\ttc := \u0026testCommandGame{}\n\t\t\tif len(command) == 2 {\n\t\t\t\ttc.idWanted = command[1]\n\t\t\t}\n\t\t\tfuncs = append(funcs, tc)\n\t\tcase \"player\":\n\t\t\tif len(command) != 2 {\n\t\t\t\tpanic(\"invalid player command \" + line)\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testCommandPlayer{\"g1\" + command[1]})\n\t\tcase \"name\":\n\t\t\ttestName = strings.Join(command[1:], \" \")\n\t\tcase \"copy\":\n\t\t\tif len(command) \u003e 3 || len(command) \u003c 2 {\n\t\t\t\tpanic(\"invalid copy command \" + line)\n\t\t\t}\n\t\t\ttc := \u0026testCommandCopy{dst: command[1], src: \"result\"}\n\t\t\tif len(command) == 3 {\n\t\t\t\ttc.src = command[2]\n\t\t\t}\n\t\t\tfuncs = append(funcs, tc)\n\t\tcase \"sleep\":\n\t\t\tif len(command) != 2 {\n\t\t\t\tpanic(\"invalid sleep command \" + line)\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testCommandSleep{\n\t\t\t\ttime.Duration(atoi(command[1])) * time.Second,\n\t\t\t})\n\t\tdefault:\n\t\t\tpanic(\"invalid command \" + cmd)\n\t\t}\n\n\t\tif len(checker) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif len(checker) == 1 {\n\t\t\tpanic(\"no checker specified \" + line)\n\t\t}\n\n\t\tbufp := checker[0]\n\t\tuseFatal := false\n\t\tif len(bufp) \u003e 1 \u0026\u0026 bufp[1] == '!' {\n\t\t\tbufp = bufp[2:]\n\t\t\tuseFatal = true\n\t\t} else {\n\t\t\tbufp = bufp[1:]\n\t\t}\n\t\tif bufp == \"\" {\n\t\t\tbufp = \"result\"\n\t\t}\n\t\tif bufp == \"panic\" \u0026\u0026 !hasPanicChecker {\n\t\t\thasPanicChecker = true\n\t\t}\n\t\ttf := func(ln int, testName string, useFatal bool) func(*testing.T, string, ...interface{}) {\n\t\t\treturn func(t *testing.T, s string, v ...interface{}) {\n\t\t\t\tfn := t.Errorf\n\t\t\t\tif useFatal {\n\t\t\t\t\tfn = t.Fatalf\n\t\t\t\t}\n\t\t\t\tfn(\"%s:%d: \"+s, append([]interface{}{testName, ln}, v...)...)\n\t\t\t}\n\t\t}(lineNum+1, testName, useFatal)\n\n\t\tswitch checker[1] {\n\t\tcase \"empty\":\n\t\t\tif len(checker) != 2 {\n\t\t\t\tpanic(\"invalid empty checker \" + line)\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testChecker{\n\t\t\t\tfn: func(t *testing.T, bufs map[string]string, tc *testChecker) {\n\t\t\t\t\tif bufs[tc.bufp] != \"\" {\n\t\t\t\t\t\ttc.tf(t, \"buffer %q is not empty (%v)\", tc.bufp, bufs[tc.bufp])\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\ttf: tf,\n\t\t\t\tbufp: bufp,\n\t\t\t})\n\t\tcase \"equal\":\n\t\t\tpred := strings.Join(checker[2:], \" \")\n\t\t\tfuncs = append(funcs, \u0026testChecker{\n\t\t\t\tfn: func(t *testing.T, bufs map[string]string, tc *testChecker) {\n\t\t\t\t\texp := tc.preds[0]\n\t\t\t\t\tif exp[0] == '#' {\n\t\t\t\t\t\texp = bufs[exp[1:]]\n\t\t\t\t\t}\n\t\t\t\t\tif bufs[tc.bufp] != exp {\n\t\t\t\t\t\ttc.tf(t, \"buffer %q: want %v got %v\", tc.bufp, exp, bufs[tc.bufp])\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\ttf: tf,\n\t\t\t\tbufp: bufp,\n\t\t\t\tpreds: []string{pred},\n\t\t\t})\n\t\tcase \"contains\":\n\t\t\tpreds := checker[2:]\n\t\t\tif len(preds) == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testChecker{\n\t\t\t\tfn: func(t *testing.T, bufs map[string]string, tc *testChecker) {\n\t\t\t\t\tfor _, pred := range tc.preds {\n\t\t\t\t\t\tif !strings.Contains(bufs[tc.bufp], pred) {\n\t\t\t\t\t\t\ttc.tf(t, \"buffer %q: %v does not contain %v\", tc.bufp, bufs[tc.bufp], pred)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\ttf: tf,\n\t\t\t\tbufp: bufp,\n\t\t\t\tpreds: preds,\n\t\t\t})\n\t\tcase \"containssp\":\n\t\t\tpred := strings.Join(checker[2:], \" \")\n\t\t\tif pred == \"\" {\n\t\t\t\tpanic(\"invalid contanssp checker \" + line)\n\t\t\t}\n\t\t\tfuncs = append(funcs, \u0026testChecker{\n\t\t\t\tfn: func(t *testing.T, bufs map[string]string, tc *testChecker) {\n\t\t\t\t\tif !strings.Contains(bufs[tc.bufp], tc.preds[0]) {\n\t\t\t\t\t\ttc.tf(t, \"buffer %q: %v does not contain %v\", tc.bufp, bufs[tc.bufp], tc.preds[0])\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\ttf: tf,\n\t\t\t\tbufp: bufp,\n\t\t\t\tpreds: []string{pred},\n\t\t\t})\n\t\tdefault:\n\t\t\tpanic(\"invalid checker \" + checker[1])\n\t\t}\n\t}\n\tif !hasPanicChecker {\n\t\tfuncs = append(funcs, panicChecker(len(lines), testName))\n\t}\n\treturn\n}\n\nfunc runCommandTest(t *testing.T, command string) {\n\tfuncs, testName := parseCommandTest(t, command)\n\n\tt.Run(testName, func(t *testing.T) {\n\t\tcleanup()\n\t\tbufs := make(map[string]string, 3)\n\t\tfor idx, f := range funcs {\n\t\t\tif _, ok := f.(interface{ Checker() }); ok {\n\t\t\t\tf.Run(t, bufs)\n\t\t\t} else {\n\t\t\t\tcatchPanic(f, t, bufs)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc catchPanic(tc testCommandRunner, t *testing.T, bufs map[string]string) {\n\tdefer func() {\n\t\t// XXX: should prefer testing.Recover, but see: https://github.com/gnolang/gno/issues/1650\n\t\te := recover()\n\t\tif e == nil {\n\t\t\tbufs[\"panic\"] = \"\"\n\t\t\treturn\n\t\t}\n\t\tbufs[\"result\"] = \"\"\n\t\tbufs[\"panic\"] = fmt.Sprint(e)\n\t}()\n\ttc.Run(t, bufs)\n}\n" + }, + { + "name": "discovery.gno", + "body": "// this file concerns mostly with \"discovery\"; ie. finding information\n// about a user's chess playing and previous games\n\npackage chess\n\nimport (\n\t\"bytes\"\n\t\"sort\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/morgan/chess/glicko2\"\n\t\"gno.land/r/sys/users\"\n)\n\ntype Category byte\n\nconst (\n\t// blanks are reserved for future use (bullet and classic)\n\t_ Category = iota\n\tBlitz\n\tRapid\n\t_\n\tCorrespondence\n\tCategoryMax\n)\n\nvar categoryString = [CategoryMax]string{\n\tBlitz: \"blitz\",\n\tRapid: \"rapid\",\n\tCorrespondence: \"correspondence\",\n}\n\nvar categoryList = [...]Category{Blitz, Rapid, Correspondence}\n\nfunc (c Category) String() string {\n\tif c \u003e CategoryMax || categoryString[c] == \"\" {\n\t\tpanic(\"invalid category\")\n\t}\n\treturn categoryString[c]\n}\n\nfunc CategoryFromString(s string) Category {\n\tfor i, cs := range categoryString {\n\t\tif s == cs {\n\t\t\treturn Category(i)\n\t\t}\n\t}\n\tpanic(\"invalid category\")\n}\n\nfunc (tc *TimeControl) Category() Category {\n\t// https://lichess.org/faq#time-controls\n\tif tc == nil {\n\t\treturn Correspondence\n\t}\n\n\ttotalTime := tc.Seconds + tc.Increment*40\n\tswitch {\n\tcase tc.Seconds \u003c= 0 || tc.Increment \u003c 0:\n\t\t// should not happen\n\t\treturn Correspondence\n\tcase totalTime \u003c 60*8:\n\t\treturn Blitz\n\tdefault:\n\t\treturn Rapid\n\t}\n}\n\n// realm state\nvar (\n\tplayerStore avl.Tree // std.Address -\u003e *Player\n\tleaderboard [CategoryMax]leaderboardType\n\tplayerRatings [CategoryMax][]*glicko2.PlayerRating\n)\n\nfunc GetPlayer(player string) string {\n\taddr := parsePlayer(player)\n\tv, ok := playerStore.Get(addr.String())\n\tif !ok {\n\t\tpanic(\"player not found\")\n\t}\n\tb, err := v.(*Player).MarshalJSON()\n\tcheckErr(err)\n\treturn string(b)\n}\n\n// Player contains game-related player information.\ntype Player struct {\n\tAddress std.Address\n\tCategoryInfo [CategoryMax]CategoryInfo\n}\n\ntype CategoryInfo struct {\n\tWins, Losses, Draws int\n\t*glicko2.PlayerRating\n}\n\n// Score for determining leaderboards.\nfunc (p Player) Score(cat Category) float64 {\n\treturn p.CategoryInfo[cat].Rating\n}\n\n// Leaderboard position, 0 indexed.\n// Dynamically calculated to avoid having to shift positions when LB changes.\nfunc (p Player) LeaderboardPosition(cat Category) int {\n\tpos, ok := leaderboard[cat].find(p.Score(cat), p.Address)\n\tif !ok {\n\t\treturn -1\n\t}\n\treturn pos\n}\n\nfunc (g *Game) saveResult() {\n\tw, b := getPlayer(g.White), getPlayer(g.Black)\n\n\tcat := g.Time.Category()\n\n\t// Get numeric result for glicko2.\n\tvar result float64\n\tswitch g.Winner {\n\tcase WinnerWhite:\n\t\tw.CategoryInfo[cat].Wins++\n\t\tb.CategoryInfo[cat].Losses++\n\t\tresult = 1\n\tcase WinnerBlack:\n\t\tw.CategoryInfo[cat].Losses++\n\t\tb.CategoryInfo[cat].Wins++\n\t\tresult = 0\n\tcase WinnerDraw:\n\t\tw.CategoryInfo[cat].Draws++\n\t\tb.CategoryInfo[cat].Draws++\n\t\tresult = 0.5\n\tdefault:\n\t\treturn // TODO: maybe panic\n\t}\n\n\t// Call glicko 2 rating calculator.\n\towr, obr := w.CategoryInfo[cat].Rating, b.CategoryInfo[cat].Rating\n\tglicko2.UpdateRatings(playerRatings[cat], []glicko2.RatingScore{{\n\t\tWhite: g.White,\n\t\tBlack: g.Black,\n\t\tScore: result,\n\t}})\n\n\t// Save in playerStore.\n\tplayerStore.Set(w.Address.String(), w)\n\tplayerStore.Set(b.Address.String(), b)\n\tleaderboard[cat], _ = leaderboard[cat].push(g.White, owr, w.CategoryInfo[cat].Rating)\n\tleaderboard[cat], _ = leaderboard[cat].push(g.Black, obr, b.CategoryInfo[cat].Rating)\n}\n\nfunc getPlayer(addr std.Address) *Player {\n\tpraw, ok := playerStore.Get(addr.String())\n\tif ok {\n\t\treturn praw.(*Player)\n\t}\n\tp := new(Player)\n\tp.Address = addr\n\tfor _, cat := range categoryList {\n\t\tpr := glicko2.NewPlayerRating(addr)\n\t\tp.CategoryInfo[cat] = CategoryInfo{\n\t\t\tPlayerRating: pr,\n\t\t}\n\t\tplayerRatings[cat] = append(playerRatings[cat], pr)\n\t}\n\tplayerStore.Set(addr.String(), p)\n\treturn p\n}\n\ntype lbEntry struct {\n\taddr std.Address\n\tscore float64\n}\n\ntype leaderboardType []lbEntry\n\n// find performs binary search on leaderboard to find the first\n// position where score appears, or anything lesser than it.\n// Additionally, if addr is given, it finds the position where the given address appears.\n// The second return parameter returns whether the address was found.\n//\n// The index will be 0 if the score is higher than any other on the leaderboard,\n// and len(leaderboards) if it is lower than any other.\nfunc (lb leaderboardType) find(score float64, addr std.Address) (int, bool) {\n\ti := sort.Search(len(lb), func(i int) bool {\n\t\treturn lb[i].score \u003c= score\n\t})\n\n\tif addr == \"\" || i == len(lb) {\n\t\treturn i, false\n\t}\n\n\tfor j := 0; i+j \u003c len(lb) \u0026\u0026 lb[i+j].score == score; j++ {\n\t\tif lb[i+j].addr == addr {\n\t\t\treturn i + j, true\n\t\t}\n\t}\n\n\treturn i, false\n}\n\n// push adds or modifies the player's position in the leaderboard.\n// the new leaderboard, and the new position of the player in the leaderboard is returned (0-indexed)\nfunc (lb leaderboardType) push(player std.Address, oldScore, newScore float64) (leaderboardType, int) {\n\t// determine where the player is, currently\n\toldPos, found := lb.find(oldScore, player)\n\tif found \u0026\u0026 (oldScore == newScore) {\n\t\treturn lb, oldPos\n\t}\n\n\t// determine where to place the player next.\n\tnewPos, _ := lb.find(newScore, \"\")\n\n\tvar n leaderboardType\n\tswitch {\n\tcase !found:\n\t\tn = append(leaderboardType{}, lb[:newPos]...)\n\t\tn = append(n, lbEntry{player, newScore})\n\t\tn = append(n, lb[newPos:]...)\n\n\tcase oldPos == newPos:\n\t\tn = lb\n\t\tn[newPos] = lbEntry{player, newScore}\n\tcase oldPos \u003e newPos:\n\t\tn = append(leaderboardType{}, lb[:newPos]...)\n\t\tn = append(n, lbEntry{player, newScore})\n\t\tn = append(n, lb[newPos:oldPos]...)\n\t\tn = append(n, lb[oldPos+1:]...)\n\tdefault: // oldPos \u003c newPos\n\t\tn = append(leaderboardType{}, lb[:oldPos]...)\n\t\tn = append(n, lb[oldPos+1:newPos]...)\n\t\tn = append(n, lbEntry{player, newScore})\n\t\tn = append(n, lb[newPos:]...)\n\t}\n\treturn n, newPos\n}\n\n// Leaderboard returns a list of all users, ordered by their position in the leaderboard.\n// category is one of blitz, rapid or correspondence.\nfunc Leaderboard(category string) string {\n\tcat := CategoryFromString(category)\n\tvar buf bytes.Buffer\n\tbuf.WriteByte('[')\n\tfor idx, entry := range leaderboard[cat] {\n\t\tp, _ := playerStore.Get(entry.addr.String())\n\t\td, err := p.(*Player).MarshalJSON()\n\t\tcheckErr(err)\n\t\tbuf.Write(d)\n\t\tif idx != len(leaderboard[cat])-1 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t}\n\tbuf.WriteByte(']')\n\treturn buf.String()\n}\n\n// ListGames provides game listing functionality, with filter-based search functionality.\n//\n// available filters:\n//\n//\tplayer:\u003cplayer\u003e white:\u003cplayer\u003e black:\u003cplayer\u003e finished:bool\n//\tlimit:int id\u003ccmp\u003eint sort:asc/desc\n//\t\u003ccmp\u003e: '\u003c' or '\u003e'\n//\t\u003cplayer\u003e: either a bech32 address, \"@user\" (r/demo/users), or \"caller\"\nfunc ListGames(filters string) string {\n\tft := parseFilters(filters)\n\tresults := make([]*Game, 0, ft.limit)\n\tcb := func(g *Game) (stop bool) {\n\t\tif !ft.valid(g) {\n\t\t\treturn false\n\t\t}\n\t\tresults = append(results, g)\n\t\treturn len(results) \u003e= ft.limit\n\t}\n\n\t// iterate over user2games array if we have one;\n\t// if we don't, iterate over games.\n\tif ft.u2gAddr != \"\" {\n\t\tv, ok := user2Games.Get(ft.u2gAddr.String())\n\t\tif !ok {\n\t\t\treturn \"[]\"\n\t\t}\n\t\tgames := v.([]*Game)\n\t\tif ft.reverse {\n\t\t\tfor i := len(games) - 1; i \u003e= 0; i-- {\n\t\t\t\tif cb(games[i]) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tfor _, game := range games {\n\t\t\t\tif cb(game) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfn := gameStore.Iterate\n\t\tif ft.reverse {\n\t\t\tfn = gameStore.ReverseIterate\n\t\t}\n\t\tfn(ft.minID, ft.maxID, func(_ string, v interface{}) bool {\n\t\t\treturn cb(v.(*Game))\n\t\t})\n\t}\n\n\t// fast path: no results\n\tif len(results) == 0 {\n\t\treturn \"[]\"\n\t}\n\n\t// encode json\n\tvar buf bytes.Buffer\n\tbuf.WriteByte('[')\n\tfor idx, g := range results {\n\t\tbuf.WriteString(g.json())\n\t\tif idx != len(results)-1 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t}\n\tbuf.WriteByte(']')\n\n\treturn buf.String()\n}\n\ntype listGamesFilters struct {\n\tfilters []func(*Game) bool\n\tu2gAddr std.Address\n\tmaxID string\n\tminID string\n\tlimit int\n\treverse bool\n}\n\nfunc (l *listGamesFilters) valid(game *Game) bool {\n\tfor _, filt := range l.filters {\n\t\tif !filt(game) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc parseFilters(filters string) (r listGamesFilters) {\n\t// default to desc order\n\tr.reverse = true\n\n\tparts := strings.Fields(filters)\n\tfor _, part := range parts {\n\t\tidx := strings.IndexAny(part, \":\u003c\u003e\")\n\t\tif idx \u003c 0 {\n\t\t\tpanic(\"invalid filter: \" + part)\n\t\t}\n\t\tfilt, pred := part[:idx+1], part[idx+1:]\n\t\tswitch filt {\n\t\tcase \"player:\":\n\t\t\ta := parsePlayer(pred)\n\t\t\tr.filters = append(r.filters, func(g *Game) bool { return g.White == a || g.Black == a })\n\t\t\tif r.u2gAddr == \"\" {\n\t\t\t\tr.u2gAddr = a\n\t\t\t}\n\t\tcase \"white:\":\n\t\t\ta := parsePlayer(pred)\n\t\t\tr.filters = append(r.filters, func(g *Game) bool { return g.White == a })\n\t\t\tif r.u2gAddr == \"\" {\n\t\t\t\tr.u2gAddr = a\n\t\t\t}\n\t\tcase \"black:\":\n\t\t\ta := parsePlayer(pred)\n\t\t\tr.filters = append(r.filters, func(g *Game) bool { return g.Black == a })\n\t\t\tif r.u2gAddr == \"\" {\n\t\t\t\tr.u2gAddr = a\n\t\t\t}\n\t\tcase \"finished:\":\n\t\t\tb := parseBool(pred)\n\t\t\tr.filters = append(r.filters, func(g *Game) bool { return g.State.IsFinished() == b })\n\t\tcase \"id\u003c\":\n\t\t\tr.maxID = pred\n\t\tcase \"id\u003e\":\n\t\t\tr.minID = pred\n\t\tcase \"limit:\":\n\t\t\tn, err := strconv.Atoi(pred)\n\t\t\tcheckErr(err)\n\t\t\tr.limit = n\n\t\tcase \"sort:\":\n\t\t\tr.reverse = pred == \"desc\"\n\t\tdefault:\n\t\t\tpanic(\"invalid filter: \" + filt)\n\t\t}\n\t}\n\treturn\n}\n\nfunc parseBool(s string) bool {\n\tswitch s {\n\tcase \"true\", \"True\", \"TRUE\", \"1\":\n\t\treturn true\n\tcase \"false\", \"False\", \"FALSE\", \"0\":\n\t\treturn false\n\t}\n\tpanic(\"invalid bool \" + s)\n}\n\nfunc parsePlayer(s string) std.Address {\n\tswitch {\n\tcase s == \"\":\n\t\tpanic(\"invalid address/user\")\n\tcase s == \"caller\":\n\t\treturn std.PreviousRealm().Address()\n\tcase s[0] == '@':\n\t\tu, _ := users.ResolveName(s[1:])\n\t\tif u == nil {\n\t\t\tpanic(\"user not found: \" + s[1:])\n\t\t}\n\t\treturn u.Addr()\n\tcase s[0] == 'g':\n\t\treturn std.Address(s)\n\tdefault:\n\t\tpanic(\"invalid address/user: \" + s)\n\t}\n}\n" + }, + { + "name": "json.gno", + "body": "package chess\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/morgan/chess\"\n\t\"gno.land/p/morgan/chess/glicko2\"\n\t\"gno.land/r/sys/users\"\n)\n\n// This file contains a bunch of JSON marshalers.\n// These should disappear eventually!\n// https://github.com/gnolang/gno/issues/1655\n\nfunc (g Game) MarshalJSON() (retBytes []byte, err error) {\n\tvar b bytes.Buffer\n\tb.WriteByte('{')\n\n\tnilAddr := func(na *std.Address) string {\n\t\tif na == nil {\n\t\t\treturn `null`\n\t\t}\n\t\treturn `\"` + na.String() + `\"`\n\t}\n\tmjson := func(s string, val interface{ MarshalJSON() ([]byte, error) }, comma bool) {\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tvar res []byte\n\t\tres, err = val.MarshalJSON()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tb.WriteString(`\"` + s + `\":`)\n\t\tb.Write(res)\n\t\tif comma {\n\t\t\tb.WriteByte(',')\n\t\t}\n\t}\n\n\tb.WriteString(`\"id\":\"` + g.ID + `\",`)\n\tb.WriteString(`\"white\":\"` + g.White.String() + `\",`)\n\tb.WriteString(`\"black\":\"` + g.Black.String() + `\",`)\n\n\tmjson(\"position\", jsonPosition{g.Position}, true)\n\tmjson(\"state\", g.State, true)\n\tmjson(\"winner\", g.Winner, true)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb.WriteString(`\"creator\":\"` + g.Creator.String() + `\",`)\n\tb.WriteString(`\"created_at\":\"` + g.CreatedAt.Format(time.RFC3339) + `\",`)\n\tb.WriteString(`\"draw_offerer\":` + nilAddr(g.DrawOfferer) + \",\")\n\tb.WriteString(`\"concluder\":` + nilAddr(g.Concluder) + \",\")\n\n\tmjson(\"time\", g.Time, false)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb.WriteByte('}')\n\treturn b.Bytes(), nil\n}\n\ntype jsonPosition struct {\n\tchess.Position\n}\n\nfunc (p jsonPosition) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\tb.WriteByte('{')\n\n\tbfen := p.EncodeFEN()\n\tb.WriteString(`\"fen\":\"` + bfen + `\",`)\n\n\tb.WriteString(`\"moves\":[`)\n\n\tfor idx, m := range p.Moves {\n\t\tb.WriteString(`\"` + m.String() + `\"`)\n\t\tif idx != len(p.Moves)-1 {\n\t\t\tb.WriteByte(',')\n\t\t}\n\t}\n\n\tb.WriteByte(']')\n\tb.WriteByte('}')\n\treturn b.Bytes(), nil\n}\n\nfunc (w Winner) MarshalJSON() ([]byte, error) {\n\tif n := int(w); n \u003c len(winnerString) {\n\t\treturn []byte(`\"` + winnerString[n] + `\"`), nil\n\t}\n\treturn nil, errors.New(\"invalid winner value\")\n}\n\nfunc (g GameState) MarshalJSON() ([]byte, error) {\n\tif int(g) \u003e= len(gameStatesSnake) {\n\t\treturn nil, errors.New(\"invalid game state\")\n\t}\n\treturn []byte(`\"` + gameStatesSnake[g] + `\"`), nil\n}\n\nfunc (tc *TimeControl) MarshalJSON() ([]byte, error) {\n\tif tc == nil {\n\t\treturn []byte(\"null\"), nil\n\t}\n\tvar buf bytes.Buffer\n\n\tbuf.WriteByte('{')\n\tbuf.WriteString(`\"seconds\":` + strconv.Itoa(tc.Seconds) + `,`)\n\tbuf.WriteString(`\"increment\":` + strconv.Itoa(tc.Increment) + `,`)\n\tbuf.WriteString(`\"started_at\":\"` + tc.StartedAt.Format(time.RFC3339) + `\",`)\n\n\tbuf.WriteString(`\"move_timestamps\":[`)\n\tfor idx, mt := range tc.MoveTimestamps {\n\t\tbuf.WriteString(`\"` + mt.Format(time.RFC3339) + `\"`)\n\t\tif idx != len(tc.MoveTimestamps)-1 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t}\n\tbuf.WriteString(\"],\")\n\n\tbuf.WriteString(`\"white_time\":` + strconv.FormatInt(tc.WhiteTime.Milliseconds(), 10) + \",\")\n\tbuf.WriteString(`\"black_time\":` + strconv.FormatInt(tc.BlackTime.Milliseconds(), 10))\n\tbuf.WriteByte('}')\n\n\treturn buf.Bytes(), nil\n}\n\nfunc (p Player) MarshalJSON() ([]byte, error) {\n\tu := users.ResolveAddress(p.Address)\n\n\tvar buf bytes.Buffer\n\tbuf.WriteByte('{')\n\n\tbuf.WriteString(`\"address\":\"` + p.Address.String() + `\",`)\n\tif u == nil {\n\t\tbuf.WriteString(`\"username\":\"\",`)\n\t} else {\n\t\tbuf.WriteString(`\"username\":\"` + u.Name() + `\",`)\n\t}\n\n\tfor idx, cat := range categoryList {\n\t\tstat := p.CategoryInfo[cat]\n\t\tbuf.WriteString(`\"` + cat.String() + `\":{`)\n\t\tbuf.WriteString(`\"wins\":` + strconv.Itoa(stat.Wins) + \",\")\n\t\tbuf.WriteString(`\"losses\":` + strconv.Itoa(stat.Losses) + \",\")\n\t\tbuf.WriteString(`\"draws\":` + strconv.Itoa(stat.Draws) + \",\")\n\t\tbuf.WriteString(`\"rating\":`)\n\t\tif res, err := (jsonPlayerRating{stat.PlayerRating}).MarshalJSON(); err != nil {\n\t\t\treturn nil, err\n\t\t} else {\n\t\t\tbuf.Write(res)\n\t\t}\n\t\tbuf.WriteByte(',')\n\t\tbuf.WriteString(`\"position\":` + strconv.Itoa(p.LeaderboardPosition(cat)))\n\t\tbuf.WriteByte('}')\n\t\tif idx != len(categoryList)-1 {\n\t\t\tbuf.WriteByte(',')\n\t\t}\n\t}\n\n\tbuf.WriteByte('}')\n\treturn buf.Bytes(), nil\n}\n\ntype jsonPlayerRating struct{ *glicko2.PlayerRating }\n\nfunc (p jsonPlayerRating) MarshalJSON() ([]byte, error) {\n\tvar buf bytes.Buffer\n\tbuf.WriteByte('{')\n\tbuf.WriteString(`\"rating\":` + strconv.FormatFloat(p.Rating, 'f', 5, 64) + `,`)\n\tbuf.WriteString(`\"deviation\":` + strconv.FormatFloat(p.RatingDeviation, 'f', 5, 64) + `,`)\n\tbuf.WriteString(`\"volatility\":` + strconv.FormatFloat(p.RatingVolatility, 'f', 5, 64))\n\tbuf.WriteByte('}')\n\treturn buf.Bytes(), nil\n}\n" + }, + { + "name": "lobby.gno", + "body": "package chess\n\nimport (\n\t\"std\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype lobbyPlayer struct {\n\tjoinedAt time.Time\n\tseenAt time.Time\n\tplayer *Player\n}\n\nfunc (l lobbyPlayer) r(cat Category) float64 { return l.player.CategoryInfo[cat].Rating }\n\n// returns whether the rating r is within l's current \"acceptable\" range.\nfunc (l lobbyPlayer) acceptRating(cat Category, r float64) bool {\n\tdelta := int(time.Since(l.joinedAt) / time.Second)\n\tif delta \u003e= 10 {\n\t\treturn true\n\t}\n\n\thalfsize := bracketSize[delta] / 2\n\trat := l.r(cat)\n\treturn r \u003e= (rat-halfsize) \u0026\u0026 r \u003c= (rat+halfsize)\n}\n\ntype tcLobby byte\n\n// time control lobby divisions. N+M -\u003e tcLobbyNpM\nconst (\n\ttcLobby5p0 tcLobby = iota\n\ttcLobby10p5\n\ttcLobbyMax\n)\n\nfunc (tc tcLobby) Category() Category {\n\tswitch tc {\n\tcase tcLobby5p0:\n\t\treturn Blitz\n\tdefault:\n\t\treturn Rapid\n\t}\n}\n\nfunc (tc tcLobby) Time() (secs, incr int) {\n\tswitch tc {\n\tcase tcLobby5p0:\n\t\treturn 60 * 5, 0\n\tcase tcLobby10p5:\n\t\treturn 60 * 10, 5\n\tdefault:\n\t\tpanic(\"invalid tc value\")\n\t}\n}\n\nvar (\n\tlobby [tcLobbyMax][]lobbyPlayer\n\tlobbyPlayer2Game avl.Tree // player addr -\u003e *Game. set after a user is matched; reset when joining again.\n)\n\nfunc LobbyJoin(seconds, increment int) {\n\tassertOriginCall()\n\n\tvar tc tcLobby\n\tswitch {\n\tcase seconds == (60*5) \u0026\u0026 increment == 0:\n\t\ttc = tcLobby5p0\n\tcase seconds == (60*10) \u0026\u0026 increment == 5:\n\t\ttc = tcLobby10p5\n\tdefault:\n\t\tpanic(\"can only use time controls 5+0 or 10+5\")\n\t}\n\n\t// Ensure that user's previous games are finished (or timed out).\n\tcaller := std.PreviousRealm().Address()\n\n\tgames := getUserGames(caller)\n\t// XXX: temporary to avoid ever prohibiting a user from joining the lobby.\n\t// when possible, change back to assertGamesFinished(games)\n\tfor _, g := range games {\n\t\tif !g.State.IsFinished() {\n\t\t\tif err := resign(g); err != nil {\n\t\t\t\tpanic(\"internal error (could not resign game \" + g.ID + \"): \" + err.Error())\n\t\t\t}\n\t\t}\n\t}\n\tassertUserNotInLobby(caller)\n\n\t// remove caller from lobbyPlayer2Game, so LobbyGameFound\n\t// returns the right value.\n\tlobbyPlayer2Game.Remove(caller.String())\n\n\tnow := time.Now()\n\tlobby[tc] = append(lobby[tc], lobbyPlayer{joinedAt: now, seenAt: now, player: getPlayer(caller)})\n\trefreshLobby(tc)\n}\n\nfunc assertUserNotInLobby(caller std.Address) {\n\tfor _, sublob := range lobby {\n\t\tfor _, pl := range sublob {\n\t\t\tif pl.player.Address == caller {\n\t\t\t\tpanic(\"you are already in the lobby\")\n\t\t\t}\n\t\t}\n\t}\n}\n\n// refreshLobby serves to run through the lobby, kick timed out users, and see if any users\n// can be matched with the current user.\nfunc refreshLobby(tc tcLobby) {\n\tcallerAddr := std.PreviousRealm().Address()\n\tnow := time.Now()\n\tfor idx, player := range lobby[tc] {\n\t\tif player.player.Address == callerAddr {\n\t\t\t// mark player as seen now\n\t\t\tlobby[tc][idx].seenAt = now\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// lobby housekeeping: kick any player that hasn't contacted us for the\n\t// past 30 seconds.\n\t// do this BEFORE matching the caller, as we want to give them someone who\n\t// is seemingly active in the lobby.\n\tfor i := 0; i \u003c len(lobby[tc]); i++ {\n\t\tif now.Sub(lobby[tc][i].seenAt) \u003e= time.Second*30 {\n\t\t\tnewLobby := append([]lobbyPlayer{}, lobby[tc][:i]...)\n\t\t\tlobby[tc] = append(newLobby, lobby[tc][i+1:]...)\n\t\t\ti--\n\t\t}\n\t}\n\n\t// determine sub lobby\n\tsublob := lobby[tc]\n\n\tcallerPos := -1\n\tvar caller lobbyPlayer\n\tfor idx, player := range sublob {\n\t\tif player.player.Address == callerAddr {\n\t\t\tcallerPos = idx\n\t\t\tcaller = player\n\t\t\tbreak\n\t\t}\n\t}\n\t// caller is not involved in lobby, or lobby only contains the player\n\tif callerPos \u003c 0 || len(sublob) \u003c 2 {\n\t\treturn\n\t}\n\n\tcat := tc.Category()\n\tcallerRating := caller.r(cat)\n\tcallerForce := now.Sub(caller.joinedAt) \u003e= time.Second*10\n\n\tfor i, player := range sublob {\n\t\tif i == callerPos {\n\t\t\tcontinue\n\t\t}\n\t\t// force if either the caller or the player have been waiting for more than 10s.\n\t\tforce := callerForce || (now.Sub(player.joinedAt) \u003e= time.Second*10)\n\t\t// find player whose rating falls in each other's range.\n\t\tif force || (caller.acceptRating(cat, player.r(cat)) \u0026\u0026\n\t\t\tplayer.acceptRating(cat, callerRating)) {\n\t\t\tlobbyMatch(tc, callerPos, i)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc lobbyMatch(tc tcLobby, p1, p2 int) {\n\t// Get the two players, create a new game with them.\n\tsecs, incr := tc.Time()\n\ta1, a2 := lobby[tc][p1].player.Address, lobby[tc][p2].player.Address\n\n\tgame := newGame(a1, a2, secs, incr)\n\n\t// remove p1 and p2 from lobby\n\tif p1 \u003e p2 {\n\t\tp1, p2 = p2, p1\n\t}\n\tnl := append([]lobbyPlayer{}, lobby[tc][:p1]...)\n\tnl = append(nl, lobby[tc][p1+1:p2]...)\n\tnl = append(nl, lobby[tc][p2+1:]...)\n\tlobby[tc] = nl\n\n\t// add to lobbyPlayer2Game\n\tlobbyPlayer2Game.Set(a1.String(), game)\n\tlobbyPlayer2Game.Set(a2.String(), game)\n}\n\n/*\ngenerated by python code:\n\n\tfor i in range(0,10):\n\t\tprint((i+1)**(3.694692926)+49, ',')\n\nrationale: give brackets in an exponential range between\n50 and 5000, dividing it into 10 steps.\n\"magic constant\" obtained solving for in c in the equation:\n\n\t5000=(x+1)^c+49 (x = steps, 10 in our case)\n\nwhich comes out to be ln(delta)/ln(steps), delta = 5000-49, steps = 10.\n*/\nvar bracketSize = [...]float64{\n\t50.0,\n\t61.9483191543645,\n\t106.91839582826664,\n\t216.65896892328266,\n\t431.3662312611604,\n\t798.94587409321,\n\t1374.4939512498888,\n\t2219.9018387103433,\n\t3403.5405753197747,\n\t5000,\n}\n\nfunc LobbyGameFound() string {\n\trefreshLobby(tcLobby5p0)\n\trefreshLobby(tcLobby10p5)\n\n\tval, ok := lobbyPlayer2Game.Get(std.PreviousRealm().Address().String())\n\tif !ok {\n\t\treturn \"null\"\n\t}\n\treturn val.(*Game).json()\n}\n\nfunc LobbyQuit() {\n\tcaller := std.PreviousRealm().Address()\n\tfor tc, sublob := range lobby {\n\t\tfor i, pl := range sublob {\n\t\t\tif pl.player.Address == caller {\n\t\t\t\tnewLobby := append([]lobbyPlayer{}, sublob[:i]...)\n\t\t\t\tnewLobby = append(newLobby, sublob[i+1:]...)\n\t\t\t\tlobby[tc] = newLobby\n\t\t\t\tlobbyPlayer2Game.Remove(caller.String())\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\tpanic(\"you are not in the lobby\")\n}\n" + }, + { + "name": "lobby_test.gno", + "body": "package chess\n\nimport (\n\t\"os\"\n\t\"std\"\n\t\"testing\"\n\t\"time\"\n\n\t\"gno.land/p/morgan/chess/glicko2\"\n)\n\nfunc TestLobbyJoin(t *testing.T) {\n\tcleanup()\n\ttesting.SetRealm(std.NewUserRealm(white))\n\tLobbyJoin(10*60, 5)\n\tos.Sleep(time.Second * 5)\n\ttesting.SetRealm(std.NewUserRealm(black))\n\tLobbyJoin(10*60, 5)\n\tres := LobbyGameFound()\n\tif res == \"null\" {\n\t\tt.Errorf(\"LobbyGameFound is null\")\n\t}\n}\n\nfunc sublobbyToIDs(pl []lobbyPlayer) []string {\n\ts := make([]string, len(pl))\n\tfor idx, p := range pl {\n\t\ts[idx] = string(p.player.Address)\n\t}\n\treturn s\n}\n\nfunc TestLobbyGameFound(t *testing.T) {\n\tcheck := func(checker ...func(t *testing.T)) func(t *testing.T) {\n\t\treturn func(t *testing.T) {\n\t\t\tfor _, ck := range checker {\n\t\t\t\tck(t)\n\t\t\t}\n\t\t}\n\t}\n\tids := func(ids ...std.Address) func(t *testing.T) {\n\t\treturn func(t *testing.T) {\n\t\t\tif len(ids) != len(lobby[0]) {\n\t\t\t\tt.Errorf(\"lobby doesn't match expected ids: lobby: %v, newIDs: %v\", sublobbyToIDs(lobby[0]), ids)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor idx, i := range ids {\n\t\t\t\tif pa := lobby[0][idx].player.Address; pa != i {\n\t\t\t\t\tt.Errorf(\"check pos %d: player id doesnt match (got %q want %q)\", idx, pa, i)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tnumGames := func(n int) func(t *testing.T) {\n\t\treturn func(t *testing.T) {\n\t\t\tl := gameStore.Size()\n\t\t\tif l != n {\n\t\t\t\tt.Errorf(\"invalid gameStore size; want %d got %d\", n, l)\n\t\t\t}\n\t\t}\n\t}\n\n\ttype pl struct {\n\t\tid std.Address\n\t\trating float64\n\t\t// use negative values here to indicate how many seconds in the past;\n\t\t// ie: joinedAt: -1, means player joined 1 second ago.\n\t\tjoinedAt int\n\t\tseenAt int\n\t}\n\ttt := []struct {\n\t\tname string\n\t\tpre []pl\n\t\tcaller std.Address\n\t\tcheck func(t *testing.T)\n\t}{\n\t\t{\n\t\t\t\"equalRating\",\n\t\t\t[]pl{{\"1\", 1200, -1, -1}, {\"2\", 1200, 0, 0}},\n\t\t\t\"1\",\n\t\t\tcheck(ids(), numGames(1)),\n\t\t},\n\t\t{\n\t\t\t\"minimumApart\", // delta \u003c= 25\n\t\t\t[]pl{{\"1\", 1200, 0, 0}, {\"2\", 1225, 0, 0}},\n\t\t\t\"2\",\n\t\t\tcheck(ids(), numGames(1)),\n\t\t},\n\t\t{\n\t\t\t\"tooFarApart\", // delta \u003e 25\n\t\t\t[]pl{{\"1\", 1200, 0, 0}, {\"2\", 1230, 0, 0}},\n\t\t\t\"2\",\n\t\t\tcheck(ids(\"1\", \"2\"), numGames(0)),\n\t\t},\n\t\t{\n\t\t\t\"oldHighPriority\",\n\t\t\t// kicked hasn't been seen in too long, so should not be considered.\n\t\t\t// 1 is active and has been looking for 30s, so it gets priority, even if 2-3 is\n\t\t\t// a closer match.\n\t\t\t[]pl{{\"kicked\", 1800, -60, -50}, {\"1\", 1900, -30, -10}, {\"2\", 1400, 0, 0}, {\"3\", 1420, 0, 0}},\n\t\t\t\"3\",\n\t\t\tcheck(ids(\"2\"), numGames(1)),\n\t\t},\n\t\t{\n\t\t\t\"oldHighPriority2\",\n\t\t\t[]pl{{\"comeback\", 1800, -60, -50}, {\"1\", 1900, -30, -10}, {\"2\", 1400, 0, 0}, {\"3\", 1420, 0, 0}},\n\t\t\t// same as last one, except the player who was kicked last time, because\n\t\t\t// he's the caller, has their seenAt set back to the current time, so they're matched with 1.\n\t\t\t\"comeback\",\n\t\t\tcheck(ids(\"2\", \"3\"), numGames(1)),\n\t\t},\n\t\t{\n\t\t\t\"alone\",\n\t\t\t[]pl{{\"1\", 1200, 0, 0}},\n\t\t\t\"1\",\n\t\t\tcheck(ids(\"1\"), numGames(0)),\n\t\t},\n\t\t{\n\t\t\t\"brackFail\",\n\t\t\t[]pl{{\"1\", 1200, -4, -4}, {\"2\", 1450, -5, -5}},\n\t\t\t\"1\",\n\t\t\tcheck(ids(\"1\", \"2\"), numGames(0)),\n\t\t},\n\t\t{\n\t\t\t\"brackFail2\",\n\t\t\t[]pl{{\"1\", 1200, -5, -5}, {\"2\", 1450, -4, -4}},\n\t\t\t\"1\",\n\t\t\tcheck(ids(\"1\", \"2\"), numGames(0)),\n\t\t},\n\t\t{\n\t\t\t\"brackSuccess\",\n\t\t\t[]pl{{\"1\", 1200, -5, -5}, {\"2\", 1450, -5, -5}},\n\t\t\t\"1\",\n\t\t\tcheck(ids(), numGames(1)),\n\t\t},\n\t}\n\n\tfor _, tc := range tt {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcleanup()\n\t\t\tnow := time.Now()\n\t\t\tfor _, p := range tc.pre {\n\t\t\t\tlobby[0] = append(lobby[0], lobbyPlayer{\n\t\t\t\t\tjoinedAt: now.Add(time.Duration(p.joinedAt) * time.Second),\n\t\t\t\t\tseenAt: now.Add(time.Duration(p.seenAt) * time.Second),\n\t\t\t\t\tplayer: \u0026Player{\n\t\t\t\t\t\tAddress: p.id,\n\t\t\t\t\t\tCategoryInfo: [CategoryMax]CategoryInfo{\n\t\t\t\t\t\t\tBlitz: {PlayerRating: \u0026glicko2.PlayerRating{Rating: p.rating}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\ttesting.SetRealm(std.NewUserRealm(tc.caller))\n\t\t\tgame := LobbyGameFound()\n\n\t\t\tif tc.check != nil {\n\t\t\t\ttc.check(t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLobbyJoin_HasOpenGames(t *testing.T) {\n\tcleanup()\n\tg := \u0026Game{\n\t\tID: \"123\",\n\t\tWhite: white,\n\t\tBlack: black,\n\t\tState: GameStateOpen,\n\t}\n\tgameStore.Set(g.ID, g)\n\taddToUser2Games(white, g)\n\taddToUser2Games(black, g)\n\n\ttesting.SetRealm(std.NewUserRealm(white))\n\tLobbyJoin(10*60, 5)\n\tif g.State != GameStateAborted {\n\t\tt.Errorf(\"state wrong: want %d got %d\", GameStateAborted, g.State)\n\t}\n\tif g.Winner != WinnerNone {\n\t\tt.Errorf(\"winner wrong: want %q got %q\", \"none\", g.Winner)\n\t}\n}\n" + }, + { + "name": "time.gno", + "body": "package chess\n\nimport (\n\t\"time\"\n)\n\n// TimeControl keeps track of time control information for the game.\ntype TimeControl struct {\n\tSeconds int\n\tIncrement int\n\n\tStartedAt time.Time\n\tMoveTimestamps []time.Time\n\tWhiteTime time.Duration\n\tBlackTime time.Duration\n}\n\nfunc NewTimeControl(seconds, incr int) *TimeControl {\n\tif seconds \u003c= 0 {\n\t\treturn nil\n\t}\n\treturn \u0026TimeControl{\n\t\tSeconds: seconds,\n\t\tIncrement: incr,\n\n\t\tStartedAt: time.Now(),\n\t\tWhiteTime: time.Duration(seconds) * time.Second,\n\t\tBlackTime: time.Duration(seconds) * time.Second,\n\t}\n}\n\n// AddMove records that at the current time, a new move was added.\nfunc (tc *TimeControl) AddMove() (valid bool) {\n\tnd, v := tc.timedOut()\n\tif v {\n\t\treturn false\n\t}\n\tif len(tc.MoveTimestamps)\u00261 == 0 {\n\t\ttc.WhiteTime = nd\n\t} else {\n\t\ttc.BlackTime = nd\n\t}\n\ttc.MoveTimestamps = append(tc.MoveTimestamps, time.Now())\n\treturn true\n}\n\nfunc (tc *TimeControl) TimedOut() bool {\n\t_, v := tc.timedOut()\n\treturn v\n}\n\nfunc (tc *TimeControl) timedOut() (time.Duration, bool) {\n\tmts := tc.MoveTimestamps\n\n\t// First move for each player: they both have up to 30 seconds.\n\tswitch len(mts) {\n\tcase 0:\n\t\tdelta := time.Since(tc.StartedAt)\n\t\treturn tc.WhiteTime, delta \u003e time.Second*30\n\tcase 1:\n\t\tdelta := time.Since(mts[0])\n\t\treturn tc.BlackTime, delta \u003e time.Second*30\n\t}\n\n\t// Determine color. Determine time since last move. Try subtracting from\n\t// color's time. If \u003e= 0, good. If \u003c 0, timeout.\n\tdelta := time.Since(mts[len(mts)-1])\n\n\tif len(mts)\u00261 == 0 { // white\n\t\tnt := tc.WhiteTime - delta\n\t\treturn nt + tc.incr(), nt \u003c 0\n\t}\n\n\tnt := tc.BlackTime - delta\n\treturn nt + tc.incr(), nt \u003c 0\n}\n\nfunc (tc *TimeControl) incr() time.Duration {\n\t// there is always at least a one second increment, to account for\n\t// block time and the delay between user making a move and tx happening\n\treturn time.Second + time.Duration(tc.Increment)*time.Second\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "0RufFFxd7EN7JbyS0rHlYM8ZeXX5PjXYRgQfnaJyBKmeW6awz8I8cVSbeScBfJujBnr1JCMCLYnIAl4Px0W7CQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "guestbook", + "path": "gno.land/r/morgan/guestbook", + "files": [ + { + "name": "admin.gno", + "body": "package guestbook\n\nimport (\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/seqid\"\n)\n\nvar owner = ownable.New()\n\n// AdminDelete removes the guestbook message with the given ID.\n// The user will still be marked as having submitted a message, so they\n// won't be able to re-submit a new message.\nfunc AdminDelete(signatureID string) {\n\towner.AssertCallerIsOwner()\n\n\tid, err := seqid.FromString(signatureID)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tidb := id.Binary()\n\tif !guestbook.Has(idb) {\n\t\tpanic(\"signature does not exist\")\n\t}\n\tguestbook.Remove(idb)\n}\n" + }, + { + "name": "guestbook.gno", + "body": "// Realm guestbook contains an implementation of a simple guestbook.\n// Come and sign yourself up!\npackage guestbook\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/seqid\"\n)\n\n// Signature is a single entry in the guestbook.\ntype Signature struct {\n\tMessage string\n\tAuthor std.Address\n\tTime time.Time\n}\n\nconst (\n\tmaxMessageLength = 140\n\tmaxPerPage = 25\n)\n\nvar (\n\tsignatureID seqid.ID\n\tguestbook avl.Tree // id -\u003e Signature\n\thasSigned avl.Tree // address -\u003e struct{}\n)\n\nfunc init() {\n\tSign(\"You reached the end of the guestbook!\")\n}\n\nconst (\n\terrNotAUser = \"this guestbook can only be signed by users\"\n\terrAlreadySigned = \"you already signed the guestbook!\"\n\terrInvalidCharacterInMessage = \"invalid character in message\"\n)\n\n// Sign signs the guestbook, with the specified message.\nfunc Sign(message string) {\n\tprev := std.PreviousRealm()\n\tswitch {\n\tcase !prev.IsUser():\n\t\tpanic(errNotAUser)\n\tcase hasSigned.Has(prev.Address().String()):\n\t\tpanic(errAlreadySigned)\n\t}\n\tmessage = validateMessage(message)\n\n\tguestbook.Set(signatureID.Next().Binary(), Signature{\n\t\tMessage: message,\n\t\tAuthor: prev.Address(),\n\t\t// NOTE: time.Now() will yield the \"block time\", which is deterministic.\n\t\tTime: time.Now(),\n\t})\n\thasSigned.Set(prev.Address().String(), struct{}{})\n}\n\nfunc validateMessage(msg string) string {\n\tif len(msg) \u003e maxMessageLength {\n\t\tpanic(\"Keep it brief! (max \" + strconv.Itoa(maxMessageLength) + \" bytes!)\")\n\t}\n\tout := \"\"\n\tfor _, ch := range msg {\n\t\tswitch {\n\t\tcase unicode.IsLetter(ch),\n\t\t\tunicode.IsNumber(ch),\n\t\t\tunicode.IsSpace(ch),\n\t\t\tunicode.IsPunct(ch):\n\t\t\tout += string(ch)\n\t\tdefault:\n\t\t\tpanic(errInvalidCharacterInMessage)\n\t\t}\n\t}\n\treturn out\n}\n\nfunc Render(maxID string) string {\n\tvar bld strings.Builder\n\n\tbld.WriteString(\"# Guestbook 📝\\n\\n[Come sign the guestbook!](./guestbook$help\u0026func=Sign)\\n\\n---\\n\\n\")\n\n\tvar maxIDBinary string\n\tif maxID != \"\" {\n\t\tmid, err := seqid.FromString(maxID)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t// AVL iteration is exclusive, so we need to decrease the ID value to get the \"true\" maximum.\n\t\tmid--\n\t\tmaxIDBinary = mid.Binary()\n\t}\n\n\tvar lastID seqid.ID\n\tvar printed int\n\tguestbook.ReverseIterate(\"\", maxIDBinary, func(key string, val any) bool {\n\t\tsig := val.(Signature)\n\t\tmessage := strings.ReplaceAll(sig.Message, \"\\n\", \"\\n\u003e \")\n\t\tbld.WriteString(\"\u003e \" + message + \"\\n\u003e\\n\")\n\t\tidValue, ok := seqid.FromBinary(key)\n\t\tif !ok {\n\t\t\tpanic(\"invalid seqid id\")\n\t\t}\n\n\t\tbld.WriteString(\"\u003e _Written by \" + sig.Author.String() + \" at \" + sig.Time.Format(time.DateTime) + \"_ (#\" + idValue.String() + \")\\n\\n---\\n\\n\")\n\t\tlastID = idValue\n\n\t\tprinted++\n\t\t// stop after exceeding limit\n\t\treturn printed \u003e= maxPerPage\n\t})\n\n\tif printed == 0 {\n\t\tbld.WriteString(\"No messages!\")\n\t} else if printed \u003e= maxPerPage {\n\t\tbld.WriteString(\"\u003cp style='text-align:right'\u003e\u003ca href='./guestbook:\" + lastID.String() + \"'\u003eNext page\u003c/a\u003e\u003c/p\u003e\")\n\t}\n\n\treturn bld.String()\n}\n" + }, + { + "name": "guestbook_test.gno", + "body": "package guestbook\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n)\n\nfunc TestSign(t *testing.T) {\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\ttesting.SetRealm(std.NewUserRealm(\"g1user\"))\n\tSign(\"Hello!\")\n\n\ttesting.SetRealm(std.NewUserRealm(\"g1user2\"))\n\tSign(\"Hello2!\")\n\n\tres := Render(\"\")\n\tt.Log(res)\n\tif !strings.Contains(res, \"\u003e Hello!\\n\u003e\\n\u003e _Written by g1user \") {\n\t\tt.Error(\"does not contain first user's message\")\n\t}\n\tif !strings.Contains(res, \"\u003e Hello2!\\n\u003e\\n\u003e _Written by g1user2 \") {\n\t\tt.Error(\"does not contain second user's message\")\n\t}\n\tif guestbook.Size() != 2 {\n\t\tt.Error(\"invalid guestbook size\")\n\t}\n}\n\nfunc TestSign_FromRealm(t *testing.T) {\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/gnoland/users/v1\"))\n\n\tdefer func() {\n\t\trec := recover()\n\t\tif rec == nil {\n\t\t\tt.Fatal(\"expected panic\")\n\t\t}\n\t\trecString, ok := rec.(string)\n\t\tif !ok {\n\t\t\tt.Fatal(\"not a string\", rec)\n\t\t} else if recString != errNotAUser {\n\t\t\tt.Fatal(\"invalid error\", recString)\n\t\t}\n\t}()\n\tSign(\"Hey!\")\n}\n\nfunc TestSign_Double(t *testing.T) {\n\t// Should not allow signing twice.\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\ttesting.SetRealm(std.NewUserRealm(\"g1user\"))\n\tSign(\"Hello!\")\n\n\tdefer func() {\n\t\trec := recover()\n\t\tif rec == nil {\n\t\t\tt.Fatal(\"expected panic\")\n\t\t}\n\t\trecString, ok := rec.(string)\n\t\tif !ok {\n\t\t\tt.Error(\"type assertion failed\", rec)\n\t\t} else if recString != errAlreadySigned {\n\t\t\tt.Error(\"invalid error message\", recString)\n\t\t}\n\t}()\n\n\tSign(\"Hello again!\")\n}\n\nfunc TestSign_InvalidMessage(t *testing.T) {\n\t// Should not allow control characters in message.\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\n\ttesting.SetRealm(std.NewUserRealm(\"g1user\"))\n\n\tdefer func() {\n\t\trec := recover()\n\t\tif rec == nil {\n\t\t\tt.Fatal(\"expected panic\")\n\t\t}\n\t\trecString, ok := rec.(string)\n\t\tif !ok {\n\t\t\tt.Error(\"type assertion failed\", rec)\n\t\t} else if recString != errInvalidCharacterInMessage {\n\t\t\tt.Error(\"invalid error message\", recString)\n\t\t}\n\t}()\n\tSign(\"\\x00Hello!\")\n}\n\nfunc TestAdminDelete(t *testing.T) {\n\tconst (\n\t\tuserAddr std.Address = \"g1user\"\n\t\tadminAddr std.Address = \"g1admin\"\n\t)\n\n\tguestbook = avl.Tree{}\n\thasSigned = avl.Tree{}\n\towner = ownable.NewWithAddress(adminAddr)\n\tsignatureID = 0\n\n\ttesting.SetRealm(std.NewUserRealm(userAddr))\n\n\tconst bad = \"Very Bad Message! Nyeh heh heh!\"\n\tSign(bad)\n\n\tif rnd := Render(\"\"); !strings.Contains(rnd, bad) {\n\t\tt.Fatal(\"render does not contain bad message\", rnd)\n\t}\n\n\ttesting.SetRealm(std.NewUserRealm(adminAddr))\n\tAdminDelete(signatureID.String())\n\n\tif rnd := Render(\"\"); strings.Contains(rnd, bad) {\n\t\tt.Error(\"render contains bad message\", rnd)\n\t}\n\tif guestbook.Size() != 0 {\n\t\tt.Error(\"invalid guestbook size\")\n\t}\n\tif hasSigned.Size() != 1 {\n\t\tt.Error(\"invalid hasSigned size\")\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "yeJN0uIWyN9eMq1CvVywDynlHAJJfKX+epti2bu3uH0H4YT8MuQGj1NWBCCe8XLW0HxdhmFabN/4Mt9KhYGyBw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/morgan/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport \"gno.land/r/leon/hof\"\n\nconst staticHome = `# morgan's (gn)home\n\n- [📝 sign my guestbook](/r/morgan/guestbook)\n`\n\nfunc init() { hof.Register(\"Morgan's Home Realm\", \"\") }\n\nfunc Render(path string) string {\n\treturn staticHome\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "jb6moW3I/LNBffvhsOyGRvAAenMdBiT8GQ7OTzBM+Wbh4K1LAgn8PhnZSVNNYmWTid/YyPsHMO+jyb3tQ11IAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "config", + "path": "gno.land/r/moul/config", + "files": [ + { + "name": "config.gno", + "body": "package config\n\nimport \"std\"\n\nvar addr = std.Address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // @moul\n\nfunc Addr() std.Address {\n\treturn addr\n}\n\nfunc UpdateAddr(newAddr std.Address) {\n\tAssertIsAdmin()\n\taddr = newAddr\n}\n\nfunc AssertIsAdmin() {\n\tif std.OriginCaller() != addr {\n\t\tpanic(\"restricted area\")\n\t}\n}\n" + }, + { + "name": "config_test.gno", + "body": "package config\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "5HOauSsZ8ITyskfL4VjBDOCfpyLRSKRFvFe7/zjiWTpRuCcmHJS6/j2WBGLYRisnql9Yq38V0+KJBr0tq7eiDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/moul/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/svg\"\n\t\"gno.land/p/moul/debug\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/moul/txlink\"\n\t\"gno.land/p/moul/web25\"\n\t\"gno.land/r/leon/hof\"\n\t\"gno.land/r/moul/config\"\n)\n\nvar (\n\ttodos []string\n\tstatus string\n\tmemeImgURL string\n\tweb25config = web25.Config{URL: \"https://moul.github.io/gno-moul-home-web25/\"}\n)\n\nfunc init() {\n\ttodos = append(todos, \"fill this todo list...\")\n\tstatus = \"Online\" // Initial status set to \"Online\"\n\tmemeImgURL = \"https://i.imgflip.com/7ze8dc.jpg\"\n\thof.Register(\"Moul's Home Realm!\", \"\")\n}\n\nfunc Render(path string) string {\n\tcontent := web25config.Render(path)\n\tvar d debug.Debug\n\n\tcontent += md.H1(\"Manfred's (gn)home Dashboard\")\n\n\tcontent += md.H2(\"Meme\")\n\tcontent += md.Paragraph(\n\t\tmd.Image(\"meme\", memeImgURL),\n\t)\n\n\tcontent += md.H2(\"Status\")\n\tcontent += md.Paragraph(status)\n\tcontent += md.Paragraph(md.Link(\"update\", txlink.Call(\"UpdateStatus\")))\n\n\td.Log(\"hello world!\")\n\n\tcontent += md.H2(\"Personal TODO List (bullet list)\")\n\tfor i, todo := range todos {\n\t\tidstr := strconv.Itoa(i)\n\t\tdeleteLink := md.Link(\"x\", txlink.Call(\"DeleteTodo\", \"idx\", idstr))\n\t\tcontent += md.BulletItem(todo + \" \" + deleteLink)\n\t}\n\tcontent += md.BulletItem(md.Link(\"[new]\", txlink.Call(\"AddTodo\")))\n\n\tcontent += md.H2(\"Personal TODO List (table)\")\n\ttable := mdtable.Table{\n\t\tHeaders: []string{\"ID\", \"Item\", \"Links\"},\n\t}\n\tfor i, todo := range todos {\n\t\tidstr := strconv.Itoa(i)\n\t\tdeleteLink := md.Link(\"[del]\", txlink.Call(\"DeleteTodo\", \"idx\", idstr))\n\t\ttable.Append([]string{\"#\" + idstr, todo, deleteLink})\n\t}\n\tcontent += table.String()\n\n\tcontent += md.H2(\"SVG Example\")\n\tcontent += md.Paragraph(\"this feature may not work with the current gnoweb version and/or configuration.\")\n\tcontent += md.Paragraph(svg.Canvas{\n\t\tWidth: 500, Height: 500,\n\t\tElems: []svg.Elem{\n\t\t\tsvg.Rectangle{50, 50, 100, 100, \"red\"},\n\t\t\tsvg.Circle{50, 50, 100, \"red\"},\n\t\t\tsvg.Text{100, 100, \"hello world!\", \"magenta\"},\n\t\t},\n\t}.String())\n\n\tcontent += md.H2(\"Debug\")\n\tcontent += md.Paragraph(\"this feature may not work with the current gnoweb version and/or configuration.\")\n\tcontent += md.Paragraph(\n\t\tmd.Link(\"toggle debug\", debug.ToggleURL(path)),\n\t)\n\n\t// TODO: my r/boards posts\n\t// TODO: my r/events events\n\tcontent += d.Render(path)\n\treturn content\n}\n\nfunc AddTodo(todo string) {\n\tconfig.AssertIsAdmin()\n\ttodos = append(todos, todo)\n}\n\nfunc DeleteTodo(idx int) {\n\tconfig.AssertIsAdmin()\n\tif idx \u003e= 0 \u0026\u0026 idx \u003c len(todos) {\n\t\t// Remove the todo from the list by merging slices from before and after the todo\n\t\ttodos = append(todos[:idx], todos[idx+1:]...)\n\t} else {\n\t\tpanic(\"Invalid todo index\")\n\t}\n}\n\nfunc UpdateStatus(newStatus string) {\n\tconfig.AssertIsAdmin()\n\tstatus = newStatus\n}\n" + }, + { + "name": "z1_filetest.gno", + "body": "package main\n\nimport \"gno.land/r/moul/home\"\n\nfunc main() {\n\tprintln(home.Render(\"\"))\n}\n\n// Output:\n// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience.\n// # Manfred's (gn)home Dashboard\n// ## Meme\n// ![meme](https://i.imgflip.com/7ze8dc.jpg)\n//\n// ## Status\n// Online\n//\n// [update](/r/moul/home$help\u0026func=UpdateStatus)\n//\n// ## Personal TODO List (bullet list)\n// - fill this todo list... [x](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=0)\n// - [\\[new\\]](/r/moul/home$help\u0026func=AddTodo)\n// ## Personal TODO List (table)\n// | ID | Item | Links |\n// | --- | --- | --- |\n// | #0 | fill this todo list... | [\\[del\\]](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=0) |\n// ## SVG Example\n// this feature may not work with the current gnoweb version and/or configuration.\n//\n// \u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"500\" height=\"500\"\u003e\u003crect x=\"50\" y=\"50\" width=\"100\" height=\"100\" fill=\"red\" /\u003e\u003ccircle cx=\"50\" cy=\"50\" r=\"100\" fill=\"red\" /\u003e\u003ctext x=\"100\" y=\"100\" fill=\"magenta\"\u003ehello world!\u003c/text\u003e\u003c/svg\u003e\n//\n// ## Debug\n// this feature may not work with the current gnoweb version and/or configuration.\n//\n// [toggle debug](/r/moul/home:?debug=1)\n//\n//\n" + }, + { + "name": "z2_filetest.gno", + "body": "package main\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/r/moul/home\"\n)\n\nfunc main() {\n\ttesting.SetOriginCaller(std.Address(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"))\n\thome.AddTodo(\"aaa\")\n\thome.AddTodo(\"bbb\")\n\thome.AddTodo(\"ccc\")\n\thome.AddTodo(\"ddd\")\n\thome.AddTodo(\"eee\")\n\thome.UpdateStatus(\"Lorem Ipsum\")\n\thome.DeleteTodo(3)\n\tprintln(home.Render(\"?debug=1\"))\n}\n\n// Output:\n// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience.\n// # Manfred's (gn)home Dashboard\n// ## Meme\n// ![meme](https://i.imgflip.com/7ze8dc.jpg)\n//\n// ## Status\n// Lorem Ipsum\n//\n// [update](/r/moul/home$help\u0026func=UpdateStatus)\n//\n// ## Personal TODO List (bullet list)\n// - fill this todo list... [x](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=0)\n// - aaa [x](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=1)\n// - bbb [x](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=2)\n// - ddd [x](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=3)\n// - eee [x](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=4)\n// - [\\[new\\]](/r/moul/home$help\u0026func=AddTodo)\n// ## Personal TODO List (table)\n// | ID | Item | Links |\n// | --- | --- | --- |\n// | #0 | fill this todo list... | [\\[del\\]](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=0) |\n// | #1 | aaa | [\\[del\\]](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=1) |\n// | #2 | bbb | [\\[del\\]](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=2) |\n// | #3 | ddd | [\\[del\\]](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=3) |\n// | #4 | eee | [\\[del\\]](/r/moul/home$help\u0026func=DeleteTodo\u0026idx=4) |\n// ## SVG Example\n// this feature may not work with the current gnoweb version and/or configuration.\n//\n// \u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"500\" height=\"500\"\u003e\u003crect x=\"50\" y=\"50\" width=\"100\" height=\"100\" fill=\"red\" /\u003e\u003ccircle cx=\"50\" cy=\"50\" r=\"100\" fill=\"red\" /\u003e\u003ctext x=\"100\" y=\"100\" fill=\"magenta\"\u003ehello world!\u003c/text\u003e\u003c/svg\u003e\n//\n// ## Debug\n// this feature may not work with the current gnoweb version and/or configuration.\n//\n// [toggle debug](/r/moul/home:)\n//\n// \u003cdetails\u003e\u003csummary\u003edebug\u003c/summary\u003e\n//\n// ### Logs\n// - hello world!\n// ### Metadata\n// | Key | Value |\n// | --- | --- |\n// | `std.CurrentRealm().PkgPath()` | gno.land/r/moul/home |\n// | `std.CurrentRealm().Address()` | g1h8h57ntxadcze3f703skymfzdwa6t3ugf0nq3z |\n// | `std.PreviousRealm().PkgPath()` | |\n// | `std.PreviousRealm().Address()` | g1manfred47kzduec920z88wfr64ylksmdcedlf5 |\n// | `std.ChainHeight()` | 123 |\n// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z |\n//\n// \u003c/details\u003e\n//\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "l5bVZUy4BQb5d4xTHROgaYpTYbUU31cLTZaqKuID3NfwjA27C8nzYlpkWK/8y+wLK63vY6ibZhzonig8R7rFAQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "microposts", + "path": "gno.land/r/moul/microposts", + "files": [ + { + "name": "README.md", + "body": "# fork of `leon/fosdem25/microposts`\n\nremoving optional lines to make the code more concise for slides.\n\nOriginal work here: https://gno.land/r/leon/fosdem25/microposts\n" + }, + { + "name": "microposts_test.gno", + "body": "package microposts\n\n// empty file just to make sure that `gno test` tries to parse the implementation.\n" + }, + { + "name": "post.gno", + "body": "package microposts\n\nimport (\n\t\"std\"\n\t\"time\"\n)\n\ntype Post struct {\n\ttext string\n\tauthor std.Address\n\tcreatedAt time.Time\n}\n\nfunc (p Post) String() string {\n\tout := p.text + \"\\n\"\n\tout += \"_\" + p.createdAt.Format(\"02 Jan 2006, 15:04\") + \", by \" + p.author.String() + \"_\"\n\treturn out\n}\n" + }, + { + "name": "realm.gno", + "body": "package microposts\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n)\n\nvar posts []*Post\n\nfunc CreatePost(text string) {\n\tposts = append(posts, \u0026Post{\n\t\ttext: text,\n\t\tauthor: std.PreviousRealm().Address(), // provided by env\n\t\tcreatedAt: time.Now(),\n\t})\n}\n\nfunc Render(_ string) string {\n\tout := \"# Posts\\n\"\n\tfor i := len(posts) - 1; i \u003e= 0; i-- {\n\t\tout += \"### Post \" + strconv.Itoa(i) + \"\\n\" + posts[i].String()\n\t}\n\treturn out\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "9tYXB7tMAKAlYWTyFqbeSVg/ee97tN6QXKCJfsKeJsjrZwtjylcbe89OJvOaBNCBoOc0XUsBoDvEGWxkDyq4AA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "present", + "path": "gno.land/r/moul/present", + "files": [ + { + "name": "present.gno", + "body": "package present\n\nimport (\n\t\"net/url\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl/pager\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/seqid\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/collection\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/p/moul/mdtable\"\n\t\"gno.land/p/moul/realmpath\"\n\t\"gno.land/p/moul/txlink\"\n)\n\nvar chainDomain = std.ChainDomain()\n\ntype Presentation struct {\n\tSlug string\n\tTitle string\n\tEvent string\n\tAuthor string\n\tUploader std.Address\n\tDate time.Time\n\tContent string\n\tEditDate time.Time\n\tNumSlides int\n}\n\nvar (\n\tpresentations *collection.Collection\n\tOwnable *ownable.Ownable\n)\n\nfunc init() {\n\tpresentations = collection.New()\n\t// for /view and /slides\n\tpresentations.AddIndex(\"slug\", func(v any) string {\n\t\treturn v.(*Presentation).Slug\n\t}, collection.UniqueIndex)\n\n\t// for table sorting\n\tpresentations.AddIndex(\"date\", func(v any) string {\n\t\treturn v.(*Presentation).Date.String()\n\t}, collection.DefaultIndex)\n\tpresentations.AddIndex(\"author\", func(v any) string {\n\t\treturn v.(*Presentation).Author\n\t}, collection.DefaultIndex)\n\tpresentations.AddIndex(\"title\", func(v any) string {\n\t\treturn v.(*Presentation).Title\n\t}, collection.DefaultIndex)\n\n\tOwnable = ownable.New()\n}\n\n// Render handles the realm's rendering logic\nfunc Render(path string) string {\n\treq := realmpath.Parse(path)\n\n\t// Get slug from path\n\tslug := req.PathPart(0)\n\n\t// List view (home)\n\tif slug == \"\" {\n\t\treturn renderList(req)\n\t}\n\n\t// Slides view\n\tif req.PathPart(1) == \"slides\" {\n\t\tpage := 1\n\t\tif pageStr := req.Query.Get(\"page\"); pageStr != \"\" {\n\t\t\tvar err error\n\t\t\tpage, err = strconv.Atoi(pageStr)\n\t\t\tif err != nil {\n\t\t\t\treturn \"400: invalid page number\"\n\t\t\t}\n\t\t}\n\t\treturn renderSlides(slug, page)\n\t}\n\n\t// Regular view\n\treturn renderView(slug)\n}\n\n// Set adds or updates a presentation\nfunc Set(slug, title, event, author, date, content string) string {\n\tOwnable.AssertCallerIsOwner()\n\n\tparsedDate, err := time.Parse(\"2006-01-02\", date)\n\tif err != nil {\n\t\treturn \"400: invalid date format (expected: YYYY-MM-DD)\"\n\t}\n\n\tnumSlides := 1 // Count intro slide\n\tfor _, line := range strings.Split(content, \"\\n\") {\n\t\tif strings.HasPrefix(line, \"## \") {\n\t\t\tnumSlides++\n\t\t}\n\t}\n\tnumSlides++ // Count thank you slide\n\n\tp := \u0026Presentation{\n\t\tSlug: slug,\n\t\tTitle: title,\n\t\tEvent: event,\n\t\tAuthor: author,\n\t\tUploader: std.PreviousRealm().Address(),\n\t\tDate: parsedDate,\n\t\tContent: content,\n\t\tEditDate: time.Now(),\n\t\tNumSlides: numSlides,\n\t}\n\n\tpresentations.Set(p)\n\treturn \"presentation saved successfully\"\n}\n\n// Delete removes a presentation\nfunc Delete(slug string) string {\n\tOwnable.AssertCallerIsOwner()\n\n\tentry := presentations.GetFirst(\"slug\", slug)\n\tif entry == nil {\n\t\treturn \"404: presentation not found\"\n\t}\n\n\t// XXX: consider this:\n\t// if entry.Obj.(*Presentation).Uploader != std.PreviousRealm().Address() {\n\t// \treturn \"401: unauthorized - only the uploader can delete their presentations\"\n\t// }\n\n\t// Convert the entry's ID from string to uint64 and delete\n\tnumericID, err := seqid.FromString(entry.ID)\n\tif err != nil {\n\t\treturn \"500: invalid entry ID format\"\n\t}\n\n\tpresentations.Delete(uint64(numericID))\n\treturn \"presentation deleted successfully\"\n}\n\nfunc renderList(req *realmpath.Request) string {\n\tvar out strings.Builder\n\tout.WriteString(md.H1(\"Presentations\"))\n\n\t// Setup pager\n\tindex := presentations.GetIndex(getSortField(req))\n\tpgr := pager.NewPager(index, 10, isSortReversed(req))\n\n\t// Get current page\n\tpage := pgr.MustGetPageByPath(req.String())\n\n\t// Create table\n\tdateColumn := renderSortLink(req, \"date\", \"Date\")\n\ttitleColumn := renderSortLink(req, \"title\", \"Title\")\n\tauthorColumn := renderSortLink(req, \"author\", \"Author\")\n\ttable := mdtable.Table{\n\t\tHeaders: []string{dateColumn, titleColumn, \"Event\", authorColumn, \"Slides\"},\n\t}\n\n\t// Add rows from current page\n\tfor _, item := range page.Items {\n\t\t// Get the actual presentation using the ID from the index\n\t\t// XXX: improve p/moul/collection to make this more convenient.\n\t\t// - no need to make per-id lookup.\n\t\t// - transparently support multi-values.\n\t\t// - integrate a sortable pager?\n\t\tvar ids []string\n\t\tif ids_, ok := item.Value.([]string); ok {\n\t\t\tids = ids_\n\t\t} else if id, ok := item.Value.(string); ok {\n\t\t\tids = []string{id}\n\t\t}\n\n\t\tfor _, id := range ids {\n\t\t\tentry := presentations.GetFirst(collection.IDIndex, id)\n\t\t\tif entry == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tp := entry.Obj.(*Presentation)\n\n\t\t\ttable.Append([]string{\n\t\t\t\tp.Date.Format(\"2006-01-02\"),\n\t\t\t\tmd.Link(p.Title, localPath(p.Slug, nil)),\n\t\t\t\tp.Event,\n\t\t\t\tp.Author,\n\t\t\t\tufmt.Sprintf(\"%d\", p.NumSlides),\n\t\t\t})\n\t\t}\n\t}\n\n\tout.WriteString(table.String())\n\tout.WriteString(page.Picker(req.String()))\n\treturn out.String()\n}\n\nfunc (p *Presentation) FirstSlide() string {\n\tvar out strings.Builder\n\tout.WriteString(md.H1(p.Title))\n\tout.WriteString(md.Paragraph(md.Bold(p.Event) + \", \" + p.Date.Format(\"2 Jan 2006\")))\n\tout.WriteString(md.Paragraph(\"by \" + md.Bold(p.Author))) // XXX: link to u/?\n\treturn out.String()\n}\n\nfunc (p *Presentation) LastSlide() string {\n\tvar out strings.Builder\n\tout.WriteString(md.H1(p.Title))\n\tout.WriteString(md.H2(\"Thank You!\"))\n\tout.WriteString(md.Paragraph(p.Author))\n\tfullPath := \"https://\" + chainDomain + localPath(p.Slug, nil)\n\tout.WriteString(md.Paragraph(\"🔗 \" + md.Link(fullPath, fullPath)))\n\t// XXX: QRCode\n\treturn out.String()\n}\n\nfunc renderView(slug string) string {\n\tif slug == \"\" {\n\t\treturn \"400: missing presentation slug\"\n\t}\n\n\tentry := presentations.GetFirst(\"slug\", slug)\n\tif entry == nil {\n\t\treturn \"404: presentation not found\"\n\t}\n\n\tp := entry.Obj.(*Presentation)\n\tvar out strings.Builder\n\n\t// Header using FirstSlide helper\n\tout.WriteString(p.FirstSlide())\n\n\t// Slide mode link\n\tout.WriteString(md.Link(\"View as slides\", localPath(p.Slug+\"/slides\", nil)) + \"\\n\\n\")\n\tout.WriteString(md.HorizontalRule())\n\tout.WriteString(md.Paragraph(p.Content))\n\n\t// Metadata footer\n\tout.WriteString(md.HorizontalRule())\n\tout.WriteString(ufmt.Sprintf(\"Last edited: %s\\n\\n\", p.EditDate.Format(\"2006-01-02 15:04:05\")))\n\tout.WriteString(ufmt.Sprintf(\"Uploader: `%s`\\n\\n\", p.Uploader))\n\tout.WriteString(ufmt.Sprintf(\"Number of slides: %d\\n\\n\", p.NumSlides))\n\n\t// Admin actions\n\t// XXX: consider a dynamic toggle for admin actions\n\teditLink := txlink.Call(\"Set\",\n\t\t\"slug\", p.Slug,\n\t\t\"title\", p.Title,\n\t\t\"author\", p.Author,\n\t\t\"event\", p.Event,\n\t\t\"date\", p.Date.Format(\"2006-01-02\"),\n\t)\n\tdeleteLink := txlink.Call(\"Delete\", \"slug\", p.Slug)\n\tout.WriteString(md.Paragraph(md.Link(\"Edit\", editLink) + \" | \" + md.Link(\"Delete\", deleteLink)))\n\n\treturn out.String()\n}\n\n// renderSlidesNavigation returns the navigation bar for slides\nfunc renderSlidesNavigation(slug string, currentPage, totalSlides int) string {\n\tvar out strings.Builder\n\tif currentPage \u003e 1 {\n\t\tprevLink := localPath(slug+\"/slides\", url.Values{\"page\": {ufmt.Sprintf(\"%d\", currentPage-1)}})\n\t\tout.WriteString(md.Link(\"← Prev\", prevLink) + \" \")\n\t}\n\tout.WriteString(ufmt.Sprintf(\"| %d/%d |\", currentPage, totalSlides))\n\tif currentPage \u003c totalSlides {\n\t\tnextLink := localPath(slug+\"/slides\", url.Values{\"page\": {ufmt.Sprintf(\"%d\", currentPage+1)}})\n\t\tout.WriteString(\" \" + md.Link(\"Next →\", nextLink))\n\t}\n\treturn md.Paragraph(out.String())\n}\n\nfunc renderSlides(slug string, currentPage int) string {\n\tif slug == \"\" {\n\t\treturn \"400: missing presentation ID\"\n\t}\n\n\tentry := presentations.GetFirst(\"slug\", slug)\n\tif entry == nil {\n\t\treturn \"404: presentation not found\"\n\t}\n\n\tp := entry.Obj.(*Presentation)\n\tslides := strings.Split(\"\\n\"+p.Content, \"\\n## \")\n\tif currentPage \u003c 1 || currentPage \u003e p.NumSlides {\n\t\treturn \"404: invalid slide number\"\n\t}\n\n\tvar out strings.Builder\n\n\t// Display current slide\n\tif currentPage == 1 {\n\t\tout.WriteString(p.FirstSlide())\n\t} else if currentPage == p.NumSlides {\n\t\tout.WriteString(p.LastSlide())\n\t} else {\n\t\tout.WriteString(md.H1(p.Title))\n\t\tout.WriteString(\"## \" + slides[currentPage-1] + \"\\n\\n\")\n\t}\n\n\tout.WriteString(renderSlidesNavigation(slug, currentPage, p.NumSlides))\n\treturn out.String()\n}\n\n// Helper functions for sorting and pagination\nfunc getSortField(req *realmpath.Request) string {\n\tfield := req.Query.Get(\"sort\")\n\tswitch field {\n\tcase \"date\", \"slug\", \"author\", \"title\":\n\t\treturn field\n\t}\n\treturn \"date\"\n}\n\nfunc isSortReversed(req *realmpath.Request) bool {\n\treturn req.Query.Get(\"order\") != \"asc\"\n}\n\nfunc renderSortLink(req *realmpath.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 := req.Query\n\tquery.Set(\"sort\", field)\n\tquery.Set(\"order\", newOrder)\n\n\tif field == currentField {\n\t\tif newOrder == \"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\n// helper to create local realm links\nfunc localPath(path string, query url.Values) string {\n\treq := \u0026realmpath.Request{\n\t\tPath: path,\n\t\tQuery: query,\n\t}\n\treturn req.String()\n}\n" + }, + { + "name": "present_filetest.gno", + "body": "package main\n\nimport (\n\t\"gno.land/r/moul/present\"\n)\n\nfunc main() {\n\t// Cleanup initial state\n\tret := present.Delete(\"demo\")\n\tif ret != \"presentation deleted successfully\" {\n\t\tpanic(\"internal error\")\n\t}\n\n\t// Create presentations with IDs from 10-20\n\tpresentations := []struct {\n\t\tid string\n\t\ttitle string\n\t\tevent string\n\t\tauthor string\n\t\tdate string\n\t\tcontent string\n\t}{\n\t\t{\"s10\", \"title10\", \"event3\", \"author1\", \"2024-01-01\", \"## s10.0\\n## s10.1\"},\n\t\t{\"s11\", \"title11\", \"event1\", \"author2\", \"2024-01-15\", \"## s11.0\\n## s11.1\"},\n\t\t{\"s12\", \"title12\", \"event2\", \"author1\", \"2024-02-01\", \"## s12.0\\n## s12.1\"},\n\t\t{\"s13\", \"title13\", \"event1\", \"author3\", \"2024-01-20\", \"## s13.0\\n## s13.1\"},\n\t\t{\"s14\", \"title14\", \"event3\", \"author2\", \"2024-03-01\", \"## s14.0\\n## s14.1\"},\n\t\t{\"s15\", \"title15\", \"event2\", \"author1\", \"2024-02-15\", \"## s15.0\\n## s15.1\\n## s15.2\"},\n\t\t{\"s16\", \"title16\", \"event1\", \"author4\", \"2024-03-15\", \"## s16.0\\n## s16.1\"},\n\t\t{\"s17\", \"title17\", \"event3\", \"author2\", \"2024-01-10\", \"## s17.0\\n## s17.1\"},\n\t\t{\"s18\", \"title18\", \"event2\", \"author3\", \"2024-02-20\", \"## s18.0\\n## s18.1\"},\n\t\t{\"s19\", \"title19\", \"event1\", \"author1\", \"2024-03-10\", \"## s19.0\\n## s19.1\"},\n\t\t{\"s20\", \"title20\", \"event3\", \"author4\", \"2024-01-05\", \"## s20.0\\n## s20.1\"},\n\t}\n\n\tfor _, p := range presentations {\n\t\tresult := present.Set(p.id, p.title, p.event, p.author, p.date, p.content)\n\t\tif result != \"presentation saved successfully\" {\n\t\t\tpanic(\"failed to add presentation: \" + result)\n\t\t}\n\t}\n\n\t// Test different sorting scenarios\n\tprintRender(\"\") // default\n\tprintRender(\"?order=asc\u0026sort=date\") // by date ascending\n\tprintRender(\"?order=asc\u0026sort=title\") // by title ascending\n\tprintRender(\"?order=asc\u0026sort=author\") // by author ascending (multiple entries per author)\n\n\t// Test pagination\n\tprintRender(\"?order=asc\u0026sort=title\u0026page=2\") // second page\n\n\t// Test view\n\tprintRender(\"s15\") // view by slug\n\n\t// Test slides\n\tprintRender(\"s15/slides\") // slides by slug\n\tprintRender(\"s15/slides?page=2\") // slides by slug, second page\n\tprintRender(\"s15/slides?page=3\") // slides by slug, third page\n\tprintRender(\"s15/slides?page=4\") // slides by slug, fourth page\n\tprintRender(\"s15/slides?page=5\") // slides by slug, fifth page\n}\n\n// Helper function to print path and render result\nfunc printRender(path string) {\n\tprintln(\"+-------------------------------\")\n\tprintln(\"| PATH:\", path)\n\tprintln(\"| RESULT:\\n\" + present.Render(path) + \"\\n\")\n}\n\n// Output:\n// +-------------------------------\n// | PATH:\n// | RESULT:\n// # Presentations\n// | [Date ↑](?order=asc\u0026sort=date) | [Title](?order=desc\u0026sort=title) | Event | [Author](?order=desc\u0026sort=author) | Slides |\n// | --- | --- | --- | --- | --- |\n// | 2024-03-15 | [title16](/r/moul/present:s16) | event1 | author4 | 4 |\n// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 |\n// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 |\n// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 |\n// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 |\n// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 |\n// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 |\n// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 |\n// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 |\n// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 |\n// **1** | [2](?page=2\u0026order=desc\u0026sort=author)\n//\n// +-------------------------------\n// | PATH: ?order=asc\u0026sort=date\n// | RESULT:\n// # Presentations\n// | [Date ↓](?order=desc\u0026sort=date) | [Title](?order=desc\u0026sort=title) | Event | [Author](?order=desc\u0026sort=author) | Slides |\n// | --- | --- | --- | --- | --- |\n// | 2024-01-01 | [title10](/r/moul/present:s10) | event3 | author1 | 4 |\n// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 |\n// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 |\n// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 |\n// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 |\n// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 |\n// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 |\n// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 |\n// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 |\n// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 |\n// **1** | [2](?page=2\u0026order=desc\u0026sort=author)\n//\n// +-------------------------------\n// | PATH: ?order=asc\u0026sort=title\n// | RESULT:\n// # Presentations\n// | [Date](?order=desc\u0026sort=date) | [Title](?order=desc\u0026sort=title) | Event | [Author](?order=desc\u0026sort=author) | Slides |\n// | --- | --- | --- | --- | --- |\n// | 2024-01-01 | [title10](/r/moul/present:s10) | event3 | author1 | 4 |\n// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 |\n// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 |\n// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 |\n// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 |\n// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 |\n// | 2024-03-15 | [title16](/r/moul/present:s16) | event1 | author4 | 4 |\n// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 |\n// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 |\n// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 |\n// **1** | [2](?page=2\u0026order=desc\u0026sort=author)\n//\n// +-------------------------------\n// | PATH: ?order=asc\u0026sort=author\n// | RESULT:\n// # Presentations\n// | [Date](?order=desc\u0026sort=date) | [Title](?order=desc\u0026sort=title) | Event | [Author](?order=desc\u0026sort=author) | Slides |\n// | --- | --- | --- | --- | --- |\n// | 2024-01-01 | [title10](/r/moul/present:s10) | event3 | author1 | 4 |\n// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 |\n// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 |\n// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 |\n// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 |\n// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 |\n// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 |\n// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 |\n// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 |\n// | 2024-03-15 | [title16](/r/moul/present:s16) | event1 | author4 | 4 |\n// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 |\n//\n//\n// +-------------------------------\n// | PATH: ?order=asc\u0026sort=title\u0026page=2\n// | RESULT:\n// # Presentations\n// | [Date](?order=desc\u0026page=2\u0026sort=date) | [Title](?order=desc\u0026page=2\u0026sort=title) | Event | [Author](?order=desc\u0026page=2\u0026sort=author) | Slides |\n// | --- | --- | --- | --- | --- |\n// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 |\n// [1](?page=1\u0026order=desc\u0026sort=author) | **2**\n//\n// +-------------------------------\n// | PATH: s15\n// | RESULT:\n// # title15\n// **event2**, 15 Feb 2024\n//\n// by **author1**\n//\n// [View as slides](/r/moul/present:s15/slides)\n//\n// ---\n// ## s15.0\n// ## s15.1\n// ## s15.2\n//\n// ---\n// Last edited: 2009-02-13 23:31:30\n//\n// Uploader: `g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm`\n//\n// Number of slides: 5\n//\n// [Edit](/r/moul/present$help\u0026func=Set\u0026author=author1\u0026date=2024-02-15\u0026event=event2\u0026slug=s15\u0026title=title15) | [Delete](/r/moul/present$help\u0026func=Delete\u0026slug=s15)\n//\n//\n//\n// +-------------------------------\n// | PATH: s15/slides\n// | RESULT:\n// # title15\n// **event2**, 15 Feb 2024\n//\n// by **author1**\n//\n// | 1/5 | [Next →](/r/moul/present:s15/slides?page=2)\n//\n//\n//\n// +-------------------------------\n// | PATH: s15/slides?page=2\n// | RESULT:\n// # title15\n// ## s15.0\n//\n// [← Prev](/r/moul/present:s15/slides?page=1) | 2/5 | [Next →](/r/moul/present:s15/slides?page=3)\n//\n//\n//\n// +-------------------------------\n// | PATH: s15/slides?page=3\n// | RESULT:\n// # title15\n// ## s15.1\n//\n// [← Prev](/r/moul/present:s15/slides?page=2) | 3/5 | [Next →](/r/moul/present:s15/slides?page=4)\n//\n//\n//\n// +-------------------------------\n// | PATH: s15/slides?page=4\n// | RESULT:\n// # title15\n// ## s15.2\n//\n// [← Prev](/r/moul/present:s15/slides?page=3) | 4/5 | [Next →](/r/moul/present:s15/slides?page=5)\n//\n//\n//\n// +-------------------------------\n// | PATH: s15/slides?page=5\n// | RESULT:\n// # title15\n// ## Thank You!\n// author1\n//\n// 🔗 [https://gno\\.land/r/moul/present:s15](https://gno.land/r/moul/present:s15)\n//\n// [← Prev](/r/moul/present:s15/slides?page=4) | 5/5 |\n//\n//\n//\n" + }, + { + "name": "present_init.gno", + "body": "package present\n\nfunc init() {\n\t_ = Set(\n\t\t\"demo\", // id\n\t\t\"Demo Slides\", // title\n\t\t\"Demo Event\", // event\n\t\t\"@demo\", // author\n\t\t\"2025-02-02\", // date\n\t\t`## Slide One\n- Point A\n- Point B\n- Point C\n\n## Slide Two\n- Feature 1\n- Feature 2\n- Feature 3\n\n## Slide Three\n- Next step\n- Another step\n- Final step`,\n\t)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "0LIDT8k/6IQRA+8TdLTfAE/ofkEd/0qspk7IdynkKSqirSmI/KCu1QBG9kJYDVU2K3r/JAlye4cZwNJDWAXhAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "config", + "path": "gno.land/r/mouss/config", + "files": [ + { + "name": "config.gno", + "body": "package config\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n)\n\nvar (\n\tOwnableMain = ownable.NewWithAddress(\"g1wq2h93ppkf2gkgncz5unayrsmt7pl8npktnznd\")\n\tOwnableBackup = ownable.NewWithAddress(\"g1hrfvdh7jdvnlxpk2y20tp3scj9jqal3zzu7wjz\")\n\n\tErrUnauthorized = errors.New(\"config: unauthorized\")\n)\n\nfunc SetMainAddr(addr std.Address) error {\n\treturn OwnableMain.TransferOwnership(addr)\n}\n\nfunc SetBackupAddr(addr std.Address) error {\n\treturn OwnableBackup.TransferOwnership(addr)\n}\n\nfunc IsAuthorized(addr std.Address) bool {\n\treturn addr == OwnableMain.Owner() || addr == OwnableBackup.Owner()\n}\n\nfunc Render(path string) string {\n\tout := \"# mouss configuration\\n\\n\"\n\n\tout += \"## Authorized Addresses\\n\\n\"\n\tout += \"- main: \" + OwnableMain.Owner().String() + \"\\n\"\n\tout += \"- backup: \" + OwnableBackup.Owner().String() + \"\\n\\n\"\n\n\treturn out\n}\n" + }, + { + "name": "config_test.gno", + "body": "package config\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n)\n\nvar (\n\tmainAddr = std.Address(\"g1wq2h93ppkf2gkgncz5unayrsmt7pl8npktnznd\")\n\tbackupAddr = std.Address(\"g1hrfvdh7jdvnlxpk2y20tp3scj9jqal3zzu7wjz\")\n\n\taddr1 = testutils.TestAddress(\"addr1\")\n\taddr2 = testutils.TestAddress(\"addr2\")\n\taddr3 = testutils.TestAddress(\"addr3\")\n)\n\nfunc TestInitialOwnership(t *testing.T) {\n\tuassert.Equal(t, OwnableMain.Owner(), mainAddr)\n\tuassert.Equal(t, OwnableBackup.Owner(), backupAddr)\n}\n\nfunc TestIsAuthorized(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\taddr std.Address\n\t\twant bool\n\t}{\n\t\t{\"main address is authorized\", mainAddr, true},\n\t\t{\"backup address is authorized\", backupAddr, true},\n\t\t{\"random address not authorized\", addr3, false},\n\t\t{\"empty address not authorized\", \"\", false},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := IsAuthorized(tt.addr)\n\t\t\tuassert.Equal(t, got, tt.want)\n\t\t})\n\t}\n}\n\nfunc TestSetMainAddr(t *testing.T) {\n\ttesting.SetOriginCaller(mainAddr)\n\n\t// Test successful transfer\n\terr := SetMainAddr(addr2)\n\turequire.NoError(t, err)\n\tuassert.Equal(t, OwnableMain.Owner(), addr2)\n\n\t// Test unauthorized transfer\n\ttesting.SetOriginCaller(addr3)\n\terr = SetMainAddr(addr1)\n\tuassert.ErrorContains(t, err, \"ownable: caller is not owner\")\n\n\t// Test invalid address\n\ttesting.SetOriginCaller(addr2)\n\terr = SetMainAddr(\"\")\n\tuassert.ErrorContains(t, err, \"ownable: new owner address is invalid\")\n\n\t// Reset state\n\ttesting.SetOriginCaller(addr2)\n\terr = SetMainAddr(mainAddr)\n\turequire.NoError(t, err)\n}\n\nfunc TestSetBackupAddr(t *testing.T) {\n\ttesting.SetOriginCaller(backupAddr)\n\n\terr := SetBackupAddr(addr2)\n\turequire.NoError(t, err)\n\tuassert.Equal(t, OwnableBackup.Owner(), addr2)\n\n\ttesting.SetOriginCaller(addr3)\n\terr = SetBackupAddr(addr1)\n\tuassert.ErrorContains(t, err, \"ownable: caller is not owner\")\n\n\ttesting.SetOriginCaller(addr2)\n\terr = SetBackupAddr(\"\")\n\tuassert.ErrorContains(t, err, \"ownable: new owner address is invalid\")\n\n\ttesting.SetOriginCaller(addr2)\n\terr = SetBackupAddr(backupAddr)\n\turequire.NoError(t, err)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "qFiugmuHCaURwJVjSAzg+9zuDYMuN8KjYmSnnPWlsq5ESM1UnC2qsxlUxn25VpUQ3S3sgQvM9ZjoLRAjdDSvAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/mouss/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/mux\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/moul/addrset\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/r/leon/hof\"\n\t\"gno.land/r/mouss/config\"\n)\n\n// Profile represents my personal profile information.\ntype Profile struct {\n\tAboutMe string\n\tAvatar string\n\tEmail string\n\tGithub string\n\tLinkedIn string\n\tFollowers *addrset.Set // Set of followers addresses.\n}\n\n// Recipe represents a cooking recipe with its details.\ntype Recipe struct {\n\tName string\n\tOrigin string\n\tAuthor std.Address\n\tIngredients string\n\tInstructions string\n\tTips string\n}\n\nconst (\n\trealmURL = \"/r/mouss/home\"\n\trec = realmURL + \":recipe/\"\n\tgnoArt = `\n -==++. \n\t *@@@@= @- -@\n\t #@@@@@: -==-.-- :-::===: .-++-. @- .===:.- .-.-==- .===:=@\n #@@@@@@@: -@@%**%@@ #@@#*#@@- *@@**@@* @- +%=::-*@ +@=-:-@* +%=::-*@\n +@%#**#%@@ %@+ :@@ *@+ #@=+@% %@+ @= :@: -@ +% +%.@: -@\n -: - *@%:..+@@ *@+ #@=-@@: :@@= @- .@= =@ +@ *%.@= =@\n --:==+=-:=. =%@%#*@@ *@+ #@+ =%@%%@%= #* %#=.:%*===*@ +% +% -%*===*@\n -++++=++++. =-:::*@# . . .::. .. :: .:: . . .:: .\n .-=+++=: .*###%#= \n\t :: \n`\n)\n\nvar (\n\trouter = mux.NewRouter()\n\tprofile Profile\n\trecipes = avl.NewTree()\n\tmargheritaPizza *Recipe\n)\n\n// init initializes the router with the home page and recipe routes\n// sets up my profile information, and my recipe\n// and registers the home page in the hall of fame.\nfunc init() {\n\trouter.HandleFunc(\"\", renderHomepage)\n\trouter.HandleFunc(\"recipe/\", renderRecipes)\n\trouter.HandleFunc(\"recipe/{name}\", renderRecipe)\n\tprofile = Profile{\n\t\tAboutMe: \"👋 I'm Mustapha, a contributor to gno.land project from France. I'm passionate about coding, exploring new technologies, and contributing to open-source projects. Besides my tech journey, I'm also a pizzaiolo 🍕 who loves cooking and savoring good food.\",\n\t\tAvatar: \"https://github.com/mous1985/assets/blob/master/avatar.png?raw=true\",\n\t\tEmail: \"mustapha.benazzouz@outlook.fr\",\n\t\tGithub: \"https://github.com/mous1985\",\n\t\tLinkedIn: \"https://www.linkedin.com/in/mustapha-benazzouz-88646887/\",\n\t\tFollowers: \u0026addrset.Set{},\n\t}\n\tmargheritaPizza = \u0026Recipe{\n\t\tName: \"Authentic Margherita Pizza 🤌\",\n\t\tOrigin: \"Naples, 🇮🇹\",\n\t\tAuthor: config.OwnableMain.Owner(),\n\t\tIngredients: \" 1kg 00 flour\\n 500ml water\\n 3g fresh yeast\\n 20g sea salt\\n San Marzano tomatoes\\n Fresh buffalo mozzarella\\n Fresh basil\\n Extra virgin olive oil\",\n\t\tInstructions: \" Mix flour and water until incorporated\\n Add yeast and salt, knead for 20 minutes\\n Let rise for 2 hours at room temperature\\n Divide into 250g balls\\n Cold ferment for 24-48 hours\\n Shape by hand, being gentle with the dough\\n Top with crushed tomatoes, torn mozzarella, and basil\\n Cook at 450°C for 60-90 seconds\",\n\t\tTips: \"Use a pizza steel or stone preheated for at least 1 hour. The dough should be soft and extensible. For best results, cook in a wood-fired oven.\",\n\t}\n\thof.Register(\"Mouss's Home Realm\", \"\")\n}\n\n// AddRecipe adds a new recipe in recipe page by users\nfunc AddRecipe(name, origin, ingredients, instructions, tips string) string {\n\tif err := validateRecipe(name, ingredients, instructions); err != nil {\n\t\tpanic(err)\n\t}\n\trecipe := \u0026Recipe{\n\t\tName: name,\n\t\tOrigin: origin,\n\t\tAuthor: std.PreviousRealm().Address(),\n\t\tIngredients: ingredients,\n\t\tInstructions: instructions,\n\t\tTips: tips,\n\t}\n\trecipes.Set(name, recipe)\n\treturn \"Recipe added successfully\"\n}\n\nfunc UpdateAboutMe(about string) error {\n\tif !config.IsAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\tprofile.AboutMe = about\n\treturn nil\n}\n\nfunc UpdateAvatar(avatar string) error {\n\tif !config.IsAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\tprofile.Avatar = avatar\n\treturn nil\n}\n\n// validateRecipe checks if the provided recipe details are valid.\nfunc validateRecipe(name, ingredients, instructions string) error {\n\tif name == \"\" {\n\t\treturn ufmt.Errorf(\"recipe name cannot be empty\")\n\t}\n\tif len(ingredients) == 0 {\n\t\treturn ufmt.Errorf(\"ingredients cannot be empty\")\n\t}\n\tif len(instructions) == 0 {\n\t\treturn ufmt.Errorf(\"instructions cannot be empty\")\n\t}\n\treturn nil\n}\n\n// Follow allows a users to follow my home page.\n// If the caller is admin it returns error.\nfunc Follow() error {\n\tcaller := std.PreviousRealm().Address()\n\n\tif caller == config.OwnableMain.Owner() {\n\t\treturn ufmt.Errorf(\"you cannot follow yourself\")\n\t}\n\tif profile.Followers.Add(caller) {\n\t\treturn nil\n\t}\n\treturn ufmt.Errorf(\"you are already following\")\n\n}\n\n// Unfollow allows a user to unfollow my home page.\nfunc Unfollow() error {\n\tcaller := std.PreviousRealm().Address()\n\n\tif profile.Followers.Remove(caller) {\n\t\treturn nil\n\t}\n\treturn ufmt.Errorf(\"you are not following\")\n}\n\n// renderRecipes renders the list of recipes.\nfunc renderRecipes(res *mux.ResponseWriter, req *mux.Request) {\n\tvar out string\n\tout += Header()\n\tout += \"## World Kitchen\\n\\n------\\n\\n\"\n\n\t// Link to margarita pizza recipe\n\tout += \"### Available Recipes:\\n\\n\"\n\tout += \"* \" + md.Link(margheritaPizza.Name, rec+\"margheritaPizza\") + \"By : \" + string(margheritaPizza.Author) + \"\\n\"\n\n\t// The list of all other recipes with clickable links\n\tif recipes.Size() \u003e 0 {\n\t\trecipes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\trecipe := value.(*Recipe)\n\t\t\tout += \"* \" + md.Link(recipe.Name, rec+recipe.Name) + \" By : \" + recipe.Author.String() + \"\\n\"\n\t\t\treturn false // continue iterating\n\t\t})\n\t\tout += \"\\n------\\n\\n\"\n\t} else {\n\t\tout += \"\\nNo additional recipes yet. Be the first to add one!\\n\"\n\t}\n\tres.Write(out)\n}\n\n// renderRecipe renders the recipe details.\nfunc renderRecipe(res *mux.ResponseWriter, req *mux.Request) {\n\tname := req.GetVar(\"name\")\n\tif name == \"margheritaPizza\" {\n\t\tres.Write(margheritaPizza.Render())\n\t\treturn\n\t}\n\tvalue, exists := recipes.Get(name)\n\tif !exists {\n\t\tres.Write(\"Recipe not found\")\n\t\treturn\n\t}\n\trecipe := value.(*Recipe)\n\tres.Write(recipe.Render())\n}\n\nfunc (r Recipe) Render() string {\n\tvar out string\n\tout += Header()\n\tout += md.H2(r.Name)\n\tout += md.Bold(\"Author:\") + \"\\n\" + r.Author.String() + \"\\n\\n\"\n\tout += md.Bold(\"Origin:\") + \"\\n\" + r.Origin + \"\\n\\n\"\n\tout += md.Bold(\"Ingredients:\") + \"\\n\" + md.BulletList(strings.Split(r.Ingredients, \"\\n\")) + \"\\n\\n\"\n\tout += md.Bold(\"Instructions:\") + \"\\n\" + md.OrderedList(strings.Split(r.Instructions, \"\\n\")) + \"\\n\\n\"\n\tif r.Tips != \"\" {\n\t\tout += md.Italic(\"💡 Tips:\"+\"\\n\"+r.Tips) + \"\\n\\n\"\n\t}\n\tout += md.HorizontalRule() + \"\\n\"\n\treturn out\n}\n\nfunc renderHomepage(res *mux.ResponseWriter, req *mux.Request) {\n\tvar out string\n\tout += Header()\n\tout += profile.Render()\n\tres.Write(out)\n}\n\nfunc (p Profile) Render() string {\n\tvar out string\n\tout += md.H1(\"Welcome to my Homepage\") + \"\\n\\n\" + md.HorizontalRule() + \"\\n\\n\"\n\tout += \"```\\n\"\n\tout += gnoArt\n\tout += \"```\\n------\"\n\tout += md.HorizontalRule() + \"\\n\\n\" + md.H2(\"About Me\") + \"\\n\\n\"\n\tout += md.Image(\"avatar\", p.Avatar) + \"\\n\\n\"\n\tout += p.AboutMe + \"\\n\\n\" + md.HorizontalRule() + \"\\n\\n\"\n\tout += md.H3(\"Contact\") + \"\\n\\n\"\n\tout += md.BulletList([]string{\n\t\t\"Email: \" + p.Email,\n\t\t\"GitHub: \" + md.Link(\"@mous1985\", p.Github),\n\t\t\"LinkedIn: \" + md.Link(\"Mustapha\", p.LinkedIn),\n\t})\n\tout += \"\\n\\n\" + md.Bold(\"👤 Followers: \") + strconv.Itoa(p.Followers.Size())\n\treturn out\n}\n\nfunc Header() string {\n\tnavItems := []string{\n\t\tmd.Link(\"Home\", realmURL),\n\t\tmd.Link(\"World Kitchen\", rec),\n\t\tmd.Link(\"Hackerspace\", \"https://github.com/gnolang/hackerspace/issues/86#issuecomment-2535795751\"),\n\t}\n\treturn strings.Join(navItems, \" | \") + \"\\n\\n\" + md.HorizontalRule() + \"\\n\\n\"\n}\n\nfunc Render(path string) string {\n\treturn router.Render(path)\n}\n" + }, + { + "name": "home_test.gno", + "body": "package home\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/r/mouss/config\"\n)\n\nvar (\n\tuser1 = testutils.TestAddress(\"user1\")\n\tuser2 = testutils.TestAddress(\"user2\")\n\tmainAddr = config.OwnableMain.Owner()\n)\n\nfunc TestProfile(t *testing.T) {\n\tuassert.NotEmpty(t, profile.AboutMe, \"AboutMe should not be empty\")\n\tuassert.NotEmpty(t, profile.Avatar, \"Avatar should not be empty\")\n\tuassert.NotEmpty(t, profile.Email, \"Email should not be empty\")\n\tuassert.NotEmpty(t, profile.Github, \"Github should not be empty\")\n\tuassert.NotEmpty(t, profile.LinkedIn, \"LinkedIn should not be empty\")\n}\n\nfunc TestAddRecipe(t *testing.T) {\n\ttesting.SetOriginCaller(user1)\n\tname := \"Test Recipe\"\n\torigin := \"Test Origin\"\n\tingredients := \"Ingredient 1\\nIngredient 2\"\n\tinstructions := \"Step 1\\nStep 2\"\n\ttips := \"Test Tips\"\n\n\tresult := AddRecipe(name, origin, ingredients, instructions, tips)\n\tuassert.Equal(t, \"Recipe added successfully\", result)\n\tuassert.Equal(t, 1, recipes.Size())\n\tvalue, exist := recipes.Get(name)\n\tuassert.True(t, exist)\n\trecipe := value.(*Recipe)\n\tuassert.Equal(t, name, recipe.Name)\n\tuassert.Equal(t, origin, recipe.Origin)\n\tuassert.Equal(t, ingredients, recipe.Ingredients)\n\tuassert.Equal(t, instructions, recipe.Instructions)\n\tuassert.Equal(t, tips, recipe.Tips)\n\tuassert.Equal(t, user1, recipe.Author)\n\n\t// Verify recipe is correctly stored in AVL tree with matching fields\n\tvar found bool\n\trecipes.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif key == name {\n\t\t\tfound = true\n\t\t\tfoundRecipe := value.(*Recipe)\n\t\t\tuassert.Equal(t, recipe.Name, foundRecipe.Name)\n\t\t\tuassert.Equal(t, recipe.Origin, foundRecipe.Origin)\n\t\t\tuassert.Equal(t, recipe.Ingredients, foundRecipe.Ingredients)\n\t\t\tuassert.Equal(t, recipe.Instructions, foundRecipe.Instructions)\n\t\t\tuassert.Equal(t, recipe.Tips, foundRecipe.Tips)\n\t\t\tuassert.Equal(t, recipe.Author, foundRecipe.Author)\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\tuassert.Equal(t, true, found)\n}\n\nfunc TestFollow(t *testing.T) {\n\t// Test user following admin's profile\n\ttesting.SetOriginCaller(user1)\n\terr := Follow()\n\tuassert.NoError(t, err, \"user should be able to follow admin's profile\")\n\n\t// Test admin trying to follow themselves\n\ttesting.SetOriginCaller(mainAddr)\n\terr = Follow()\n\tuassert.Error(t, err, \"you cannot follow yourself\")\n\n\t// Test following same address twice\n\ttesting.SetOriginCaller(user1)\n\terr = Follow()\n\tuassert.Error(t, err, \"should not be able to follow same address twice\")\n\n\t// Test multiple users following admin\n\ttesting.SetOriginCaller(user2)\n\terr = Follow()\n\tuassert.NoError(t, err, \"another user should be able to follow admin's profile\")\n}\n\nfunc TestUnfollow(t *testing.T) {\n\t// Test successful unfollow\n\ttesting.SetOriginCaller(user1)\n\terr := Unfollow()\n\tuassert.NoError(t, err)\n\tuassert.False(t, profile.Followers.Has(user1))\n\n\t// Test unfollowing when not following\n\terr = Unfollow()\n\tuassert.Error(t, err)\n\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "H75L49rORpjd+7fRoNBFDgpZ3b8Dl0w9mabu8eBHi+x+z36Cv6gUryYKUnZba5ScuPzbXTDHh0DFfnPSKWeiCA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "config", + "path": "gno.land/r/n2p5/config", + "files": [ + { + "name": "config.gno", + "body": "package config\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/n2p5/mgroup\"\n)\n\nconst (\n\toriginalOwner = \"g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t\" // n2p5\n)\n\nvar (\n\tadminGroup = mgroup.New(originalOwner)\n\tdescription = \"\"\n)\n\n// AddBackupOwner adds a backup owner to the Owner Group.\n// A backup owner can claim ownership of the contract.\nfunc AddBackupOwner(addr std.Address) {\n\terr := adminGroup.AddBackupOwner(addr)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// RemoveBackupOwner removes a backup owner from the Owner Group.\n// The primary owner cannot be removed.\nfunc RemoveBackupOwner(addr std.Address) {\n\terr := adminGroup.RemoveBackupOwner(addr)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// ClaimOwnership allows an authorized user in the ownerGroup\n// to claim ownership of the contract.\nfunc ClaimOwnership() {\n\terr := adminGroup.ClaimOwnership()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// AddAdmin adds an admin to the Admin Group.\nfunc AddAdmin(addr std.Address) {\n\terr := adminGroup.AddMember(addr)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// RemoveAdmin removes an admin from the Admin Group.\n// The primary owner cannot be removed.\nfunc RemoveAdmin(addr std.Address) {\n\terr := adminGroup.RemoveMember(addr)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Owner returns the current owner of the claims contract.\nfunc Owner() std.Address {\n\treturn adminGroup.Owner()\n}\n\n// BackupOwners returns the current backup owners of the claims contract.\nfunc BackupOwners() []string {\n\treturn adminGroup.BackupOwners()\n}\n\n// Admins returns the current admin members of the claims contract.\nfunc Admins() []string {\n\treturn adminGroup.Members()\n}\n\n// IsAdmin checks if an address is in the config adminGroup.\nfunc IsAdmin(addr std.Address) bool {\n\treturn adminGroup.IsMember(addr)\n}\n\n// toMarkdownList formats a slice of strings as a markdown list.\nfunc toMarkdownList(items []string) string {\n\tvar result string\n\tfor _, item := range items {\n\t\tresult += ufmt.Sprintf(\"- %s\\n\", item)\n\t}\n\treturn result\n}\n\nfunc Render(path string) string {\n\towner := adminGroup.Owner().String()\n\tbackupOwners := toMarkdownList(BackupOwners())\n\tadminMembers := toMarkdownList(Admins())\n\treturn ufmt.Sprintf(`\n# Config Dashboard\n\nThis dashboard shows the current configuration owner, backup owners, and admin members.\n- The owner has the exclusive ability to manage the backup owners and admin members.\n- Backup owners can claim ownership of the contract and become the owner.\n- Admin members are used to authorize actions in other realms, such as [my home realm](/r/n2p5/home).\n\n#### Owner\n\n%s\n\n#### Backup Owners\n\n%s\n\n#### Admin Members\n\n%s\n\n`,\n\t\towner,\n\t\tbackupOwners,\n\t\tadminMembers)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Qd0kcP6Roe5VNKpmECAgQxC4mqC6y6P8/Jxl6DFfZtF2N/4Q9nuBLa6yvYeX2t5ga8BHpbyu2d7Hmvf95DI3BA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "haystack", + "path": "gno.land/r/n2p5/haystack", + "files": [ + { + "name": "haystack.gno", + "body": "package haystack\n\nimport (\n\t\"gno.land/p/n2p5/haystack\"\n)\n\nvar storage = haystack.New()\n\nfunc Render(path string) string {\n\treturn `\nPut a Needle in the Haystack.\n`\n}\n\n// Add takes a fixed-length hex-encoded needle bytes and adds it to the haystack key-value store.\n// If storage encounters an error, it will panic.\nfunc Add(needleHex string) {\n\terr := storage.Add(needleHex)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// Get takes a fixed-length hex-encoded needle hash and returns the hex-encoded needle bytes.\n// If storage encounters an error, it will panic.\nfunc Get(hashHex string) string {\n\tneedleHex, err := storage.Get(hashHex)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn needleHex\n}\n" + }, + { + "name": "haystack_test.gno", + "body": "package haystack\n\nimport (\n\t\"encoding/hex\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n\t\"gno.land/p/n2p5/haystack\"\n\t\"gno.land/p/n2p5/haystack/needle\"\n)\n\nfunc TestHaystack(t *testing.T) {\n\tt.Parallel()\n\t// needleHex returns a hex-encoded needle and its hash for a given index.\n\tgenNeedleHex := func(i int) (string, string) {\n\t\tb := make([]byte, needle.PayloadLength)\n\t\tb[0] = byte(i)\n\t\tn, _ := needle.New(b)\n\t\treturn hex.EncodeToString(n.Bytes()), hex.EncodeToString(n.Hash())\n\t}\n\n\tu1 := testutils.TestAddress(\"u1\")\n\tu2 := testutils.TestAddress(\"u2\")\n\n\tt.Run(\"Add\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tn1, _ := genNeedleHex(1)\n\t\tn2, _ := genNeedleHex(2)\n\t\tn3, _ := genNeedleHex(3)\n\n\t\ttesting.SetOriginCaller(u1)\n\t\turequire.NotPanics(t, func() { Add(n1) })\n\t\turequire.PanicsWithMessage(t,\n\t\t\thaystack.ErrorDuplicateNeedle.Error(),\n\t\t\tfunc() {\n\t\t\t\tAdd(n1)\n\t\t\t})\n\t\ttesting.SetOriginCaller(u2)\n\t\turequire.NotPanics(t, func() { Add(n2) })\n\t\turequire.NotPanics(t, func() { Add(n3) })\n\t})\n\n\tt.Run(\"Get\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tn1, h1 := genNeedleHex(4)\n\t\t_, h2 := genNeedleHex(5)\n\n\t\ttesting.SetOriginCaller(u1)\n\t\turequire.NotPanics(t, func() { Add(n1) })\n\t\turequire.NotPanics(t, func() {\n\t\t\tresult := Get(h1)\n\t\t\turequire.Equal(t, n1, result)\n\t\t})\n\n\t\ttesting.SetOriginCaller(u2)\n\t\turequire.NotPanics(t, func() {\n\t\t\tresult := Get(h1)\n\t\t\turequire.Equal(t, n1, result)\n\t\t})\n\t\turequire.PanicsWithMessage(t,\n\t\t\thaystack.ErrorNeedleNotFound.Error(),\n\t\t\tfunc() {\n\t\t\t\tGet(h2)\n\t\t\t})\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "pwgyBnJZmfkLqI5LszzpSocQxjXmN1vewKnR4hKQdMruHcMkxBDqk7SnwUNeXf1XfZyHFKfvBjy7WAsc/8+aAA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/n2p5/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/n2p5/chonk\"\n\n\t\"gno.land/r/leon/hof\"\n\t\"gno.land/r/n2p5/config\"\n)\n\nvar (\n\tactive = chonk.New()\n\tpreview = chonk.New()\n)\n\nfunc init() {\n\thof.Register(\"N2p5's Home Realm\", \"\")\n\n}\n\n// Add appends a string to the preview Chonk.\nfunc Add(chunk string) {\n\tassertAdmin()\n\tpreview.Add(chunk)\n}\n\n// Flush clears the preview Chonk.\nfunc Flush() {\n\tassertAdmin()\n\tpreview.Flush()\n}\n\n// Promote promotes the preview Chonk to the active Chonk\n// and creates a new preview Chonk.\nfunc Promote() {\n\tassertAdmin()\n\tactive = preview\n\tpreview = chonk.New()\n}\n\n// Render returns the contents of the scanner for the active or preview Chonk\n// based on the path provided.\nfunc Render(path string) string {\n\tvar result string\n\tscanner := getScanner(path)\n\tfor scanner.Scan() {\n\t\tresult += scanner.Text()\n\t}\n\treturn result\n}\n\n// assertAdmin panics if the caller is not an admin as defined in the config realm.\nfunc assertAdmin() {\n\tcaller := std.PreviousRealm().Address()\n\tif !config.IsAdmin(caller) {\n\t\tpanic(\"forbidden: must be admin\")\n\t}\n}\n\n// getScanner returns the scanner for the active or preview Chonk based\n// on the path provided.\nfunc getScanner(path string) *chonk.Scanner {\n\tif isPreview(path) {\n\t\treturn preview.Scanner()\n\t}\n\treturn active.Scanner()\n}\n\n// isPreview returns true if the path prefix is \"preview\".\nfunc isPreview(path string) bool {\n\treturn strings.HasPrefix(path, \"preview\")\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "PB2SQj0N1wtfH46l0TCSKMuEilVr4VEvN1n3L6yDtQ1V2yTg3TgE2d+9igM0QSCNw8CBoC5oRMtBEkJq82TPAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "loci", + "path": "gno.land/r/n2p5/loci", + "files": [ + { + "name": "loci.gno", + "body": "package loci\n\nimport (\n\t\"encoding/base64\"\n\t\"std\"\n\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/n2p5/loci\"\n)\n\nvar store *loci.LociStore\n\nfunc init() {\n\tstore = loci.New()\n}\n\n// Set takes a base64 encoded string and stores it in the Loci store.\n// Keyed by the address of the caller. It also emits a \"set\" event with\n// the address of the caller.\nfunc Set(value string) {\n\tb, err := base64.StdEncoding.DecodeString(value)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tstore.Set(b)\n\tstd.Emit(\"SetValue\", \"ForAddr\", string(std.PreviousRealm().Address()))\n}\n\n// Get retrieves the value stored at the provided address and\n// returns it as a base64 encoded string.\nfunc Get(addr std.Address) string {\n\treturn base64.StdEncoding.EncodeToString(store.Get(addr))\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn about\n\t}\n\treturn renderGet(std.Address(path))\n}\n\nfunc renderGet(addr std.Address) string {\n\tvalue := \"```\\n\" + Get(addr) + \"\\n```\"\n\n\treturn ufmt.Sprintf(`\n# Loci Value Viewer\n\n**Address:** %s\n\n%s\n\n`, addr, value)\n}\n\nconst about = `\n# Welcome to Loci\n\nLoci is a simple key-value store keyed by the caller's gno.land address. \nOnly the caller can set the value for their address, but anyone can \nretrieve the value for any address. There are only two functions: Set and Get.\nIf you'd like to set a value, simply base64 encode any message you'd like and\nit will be stored in in Loci. If you'd like to retrieve a value, simply provide \nthe address of the value you'd like to retrieve.\n\nFor convenience, you can also use gnoweb to view the value for a given address,\nif one exists. For instance append :g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t to\nthis URL to view the value stored at that address.\n`\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "o5arn9gJhn1P2KiJ9AAhPyyeNFan3hp/I1FXqKP2oRx5eTuDQLJtv/uPyg06T+tZqAqHA9xaUVUbDR3KAcmeDw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "config", + "path": "gno.land/r/nemanya/config", + "files": [ + { + "name": "config.gno", + "body": "package config\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nvar (\n\tmain std.Address\n\tbackup std.Address\n\n\tErrInvalidAddr = errors.New(\"Invalid address\")\n\tErrUnauthorized = errors.New(\"Unauthorized\")\n)\n\nfunc init() {\n\tmain = \"g1x9qyf6f34v2g52k4q5smn5tctmj3hl2kj7l2ql\"\n}\n\nfunc Address() std.Address {\n\treturn main\n}\n\nfunc Backup() std.Address {\n\treturn backup\n}\n\nfunc SetAddress(a std.Address) error {\n\tif !a.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tif err := checkAuthorized(); err != nil {\n\t\treturn err\n\t}\n\n\tmain = a\n\treturn nil\n}\n\nfunc SetBackup(a std.Address) error {\n\tif !a.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tif err := checkAuthorized(); err != nil {\n\t\treturn err\n\t}\n\n\tbackup = a\n\treturn nil\n}\n\nfunc checkAuthorized() error {\n\tcaller := std.PreviousRealm().Address()\n\tisAuthorized := caller == main || caller == backup\n\n\tif !isAuthorized {\n\t\treturn ErrUnauthorized\n\t}\n\n\treturn nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "W3LAq/a1n2UCPNGc+n+AI10rnXGb5esjxk4KU+i8lPVsD+EFb653+5s30/PPqivo+IN3lRZVx43bmA0c8Q6ICQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/nemanya/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/nemanya/config\"\n)\n\ntype SocialLink struct {\n\tURL string\n\tText string\n}\n\ntype Sponsor struct {\n\tAddress std.Address\n\tAmount std.Coins\n}\n\ntype Project struct {\n\tName string\n\tDescription string\n\tURL string\n\tImageURL string\n\tSponsors map[std.Address]Sponsor\n}\n\nvar (\n\ttextArt string\n\taboutMe string\n\tsponsorInfo string\n\tsocialLinks map[string]SocialLink\n\tgnoProjects map[string]Project\n\totherProjects map[string]Project\n\ttotalDonations std.Coins\n)\n\nfunc init() {\n\ttextArt = renderTextArt()\n\taboutMe = \"I am a student of IT at Faculty of Sciences in Novi Sad, Serbia. My background is mainly in web and low-level programming, but since Web3 Bootcamp at Petnica this year I've been actively learning about blockchain and adjacent technologies. I am excited about contributing to the gno.land ecosystem and learning from the community.\\n\\n\"\n\tsponsorInfo = \"You can sponsor a project by sending GNOT to this address. Your sponsorship will be displayed on the project page. Thank you for supporting the development of gno.land!\\n\\n\"\n\n\tsocialLinks = map[string]SocialLink{\n\t\t\"GitHub\": {URL: \"https://github.com/Nemanya8\", Text: \"Explore my repositories and open-source contributions.\"},\n\t\t\"LinkedIn\": {URL: \"https://www.linkedin.com/in/nemanjamatic/\", Text: \"Connect with me professionally.\"},\n\t\t\"Email Me\": {URL: \"mailto:matic.nemanya@gmail.com\", Text: \"Reach out for collaboration or inquiries.\"},\n\t}\n\n\tgnoProjects = make(map[string]Project)\n\totherProjects = make(map[string]Project)\n\n\tgnoProjects[\"Liberty Bridge\"] = Project{\n\t\tName: \"Liberty Bridge\",\n\t\tDescription: \"Liberty Bridge was my first Web3 project, developed as part of the Web3 Bootcamp at Petnica. This project served as a centralized bridge between Ethereum and gno.land, enabling seamless asset transfers and fostering interoperability between the two ecosystems.\\n\\n The primary objective of Liberty Bridge was to address the challenges of connecting decentralized networks by implementing a user-friendly solution that simplified the process for users. The project incorporated mechanisms to securely transfer assets between the Ethereum and gno.land blockchains, ensuring efficiency and reliability while maintaining a centralized framework for governance and operations.\\n\\n Through this project, I gained hands-on knowledge of blockchain interoperability, Web3 protocols, and the intricacies of building solutions that bridge different blockchain ecosystems.\\n\\n\",\n\t\tURL: \"https://gno.land\",\n\t\tImageURL: \"https://github.com/Milosevic02/LibertyBridge/raw/main/lb_banner.png\",\n\t\tSponsors: make(map[std.Address]Sponsor),\n\t}\n\n\totherProjects[\"Incognito\"] = Project{\n\t\tName: \"Incognito\",\n\t\tDescription: \"Incognito is a Web3 platform built for Ethereum-based chains, designed to connect advertisers with users in a privacy-first and mutually beneficial way. Its modular architecture makes it easily expandable to other blockchains. Developed during the ETH Sofia Hackathon, it was recognized as a winning project for its innovation and impact.\\n\\n The platform allows advertisers to send personalized ads while sharing a portion of the marketing budget with users. It uses machine learning to match users based on wallet activity, ensuring precise targeting. User emails are stored securely on-chain and never shared, prioritizing privacy and transparency.\\n\\n With all campaign data stored on-chain, Incognito ensures decentralization and accountability. By rewarding users and empowering advertisers, it sets a new standard for fair and transparent blockchain-based advertising.\",\n\t\tURL: \"https://github.com/Milosevic02/Incognito-ETHSofia\",\n\t\tImageURL: \"\",\n\t\tSponsors: make(map[std.Address]Sponsor),\n\t}\n}\n\nfunc Render(path string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"# Hi, I'm\\n\")\n\tsb.WriteString(textArt)\n\tsb.WriteString(\"---\\n\")\n\tsb.WriteString(\"## About me\\n\")\n\tsb.WriteString(aboutMe)\n\tsb.WriteString(sponsorInfo)\n\tsb.WriteString(ufmt.Sprintf(\"# Total Sponsor Donations: %s\\n\", totalDonations.String()))\n\tsb.WriteString(\"---\\n\")\n\tsb.WriteString(renderProjects(gnoProjects, \"Gno Projects\"))\n\tsb.WriteString(\"---\\n\")\n\tsb.WriteString(renderProjects(otherProjects, \"Other Projects\"))\n\tsb.WriteString(\"---\\n\")\n\tsb.WriteString(renderSocialLinks())\n\n\treturn sb.String()\n}\n\nfunc renderTextArt() string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"```\\n\")\n\tsb.WriteString(\" ___ ___ ___ ___ ___ ___ ___ \\n\")\n\tsb.WriteString(\" /\\\\__\\\\ /\\\\ \\\\ /\\\\__\\\\ /\\\\ \\\\ /\\\\__\\\\ |\\\\__\\\\ /\\\\ \\\\ \\n\")\n\tsb.WriteString(\" /::| | /::\\\\ \\\\ /::| | /::\\\\ \\\\ /::| | |:| | /::\\\\ \\\\ \\n\")\n\tsb.WriteString(\" /:|:| | /:/\\\\:\\\\ \\\\ /:|:| | /:/\\\\:\\\\ \\\\ /:|:| | |:| | /:/\\\\:\\\\ \\\\ \\n\")\n\tsb.WriteString(\" /:/|:| |__ /::\\\\~\\\\:\\\\ \\\\ /:/|:|__|__ /::\\\\~\\\\:\\\\ \\\\ /:/|:| |__ |:|__|__ /::\\\\~\\\\:\\\\ \\\\ \\n\")\n\tsb.WriteString(\" /:/ |:| /\\\\__\\\\ /:/\\\\:\\\\ \\\\:\\\\__\\\\ /:/ |::::\\\\__\\\\ /:/\\\\:\\\\ \\\\:\\\\__\\\\ /:/ |:| /\\\\__\\\\ /::::\\\\__\\\\ /:/\\\\:\\\\ \\\\:\\\\__\\\\\\n\")\n\tsb.WriteString(\" \\\\/__|:|/:/ / \\\\:\\\\~\\\\:\\\\ \\\\/__/ \\\\/__/~~/:/ / \\\\/__\\\\:\\\\/:/ / \\\\/__|:|/:/ / /:/~~/~ \\\\/__\\\\:\\\\/:/ / \\n\")\n\tsb.WriteString(\" |:/:/ / \\\\:\\\\ \\\\:\\\\__\\\\ /:/ / \\\\::/ / |:/:/ / /:/ / \\\\::/ / \\n\")\n\tsb.WriteString(\" |::/ / \\\\:\\\\ \\\\/__/ /:/ / /:/ / |::/ / \\\\/__/ /:/ / \\n\")\n\tsb.WriteString(\" /:/ / \\\\:\\\\__\\\\ /:/ / /:/ / /:/ / /:/ / \\n\")\n\tsb.WriteString(\" \\\\/__/ \\\\/__/ \\\\/__/ \\\\/__/ \\\\/__/ \\\\/__/ \\n\")\n\tsb.WriteString(\"\\n```\\n\")\n\treturn sb.String()\n}\n\nfunc renderSocialLinks() string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"## Links\\n\\n\")\n\tsb.WriteString(\"You can find me here:\\n\\n\")\n\tsb.WriteString(ufmt.Sprintf(\"- [GitHub](%s) - %s\\n\", socialLinks[\"GitHub\"].URL, socialLinks[\"GitHub\"].Text))\n\tsb.WriteString(ufmt.Sprintf(\"- [LinkedIn](%s) - %s\\n\", socialLinks[\"LinkedIn\"].URL, socialLinks[\"LinkedIn\"].Text))\n\tsb.WriteString(ufmt.Sprintf(\"- [Email Me](%s) - %s\\n\", socialLinks[\"Email Me\"].URL, socialLinks[\"Email Me\"].Text))\n\tsb.WriteString(\"\\n\")\n\treturn sb.String()\n}\n\nfunc renderProjects(projectsMap map[string]Project, title string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(ufmt.Sprintf(\"## %s\\n\\n\", title))\n\tfor _, project := range projectsMap {\n\t\tif project.ImageURL != \"\" {\n\t\t\tsb.WriteString(ufmt.Sprintf(\"![%s](%s)\\n\\n\", project.Name, project.ImageURL))\n\t\t}\n\t\tsb.WriteString(ufmt.Sprintf(\"### [%s](%s)\\n\\n\", project.Name, project.URL))\n\t\tsb.WriteString(project.Description + \"\\n\\n\")\n\n\t\tif len(project.Sponsors) \u003e 0 {\n\t\t\tsb.WriteString(ufmt.Sprintf(\"#### %s Sponsors\\n\", project.Name))\n\t\t\tfor _, sponsor := range project.Sponsors {\n\t\t\t\tsb.WriteString(ufmt.Sprintf(\"- %s: %s\\n\", sponsor.Address.String(), sponsor.Amount.String()))\n\t\t\t}\n\t\t\tsb.WriteString(\"\\n\")\n\t\t}\n\t}\n\treturn sb.String()\n}\n\nfunc UpdateLink(name, newURL string) {\n\tif !isAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tif _, exists := socialLinks[name]; !exists {\n\t\tpanic(\"Link with the given name does not exist\")\n\t}\n\n\tsocialLinks[name] = SocialLink{\n\t\tURL: newURL,\n\t\tText: socialLinks[name].Text,\n\t}\n}\n\nfunc UpdateAboutMe(text string) {\n\tif !isAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\taboutMe = text\n}\n\nfunc AddGnoProject(name, description, url, imageURL string) {\n\tif !isAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\tproject := Project{\n\t\tName: name,\n\t\tDescription: description,\n\t\tURL: url,\n\t\tImageURL: imageURL,\n\t\tSponsors: make(map[std.Address]Sponsor),\n\t}\n\tgnoProjects[name] = project\n}\n\nfunc DeleteGnoProject(projectName string) {\n\tif !isAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tif _, exists := gnoProjects[projectName]; !exists {\n\t\tpanic(\"Project not found\")\n\t}\n\n\tdelete(gnoProjects, projectName)\n}\n\nfunc AddOtherProject(name, description, url, imageURL string) {\n\tif !isAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\tproject := Project{\n\t\tName: name,\n\t\tDescription: description,\n\t\tURL: url,\n\t\tImageURL: imageURL,\n\t\tSponsors: make(map[std.Address]Sponsor),\n\t}\n\totherProjects[name] = project\n}\n\nfunc RemoveOtherProject(projectName string) {\n\tif !isAuthorized(std.PreviousRealm().Address()) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tif _, exists := otherProjects[projectName]; !exists {\n\t\tpanic(\"Project not found\")\n\t}\n\n\tdelete(otherProjects, projectName)\n}\n\nfunc isAuthorized(addr std.Address) bool {\n\treturn addr == config.Address() || addr == config.Backup()\n}\n\nfunc SponsorGnoProject(projectName string) {\n\taddress := std.OriginCaller()\n\tamount := std.OriginSend()\n\n\tif amount.AmountOf(\"ugnot\") == 0 {\n\t\tpanic(\"Donation must include GNOT\")\n\t}\n\n\tproject, exists := gnoProjects[projectName]\n\tif !exists {\n\t\tpanic(\"Gno project not found\")\n\t}\n\n\tproject.Sponsors[address] = Sponsor{\n\t\tAddress: address,\n\t\tAmount: project.Sponsors[address].Amount.Add(amount),\n\t}\n\n\ttotalDonations = totalDonations.Add(amount)\n\n\tgnoProjects[projectName] = project\n}\n\nfunc SponsorOtherProject(projectName string) {\n\taddress := std.OriginCaller()\n\tamount := std.OriginSend()\n\n\tif amount.AmountOf(\"ugnot\") == 0 {\n\t\tpanic(\"Donation must include GNOT\")\n\t}\n\n\tproject, exists := otherProjects[projectName]\n\tif !exists {\n\t\tpanic(\"Other project not found\")\n\t}\n\n\tproject.Sponsors[address] = Sponsor{\n\t\tAddress: address,\n\t\tAmount: project.Sponsors[address].Amount.Add(amount),\n\t}\n\n\ttotalDonations = totalDonations.Add(amount)\n\n\totherProjects[projectName] = project\n}\n\nfunc Withdraw() string {\n\trealmAddress := std.PreviousRealm().Address()\n\tif !isAuthorized(realmAddress) {\n\t\tpanic(config.ErrUnauthorized)\n\t}\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\tcoins := banker.GetCoins(realmAddress)\n\n\tif len(coins) == 0 {\n\t\treturn \"No coins available to withdraw\"\n\t}\n\n\tbanker.SendCoins(realmAddress, config.Address(), coins)\n\n\treturn \"Successfully withdrew all coins to config address\"\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "9CdgIu9iDTfb1fucj4ispewHyZ+PfFLkHk/XDWULJUUXOSIrfyufY3DoMqYZGOSN+hxPMxKDhxHvGkRNiFDyDw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "fomo3d", + "path": "gno.land/r/stefann/fomo3d", + "files": [ + { + "name": "errors.gno", + "body": "package fomo3d\n\nimport \"errors\"\n\nvar (\n\t// Game state errors\n\tErrGameInProgress = errors.New(\"fomo3d: game already in progress\")\n\tErrGameNotInProgress = errors.New(\"fomo3d: game not in progress\")\n\tErrGameEnded = errors.New(\"fomo3d: game has ended\")\n\tErrGameTimeExpired = errors.New(\"fomo3d: game time expired\")\n\tErrNoKeysPurchased = errors.New(\"fomo3d: no keys purchased\")\n\tErrPlayerNotInGame = errors.New(\"fomo3d: player is not in the game\")\n\n\t// Payment errors\n\tErrInvalidPayment = errors.New(\"fomo3d: must send ugnot only\")\n\tErrInsufficientPayment = errors.New(\"fomo3d: insufficient payment for key\")\n\n\t// Dividend errors\n\tErrNoDividendsToClaim = errors.New(\"fomo3d: no dividends to claim\")\n\n\t// Fee errors\n\tErrNoFeesToClaim = errors.New(\"fomo3d: no owner fees to claim\")\n\n\t// Resolution errors\n\tErrInvalidAddressOrName = errors.New(\"fomo3d: invalid address or unregistered username\")\n\n\t// NFT errors\n\tErrUnauthorizedMint = errors.New(\"fomo3d: only the Fomo3D game realm can mint winner NFTs\")\n\tErrZeroAddress = errors.New(\"fomo3d: zero address\")\n)\n" + }, + { + "name": "events.gno", + "body": "package fomo3d\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Event names\nconst (\n\t// Game events\n\tGameStartedEvent = \"GameStarted\"\n\tGameEndedEvent = \"GameEnded\"\n\tKeysPurchasedEvent = \"KeysPurchased\"\n\n\t// Player events\n\tDividendsClaimedEvent = \"DividendsClaimed\"\n\n\t// Admin events\n\tOwnerFeeClaimedEvent = \"OwnerFeeClaimed\"\n)\n\n// Event keys\nconst (\n\t// Common keys\n\tEventRoundKey = \"round\"\n\tEventAmountKey = \"amount\"\n\n\t// Game keys\n\tEventStartBlockKey = \"startBlock\"\n\tEventEndBlockKey = \"endBlock\"\n\tEventStartingPotKey = \"startingPot\"\n\tEventWinnerKey = \"winner\"\n\tEventJackpotKey = \"jackpot\"\n\n\t// Player keys\n\tEventBuyerKey = \"buyer\"\n\tEventNumKeysKey = \"numKeys\"\n\tEventPriceKey = \"price\"\n\tEventJackpotShareKey = \"jackpotShare\"\n\tEventDividendShareKey = \"dividendShare\"\n\tEventClaimerKey = \"claimer\"\n\n\t// Admin keys\n\tEventOwnerKey = \"owner\"\n\tEventPreviousOwnerKey = \"previousOwner\"\n\tEventNewOwnerKey = \"newOwner\"\n)\n\nfunc emitGameStarted(round, startBlock, endBlock, startingPot int64) {\n\tstd.Emit(\n\t\tGameStartedEvent,\n\t\tEventRoundKey, ufmt.Sprintf(\"%d\", round),\n\t\tEventStartBlockKey, ufmt.Sprintf(\"%d\", startBlock),\n\t\tEventEndBlockKey, ufmt.Sprintf(\"%d\", endBlock),\n\t\tEventStartingPotKey, ufmt.Sprintf(\"%d\", startingPot),\n\t)\n}\n\nfunc emitGameEnded(round int64, winner std.Address, jackpot int64) {\n\tstd.Emit(\n\t\tGameEndedEvent,\n\t\tEventRoundKey, ufmt.Sprintf(\"%d\", round),\n\t\tEventWinnerKey, winner.String(),\n\t\tEventJackpotKey, ufmt.Sprintf(\"%d\", jackpot),\n\t)\n}\n\nfunc emitKeysPurchased(buyer std.Address, numKeys, price, jackpotShare, dividendShare int64) {\n\tstd.Emit(\n\t\tKeysPurchasedEvent,\n\t\tEventBuyerKey, buyer.String(),\n\t\tEventNumKeysKey, ufmt.Sprintf(\"%d\", numKeys),\n\t\tEventPriceKey, ufmt.Sprintf(\"%d\", price),\n\t\tEventJackpotShareKey, ufmt.Sprintf(\"%d\", jackpotShare),\n\t\tEventDividendShareKey, ufmt.Sprintf(\"%d\", dividendShare),\n\t)\n}\n\nfunc emitDividendsClaimed(claimer std.Address, amount int64) {\n\tstd.Emit(\n\t\tDividendsClaimedEvent,\n\t\tEventClaimerKey, claimer.String(),\n\t\tEventAmountKey, ufmt.Sprintf(\"%d\", amount),\n\t)\n}\n\nfunc emitOwnerFeeClaimed(owner std.Address, amount int64) {\n\tstd.Emit(\n\t\tOwnerFeeClaimedEvent,\n\t\tEventOwnerKey, owner.String(),\n\t\tEventAmountKey, ufmt.Sprintf(\"%d\", amount),\n\t)\n}\n" + }, + { + "name": "fomo3d.gno", + "body": "package fomo3d\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\n\t\"gno.land/r/leon/hof\"\n\t\"gno.land/r/sys/users\"\n)\n\n// FOMO3D (Fear Of Missing Out 3D) is a blockchain-based game that combines elements\n// of a lottery and investment mechanics. Players purchase keys using GNOT tokens,\n// where each key purchase:\n// - Extends the game timer\n// - Increases the key price by 1%\n// - Makes the buyer the potential winner of the jackpot\n// - Distributes dividends to all key holders\n//\n// Game Mechanics:\n// - The last person to buy a key before the timer expires wins the jackpot (47% of all purchases)\n// - Key holders earn dividends from each purchase (28% of all purchases)\n// - 20% of purchases go to the next round's starting pot\n// - 5% goes to development fee\n// - Game ends when the timer expires\n//\n// Inspired by the original Ethereum FOMO3D game but implemented in Gno.\n\nconst (\n\tMIN_KEY_PRICE int64 = 100000 // minimum key price in ugnot\n\tTIME_EXTENSION int64 = 86400 // time extension in blocks when new key is bought (~24 hours @ 1s blocks)\n\n\t// Distribution percentages (total 100%)\n\tJACKPOT_PERCENT int64 = 47 // 47% goes to jackpot\n\tDIVIDENDS_PERCENT int64 = 28 // 28% distributed to key holders\n\tNEXT_ROUND_POT int64 = 20 // 20% goes to next round's starting pot\n\tOWNER_FEE_PERCENT int64 = 5 // 5% goes to contract owner\n)\n\ntype PlayerInfo struct {\n\tKeys int64 // number of keys owned\n\tDividends int64 // unclaimed dividends in ugnot\n}\n\n// GameState represents the current state of the FOMO3D game\ntype GameState struct { // TODO: Separate GameState and RoundState and save round history tree in GameState\n\tStartBlock int64 // Block when the game started\n\tEndBlock int64 // Block when the game will end\n\tLastKeyBlock int64 // Block of last key purchase\n\tLastBuyer std.Address // Address of last key buyer\n\tJackpot int64 // Current jackpot in ugnot\n\tKeyPrice int64 // Current price of keys in ugnot\n\tTotalKeys int64 // Total number of keys in circulation\n\tEnded bool // Whether the game has ended\n\tCurrentRound int64 // Current round number\n\tNextPot int64 // Next round's starting pot\n\tOwnerFee int64 // Accumulated owner fees\n\tBuyKeysLink string // Link to BuyKeys function\n\tClaimDividendsLink string // Link to ClaimDividends function\n\tStartGameLink string // Link to StartGame function\n}\n\nvar (\n\tgameState GameState\n\tplayers *avl.Tree // maps address -\u003e PlayerInfo\n\tOwnable *ownable.Ownable\n)\n\nfunc init() {\n\tOwnable = ownable.New()\n\tplayers = avl.NewTree()\n\tgameState.Ended = true\n\thof.Register(\"FOMO3D Game!\", \"\")\n}\n\n// StartGame starts a new game round\nfunc StartGame() {\n\tif !gameState.Ended \u0026\u0026 gameState.StartBlock != 0 {\n\t\tpanic(ErrGameInProgress.Error())\n\t}\n\n\tgameState.CurrentRound++\n\tgameState.StartBlock = std.ChainHeight()\n\tgameState.EndBlock = gameState.StartBlock + TIME_EXTENSION // Initial 24h window\n\tgameState.LastKeyBlock = gameState.StartBlock\n\tgameState.Jackpot = gameState.NextPot\n\tgameState.NextPot = 0\n\tgameState.Ended = false\n\tgameState.KeyPrice = MIN_KEY_PRICE\n\tgameState.TotalKeys = 0\n\n\t// Clear previous round's player data\n\tplayers = avl.NewTree()\n\n\temitGameStarted(\n\t\tgameState.CurrentRound,\n\t\tgameState.StartBlock,\n\t\tgameState.EndBlock,\n\t\tgameState.Jackpot,\n\t)\n}\n\n// BuyKeys allows players to purchase keys\nfunc BuyKeys() {\n\tif gameState.Ended {\n\t\tpanic(ErrGameEnded.Error())\n\t}\n\n\tcurrentBlock := std.ChainHeight()\n\tif currentBlock \u003e gameState.EndBlock {\n\t\tpanic(ErrGameTimeExpired.Error())\n\t}\n\n\t// Get sent coins\n\tsent := std.OriginSend()\n\tif len(sent) != 1 || sent[0].Denom != \"ugnot\" {\n\t\tpanic(ErrInvalidPayment.Error())\n\t}\n\n\tpayment := sent.AmountOf(\"ugnot\")\n\tif payment \u003c gameState.KeyPrice {\n\t\tpanic(ErrInsufficientPayment.Error())\n\t}\n\n\t// Calculate number of keys that can be bought and actual cost\n\tnumKeys := payment / gameState.KeyPrice\n\tactualCost := numKeys * gameState.KeyPrice\n\texcess := payment - actualCost\n\n\t// Update buyer's info\n\tbuyer := std.PreviousRealm().Address()\n\tvar buyerInfo PlayerInfo\n\tif info, exists := players.Get(buyer.String()); exists {\n\t\tbuyerInfo = info.(PlayerInfo)\n\t}\n\n\tbuyerInfo.Keys += numKeys\n\tgameState.TotalKeys += numKeys\n\n\t// Distribute actual cost\n\tjackpotShare := actualCost * JACKPOT_PERCENT / 100\n\tdividendShare := actualCost * DIVIDENDS_PERCENT / 100\n\tnextPotShare := actualCost * NEXT_ROUND_POT / 100\n\townerShare := actualCost * OWNER_FEE_PERCENT / 100\n\n\t// Update pools\n\tgameState.Jackpot += jackpotShare\n\tgameState.NextPot += nextPotShare\n\tgameState.OwnerFee += ownerShare\n\n\t// Return excess payment to buyer if any\n\tif excess \u003e 0 {\n\t\tbanker := std.NewBanker(std.BankerTypeOriginSend)\n\t\tbanker.SendCoins(\n\t\t\tstd.CurrentRealm().Address(),\n\t\t\tbuyer,\n\t\t\tstd.NewCoins(std.NewCoin(\"ugnot\", excess)),\n\t\t)\n\t}\n\n\t// Distribute dividends to all key holders\n\tif players.Size() \u003e 0 \u0026\u0026 gameState.TotalKeys \u003e 0 {\n\t\tdividendPerKey := dividendShare / gameState.TotalKeys\n\t\tplayers.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\t\tplayerInfo := value.(PlayerInfo)\n\t\t\tplayerInfo.Dividends += playerInfo.Keys * dividendPerKey\n\t\t\tplayers.Set(key, playerInfo)\n\t\t\treturn false\n\t\t})\n\t}\n\n\t// Update game state\n\tgameState.LastBuyer = buyer\n\tgameState.LastKeyBlock = currentBlock\n\tgameState.EndBlock = currentBlock + TIME_EXTENSION // Always extend 24h from current block\n\tgameState.KeyPrice += (gameState.KeyPrice * numKeys) / 100\n\n\t// Save buyer's updated info\n\tplayers.Set(buyer.String(), buyerInfo)\n\n\temitKeysPurchased(\n\t\tbuyer,\n\t\tnumKeys,\n\t\tgameState.KeyPrice,\n\t\tjackpotShare,\n\t\tdividendShare,\n\t)\n}\n\n// ClaimDividends allows players to withdraw their earned dividends\nfunc ClaimDividends() {\n\tcaller := std.PreviousRealm().Address()\n\n\tinfo, exists := players.Get(caller.String())\n\tif !exists {\n\t\tpanic(ErrNoDividendsToClaim.Error())\n\t}\n\n\tplayerInfo := info.(PlayerInfo)\n\tif playerInfo.Dividends == 0 {\n\t\tpanic(ErrNoDividendsToClaim.Error())\n\t}\n\n\t// Reset dividends and send coins\n\tamount := playerInfo.Dividends\n\tplayerInfo.Dividends = 0\n\tplayers.Set(caller.String(), playerInfo)\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\tbanker.SendCoins(\n\t\tstd.CurrentRealm().Address(),\n\t\tcaller,\n\t\tstd.NewCoins(std.NewCoin(\"ugnot\", amount)),\n\t)\n\n\temitDividendsClaimed(caller, amount)\n}\n\n// ClaimOwnerFee allows the owner to withdraw accumulated fees\nfunc ClaimOwnerFee() {\n\tOwnable.AssertCallerIsOwner()\n\n\tif gameState.OwnerFee == 0 {\n\t\tpanic(ErrNoFeesToClaim.Error())\n\t}\n\n\tamount := gameState.OwnerFee\n\tgameState.OwnerFee = 0\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\tbanker.SendCoins(\n\t\tstd.CurrentRealm().Address(),\n\t\tOwnable.Owner(),\n\t\tstd.NewCoins(std.NewCoin(\"ugnot\", amount)),\n\t)\n\n\temitOwnerFeeClaimed(Ownable.Owner(), amount)\n}\n\n// EndGame ends the current round and distributes the jackpot\nfunc EndGame() {\n\tif gameState.Ended {\n\t\tpanic(ErrGameEnded.Error())\n\t}\n\n\tcurrentBlock := std.ChainHeight()\n\tif currentBlock \u003c= gameState.EndBlock {\n\t\tpanic(ErrGameNotInProgress.Error())\n\t}\n\n\tif gameState.LastBuyer == \"\" {\n\t\tpanic(ErrNoKeysPurchased.Error())\n\t}\n\n\tgameState.Ended = true\n\n\t// Send jackpot to winner\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\tbanker.SendCoins(\n\t\tstd.CurrentRealm().Address(),\n\t\tgameState.LastBuyer,\n\t\tstd.NewCoins(std.NewCoin(\"ugnot\", gameState.Jackpot)),\n\t)\n\n\temitGameEnded(\n\t\tgameState.CurrentRound,\n\t\tgameState.LastBuyer,\n\t\tgameState.Jackpot,\n\t)\n\n\t// Mint NFT for the winner\n\tif err := mintRoundWinnerNFT(gameState.LastBuyer, gameState.CurrentRound); err != nil {\n\t\tpanic(err.Error())\n\t}\n}\n\n// GetGameState returns current game state\nfunc GetGameState() (int64, int64, int64, std.Address, int64, int64, int64, bool, int64, int64) {\n\treturn gameState.StartBlock,\n\t\tgameState.EndBlock,\n\t\tgameState.LastKeyBlock,\n\t\tgameState.LastBuyer,\n\t\tgameState.Jackpot,\n\t\tgameState.KeyPrice,\n\t\tgameState.TotalKeys,\n\t\tgameState.Ended,\n\t\tgameState.NextPot,\n\t\tgameState.CurrentRound\n}\n\n// GetOwnerInfo returns the owner address and unclaimed fees\nfunc GetOwnerInfo() (std.Address, int64) {\n\treturn Ownable.Owner(), gameState.OwnerFee\n}\n\n// Helper to convert string (address or username) to address\nfunc stringToAddress(input string) std.Address {\n\t// Check if input is valid address\n\taddr := std.Address(input)\n\tif addr.IsValid() {\n\t\treturn addr\n\t}\n\n\t// Not an address, try to find namespace\n\tif user, _ := users.ResolveName(input); user != nil {\n\t\treturn user.Addr()\n\t}\n\n\treturn \"\"\n}\n\nfunc isPlayerInGame(addr std.Address) bool {\n\t_, exists := players.Get(addr.String())\n\treturn exists\n}\n\n// GetPlayerInfo returns a player's keys and dividends\nfunc GetPlayerInfo(addrOrName string) (int64, int64) {\n\taddr := stringToAddress(addrOrName)\n\n\tif addr == \"\" {\n\t\tpanic(ErrInvalidAddressOrName.Error())\n\t}\n\n\tif !isPlayerInGame(addr) {\n\t\tpanic(ErrPlayerNotInGame.Error())\n\t}\n\n\tinfo, _ := players.Get(addr.String())\n\tplayerInfo := info.(PlayerInfo)\n\treturn playerInfo.Keys, playerInfo.Dividends\n}\n\n// Render handles the rendering of game state\nfunc Render(path string) string {\n\tparts := strings.Split(path, \"/\")\n\tc := len(parts)\n\n\tswitch {\n\tcase path == \"\":\n\t\treturn RenderHome()\n\tcase c == 2 \u0026\u0026 parts[0] == \"player\":\n\t\tif gameState.Ended {\n\t\t\treturn ufmt.Sprintf(\"🔴 Game has not started yet.\\n\\n Call [`StartGame()`](%s) to start a new round.\\n\\n\", gameState.StartGameLink)\n\t\t}\n\t\taddr := stringToAddress(parts[1])\n\t\tif addr == \"\" || !isPlayerInGame(addr) {\n\t\t\treturn \"Address not found in game. You need to buy keys first to view your stats.\\n\\n\"\n\t\t}\n\t\tkeys, dividends := GetPlayerInfo(parts[1])\n\t\treturn RenderPlayer(addr, keys, dividends)\n\tdefault:\n\t\treturn \"404: Invalid path\\n\\n\"\n\t}\n}\n" + }, + { + "name": "fomo3d_test.gno", + "body": "package fomo3d\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/urequire\"\n)\n\n// Reset game state\nfunc setupTestGame(t *testing.T) {\n\tgameState = GameState{\n\t\tStartBlock: 0,\n\t\tEndBlock: 0,\n\t\tLastKeyBlock: 0,\n\t\tLastBuyer: \"\",\n\t\tJackpot: 0,\n\t\tKeyPrice: MIN_KEY_PRICE,\n\t\tTotalKeys: 0,\n\t\tEnded: true,\n\t\tCurrentRound: 0,\n\t\tNextPot: 0,\n\t\tOwnerFee: 0,\n\t}\n\tplayers = avl.NewTree()\n\tOwnable = ownable.New()\n}\n\n// Test ownership functionality\nfunc TestOwnership(t *testing.T) {\n\towner := testutils.TestAddress(\"owner\")\n\tnonOwner := testutils.TestAddress(\"nonOwner\")\n\n\t// Set up initial owner\n\ttesting.SetOriginCaller(owner)\n\tsetupTestGame(t)\n\n\t// Transfer ownership to nonOwner first to test ownership functions\n\ttesting.SetOriginCaller(owner)\n\turequire.NotPanics(t, func() {\n\t\tOwnable.TransferOwnership(nonOwner)\n\t})\n\n\t// Test fee accumulation\n\tStartGame()\n\tpayment := MIN_KEY_PRICE * 10\n\ttesting.SetOriginCaller(owner)\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", payment}})\n\ttesting.IssueCoins(owner, std.Coins{{\"ugnot\", payment}})\n\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"ugnot\", payment}})\n\tBuyKeys()\n\n\t// Verify fee accumulation\n\t_, fees := GetOwnerInfo()\n\texpectedFees := payment * OWNER_FEE_PERCENT / 100\n\turequire.Equal(t, expectedFees, fees)\n\n\t// Test unauthorized fee claim (using old owner)\n\ttesting.SetOriginCaller(owner)\n\turequire.PanicsWithMessage(t, \"ownable: caller is not owner\", ClaimOwnerFee)\n\n\t// Test authorized fee claim (using new owner)\n\ttesting.SetOriginCaller(nonOwner)\n\tinitialBalance := std.NewBanker(std.BankerTypeRealmSend).GetCoins(nonOwner)\n\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"ugnot\", expectedFees}})\n\turequire.NotPanics(t, ClaimOwnerFee)\n\n\t// Verify fees were claimed\n\t_, feesAfter := GetOwnerInfo()\n\turequire.Equal(t, int64(0), feesAfter)\n\n\tfinalBalance := std.NewBanker(std.BankerTypeRealmSend).GetCoins(nonOwner)\n\turequire.Equal(t, initialBalance.AmountOf(\"ugnot\")+expectedFees, finalBalance.AmountOf(\"ugnot\"))\n}\n\n// Test full game flow\nfunc TestFullGameFlow(t *testing.T) {\n\tsetupTestGame(t)\n\n\tplayer1 := testutils.TestAddress(\"player1\")\n\tplayer2 := testutils.TestAddress(\"player2\")\n\tplayer3 := testutils.TestAddress(\"player3\")\n\n\t// Test initial state\n\turequire.Equal(t, int64(0), gameState.CurrentRound)\n\turequire.Equal(t, MIN_KEY_PRICE, gameState.KeyPrice)\n\turequire.Equal(t, true, gameState.Ended)\n\n\t// Start game\n\turequire.NotPanics(t, StartGame)\n\turequire.Equal(t, false, gameState.Ended)\n\turequire.Equal(t, std.ChainHeight(), gameState.StartBlock)\n\turequire.Equal(t, int64(1), gameState.CurrentRound)\n\n\tt.Run(\"buying keys\", func(t *testing.T) {\n\t\t// Test insufficient payment\n\t\ttesting.SetOriginCaller(player1)\n\t\ttesting.IssueCoins(player1, std.Coins{{\"ugnot\", MIN_KEY_PRICE - 1}})\n\t\ttesting.SetOriginSend(std.Coins{{\"ugnot\", MIN_KEY_PRICE - 1}})\n\t\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"ugnot\", MIN_KEY_PRICE - 1}})\n\t\turequire.PanicsWithMessage(t, ErrInsufficientPayment.Error(), BuyKeys)\n\n\t\t// Test successful key purchase\n\t\tpayment := MIN_KEY_PRICE * 3\n\t\ttesting.SetOriginSend(std.Coins{{\"ugnot\", payment}})\n\t\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"ugnot\", payment}})\n\n\t\tcurrentBlock := std.ChainHeight()\n\t\turequire.NotPanics(t, BuyKeys)\n\n\t\t// Verify time extension\n\t\t_, endBlock, _, _, _, _, _, _, _, _ := GetGameState()\n\t\turequire.Equal(t, currentBlock+TIME_EXTENSION, endBlock)\n\n\t\t// Verify player state\n\t\tkeys, dividends := GetPlayerInfo(player1.String())\n\n\t\turequire.Equal(t, int64(3), keys)\n\t\turequire.Equal(t, int64(0), dividends)\n\t\turequire.Equal(t, player1, gameState.LastBuyer)\n\n\t\t// Verify game state\n\t\t_, endBlock, _, buyer, pot, price, keys, isEnded, nextPot, round := GetGameState()\n\t\turequire.Equal(t, player1, buyer)\n\t\turequire.Equal(t, int64(3), keys)\n\t\turequire.Equal(t, false, isEnded)\n\n\t\turequire.Equal(t, payment*JACKPOT_PERCENT/100, pot)\n\n\t\t// Verify owner fee\n\t\t_, ownerFees := GetOwnerInfo()\n\t\turequire.Equal(t, payment*OWNER_FEE_PERCENT/100, ownerFees)\n\t})\n\n\tt.Run(\"dividend distribution and claiming\", func(t *testing.T) {\n\t\t// Player 2 buys keys\n\t\ttesting.SetOriginCaller(player2)\n\t\tpayment := gameState.KeyPrice * 2 // Buy 2 keys using current keyPrice\n\t\ttesting.SetOriginSend(std.Coins{{\"ugnot\", payment}})\n\t\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"ugnot\", payment}})\n\t\turequire.NotPanics(t, BuyKeys)\n\n\t\t// Check player1 received dividends\n\t\tkeys1, dividends1 := GetPlayerInfo(player1.String())\n\n\t\turequire.Equal(t, int64(3), keys1)\n\t\texpectedDividends := payment * DIVIDENDS_PERCENT / 100 * 3 / gameState.TotalKeys\n\t\turequire.Equal(t, expectedDividends, dividends1)\n\n\t\t// Test claiming dividends\n\t\t{\n\t\t\t// Player1 claims dividends\n\t\t\ttesting.SetOriginCaller(player1)\n\t\t\tinitialBalance := std.NewBanker(std.BankerTypeRealmSend).GetCoins(player1)\n\t\t\turequire.NotPanics(t, ClaimDividends)\n\n\t\t\t// Verify dividends were claimed\n\t\t\t_, dividendsAfter := GetPlayerInfo(player1.String())\n\t\t\turequire.Equal(t, int64(0), dividendsAfter)\n\n\t\t\tlastBuyerBalance := std.NewBanker(std.BankerTypeRealmSend).GetCoins(player1)\n\t\t\turequire.Equal(t, initialBalance.AmountOf(\"ugnot\")+expectedDividends, lastBuyerBalance.AmountOf(\"ugnot\"))\n\t\t}\n\t})\n\n\tt.Run(\"game ending\", func(t *testing.T) {\n\t\t// Try ending too early\n\t\turequire.PanicsWithMessage(t, ErrGameNotInProgress.Error(), EndGame)\n\n\t\t// Skip to end of current time window\n\t\tcurrentEndBlock := gameState.EndBlock\n\t\ttesting.SkipHeights(currentEndBlock - std.ChainHeight() + 1)\n\n\t\t// End game successfully\n\t\turequire.NotPanics(t, EndGame)\n\t\turequire.Equal(t, true, gameState.Ended)\n\t\turequire.Equal(t, int64(1), gameState.CurrentRound)\n\n\t\t// Verify winner received jackpot\n\t\tlastBuyerBalance := std.NewBanker(std.BankerTypeRealmSend).GetCoins(gameState.LastBuyer)\n\t\turequire.Equal(t, gameState.Jackpot, lastBuyerBalance.AmountOf(\"ugnot\"))\n\n\t\t// Verify NFT was minted to winner\n\t\tbalance, err := BalanceOf(gameState.LastBuyer)\n\t\turequire.NoError(t, err)\n\t\turequire.Equal(t, uint64(1), balance)\n\n\t\t// Check NFT metadata\n\t\ttokenID := grc721.TokenID(\"1\")\n\t\tmetadata, err := TokenMetadata(tokenID)\n\n\t\turequire.NoError(t, err)\n\t\turequire.Equal(t, \"Fomo3D Winner - Round #1\", metadata.Name)\n\t})\n\n\t// Test new round\n\tt.Run(\"new round\", func(t *testing.T) {\n\t\t// Calculate expected next pot from previous round\n\t\tpayment1 := MIN_KEY_PRICE * 3\n\t\t// After buying 3 keys, price increased by 3% (1% per key)\n\t\tsecondKeyPrice := MIN_KEY_PRICE + (MIN_KEY_PRICE * 3 / 100)\n\t\tpayment2 := secondKeyPrice * 2\n\t\texpectedNextPot := (payment1 * NEXT_ROUND_POT / 100) + (payment2 * NEXT_ROUND_POT / 100)\n\n\t\t// Start new round\n\t\turequire.NotPanics(t, StartGame)\n\t\turequire.Equal(t, false, gameState.Ended)\n\t\turequire.Equal(t, int64(2), gameState.CurrentRound)\n\n\t\tstart, end, last, buyer, pot, price, keys, isEnded, nextPot, round := GetGameState()\n\t\turequire.Equal(t, int64(2), round)\n\t\turequire.Equal(t, expectedNextPot, pot)\n\t\turequire.Equal(t, int64(0), nextPot)\n\t})\n}\n\n// Test individual components\nfunc TestStartGame(t *testing.T) {\n\tsetupTestGame(t)\n\n\t// Test starting first game\n\turequire.NotPanics(t, StartGame)\n\turequire.Equal(t, false, gameState.Ended)\n\turequire.Equal(t, std.ChainHeight(), gameState.StartBlock)\n\n\t// Test cannot start while game in progress\n\turequire.PanicsWithMessage(t, ErrGameInProgress.Error(), StartGame)\n}\n\nfunc TestBuyKeys(t *testing.T) {\n\tsetupTestGame(t)\n\tStartGame()\n\n\tplayer := testutils.TestAddress(\"player\")\n\ttesting.SetOriginCaller(player)\n\n\t// Test invalid coin denomination\n\ttesting.IssueCoins(player, std.Coins{{\"invalid\", MIN_KEY_PRICE}})\n\ttesting.SetOriginSend(std.Coins{{\"invalid\", MIN_KEY_PRICE}})\n\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"invalid\", MIN_KEY_PRICE}})\n\turequire.PanicsWithMessage(t, ErrInvalidPayment.Error(), BuyKeys)\n\n\t// Test multiple coin types\n\ttesting.IssueCoins(player, std.Coins{{\"ugnot\", MIN_KEY_PRICE}, {\"other\", 100}})\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", MIN_KEY_PRICE}, {\"other\", 100}})\n\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"ugnot\", MIN_KEY_PRICE}, {\"other\", 100}})\n\turequire.PanicsWithMessage(t, ErrInvalidPayment.Error(), BuyKeys)\n\n\t// Test insufficient payment\n\ttesting.IssueCoins(player, std.Coins{{\"ugnot\", MIN_KEY_PRICE - 1}})\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", MIN_KEY_PRICE - 1}})\n\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"ugnot\", MIN_KEY_PRICE - 1}})\n\turequire.PanicsWithMessage(t, ErrInsufficientPayment.Error(), BuyKeys)\n\n\t// Test successful purchase\n\ttesting.IssueCoins(player, std.Coins{{\"ugnot\", MIN_KEY_PRICE * 2}})\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", MIN_KEY_PRICE * 2}})\n\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"ugnot\", MIN_KEY_PRICE * 2}})\n\turequire.NotPanics(t, BuyKeys)\n}\n\nfunc TestClaimDividends(t *testing.T) {\n\tsetupTestGame(t)\n\tStartGame()\n\n\tplayer := testutils.TestAddress(\"player\")\n\ttesting.SetOriginCaller(player)\n\n\t// Test claiming with no dividends\n\turequire.PanicsWithMessage(t, ErrNoDividendsToClaim.Error(), ClaimDividends)\n\n\t// Setup player with dividends\n\ttesting.IssueCoins(player, std.Coins{{\"ugnot\", MIN_KEY_PRICE}})\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", MIN_KEY_PRICE}})\n\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"ugnot\", MIN_KEY_PRICE}})\n\tBuyKeys()\n\n\t// Have another player buy to generate dividends\n\tplayer2 := testutils.TestAddress(\"player2\")\n\ttesting.SetOriginCaller(player2)\n\ttesting.IssueCoins(player2, std.Coins{{\"ugnot\", MIN_KEY_PRICE * 2}})\n\ttesting.SetOriginSend(std.Coins{{\"ugnot\", MIN_KEY_PRICE * 2}})\n\ttesting.IssueCoins(std.CurrentRealm().Address(), std.Coins{{\"ugnot\", MIN_KEY_PRICE * 2}})\n\tBuyKeys()\n\n\t// Test successful claim\n\ttesting.SetOriginCaller(player)\n\turequire.NotPanics(t, ClaimDividends)\n}\n" + }, + { + "name": "nft.gno", + "body": "package fomo3d\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\n\t\"gno.land/p/demo/grc/grc721\"\n)\n\nvar (\n\tfomo3dNFT = grc721.NewNFTWithMetadata(\"Fomo3D Winner\", \"FOMO\")\n)\n\n// Public getters\n\nfunc Name() string {\n\treturn fomo3dNFT.Name()\n}\n\nfunc Symbol() string {\n\treturn fomo3dNFT.Symbol()\n}\n\nfunc BalanceOf(owner std.Address) (uint64, error) {\n\treturn fomo3dNFT.BalanceOf(owner)\n}\n\nfunc OwnerOf(tokenID grc721.TokenID) (std.Address, error) {\n\treturn fomo3dNFT.OwnerOf(tokenID)\n}\n\nfunc TokenMetadata(tokenID grc721.TokenID) (grc721.Metadata, error) {\n\treturn fomo3dNFT.TokenMetadata(tokenID)\n}\n\n// Transfer and approval methods\n\nfunc TransferFrom(from, to std.Address, tokenID grc721.TokenID) error {\n\treturn fomo3dNFT.TransferFrom(from, to, tokenID)\n}\n\nfunc SafeTransferFrom(from, to std.Address, tokenID grc721.TokenID) error {\n\treturn fomo3dNFT.SafeTransferFrom(from, to, tokenID)\n}\n\nfunc Approve(approved std.Address, tokenID grc721.TokenID) error {\n\treturn fomo3dNFT.Approve(approved, tokenID)\n}\n\nfunc GetApproved(tokenID grc721.TokenID) (std.Address, error) {\n\treturn fomo3dNFT.GetApproved(tokenID)\n}\n\nfunc SetApprovalForAll(operator std.Address, approved bool) error {\n\treturn fomo3dNFT.SetApprovalForAll(operator, approved)\n}\n\nfunc IsApprovedForAll(owner, operator std.Address) bool {\n\treturn fomo3dNFT.IsApprovedForAll(owner, operator)\n}\n\n// Mints a new NFT for the round winner\nfunc mintRoundWinnerNFT(winner std.Address, roundNumber int64) error {\n\tif winner == \"\" {\n\t\treturn ErrZeroAddress\n\t}\n\n\troundStr := strconv.FormatInt(roundNumber, 10)\n\ttokenID := grc721.TokenID(roundStr)\n\n\t// Create metadata\n\tmetadata := grc721.Metadata{\n\t\tName: \"Fomo3D Winner - Round #\" + roundStr,\n\t\tDescription: \"Winner of Fomo3D round #\" + roundStr,\n\t\tImage: \"https://ipfs.io/ipfs/bafybeidayyli6bpewkhgtwqpgubmo77kmgjn4r5zq2i7usoyadcmvynhhq\",\n\t\tExternalURL: \"https://gno.land/r/stefann/fomo3d:round/\" + roundStr, // TODO: Add this render in main realm that shows details of specific round\n\t\tAttributes: []grc721.Trait{},\n\t\tBackgroundColor: \"2D2D2D\", // Dark theme background\n\t}\n\n\tif err := fomo3dNFT.Mint(winner, tokenID); err != nil {\n\t\treturn err\n\t}\n\n\tfomo3dNFT.SetTokenMetadata(tokenID, metadata)\n\n\treturn nil\n}\n" + }, + { + "name": "render.gno", + "body": "package fomo3d\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/grc/grc721\"\n\t\"gno.land/p/demo/ufmt\"\n\n\t\"gno.land/r/sys/users\"\n)\n\n// RenderHome renders the main game state\nfunc RenderHome() string {\n\tvar builder strings.Builder\n\tbuilder.WriteString(\"# FOMO3D - The Ultimate Game of Greed\\n\\n\")\n\n\t// About section\n\tbuilder.WriteString(\"## About the Game\\n\\n\")\n\tbuilder.WriteString(\"FOMO3D is a game that combines elements of lottery and investment mechanics. \")\n\tbuilder.WriteString(\"Players purchase keys using GNOT tokens, where each key purchase:\\n\\n\")\n\tbuilder.WriteString(\"* Extends the game timer\\n\")\n\tbuilder.WriteString(\"* Increases the key price by 1%\\n\")\n\tbuilder.WriteString(\"* Makes you the potential winner of the jackpot\\n\")\n\tbuilder.WriteString(\"* Distributes dividends to all key holders\\n\\n\")\n\tbuilder.WriteString(\"## How to Win\\n\\n\")\n\tbuilder.WriteString(\"* Be the last person to buy a key before the timer expires!\\n\\n\")\n\tbuilder.WriteString(\"**Rewards Distribution:**\\n\")\n\tbuilder.WriteString(\"* 47% goes to the jackpot (for the winner)\\n\")\n\tbuilder.WriteString(\"* 28% distributed as dividends to all key holders\\n\")\n\tbuilder.WriteString(\"* 20% goes to next round's starting pot\\n\")\n\tbuilder.WriteString(\"* 5% development fee for continuous improvement\\n\\n\")\n\n\t// Play Game section\n\tbuilder.WriteString(\"## How to Play\\n\\n\")\n\tbuilder.WriteString(ufmt.Sprintf(\"1. **Buy Keys** - Send GNOT to this realm with function [`BuyKeys()`](%s)\\n\", gameState.BuyKeysLink))\n\tbuilder.WriteString(ufmt.Sprintf(\"2. **Collect Dividends** - Call [`ClaimDividends()`](%s) to collect your earnings\\n\", gameState.ClaimDividendsLink))\n\tbuilder.WriteString(\"3. **Check Your Stats** - Append `:player/` followed by your address or namespace to the current URL to view your keys and dividends\\n\")\n\tif gameState.Ended {\n\t\tbuilder.WriteString(ufmt.Sprintf(\"4. **Start New Round** - Call [`StartGame()`](%s) to begin a new round\\n\", gameState.StartGameLink))\n\t}\n\tbuilder.WriteString(\"\\n\")\n\n\t// Game Status section\n\tbuilder.WriteString(\"## Game Status\\n\\n\")\n\tif gameState.StartBlock == 0 {\n\t\tbuilder.WriteString(\"🔴 Game has not started yet.\\n\\n\")\n\t} else {\n\t\tif gameState.Ended {\n\t\t\tbuilder.WriteString(\"🔴 **Game Status:** Ended\\n\")\n\t\t\tbuilder.WriteString(ufmt.Sprintf(\"🏆 **Winner:** %s\\n\\n\", gameState.LastBuyer))\n\t\t} else {\n\t\t\tbuilder.WriteString(\"🟢 **Game Status:** Active\\n\\n\")\n\t\t\tbuilder.WriteString(ufmt.Sprintf(\"🔄 **Round:** %d\\n\\n\", gameState.CurrentRound))\n\t\t\tbuilder.WriteString(ufmt.Sprintf(\"⏱️ **Time Remaining:** %d blocks\\n\\n\", gameState.EndBlock-std.ChainHeight()))\n\t\t}\n\t\tbuilder.WriteString(ufmt.Sprintf(\"💰 **Jackpot:** %d ugnot\\n\\n\", gameState.Jackpot))\n\t\tbuilder.WriteString(ufmt.Sprintf(\"🔑 **Key Price:** %d ugnot\\n\\n\", gameState.KeyPrice))\n\t\tbuilder.WriteString(ufmt.Sprintf(\"📊 **Total Keys:** %d\\n\\n\", gameState.TotalKeys))\n\t\tbuilder.WriteString(ufmt.Sprintf(\"👤 **Last Buyer:** %s\\n\\n\", getDisplayName(gameState.LastBuyer)))\n\t\tbuilder.WriteString(ufmt.Sprintf(\"🎮 **Next Round Pot:** %d ugnot\\n\\n\", gameState.NextPot))\n\t}\n\n\t// Separator before less important sections\n\tbuilder.WriteString(\"---\\n\\n\")\n\n\t// Vote For Me section\n\tbuilder.WriteString(\"### Vote For Us! 🗳️\\n\\n\")\n\tbuilder.WriteString(\"If you enjoy playing FOMO3D, please consider upvoting this game in the [Hall of Realms](https://gno.land/r/leon/hof)!\\n\\n\")\n\tbuilder.WriteString(\"Your support helps more players discover the game and grow our community! 🚀\\n\\n\")\n\n\t// Report Bug section\n\tbuilder.WriteString(\"### Report a Bug 🪲\\n\\n\")\n\tbuilder.WriteString(\"Something unusual happened? Help us improve the game by reporting bugs!\\n\")\n\tbuilder.WriteString(\"[Visit our GitHub repository](https://github.com/gnolang/gno/issues)\\n\\n\")\n\tbuilder.WriteString(\"Please include:\\n\")\n\tbuilder.WriteString(\"* Detailed description of what happened\\n\")\n\tbuilder.WriteString(\"* Transaction hash (if applicable)\\n\")\n\tbuilder.WriteString(\"* Your address\\n\")\n\tbuilder.WriteString(\"* Current round number\\n\")\n\n\treturn builder.String()\n}\n\n// RenderPlayer renders specific player information\nfunc RenderPlayer(addr std.Address, keys int64, dividends int64) string {\n\tvar builder strings.Builder\n\tdisplayName := getDisplayName(addr)\n\tbuilder.WriteString(ufmt.Sprintf(\"# Player Stats: %s\\n\\n\", displayName))\n\tbuilder.WriteString(\"## Your Holdings\\n\\n\")\n\tbuilder.WriteString(ufmt.Sprintf(\"🔑 **Keys Owned:** %d\\n\\n\", keys))\n\tbuilder.WriteString(ufmt.Sprintf(\"💰 **Unclaimed Dividends:** %d ugnot\\n\\n\", dividends))\n\n\t// Check if player has any NFTs\n\tnftBalance, err := BalanceOf(addr)\n\tif err == nil \u0026\u0026 nftBalance \u003e 0 {\n\t\tbuilder.WriteString(\"## Your Victory NFTs 🏆\\n\\n\")\n\n\t\t// Iterate through all rounds up to current round to find player's NFTs\n\t\tfor i := int64(1); i \u003c= gameState.CurrentRound; i++ {\n\t\t\ttokenID := grc721.TokenID(strconv.FormatInt(i, 10))\n\t\t\towner, err := OwnerOf(tokenID)\n\t\t\tif err == nil \u0026\u0026 owner == addr {\n\t\t\t\tmetadata, err := TokenMetadata(tokenID)\n\t\t\t\tif err == nil {\n\t\t\t\t\tbuilder.WriteString(ufmt.Sprintf(\"### Round #%d Winner\\n\", i))\n\t\t\t\t\tbuilder.WriteString(ufmt.Sprintf(\"![NFT](%s)\\n\\n\", metadata.Image))\n\t\t\t\t\tbuilder.WriteString(\"---\\n\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tbuilder.WriteString(\"## Actions\\n\\n\")\n\tbuilder.WriteString(ufmt.Sprintf(\"* To buy more keys, send GNOT to this realm with [`BuyKeys()`](%s)\\n\", gameState.BuyKeysLink))\n\tif dividends \u003e 0 {\n\t\tbuilder.WriteString(\"* You have unclaimed dividends! Call `ClaimDividends()` to collect them\\n\")\n\t}\n\n\treturn builder.String()\n}\n\n// Helper to get display name - just returns namespace if exists, otherwise address\nfunc getDisplayName(addr std.Address) string {\n\tif user := users.ResolveAddress(addr); user != nil {\n\t\treturn user.Name()\n\t}\n\treturn addr.String()\n}\n\n// UpdateFunctionLinks updates the links for game functions\nfunc UpdateFunctionLinks(buyKeysLink string, claimDividendsLink string, startGameLink string) {\n\tOwnable.AssertCallerIsOwner()\n\tgameState.BuyKeysLink = buyKeysLink\n\tgameState.ClaimDividendsLink = claimDividendsLink\n\tgameState.StartGameLink = startGameLink\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "SyHjKjQ3h8aIDkCvg4biCLGuCpDQrhAj6xxbwlY/LBWZBlnzqMJqowOaADEX8rgXk8bKh4wTaNqe7IAgzfZ3Bw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "registry", + "path": "gno.land/r/stefann/registry", + "files": [ + { + "name": "registry.gno", + "body": "package registry\n\nimport (\n\t\"errors\"\n\t\"std\"\n\n\t\"gno.land/p/demo/ownable\"\n)\n\nvar (\n\tmainAddr std.Address\n\tbackupAddr std.Address\n\towner *ownable.Ownable\n)\n\nfunc init() {\n\tmainAddr = \"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\"\n\tbackupAddr = \"g13awn2575t8s2vf3svlprc4dg0e9z5wchejdxk8\"\n\n\towner = ownable.NewWithAddress(mainAddr)\n}\n\nfunc MainAddr() std.Address {\n\treturn mainAddr\n}\n\nfunc BackupAddr() std.Address {\n\treturn backupAddr\n}\n\nfunc SetMainAddr(addr std.Address) error {\n\tif !addr.IsValid() {\n\t\treturn errors.New(\"config: invalid address\")\n\t}\n\n\towner.AssertCallerIsOwner()\n\n\tmainAddr = addr\n\treturn nil\n}\n\nfunc SetBackupAddr(addr std.Address) error {\n\tif !addr.IsValid() {\n\t\treturn errors.New(\"config: invalid address\")\n\t}\n\n\towner.AssertCallerIsOwner()\n\n\tbackupAddr = addr\n\treturn nil\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "WaTxHdIpJEcgEDMFOpBimyliX+RW1XBgaV6gTQ32sJaye8q1/4xnMEUBY7LGAZSIG9SVPrYS/i8VCFfolMBkDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/stefann/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"sort\"\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/r/leon/hof\"\n\t\"gno.land/r/sys/users\"\n\n\t\"gno.land/r/stefann/registry\"\n)\n\ntype City struct {\n\tName string\n\tURL string\n}\n\ntype Sponsor struct {\n\tAddress std.Address\n\tAmount std.Coins\n}\n\ntype Profile struct {\n\taboutMe []string\n}\n\ntype Travel struct {\n\tcities []City\n\tcurrentCityIndex int\n\tjarLink string\n}\n\ntype Sponsorship struct {\n\tmaxSponsors int\n\tsponsors *avl.Tree\n\tDonationsCount int\n\tsponsorsCount int\n}\n\nvar (\n\tprofile Profile\n\ttravel Travel\n\tsponsorship Sponsorship\n\towner *ownable.Ownable\n)\n\nfunc init() {\n\towner = ownable.NewWithAddress(registry.MainAddr())\n\thof.Register(\"Stefann's Home Realm\", \"\")\n\n\tprofile = Profile{\n\t\taboutMe: []string{\n\t\t\t`## About Me`,\n\t\t\t`### Hey there! I’m Stefan, a student of Computer Science. I’m all about exploring and adventure — whether it’s diving into the latest tech or discovering a new city, I’m always up for the challenge!`,\n\n\t\t\t`## Contributions`,\n\t\t\t`### I'm just getting started, but you can follow my journey through gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) 🔗`,\n\t\t},\n\t}\n\n\ttravel = Travel{\n\t\tcities: []City{\n\t\t\t{Name: \"Venice\", URL: \"https://i.ibb.co/1mcZ7b1/venice.jpg\"},\n\t\t\t{Name: \"Tokyo\", URL: \"https://i.ibb.co/wNDJv3H/tokyo.jpg\"},\n\t\t\t{Name: \"São Paulo\", URL: \"https://i.ibb.co/yWMq2Sn/sao-paulo.jpg\"},\n\t\t\t{Name: \"Toronto\", URL: \"https://i.ibb.co/pb95HJB/toronto.jpg\"},\n\t\t\t{Name: \"Bangkok\", URL: \"https://i.ibb.co/pQy3w2g/bangkok.jpg\"},\n\t\t\t{Name: \"New York\", URL: \"https://i.ibb.co/6JWLm0h/new-york.jpg\"},\n\t\t\t{Name: \"Paris\", URL: \"https://i.ibb.co/q9vf6Hs/paris.jpg\"},\n\t\t\t{Name: \"Kandersteg\", URL: \"https://i.ibb.co/60DzywD/kandersteg.jpg\"},\n\t\t\t{Name: \"Rothenburg\", URL: \"https://i.ibb.co/cr8d2rQ/rothenburg.jpg\"},\n\t\t\t{Name: \"Capetown\", URL: \"https://i.ibb.co/bPGn0v3/capetown.jpg\"},\n\t\t\t{Name: \"Sydney\", URL: \"https://i.ibb.co/TBNzqfy/sydney.jpg\"},\n\t\t\t{Name: \"Oeschinen Lake\", URL: \"https://i.ibb.co/QJQwp2y/oeschinen-lake.jpg\"},\n\t\t\t{Name: \"Barra Grande\", URL: \"https://i.ibb.co/z4RXKc1/barra-grande.jpg\"},\n\t\t\t{Name: \"London\", URL: \"https://i.ibb.co/CPGtvgr/london.jpg\"},\n\t\t},\n\t\tcurrentCityIndex: 0,\n\t\tjarLink: \"https://TODO\", // This value should be injected through UpdateJarLink after deployment.\n\t}\n\n\tsponsorship = Sponsorship{\n\t\tmaxSponsors: 3,\n\t\tsponsors: avl.NewTree(),\n\t\tDonationsCount: 0,\n\t\tsponsorsCount: 0,\n\t}\n}\n\nfunc UpdateCities(newCities []City) {\n\towner.AssertCallerIsOwner()\n\ttravel.cities = newCities\n}\n\nfunc AddCities(newCities ...City) {\n\towner.AssertCallerIsOwner()\n\n\ttravel.cities = append(travel.cities, newCities...)\n}\n\nfunc UpdateJarLink(newLink string) {\n\towner.AssertCallerIsOwner()\n\ttravel.jarLink = newLink\n}\n\nfunc UpdateAboutMe(aboutMeStr string) {\n\towner.AssertCallerIsOwner()\n\tprofile.aboutMe = strings.Split(aboutMeStr, \"|\")\n}\n\nfunc AddAboutMeRows(newRows ...string) {\n\towner.AssertCallerIsOwner()\n\n\tprofile.aboutMe = append(profile.aboutMe, newRows...)\n}\n\nfunc UpdateMaxSponsors(newMax int) {\n\towner.AssertCallerIsOwner()\n\tif newMax \u003c= 0 {\n\t\tpanic(\"maxSponsors must be greater than zero\")\n\t}\n\tsponsorship.maxSponsors = newMax\n}\n\nfunc Donate() {\n\taddress := std.OriginCaller()\n\tamount := std.OriginSend()\n\n\tif amount.AmountOf(\"ugnot\") == 0 {\n\t\tpanic(\"Donation must include GNOT\")\n\t}\n\n\texistingAmount, exists := sponsorship.sponsors.Get(address.String())\n\tif exists {\n\t\tupdatedAmount := existingAmount.(std.Coins).Add(amount)\n\t\tsponsorship.sponsors.Set(address.String(), updatedAmount)\n\t} else {\n\t\tsponsorship.sponsors.Set(address.String(), amount)\n\t\tsponsorship.sponsorsCount++\n\t}\n\n\ttravel.currentCityIndex++\n\tsponsorship.DonationsCount++\n\n\tbanker := std.NewBanker(std.BankerTypeRealmSend)\n\townerAddr := registry.MainAddr()\n\tbanker.SendCoins(std.CurrentRealm().Address(), ownerAddr, banker.GetCoins(std.CurrentRealm().Address()))\n}\n\ntype SponsorSlice []Sponsor\n\nfunc (s SponsorSlice) Len() int {\n\treturn len(s)\n}\n\nfunc (s SponsorSlice) Less(i, j int) bool {\n\treturn s[i].Amount.AmountOf(\"ugnot\") \u003e s[j].Amount.AmountOf(\"ugnot\")\n}\n\nfunc (s SponsorSlice) Swap(i, j int) {\n\ts[i], s[j] = s[j], s[i]\n}\n\nfunc GetTopSponsors() []Sponsor {\n\tvar sponsorSlice SponsorSlice\n\n\tsponsorship.sponsors.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\taddr := std.Address(key)\n\t\tamount := value.(std.Coins)\n\t\tsponsorSlice = append(sponsorSlice, Sponsor{Address: addr, Amount: amount})\n\t\treturn false\n\t})\n\n\tsort.Sort(sponsorSlice)\n\treturn sponsorSlice\n}\n\nfunc GetTotalDonations() int {\n\ttotal := 0\n\tsponsorship.sponsors.Iterate(\"\", \"\", func(key string, value any) bool {\n\t\ttotal += int(value.(std.Coins).AmountOf(\"ugnot\"))\n\t\treturn false\n\t})\n\treturn total\n}\n\nfunc Render(path string) string {\n\tout := ufmt.Sprintf(\"# Exploring %s!\\n\\n\", travel.cities[travel.currentCityIndex].Name)\n\n\tout += renderAboutMe()\n\tout += \"\\n\\n\"\n\tout += renderTips()\n\n\treturn out\n}\n\nfunc renderAboutMe() string {\n\tout := \"\"\n\n\tout += ufmt.Sprintf(\"![Current Location](%s)\\n\\n\", travel.cities[travel.currentCityIndex%len(travel.cities)].URL)\n\n\tfor _, rows := range profile.aboutMe {\n\t\tout += rows + \"\\n\\n\"\n\t}\n\n\treturn out\n}\n\nfunc renderTips() string {\n\tout := \"# Help Me Travel The World\\n\\n\"\n\n\tout += ufmt.Sprintf(\"## I am currently in %s, tip the jar to send me somewhere else!\\n\\n\", travel.cities[travel.currentCityIndex].Name)\n\tout += \"### **Click** the jar, **tip** in GNOT coins, and **watch** my background change as I head to a new adventure!\\n\\n\"\n\n\tout += renderTipsJar() + \"\\n\\n\"\n\n\tout += renderSponsors()\n\n\treturn out\n}\n\nfunc formatAddress(address string) string {\n\tif len(address) \u003c= 8 {\n\t\treturn address\n\t}\n\treturn address[:4] + \"...\" + address[len(address)-4:]\n}\n\nfunc getDisplayName(addr std.Address) string {\n\tif user := users.ResolveAddress(addr); user != nil {\n\t\treturn user.Name()\n\t}\n\treturn formatAddress(addr.String())\n}\n\nfunc formatAmount(amount std.Coins) string {\n\tugnot := amount.AmountOf(\"ugnot\")\n\tif ugnot \u003e= 1000000 {\n\t\tgnot := float64(ugnot) / 1000000\n\t\treturn ufmt.Sprintf(\"`%v`*GNOT*\", gnot)\n\t}\n\treturn ufmt.Sprintf(\"`%d`*ugnot*\", ugnot)\n}\n\nfunc renderSponsors() string {\n\tout := \"## Sponsor Leaderboard\\n\\n\"\n\n\tif sponsorship.sponsorsCount == 0 {\n\t\treturn out + \"No sponsors yet. Be the first to tip the jar!\\n\"\n\t}\n\n\ttopSponsors := GetTopSponsors()\n\tnumSponsors := len(topSponsors)\n\tif numSponsors \u003e sponsorship.maxSponsors {\n\t\tnumSponsors = sponsorship.maxSponsors\n\t}\n\n\tfor i := 0; i \u003c numSponsors; i++ {\n\t\tsponsor := topSponsors[i]\n\t\tposition := \"\"\n\t\tswitch i {\n\t\tcase 0:\n\t\t\tposition = \"🥇\"\n\t\tcase 1:\n\t\t\tposition = \"🥈\"\n\t\tcase 2:\n\t\t\tposition = \"🥉\"\n\t\tdefault:\n\t\t\tposition = ufmt.Sprintf(\"%d.\", i+1)\n\t\t}\n\n\t\tout += ufmt.Sprintf(\"%s **%s** - %s\\n\\n\",\n\t\t\tposition,\n\t\t\tgetDisplayName(sponsor.Address),\n\t\t\tformatAmount(sponsor.Amount),\n\t\t)\n\t}\n\n\treturn out + \"\\n\"\n}\n\nfunc renderTipsJar() string {\n\treturn ufmt.Sprintf(\"[![Tips Jar](https://i.ibb.co/4TH9zbw/tips-jar.png)](%s)\", travel.jarLink)\n}\n" + }, + { + "name": "home_test.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/testutils\"\n)\n\nfunc TestUpdateAboutMe(t *testing.T) {\n\towner := std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\ttesting.SetOriginCaller(owner)\n\n\tprofile.aboutMe = []string{}\n\n\tUpdateAboutMe(\"This is my new bio.|I love coding!\")\n\n\texpected := []string{\"This is my new bio.\", \"I love coding!\"}\n\n\tif len(profile.aboutMe) != len(expected) {\n\t\tt.Fatalf(\"expected aboutMe to have length %d, got %d\", len(expected), len(profile.aboutMe))\n\t}\n\n\tfor i := range profile.aboutMe {\n\t\tif profile.aboutMe[i] != expected[i] {\n\t\t\tt.Fatalf(\"expected aboutMe[%d] to be %s, got %s\", i, expected[i], profile.aboutMe[i])\n\t\t}\n\t}\n}\n\nfunc TestUpdateCities(t *testing.T) {\n\towner := std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\ttesting.SetOriginCaller(owner)\n\n\ttravel.cities = []City{}\n\n\tnewCities := []City{\n\t\t{Name: \"Berlin\", URL: \"https://example.com/berlin.jpg\"},\n\t\t{Name: \"Vienna\", URL: \"https://example.com/vienna.jpg\"},\n\t}\n\n\tUpdateCities(newCities)\n\n\tif len(travel.cities) != 2 {\n\t\tt.Fatalf(\"expected 2 cities, got %d\", len(travel.cities))\n\t}\n\n\tif travel.cities[0].Name != \"Berlin\" || travel.cities[1].Name != \"Vienna\" {\n\t\tt.Fatalf(\"expected cities to be updated to Berlin and Vienna, got %+v\", travel.cities)\n\t}\n}\n\nfunc TestUpdateJarLink(t *testing.T) {\n\towner := std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\ttesting.SetOriginCaller(owner)\n\n\ttravel.jarLink = \"\"\n\n\tUpdateJarLink(\"https://example.com/jar\")\n\n\tif travel.jarLink != \"https://example.com/jar\" {\n\t\tt.Fatalf(\"expected jarLink to be https://example.com/jar, got %s\", travel.jarLink)\n\t}\n}\n\nfunc TestUpdateMaxSponsors(t *testing.T) {\n\towner := std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\ttesting.SetOriginCaller(owner)\n\n\tsponsorship.maxSponsors = 0\n\n\tUpdateMaxSponsors(10)\n\n\tif sponsorship.maxSponsors != 10 {\n\t\tt.Fatalf(\"expected maxSponsors to be 10, got %d\", sponsorship.maxSponsors)\n\t}\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Fatalf(\"expected panic for setting maxSponsors to 0\")\n\t\t}\n\t}()\n\tUpdateMaxSponsors(0)\n}\n\nfunc TestAddCities(t *testing.T) {\n\towner := std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\ttesting.SetOriginCaller(owner)\n\n\ttravel.cities = []City{}\n\n\tAddCities(City{Name: \"Berlin\", URL: \"https://example.com/berlin.jpg\"})\n\n\tif len(travel.cities) != 1 {\n\t\tt.Fatalf(\"expected 1 city, got %d\", len(travel.cities))\n\t}\n\tif travel.cities[0].Name != \"Berlin\" || travel.cities[0].URL != \"https://example.com/berlin.jpg\" {\n\t\tt.Fatalf(\"expected city to be Berlin, got %+v\", travel.cities[0])\n\t}\n\n\tAddCities(\n\t\tCity{Name: \"Paris\", URL: \"https://example.com/paris.jpg\"},\n\t\tCity{Name: \"Tokyo\", URL: \"https://example.com/tokyo.jpg\"},\n\t)\n\n\tif len(travel.cities) != 3 {\n\t\tt.Fatalf(\"expected 3 cities, got %d\", len(travel.cities))\n\t}\n\tif travel.cities[1].Name != \"Paris\" || travel.cities[2].Name != \"Tokyo\" {\n\t\tt.Fatalf(\"expected cities to be Paris and Tokyo, got %+v\", travel.cities[1:])\n\t}\n}\n\nfunc TestAddAboutMeRows(t *testing.T) {\n\towner := std.Address(\"g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8\")\n\ttesting.SetOriginCaller(owner)\n\n\tprofile.aboutMe = []string{}\n\n\tAddAboutMeRows(\"I love exploring new technologies!\")\n\n\tif len(profile.aboutMe) != 1 {\n\t\tt.Fatalf(\"expected 1 aboutMe row, got %d\", len(profile.aboutMe))\n\t}\n\tif profile.aboutMe[0] != \"I love exploring new technologies!\" {\n\t\tt.Fatalf(\"expected first aboutMe row to be 'I love exploring new technologies!', got %s\", profile.aboutMe[0])\n\t}\n\n\tAddAboutMeRows(\"Travel is my passion!\", \"Always learning.\")\n\n\tif len(profile.aboutMe) != 3 {\n\t\tt.Fatalf(\"expected 3 aboutMe rows, got %d\", len(profile.aboutMe))\n\t}\n\tif profile.aboutMe[1] != \"Travel is my passion!\" || profile.aboutMe[2] != \"Always learning.\" {\n\t\tt.Fatalf(\"expected aboutMe rows to be 'Travel is my passion!' and 'Always learning.', got %+v\", profile.aboutMe[1:])\n\t}\n}\n\nfunc TestDonate(t *testing.T) {\n\tuser := testutils.TestAddress(\"user\")\n\ttesting.SetOriginCaller(user)\n\n\tsponsorship.sponsors = avl.NewTree()\n\tsponsorship.DonationsCount = 0\n\tsponsorship.sponsorsCount = 0\n\ttravel.currentCityIndex = 0\n\n\tcoinsSent := std.NewCoins(std.NewCoin(\"ugnot\", 500))\n\ttesting.SetOriginSend(coinsSent)\n\tDonate()\n\n\texistingAmount, exists := sponsorship.sponsors.Get(string(user))\n\tif !exists {\n\t\tt.Fatalf(\"expected sponsor to be added, but it was not found\")\n\t}\n\n\tif existingAmount.(std.Coins).AmountOf(\"ugnot\") != 500 {\n\t\tt.Fatalf(\"expected donation amount to be 500ugnot, got %d\", existingAmount.(std.Coins).AmountOf(\"ugnot\"))\n\t}\n\n\tif sponsorship.DonationsCount != 1 {\n\t\tt.Fatalf(\"expected DonationsCount to be 1, got %d\", sponsorship.DonationsCount)\n\t}\n\n\tif sponsorship.sponsorsCount != 1 {\n\t\tt.Fatalf(\"expected sponsorsCount to be 1, got %d\", sponsorship.sponsorsCount)\n\t}\n\n\tif travel.currentCityIndex != 1 {\n\t\tt.Fatalf(\"expected currentCityIndex to be 1, got %d\", travel.currentCityIndex)\n\t}\n\n\tcoinsSent = std.NewCoins(std.NewCoin(\"ugnot\", 300))\n\ttesting.SetOriginSend(coinsSent)\n\tDonate()\n\n\texistingAmount, exists = sponsorship.sponsors.Get(string(user))\n\tif !exists {\n\t\tt.Fatalf(\"expected sponsor to exist after second donation, but it was not found\")\n\t}\n\n\tif existingAmount.(std.Coins).AmountOf(\"ugnot\") != 800 {\n\t\tt.Fatalf(\"expected total donation amount to be 800ugnot, got %d\", existingAmount.(std.Coins).AmountOf(\"ugnot\"))\n\t}\n\n\tif sponsorship.DonationsCount != 2 {\n\t\tt.Fatalf(\"expected DonationsCount to be 2 after second donation, got %d\", sponsorship.DonationsCount)\n\t}\n\n\tif travel.currentCityIndex != 2 {\n\t\tt.Fatalf(\"expected currentCityIndex to be 2 after second donation, got %d\", travel.currentCityIndex)\n\t}\n}\n\nfunc TestGetTopSponsors(t *testing.T) {\n\tuser := testutils.TestAddress(\"user\")\n\ttesting.SetOriginCaller(user)\n\n\tsponsorship.sponsors = avl.NewTree()\n\tsponsorship.sponsorsCount = 0\n\n\tsponsorship.sponsors.Set(\"g1address1\", std.NewCoins(std.NewCoin(\"ugnot\", 300)))\n\tsponsorship.sponsors.Set(\"g1address2\", std.NewCoins(std.NewCoin(\"ugnot\", 500)))\n\tsponsorship.sponsors.Set(\"g1address3\", std.NewCoins(std.NewCoin(\"ugnot\", 200)))\n\tsponsorship.sponsorsCount = 3\n\n\ttopSponsors := GetTopSponsors()\n\n\tif len(topSponsors) != 3 {\n\t\tt.Fatalf(\"expected 3 sponsors, got %d\", len(topSponsors))\n\t}\n\n\tif topSponsors[0].Address.String() != \"g1address2\" || topSponsors[0].Amount.AmountOf(\"ugnot\") != 500 {\n\t\tt.Fatalf(\"expected top sponsor to be g1address2 with 500ugnot, got %s with %dugnot\", topSponsors[0].Address.String(), topSponsors[0].Amount.AmountOf(\"ugnot\"))\n\t}\n\n\tif topSponsors[1].Address.String() != \"g1address1\" || topSponsors[1].Amount.AmountOf(\"ugnot\") != 300 {\n\t\tt.Fatalf(\"expected second sponsor to be g1address1 with 300ugnot, got %s with %dugnot\", topSponsors[1].Address.String(), topSponsors[1].Amount.AmountOf(\"ugnot\"))\n\t}\n\n\tif topSponsors[2].Address.String() != \"g1address3\" || topSponsors[2].Amount.AmountOf(\"ugnot\") != 200 {\n\t\tt.Fatalf(\"expected third sponsor to be g1address3 with 200ugnot, got %s with %dugnot\", topSponsors[2].Address.String(), topSponsors[2].Amount.AmountOf(\"ugnot\"))\n\t}\n}\n\nfunc TestGetTotalDonations(t *testing.T) {\n\tuser := testutils.TestAddress(\"user\")\n\ttesting.SetOriginCaller(user)\n\n\tsponsorship.sponsors = avl.NewTree()\n\tsponsorship.sponsorsCount = 0\n\n\tsponsorship.sponsors.Set(\"g1address1\", std.NewCoins(std.NewCoin(\"ugnot\", 300)))\n\tsponsorship.sponsors.Set(\"g1address2\", std.NewCoins(std.NewCoin(\"ugnot\", 500)))\n\tsponsorship.sponsors.Set(\"g1address3\", std.NewCoins(std.NewCoin(\"ugnot\", 200)))\n\tsponsorship.sponsorsCount = 3\n\n\ttotalDonations := GetTotalDonations()\n\n\tif totalDonations != 1000 {\n\t\tt.Fatalf(\"expected total donations to be 1000ugnot, got %dugnot\", totalDonations)\n\t}\n}\n\nfunc TestRender(t *testing.T) {\n\ttravel.currentCityIndex = 0\n\ttravel.cities = []City{\n\t\t{Name: \"Venice\", URL: \"https://example.com/venice.jpg\"},\n\t\t{Name: \"Paris\", URL: \"https://example.com/paris.jpg\"},\n\t}\n\n\toutput := Render(\"\")\n\n\texpectedCity := \"Venice\"\n\tif !strings.Contains(output, expectedCity) {\n\t\tt.Fatalf(\"expected output to contain city name '%s', got %s\", expectedCity, output)\n\t}\n\n\texpectedURL := \"https://example.com/venice.jpg\"\n\tif !strings.Contains(output, expectedURL) {\n\t\tt.Fatalf(\"expected output to contain city URL '%s', got %s\", expectedURL, output)\n\t}\n\n\ttravel.currentCityIndex = 1\n\toutput = Render(\"\")\n\n\texpectedCity = \"Paris\"\n\tif !strings.Contains(output, expectedCity) {\n\t\tt.Fatalf(\"expected output to contain city name '%s', got %s\", expectedCity, output)\n\t}\n\n\texpectedURL = \"https://example.com/paris.jpg\"\n\tif !strings.Contains(output, expectedURL) {\n\t\tt.Fatalf(\"expected output to contain city URL '%s', got %s\", expectedURL, output)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "EVhikAbbSyXmbSuHOblCBZ69wUlfdjt35axvedcbBaM7engzBHRUhr5DntFwLSsSNDrlVQcm5+5LAeTNmaUYBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/sunspirit/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/ufmt\"\n\t\"gno.land/p/sunspirit/md\"\n)\n\nfunc Render(path string) string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(md.H1(\"Sunspirit's Home\") + md.LineBreak(1))\n\n\tsb.WriteString(md.Paragraph(ufmt.Sprintf(\n\t\t\"Welcome to Sunspirit’s home! This is where I’ll bring %s to Gno.land, crafted with my experience and creativity.\",\n\t\tmd.Italic(md.Bold(\"simple, useful dapps\")),\n\t)) + md.LineBreak(1))\n\n\tsb.WriteString(md.Paragraph(ufmt.Sprintf(\n\t\t\"📚 I’ve created a Markdown rendering library at %s. Feel free to use it for your own projects!\",\n\t\tmd.Link(\"gno.land/p/sunspirit/md\", \"/p/sunspirit/md\"),\n\t)) + md.LineBreak(1))\n\n\tsb.WriteString(md.Paragraph(\"💬 I’d love to hear your feedback to help improve this library!\") + md.LineBreak(1))\n\n\tsb.WriteString(md.Paragraph(ufmt.Sprintf(\n\t\t\"🌐 You can check out a demo of this package in action at %s.\",\n\t\tmd.Link(\"gno.land/r/sunspirit/md\", \"/r/sunspirit/md\"),\n\t)) + md.LineBreak(1))\n\tsb.WriteString(md.HorizontalRule())\n\n\treturn sb.String()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "zqBg3668JmxJc33mxYRWYRwx13g7Eu5GEkPOcH8GZxJYZYzn+/IJCEPbrUpXBYBYYB35Tflrg3Kc5D7I3IyYDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "md", + "path": "gno.land/r/sunspirit/md", + "files": [ + { + "name": "md.gno", + "body": "package md\n\nimport (\n\t\"gno.land/p/sunspirit/md\"\n\t\"gno.land/p/sunspirit/table\"\n)\n\nfunc Render(path string) string {\n\ttitle := \"A simple, flexible, and easy-to-use library for creating markdown documents in gno.land\"\n\n\tmdBuilder := md.NewBuilder().\n\t\tAdd(md.H1(md.Italic(md.Bold(title)))).\n\n\t\t// Bold Text section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"1. Bold Text\")),\n\t\t\tmd.Paragraph(\"To make text bold, use the `md.Bold()` function:\"),\n\t\t\tmd.Bold(\"This is bold text\"),\n\t\t).\n\n\t\t// Italic Text section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"2. Italic Text\")),\n\t\t\tmd.Paragraph(\"To make text italic, use the `md.Italic()` function:\"),\n\t\t\tmd.Italic(\"This is italic text\"),\n\t\t).\n\n\t\t// Strikethrough Text section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"3. Strikethrough Text\")),\n\t\t\tmd.Paragraph(\"To add strikethrough, use the `md.Strikethrough()` function:\"),\n\t\t\tmd.Strikethrough(\"This text is strikethrough\"),\n\t\t).\n\n\t\t// Headers section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"4. Headers (H1 to H6)\")),\n\t\t\tmd.Paragraph(\"You can create headers (H1 to H6) using the `md.H1()` to `md.H6()` functions:\"),\n\t\t\tmd.H1(\"This is a level 1 header\"),\n\t\t\tmd.H2(\"This is a level 2 header\"),\n\t\t\tmd.H3(\"This is a level 3 header\"),\n\t\t\tmd.H4(\"This is a level 4 header\"),\n\t\t\tmd.H5(\"This is a level 5 header\"),\n\t\t\tmd.H6(\"This is a level 6 header\"),\n\t\t).\n\n\t\t// Bullet List section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"5. Bullet List\")),\n\t\t\tmd.Paragraph(\"To create bullet lists, use the `md.BulletList()` function:\"),\n\t\t\tmd.BulletList([]string{\"Item 1\", \"Item 2\", \"Item 3\"}),\n\t\t).\n\n\t\t// Ordered List section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"6. Ordered List\")),\n\t\t\tmd.Paragraph(\"To create ordered lists, use the `md.OrderedList()` function:\"),\n\t\t\tmd.OrderedList([]string{\"First\", \"Second\", \"Third\"}),\n\t\t).\n\n\t\t// Todo List section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"7. Todo List\")),\n\t\t\tmd.Paragraph(\"You can create a todo list using the `md.TodoList()` function, which supports checkboxes:\"),\n\t\t\tmd.TodoList([]string{\"Task 1\", \"Task 2\"}, []bool{true, false}),\n\t\t).\n\n\t\t// Blockquote section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"8. Blockquote\")),\n\t\t\tmd.Paragraph(\"To create blockquotes, use the `md.Blockquote()` function:\"),\n\t\t\tmd.Blockquote(\"This is a blockquote.\\nIt can span multiple lines.\"),\n\t\t).\n\n\t\t// Inline Code section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"9. Inline Code\")),\n\t\t\tmd.Paragraph(\"To insert inline code, use the `md.InlineCode()` function:\"),\n\t\t\tmd.InlineCode(\"fmt.Println() // inline code\"),\n\t\t).\n\n\t\t// Code Block section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"10. Code Block\")),\n\t\t\tmd.Paragraph(\"For multi-line code blocks, use the `md.CodeBlock()` function:\"),\n\t\t\tmd.CodeBlock(\"package main\\n\\nfunc main() {\\n\\t// Your code here\\n}\"),\n\t\t).\n\n\t\t// Horizontal Rule section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"11. Horizontal Rule\")),\n\t\t\tmd.Paragraph(\"To add a horizontal rule (separator), use the `md.HorizontalRule()` function:\"),\n\t\t\tmd.LineBreak(1),\n\t\t\tmd.HorizontalRule(),\n\t\t).\n\n\t\t// Language-specific Code Block section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"12. Language-specific Code Block\")),\n\t\t\tmd.Paragraph(\"To create language-specific code blocks, use the `md.LanguageCodeBlock()` function:\"),\n\t\t\tmd.LanguageCodeBlock(\"go\", \"package main\\n\\nfunc main() {}\"),\n\t\t).\n\n\t\t// Hyperlink section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"13. Hyperlink\")),\n\t\t\tmd.Paragraph(\"To create a hyperlink, use the `md.Link()` function:\"),\n\t\t\tmd.Link(\"Gnoland official docs\", \"https://docs.gno.land\"),\n\t\t).\n\n\t\t// Image section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"14. Image\")),\n\t\t\tmd.Paragraph(\"To insert an image, use the `md.Image()` function:\"),\n\t\t\tmd.LineBreak(1),\n\t\t\tmd.Image(\"Gnoland Logo\", \"https://gnolang.github.io/blog/2024-05-21_the-gnome/src/banner.png\"),\n\t\t).\n\n\t\t// Footnote section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"15. Footnote\")),\n\t\t\tmd.Paragraph(\"To create footnotes, use the `md.Footnote()` function:\"),\n\t\t\tmd.LineBreak(1),\n\t\t\tmd.Footnote(\"1\", \"This is a footnote.\"),\n\t\t).\n\n\t\t// Table section\n\t\tAdd(\n\t\t\tmd.H3(md.Bold(\"16. Table\")),\n\t\t\tmd.Paragraph(\"To create a table, use the `md.Table()` function. Here's an example of a table:\"),\n\t\t)\n\n\t// Create a table using the table package\n\ttb, _ := table.New([]string{\"Feature\", \"Description\"}, [][]string{\n\t\t{\"Bold\", \"Make text bold using \" + md.Bold(\"double asterisks\")},\n\t\t{\"Italic\", \"Make text italic using \" + md.Italic(\"single asterisks\")},\n\t\t{\"Strikethrough\", \"Cross out text using \" + md.Strikethrough(\"double tildes\")},\n\t})\n\tmdBuilder.Add(md.Table(tb))\n\n\t// Escaping Markdown section\n\tmdBuilder.Add(\n\t\tmd.H3(md.Bold(\"17. Escaping Markdown\")),\n\t\tmd.Paragraph(\"Sometimes, you need to escape special Markdown characters (like *, _, and `). Use the `md.EscapeMarkdown()` function for this:\"),\n\t)\n\n\t// Example of escaping markdown\n\ttext := \"- Escape special chars like *, _, and ` in markdown\"\n\tmdBuilder.Add(\n\t\tmd.H4(\"Text Without Escape:\"),\n\t\ttext,\n\t\tmd.LineBreak(1),\n\t\tmd.H4(\"Text With Escape:\"),\n\t\tmd.EscapeMarkdown(text),\n\t)\n\n\treturn mdBuilder.Render(md.LineBreak(1))\n}\n" + }, + { + "name": "md_test.gno", + "body": "package md\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestRender(t *testing.T) {\n\toutput := Render(\"\")\n\tif !strings.Contains(output, \"A simple, flexible, and easy-to-use library for creating markdown documents in gno.land\") {\n\t\tt.Errorf(\"invalid output\")\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "2oY+zEQh9hX2fn1yezC6/93u8Qh3cbrO/4W2/4kBSdf8xf7+vNV0nJyJmy3VAuGMCkxzQ6S58xxkj9oW+SJxDw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "names", + "path": "gno.land/r/sys/names", + "files": [ + { + "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 provides functionality for checking of package deployments\n// by users registered in r/sys/users are done to proper namespaces.\npackage names\n\nimport (\n\t\"std\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ownable\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nvar (\n\tOwnable = ownable.NewWithAddress(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\") // dropped in genesis via Enable. XXX We should switch to something better once the GovDAO situation is stabilized.\n\n\tenabled = false\n)\n\n// IsAuthorizedAddressForNamespace ensures that the given address has ownership of the given name.\n// A user's name found in r/sys/users is equivalent to their namespace.\nfunc IsAuthorizedAddressForNamespace(address std.Address, namespace string) bool {\n\treturn verifier(enabled, address, 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() {\n\tOwnable.AssertCallerIsOwner()\n\tenabled = true\n\tOwnable.DropOwnership()\n}\n\nfunc IsEnabled() bool {\n\treturn enabled\n}\n\n// verifier checks the store to see that the\n// user has properly registered a given name/namespace.\n// This function considers as valid an `address` that matches the `namespace` (PA namespaces)\nfunc verifier(enabled bool, address std.Address, namespace string) bool {\n\tif !enabled {\n\t\treturn true // only in pre-genesis cases\n\t}\n\n\tif strings.TrimSpace(address.String()) == \"\" || strings.TrimSpace(namespace) == \"\" {\n\t\treturn false\n\t}\n\n\t// Allow user with their own address as namespace\n\t// This enables pseudo-anon namespaces\n\t// ie gno.land/{p,r}/{ADDRESS}/**\n\tif address.String() == namespace {\n\t\treturn true\n\t}\n\n\t// Can be a registered namespace or an alias\n\tuserData, _ := users.ResolveName(namespace)\n\tif userData == nil || userData.IsDeleted() {\n\t\treturn false\n\t}\n\n\t/// XXX: add check for r/sys/teams down the line\n\n\treturn userData.Addr() == address\n}\n" + }, + { + "name": "verifier_test.gno", + "body": "package names\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/uassert\"\n\t\"gno.land/p/demo/urequire\"\n\n\t\"gno.land/r/sys/users\"\n)\n\nvar alice = testutils.TestAddress(\"alice\")\n\nfunc TestDefaultVerifier(t *testing.T) {\n\t// Check 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// Check enabled\n\t// username + addr mismatch\n\tuassert.False(t, verifier(true, alice, \"notregistered\"))\n\t// PA namespace check\n\tuassert.True(t, verifier(true, alice, alice.String()))\n\n\t// Empty name/address\n\tuassert.False(t, verifier(true, std.Address(\"\"), \"\"))\n\n\t// Register proper username\n\ttesting.SetRealm(std.NewCodeRealm(\"gno.land/r/gnoland/users/v1\")) // authorized write\n\ttesting.SetOriginCaller(std.DerivePkgAddr(\"gno.land/r/gnoland/users/v1\"))\n\turequire.NoError(t, users.RegisterUser(\"alice\", alice))\n\n\t// Proper namespace\n\tuassert.True(t, verifier(true, alice, \"alice\"))\n}\n\nfunc TestEnable(t *testing.T) {\n\ttesting.SetRealm(std.NewUserRealm(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\"))\n\ttesting.SetOriginCaller(\"g1manfred47kzduec920z88wfr64ylksmdcedlf5\")\n\n\tuassert.NotPanics(t, func() {\n\t\tEnable()\n\t})\n\n\t// Confirm enable drops ownerships\n\tuassert.Equal(t, Ownable.Owner().String(), \"\")\n\tuassert.PanicsWithMessage(t, ownable.ErrUnauthorized.Error(), func() {\n\t\tEnable()\n\t})\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "fqHdR1iNctOtHiNH71K9GiLofWC6GOQiELLAepBxaJy3ROXihmeh8D+flaZOv0mpMItAFDDTSl0AiRtVN1LoBg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "params", + "path": "gno.land/r/sys/params", + "files": [ + { + "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//\texecutor := params.NewStringsPropExecutor(\"bank\", \"p\", \"restricted_denoms\")\npackage params\n\nimport (\n\t\"std\"\n\tprms \"sys/params\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/r/gov/dao/bridge\"\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 NewSysParamStringPropExecutor(module, submodule, name string, value string) dao.Executor {\n\treturn newPropExecutor(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamString(module, submodule, name, value) },\n\t)\n}\n\nfunc NewSysParamInt64PropExecutor(module, submodule, name string, value int64) dao.Executor {\n\treturn newPropExecutor(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamInt64(module, submodule, name, value) },\n\t)\n}\n\nfunc NewSysParamUint64PropExecutor(module, submodule, name string, value uint64) dao.Executor {\n\treturn newPropExecutor(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamUint64(module, submodule, name, value) },\n\t)\n}\n\nfunc NewSysParamBoolPropExecutor(module, submodule, name string, value bool) dao.Executor {\n\treturn newPropExecutor(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamBool(module, submodule, name, value) },\n\t)\n}\n\nfunc NewSysParamBytesPropExecutor(module, submodule, name string, value []byte) dao.Executor {\n\treturn newPropExecutor(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamBytes(module, submodule, name, value) },\n\t)\n}\n\nfunc NewSysParamStringsPropExecutor(module, submodule, name string, value []string) dao.Executor {\n\treturn newPropExecutor(\n\t\tsyskey(module, submodule, name),\n\t\tfunc() { prms.SetSysParamStrings(module, submodule, name, value) },\n\t)\n}\n\nfunc newPropExecutor(key string, fn func()) dao.Executor {\n\tcallback := func() error {\n\t\tfn()\n\t\tstd.Emit(\"set\", \"key\", key) // TODO document, make const, make consistent. 'k'??\n\t\treturn nil\n\t}\n\treturn bridge.GovDAO().NewGovDAOExecutor(callback)\n}\nfunc propose(exec dao.Executor, title, desc string) uint64 {\n\t// The executor's callback function is executed only after the proposal\n\t// has been voted on and approved by the GovDAO.\n\tprop := dao.ProposalRequest{\n\t\tTitle: title,\n\t\tDescription: desc,\n\t\tExecutor: exec,\n\t}\n\treturn bridge.GovDAO().Propose(prop)\n}\n" + }, + { + "name": "params_test.gno", + "body": "package params\n\nimport (\n\t\"testing\"\n\n\t_ \"gno.land/r/gov/dao/init\" // so that loader.init is executed\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 TestNewStringPropExecutor(t *testing.T) {\n\texecutor := NewSysParamStringPropExecutor(\"foo\", \"bar\", \"baz\", \"qux\")\n\tif executor == nil {\n\t\tt.Errorf(\"executor shouldn't be nil\")\n\t}\n}\n" + }, + { + "name": "unlock.gno", + "body": "package params\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)\n\nfunc ProposeUnlockTransfer() uint64 {\n\texe := NewSysParamStringsPropExecutor(bankModulePrefix, \"p\", restrictedDenomsKey, []string{})\n\treturn propose(exe, unlockTransferTitle, \"\")\n}\n\nfunc ProposeLockTransfer() uint64 {\n\texe := NewSysParamStringsPropExecutor(bankModulePrefix, \"p\", restrictedDenomsKey, []string{\"ugnot\"})\n\treturn propose(exe, lockTransferTitle, \"\")\n}\n" + }, + { + "name": "unlock_test.gno", + "body": "package params\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/dao\"\n\t\"gno.land/p/demo/simpledao\"\n\t\"gno.land/p/demo/urequire\"\n\t\"gno.land/r/gov/dao/bridge\"\n\n\t// loads the latest DAO implementation in the bridge.\n\t_ \"gno.land/r/gov/dao/init\"\n)\n\nfunc TestProUnlockTransfer(t *testing.T) {\n\tgovdao := bridge.GovDAO()\n\tid := ProposeUnlockTransfer()\n\tp, err := govdao.GetPropStore().ProposalByID(id)\n\turequire.NoError(t, err)\n\turequire.Equal(t, unlockTransferTitle, p.Title())\n}\n\nfunc TestFailUnlockTransfer(t *testing.T) {\n\tgovdao := bridge.GovDAO()\n\tid := ProposeUnlockTransfer()\n\turequire.PanicsWithMessage(\n\t\tt,\n\t\tsimpledao.ErrProposalNotAccepted.Error(),\n\t\tfunc() {\n\t\t\tgovdao.ExecuteProposal(id)\n\t\t},\n\t)\n}\n\nfunc TestExeUnlockTransfer(t *testing.T) {\n\tgovdao := bridge.GovDAO()\n\tid := ProposeUnlockTransfer()\n\tp, err := govdao.GetPropStore().ProposalByID(id)\n\turequire.NoError(t, err)\n\turequire.True(t, dao.Active == p.Status())\n\n\tgovdao.VoteOnProposal(id, dao.YesVote)\n\n\turequire.True(t, dao.Accepted == p.Status())\n\n\turequire.NotPanics(\n\t\tt,\n\t\tfunc() {\n\t\t\tgovdao.ExecuteProposal(id)\n\t\t},\n\t)\n\n\turequire.True(t, dao.ExecutionSuccessful == p.Status())\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "9Baai4PIGinWtoGwwkehHVc0PWPASB3SuEHUGyXJgt4zkF01XZs0FE+oSDv+ENcFUuTkMtbNJU0EzC6WzJ+jAw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "rewards", + "path": "gno.land/r/sys/rewards", + "files": [ + { + "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" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "0VPsjllszLxeTW/i0fK8R8fJ2fYgpSDhI43uCMgg8h1M/YGgkmfRXJv5YZ3EMnQPUFrzIFb8/4bYXoCNfMXtDw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "registry", + "path": "gno.land/r/ursulovic/registry", + "files": [ + { + "name": "registry.gno", + "body": "package registry\n\nimport (\n\t\"errors\"\n\t\"std\"\n)\n\nvar (\n\tmainAddress std.Address\n\tbackupAddress std.Address\n\n\tErrInvalidAddr = errors.New(\"Ivan's registry: Invalid address\")\n\tErrUnauthorized = errors.New(\"Ivan's registry: Unauthorized\")\n)\n\nfunc init() {\n\tmainAddress = \"g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x\"\n\tbackupAddress = \"g1mw2xft3eava9kfhqw3fjj3kkf3pkammty0mtv7\"\n}\n\nfunc MainAddress() std.Address {\n\treturn mainAddress\n}\n\nfunc BackupAddress() std.Address {\n\treturn backupAddress\n}\n\nfunc SetMainAddress(addr std.Address) error {\n\tassertAuthorized()\n\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tmainAddress = addr\n\treturn nil\n}\n\nfunc SetBackupAddress(addr std.Address) error {\n\tassertAuthorized()\n\n\tif !addr.IsValid() {\n\t\treturn ErrInvalidAddr\n\t}\n\n\tbackupAddress = addr\n\treturn nil\n}\n\n// It will stay here for now, might be useful later\nfunc assertAuthorized() {\n\tcaller := std.PreviousRealm().Address()\n\tisAuthorized := caller == mainAddress || caller == backupAddress\n\n\tif !isAuthorized {\n\t\tpanic(ErrUnauthorized)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "/ML1HV7bkSidmfBglqTooKm2T7EGalfuPUmkj8dSiP3/++DbpMl7hypRynDVf7RsLMG5FMFkGS5Z4rhHad8OAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "home", + "path": "gno.land/r/ursulovic/home", + "files": [ + { + "name": "home.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/ownable\"\n\t\"gno.land/p/moul/md\"\n\t\"gno.land/r/leon/hof\"\n\n\t\"gno.land/r/ursulovic/registry\"\n)\n\nvar (\n\taboutMe string\n\tselectedImage string\n\tOwnable *ownable.Ownable\n\n\tgithubUrl string\n\tlinkedinUrl string\n\timageUpdatePrice int64\n\n\tisValidUrl func(string) bool\n)\n\nfunc init() {\n\tOwnable = ownable.NewWithAddress(registry.MainAddress())\n\n\taboutMe = \"Hi, I'm Ivan Ursulovic, a computer engineering graduate, blockchain enthusiast, and backend developer specializing in ASP.NET. I love learning new things and taking on challenges.\"\n\tselectedImage = \"https://i.ibb.co/W28NPkw/beograd.webp\"\n\n\tgithubUrl = \"https://github.com/ursulovic\"\n\tlinkedinUrl = \"https://www.linkedin.com/in/ivan-ursulovic-953310190/\"\n\timageUpdatePrice = 5000000\n\tisValidUrl = defaultURLValidation\n\thof.Register(\"Ivan's Home Realm\", \"Welcome to my Home Realm!\")\n}\n\nfunc Render(s string) string {\n\tvar sb strings.Builder\n\tsb.WriteString(renderAboutMe())\n\tsb.WriteString(renderSelectedImage())\n\tsb.WriteString(renderContactsUrl())\n\treturn sb.String()\n}\n\nfunc defaultURLValidation(url string) bool {\n\tconst urlPrefix string = \"https://i.ibb.co/\"\n\n\tif !strings.HasPrefix(url, urlPrefix) {\n\t\treturn false\n\t}\n\n\tif !(strings.HasSuffix(url, \".jpg\") ||\n\t\tstrings.HasSuffix(url, \".png\") ||\n\t\tstrings.HasSuffix(url, \".gif\") ||\n\t\tstrings.HasSuffix(url, \".webp\")) {\n\t\treturn false\n\t}\n\n\turlPath := strings.TrimPrefix(url, \"https://i.ibb.co/\")\n\tparts := strings.Split(urlPath, \"/\")\n\n\tif len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc UpdateSelectedImage(url string) {\n\tif !isValidUrl(url) {\n\t\tpanic(\"Url is not valid!\")\n\t}\n\n\tsentCoins := std.OriginSend()\n\n\tif len(sentCoins) != 1 || sentCoins.AmountOf(\"ugnot\") != imageUpdatePrice {\n\t\tpanic(\"Please send exactly \" + strconv.Itoa(int(imageUpdatePrice)) + \" ugnot\")\n\t}\n\n\tselectedImage = url\n}\n\nfunc renderSelectedImage() string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(md.HorizontalRule())\n\tsb.WriteString(\"\\n\")\n\n\tsb.WriteString(md.H2(\"📸 Featured Image\"))\n\tsb.WriteString(\"\\n\")\n\n\tsb.WriteString(md.Image(\"\", selectedImage))\n\tsb.WriteString(\"\\n\")\n\n\tsb.WriteString(md.H4(\"✨ \" + md.Link(\"Change this image for \"+strconv.Itoa(int(imageUpdatePrice/1000000))+\" GNOT. To update, set a direct image URL from ImgBB.\", \"https://gno.studio/connect/view/gno.land/r/ursulovic/home?network=portal-loop\") + \" ✨\"))\n\n\treturn sb.String()\n}\n\nfunc renderAboutMe() string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(md.H1(\"👋 Welcome to Ivan's Homepage!\"))\n\tsb.WriteString(\"\\n\")\n\n\tsb.WriteString(md.H2(\"👨‍💻 About Me\"))\n\tsb.WriteString(\"\\n\")\n\n\tsb.WriteString(md.Blockquote(aboutMe))\n\n\treturn sb.String()\n}\n\nfunc renderContactsUrl() string {\n\tvar sb strings.Builder\n\n\tsb.WriteString(md.HorizontalRule())\n\tsb.WriteString(\"\\n\")\n\n\tsb.WriteString(md.H2(\"🔗 Let's Connect\"))\n\tsb.WriteString(\"\\n\")\n\n\titems := []string{\n\t\t\"🐙 \" + md.Link(\"GitHub\", githubUrl),\n\t\t\"💼 \" + md.Link(\"LinkedIn\", linkedinUrl),\n\t}\n\tsb.WriteString(md.BulletList(items))\n\n\treturn sb.String()\n}\n\nfunc UpdateGithubUrl(url string) {\n\tOwnable.AssertCallerIsOwner()\n\tgithubUrl = url\n}\n\nfunc UpdateLinkedinUrl(url string) {\n\tOwnable.AssertCallerIsOwner()\n\tlinkedinUrl = url\n}\n\nfunc UpdateAboutMe(text string) {\n\tOwnable.AssertCallerIsOwner()\n\taboutMe = text\n}\n\nfunc UpdateImagePrice(newPrice int64) {\n\tOwnable.AssertCallerIsOwner()\n\timageUpdatePrice = newPrice\n}\n\nfunc UpdateIsValidUrlFunction(f func(string) bool) {\n\tOwnable.AssertCallerIsOwner()\n\tisValidUrl = f\n}\n" + }, + { + "name": "home_test.gno", + "body": "package home\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n)\n\nfunc TestUpdateGithubUrl(t *testing.T) {\n\tcaller := std.Address(\"g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x\")\n\ttesting.SetOriginCaller(caller)\n\n\tnewUrl := \"https://github.com/example\"\n\n\tUpdateGithubUrl(newUrl)\n\n\tif githubUrl != newUrl {\n\t\tt.Fatalf(\"GitHub url not updated properly!\")\n\t}\n}\n\nfunc TestUpdateLinkedinUrl(t *testing.T) {\n\tcaller := std.Address(\"g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x\")\n\ttesting.SetOriginCaller(caller)\n\n\tnewUrl := \"https://www.linkedin.com/in/example\"\n\n\tUpdateGithubUrl(newUrl)\n\n\tif githubUrl != newUrl {\n\t\tt.Fatalf(\"LinkedIn url not updated properly!\")\n\t}\n}\n\nfunc TestUpdateAboutMe(t *testing.T) {\n\tcaller := std.Address(\"g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x\")\n\ttesting.SetOriginCaller(caller)\n\n\tnewAboutMe := \"This is new description!\"\n\n\tUpdateAboutMe(newAboutMe)\n\n\tif aboutMe != newAboutMe {\n\t\tt.Fatalf(\"About mew not updated properly!\")\n\t}\n}\n\nfunc TestUpdateSelectedImage(t *testing.T) {\n\tuser := testutils.TestAddress(\"user\")\n\ttesting.SetOriginCaller(user)\n\n\tvalidImageUrl := \"https://i.ibb.co/hLtmnX0/beautiful-rain-forest-ang-ka-nature-trail-doi-inthanon-national-park-thailand-36703721.webp\"\n\n\tcoinsSent := std.NewCoins(std.NewCoin(\"ugnot\", 5000000)) // Update to match the price expected by your function\n\ttesting.SetOriginSend(coinsSent)\n\n\tUpdateSelectedImage(validImageUrl)\n\n\tif selectedImage != validImageUrl {\n\t\tt.Fatalf(\"Valid image URL rejected!\")\n\t}\n\n\tinvalidImageUrl := \"https://ibb.co/Kb3rQNn\"\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Fatalf(\"Expected panic for invalid image URL, but got no panic\")\n\t\t}\n\t}()\n\n\tUpdateSelectedImage(invalidImageUrl)\n\n\tinvalidCoins := std.NewCoins(std.NewCoin(\"ugnot\", 1000000))\n\ttesting.SetOriginSend(invalidCoins)\n\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Fatalf(\"Expected panic for incorrect coin denomination or amount, but got no panic\")\n\t\t}\n\t}()\n\n\tUpdateSelectedImage(validImageUrl)\n}\n\nfunc TestUpdateImagePrice(t *testing.T) {\n\tcaller := std.Address(\"g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x\")\n\ttesting.SetOriginCaller(caller)\n\n\tvar newImageUpdatePrice int64 = 3000000\n\n\tUpdateImagePrice(newImageUpdatePrice)\n\n\tif imageUpdatePrice != newImageUpdatePrice {\n\t\tt.Fatalf(\"Image update price not updated properly!\")\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "PkwVJY1B+qkoSpZd/7+uyv9PdwtxBfVD8fIvsL8TUIw40jwrAsHm7pTjqATCC0CXPehRPpsC2pxbg1GpjC6NAg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "storage", + "path": "gno.land/r/x/benchmark/storage", + "files": [ + { + "name": "boards.gno", + "body": "package storage\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nvar boards avl.Tree\n\ntype Board interface {\n\tAddPost(title, content string)\n\tGetPost(id int) (Post, bool)\n\tSize() int\n}\n\n// posts are persisted in an avl tree\ntype TreeBoard struct {\n\tid int\n\tposts *avl.Tree\n}\n\nfunc (b *TreeBoard) AddPost(title, content string) {\n\tn := b.posts.Size()\n\tp := Post{n, title, content}\n\tb.posts.Set(strconv.Itoa(n), p)\n}\n\nfunc (b *TreeBoard) GetPost(id int) (Post, bool) {\n\tp, ok := b.posts.Get(strconv.Itoa(id))\n\tif ok {\n\t\treturn p.(Post), ok\n\t} else {\n\t\treturn Post{}, ok\n\t}\n}\n\nfunc (b *TreeBoard) Size() int {\n\treturn b.posts.Size()\n}\n\n// posts are persisted in a map\ntype MapBoard struct {\n\tid int\n\tposts map[int]Post\n}\n\nfunc (b *MapBoard) AddPost(title, content string) {\n\tn := len(b.posts)\n\tp := Post{n, title, content}\n\tb.posts[n] = p\n}\n\nfunc (b *MapBoard) GetPost(id int) (Post, bool) {\n\tp, ok := b.posts[id]\n\tif ok {\n\t\treturn p, ok\n\t} else {\n\t\treturn Post{}, ok\n\t}\n}\n\nfunc (b *MapBoard) Size() int {\n\treturn len(b.posts)\n}\n\n// posts are persisted in a slice\ntype SliceBoard struct {\n\tid int\n\tposts []Post\n}\n\nfunc (b *SliceBoard) AddPost(title, content string) {\n\tn := len(b.posts)\n\tp := Post{n, title, content}\n\tb.posts = append(b.posts, p)\n}\n\nfunc (b *SliceBoard) GetPost(id int) (Post, bool) {\n\tif id \u003c len(b.posts) {\n\t\tp := b.posts[id]\n\n\t\treturn p, true\n\t} else {\n\t\treturn Post{}, false\n\t}\n}\n\nfunc (b *SliceBoard) Size() int {\n\treturn len(b.posts)\n}\n\ntype Post struct {\n\tid int\n\ttitle string\n\tcontent string\n}\n" + }, + { + "name": "forum.gno", + "body": "package storage\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\nfunc init() {\n\t// we write to three common data structure for persistence\n\t// avl.Tree, map and slice.\n\tposts0 := avl.NewTree()\n\tb0 := \u0026TreeBoard{0, posts0}\n\tboards.Set(strconv.Itoa(0), b0)\n\n\tposts1 := make(map[int]Post)\n\tb1 := \u0026MapBoard{1, posts1}\n\tboards.Set(strconv.Itoa(1), b1)\n\n\tposts2 := []Post{}\n\tb2 := \u0026SliceBoard{2, posts2}\n\tboards.Set(strconv.Itoa(2), b2)\n}\n\n// post to all boards.\nfunc AddPost(title, content string) {\n\tfor i := 0; i \u003c boards.Size(); i++ {\n\t\tboardId := strconv.Itoa(i)\n\t\tb, ok := boards.Get(boardId)\n\t\tif ok {\n\t\t\tb.(Board).AddPost(title, content)\n\t\t}\n\t}\n}\n\nfunc GetPost(boardId, postId int) string {\n\tb, ok := boards.Get(strconv.Itoa(boardId))\n\tvar res string\n\n\tif ok {\n\t\tp, ok := b.(Board).GetPost(postId)\n\t\tif ok {\n\t\t\tres = p.title + \",\" + p.content\n\t\t}\n\t}\n\treturn res\n}\n\nfunc GetPostSize(boardId int) int {\n\tb, ok := boards.Get(strconv.Itoa(boardId))\n\tvar res int\n\n\tif ok {\n\t\tres = b.(Board).Size()\n\t} else {\n\t\tres = -1\n\t}\n\n\treturn res\n}\n\nfunc GetBoardSize() int {\n\treturn boards.Size()\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "sW+jyvHL42NvX2bEUMClH6hn+ugQ2z6oUc/LMJq1QDBgHNLLCZi7gkwh8zaKrP1zXFV0BCb8LGLrYT18pFxoCg==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "example", + "path": "gno.land/r/x/jeronimo_render_proxy", + "files": [ + { + "name": "example.gno", + "body": "package example\n\nfunc Render(string) string {\n\treturn `# Render Proxy\n\nThis example shows how proxying render calls can be used to allow updating realms to new\nversions while keeping the same realm path. The idea is to have a simple \"parent\" realm\nthat only keeps track of the latest realm version and forwards all render calls to it.\n\nBy only focusing on the 'Render()' function the proxy realm keeps its public functions\nstable allowing each version to update their public functions and exposed types without\nneeding to also update the proxy realm.\n\nAny interaction or transaction must be sent to the latest, or target, realm version while\nrender calls are sent to the proxy realm.\n\nEach realm version registers itself on deployment as the latest available version, and\nits allowed to do so because each versioned realm path shares the proxy realm path.\n`\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "Kqi4yQCO+6pM1GWHmGfFPqatZHsHrl/gfGR5UTIyHzPg4GW5cBSKV1NutpuF4o663m8/yL3oib5UXDkrr9beDQ==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "mapdelete", + "path": "gno.land/r/x/map_delete", + "files": [ + { + "name": "map_delete.gno", + "body": "package mapdelete\n\nvar mapus map[uint64]string = make(map[uint64]string)\n\nfunc init() {\n\tmapus[3] = \"three\"\n\tmapus[5] = \"five\"\n\tmapus[9] = \"nine\"\n}\n\nfunc DeleteMap(k uint64) {\n\tdelete(mapus, k)\n}\n\nfunc GetMap(k uint64) bool {\n\t_, exist := mapus[k]\n\treturn exist\n}\n" + }, + { + "name": "map_delete_test.gno", + "body": "package mapdelete\n\nimport \"testing\"\n\nfunc TestGetMap(t *testing.T) {\n\tif !(GetMap(3)) {\n\t\tt.Error(\"Expected true, got \", GetMap(3))\n\t}\n}\n\nfunc TestDeleteMap(t *testing.T) {\n\tDeleteMap(3)\n}\n\nfunc TestGetMapAfterDelete(t *testing.T) {\n\tif GetMap(3) {\n\t\tt.Error(\"Expected false, got \", GetMap(3))\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "hLVDd1iJ2UTVbosfYSXJzVnNWVEAdN381h9pRqEzVtKUDUwBf2f/WcsI2Jy9HSAtJdy7/kqpU5wMK9HWiWR2Bw==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "evaluation", + "path": "gno.land/r/x/nir1218_evaluation_proposal", + "files": [ + { + "name": "category.gno", + "body": "package evaluation\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Category struct {\n\tname string\n\tcriteria []string\n\tstatus string\n\tvotes avl.Tree\n\ttallyResult TallyResult\n}\n\nfunc NewCategory(name string, criteria []string) *Category {\n\ttallyResult := TallyResult{}\n\ttallyResult.results.Set(VoteYes, 0)\n\ttallyResult.results.Set(VoteNo, 0)\n\n\tc := \u0026Category{\n\t\tname: name,\n\t\tcriteria: criteria,\n\t\tstatus: \"Proposed\",\n\t\tvotes: avl.Tree{},\n\t\ttallyResult: tallyResult,\n\t}\n\treturn c\n}\n\nfunc (c *Category) Approve() {\n\t// TODO error handling\n\tc.status = \"Approved\"\n}\n\nfunc (c Category) Status() string {\n\treturn c.status\n}\n\nfunc (c *Category) Tally() {\n\t// TODO error handling\n\tc.votes.Iterate(\"\", \"\", func(address string, vote any) bool {\n\t\tv := vote.(Vote)\n\t\tvalue, exists := c.tallyResult.results.Get(v.option)\n\t\tif !exists {\n\t\t\treturn false\n\t\t}\n\t\tcount := value.(int)\n\t\tc.tallyResult.results.Set(v.option, count+1)\n\t\treturn true\n\t})\n}\n" + }, + { + "name": "committee.gno", + "body": "package evaluation\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Committee struct {\n\tmembers []std.Address // TODO - use avl tree or address set?\n\tcategories avl.Tree // A category is mapped to a list of evaluation criteria\n\tevaluation *Evaluation\n}\n\nconst ApprovedStatus = \"Approved\"\n\nfunc NewCommittee() *Committee {\n\tc := \u0026Committee{\n\t\tmembers: []std.Address{},\n\t\tcategories: avl.Tree{},\n\t\tevaluation: NewEvalutaion(),\n\t}\n\treturn c\n}\n\nfunc (c *Committee) DesignateMembers(members []std.Address) []std.Address {\n\tc.members = append(c.members, members...)\n\treturn c.members\n}\n\nfunc (c *Committee) DismissMembers(members []std.Address) []std.Address {\n\t// TODO\n\treturn []std.Address{}\n}\n\nfunc (c *Committee) AddCategory(name string, criteria []string) bool {\n\t// TODO error handling\n\tif !c.isMember(std.OriginCaller()) {\n\t\treturn false\n\t}\n\tcategory := NewCategory(name, criteria)\n\tc.categories.Set(name, category)\n\treturn true\n}\n\nfunc (c *Committee) ApproveCategory(name string, option string) bool {\n\tif !c.isMember(std.OriginCaller()) {\n\t\treturn false\n\t}\n\n\tvalue, exists := c.categories.Get(name)\n\tif !exists {\n\t\treturn false\n\t}\n\tcategory := value.(*Category)\n\tif category.Status() == ApprovedStatus {\n\t\treturn false\n\t}\n\n\tvote := NewVote(std.OriginCaller(), option)\n\tcategory.votes.Set(std.OriginCaller().String(), vote)\n\tcategory.Tally()\n\n\t// TODO Add threshold factor for a category approval\n\t// TODO Add quorum factor for a category approval\n\t// Current assumption is all members voted YES so category is approved\n\n\tresult, exists := category.tallyResult.results.Get(VoteYes)\n\tif !exists {\n\t\treturn false\n\t}\n\n\tif result.(int) == len(c.members) {\n\t\tcategory.Approve()\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// TODO error handling\nfunc (c *Committee) AddContribution(pr *PullRequest, contributor std.Address) (contributionId int, ok bool) {\n\tif !c.isMember(std.OriginCaller()) {\n\t\treturn -1, false\n\t}\n\t// Check the category of the PR matches a category this committee evaluates\n\t// TODO check the category is an approved category\n\tif c.categories.Has(pr.category) {\n\t\treturn c.evaluation.AddContribution(pr, contributor)\n\t}\n\n\treturn -1, false\n}\n\n// TODO error handling\nfunc (c *Committee) ApproveContribution(id int, option string) bool {\n\tif !c.isMember(std.OriginCaller()) {\n\t\treturn false\n\t}\n\n\tvalue, exists := c.evaluation.contributions.Get(ufmt.Sprintf(\"%d\", id))\n\tif !exists {\n\t\treturn false\n\t}\n\tcontribution := value.(*Contribution)\n\t// Already approved\n\tif contribution.status == ApprovedStatus {\n\t\treturn false\n\t}\n\n\tvote := NewVote(std.OriginCaller(), option)\n\tcontribution.votes = append(contribution.votes, vote)\n\tcontribution.Tally()\n\n\t// TODO Add threshold factor for a contribution approval\n\t// TODO Add quorum factor for a contribution approval\n\t// Current assumption is all members voted YES so contribution is approved\n\n\tresult, exists := contribution.tallyResult.results.Get(VoteYes)\n\tif !exists {\n\t\treturn false\n\t}\n\n\tif result.(int) == len(c.members) {\n\t\tcontribution.Approve()\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (c *Committee) isMember(m std.Address) bool {\n\tfor _, member := range c.members {\n\t\tif m == member {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n" + }, + { + "name": "committee_test.gno", + "body": "package evaluation\n\nimport (\n\t\"std\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n)\n\nfunc TestCommitteeMembers(t *testing.T) {\n\tmembers := []std.Address{testutils.TestAddress(\"member1\"), testutils.TestAddress(\"member2\"), testutils.TestAddress(\"member3\")}\n\tc := NewCommittee()\n\n\tt.Run(\"Designate Committee Members\", func(t *testing.T) {\n\t\tc.DesignateMembers(members)\n\t\tif !isEqualAddressSlice(c.members, members) {\n\t\t\tt.Errorf(\"Designated Committee members got %v expcted %v\", members, c.members)\n\t\t}\n\t})\n\n\tt.Run(\"Dismiss Committee Members\", func(t *testing.T) {\n\t\tc.DismissMembers(members)\n\t})\n}\n\nfunc TestCategoryEvaluationCriteria(t *testing.T) {\n\tmember := testutils.TestAddress(\"member\")\n\tcategory := \"document\"\n\tcriteria := []string{\"clarity\", \"usage\"}\n\tcategory2 := \"bounty\"\n\tcriteria2 := []string{\"complexity\"}\n\texpectedGategory := NewCategory(category, criteria)\n\texpectedGategory2 := NewCategory(category2, criteria2)\n\n\tc := NewCommittee()\n\tc.DesignateMembers([]std.Address{member})\n\n\tt.Run(\"Add First Committee Category and Evaluation Criteria\", func(t *testing.T) {\n\t\ttesting.SetOriginCaller(member)\n\t\tc.AddCategory(category, criteria)\n\t\tvalue, exists := c.categories.Get(category)\n\t\tif !exists {\n\t\t\tt.Errorf(\"Add first category %s failed\", category)\n\t\t}\n\t\tgotCategory := value.(*Category)\n\t\tif gotCategory.name != expectedGategory.name {\n\t\t\tt.Errorf(\"First Committee category got %s expected %s\", gotCategory.name, expectedGategory.name)\n\t\t}\n\t})\n\n\tt.Run(\"Add Second Committee Category and Evaluation Criteria\", func(t *testing.T) {\n\t\ttesting.SetOriginCaller(member)\n\t\tc.AddCategory(category2, criteria2)\n\t\tvalue2, exists2 := c.categories.Get(category2)\n\t\tif !exists2 {\n\t\t\tt.Errorf(\"Add second category %s failed\", category2)\n\t\t}\n\t\tgotCategory2 := value2.(*Category)\n\t\tif gotCategory2.name != expectedGategory2.name {\n\t\t\tt.Errorf(\"Second Committee category got %s expected %s\", gotCategory2.name, expectedGategory2.name)\n\t\t}\n\t})\n\n\tt.Run(\"Approve First Committee Category\", func(t *testing.T) {\n\t\ttesting.SetOriginCaller(member)\n\t\tapproved := c.ApproveCategory(category, VoteYes)\n\t\tif !approved {\n\t\t\tvalue, exists := c.categories.Get(category)\n\t\t\tgotCategory := value.(*Category)\n\t\t\tt.Errorf(\"Approved First Committee category got %s expected %s\", gotCategory.status, \"Approved\")\n\t\t}\n\t})\n}\n\nfunc isEqualStringSlice(a, b []string) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc isEqualAddressSlice(a, b []std.Address) bool {\n\tif len(a) != len(b) {\n\t\treturn false\n\t}\n\tfor i := range a {\n\t\tif a[i] != b[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n" + }, + { + "name": "contribution.gno", + "body": "package evaluation\n\nimport (\n\t\"std\"\n\t\"time\"\n)\n\nvar contributionStatus = map[string]string{}\n\ntype Contribution struct {\n\tid int\n\tcontributor std.Address\n\tstatus string // approved, proposed, negotiation, discussion, evaluation, etc.\n\tvotes []Vote\n\ttallyResult TallyResult\n\tsubmitTime time.Time\n\tlastEvaluateTime time.Time\n\tapproveTime time.Time\n}\n\nfunc init() {\n\tcontributionStatus = make(map[string]string)\n\tcontributionStatus[\"Proposed\"] = \"Proposed\"\n\tcontributionStatus[\"Approved\"] = \"Approved\"\n\tcontributionStatus[\"Evaluated\"] = \"Evaluated\"\n\tcontributionStatus[\"Negotiated\"] = \"Negotiated\"\n}\n\nfunc NewContribution(id int, contributor std.Address) *Contribution {\n\tc := \u0026Contribution{\n\t\tid: id,\n\t\tcontributor: contributor,\n\t\tstatus: contributionStatus[\"Proposed\"],\n\t\tvotes: []Vote{},\n\t\ttallyResult: TallyResult{},\n\t}\n\treturn c\n}\n\nfunc (c Contribution) Id() int {\n\treturn c.id\n}\n\nfunc (c Contribution) Status() string {\n\treturn c.status\n}\n\nfunc (c *Contribution) UpdateStatus(status string) bool {\n\tif c.status == contributionStatus[\"Approved\"] {\n\t\treturn false\n\t}\n\tc.status = status\n\treturn true\n}\n\nfunc (c *Contribution) Approve() {\n\t// TODO error handling\n\tc.status = \"Approved\"\n}\n\nfunc (c *Contribution) Tally() {\n\t// TODO error handling\n\tfor _, v := range c.votes {\n\t\tif c.tallyResult.results.Has(v.option) {\n\t\t\tvalue, _ := c.tallyResult.results.Get(v.option)\n\t\t\tcount := value.(int)\n\t\t\tc.tallyResult.results.Set(v.option, count+1)\n\t\t}\n\t}\n}\n" + }, + { + "name": "contribution_test.gno", + "body": "package evaluation\n\nimport \"testing\"\n\nfunc TestContributionUpdateStatus(t *testing.T) {\n\tc := NewContribution(1, \"contributor\")\n\n\tt.Run(\"Status Update Negotiated\", func(t *testing.T) {\n\t\tok := c.UpdateStatus(\"Negotiated\")\n\t\tif !ok {\n\t\t\tt.Error(\"Expected Successful Status Update but failed\")\n\t\t}\n\t})\n\n\tt.Run(\"Status Update Evaluated\", func(t *testing.T) {\n\t\tok := c.UpdateStatus(\"Evaluated\")\n\t\tif !ok {\n\t\t\tt.Error(\"Expected Successful Status Update but failed\")\n\t\t}\n\t})\n\n\tt.Run(\"Status Update Approved\", func(t *testing.T) {\n\t\tok := c.UpdateStatus(\"Approved\")\n\t\tif !ok {\n\t\t\tt.Error(\"Expected Successful Status Update but failed\")\n\t\t}\n\t})\n\n\tt.Run(\"Status Update Approved Invalid\", func(t *testing.T) {\n\t\tok := c.UpdateStatus(\"Approved\")\n\t\tif ok {\n\t\t\tt.Error(\"Expected Failed Status Update but succeded\")\n\t\t}\n\t})\n}\n" + }, + { + "name": "evaluation.gno", + "body": "package evaluation\n\nimport (\n\t\"std\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype Evaluation struct {\n\tcontributions avl.Tree\n\tpullrequests avl.Tree\n}\n\ntype Evaluator interface {\n\tEvaluate() Points\n}\n\nfunc NewEvalutaion() *Evaluation {\n\te := \u0026Evaluation{\n\t\tcontributions: avl.Tree{},\n\t\tpullrequests: avl.Tree{},\n\t}\n\treturn e\n}\n\nfunc (e *Evaluation) AddContribution(pr *PullRequest, contributor std.Address) (int, bool) {\n\tid := pr.Id()\n\te.pullrequests.Set(ufmt.Sprintf(\"%d\", id), pr)\n\tc := NewContribution(id, contributor)\n\te.contributions.Set(ufmt.Sprintf(\"%d\", id), c)\n\treturn id, true\n}\n\nfunc (e *Evaluation) UpdateContribution(id int, status string) bool {\n\tc, exists := e.contributions.Get(ufmt.Sprintf(\"%d\", id))\n\tif exists {\n\t\tcontribtution := c.(*Contribution)\n\t\treturn contribtution.UpdateStatus(status)\n\t}\n\treturn false\n}\n" + }, + { + "name": "evaluation_test.gno", + "body": "package evaluation\n\n/*\n\t1. At what stage of the PR a contribution should be evaluated?\n\t\tShould the PR be approved first?\n\t2. Can a contribution be re-evaluated before approved (current assumption is once a contribution is approved its state is final)?\n\t3. Can an evaluation criteria change up until it is approved (current assumption is that the evaluation criteria is set when the contribution is added)?\n*/\n\nimport (\n\t\"testing\"\n\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\nvar (\n\te = NewEvalutaion()\n\n\tid = 792\n\tname = \"Evaluation DAO Kick Off\"\n\tdescription = \"The PR is to initiate a discussion regarding the evaluation DAO\"\n\tstatus = \"Draft\"\n\tcategory = \"feat\"\n\tcriteria = map[string]int32{\"simplicity\": 1, \"usefullnes\": 1, \"quality\": 1}\n\taddress = testutils.TestAddress(\"contributor\")\n)\n\nfunc TestEvaluationAddContribution(t *testing.T) {\n\tpr := NewPullRequest(id, name, description, status, category)\n\tcontributionId, ok := e.AddContribution(pr, address)\n\n\tt.Run(\"\", func(t *testing.T) {\n\t\tif contributionId != id {\n\t\t\tt.Errorf(\"Got Contribution Id %d expected %d\", contributionId, id)\n\t\t}\n\t})\n\n\tt.Run(\"Contribution added using the pull request id\", func(t *testing.T) {\n\t\tc, _ := e.contributions.Get(ufmt.Sprintf(\"%d\", id))\n\t\tcontribtution := c.(*Contribution)\n\t\tif contribtution.Id() != id {\n\t\t\tt.Errorf(\"Got Contribution Id %d expected %d\", contribtution.Id(), id)\n\t\t}\n\t})\n\n\tt.Run(\"Pull Request added using the pull request id\", func(t *testing.T) {\n\t\tpr, _ := e.pullrequests.Get(ufmt.Sprintf(\"%d\", id))\n\t\tpullrequest := pr.(*PullRequest)\n\t\tif pullrequest.Id() != id {\n\t\t\tt.Errorf(\"Got Pull Request Id %d expected %d\", pullrequest.Id(), id)\n\t\t}\n\t})\n}\n\nfunc TestEvaluationUpdateContribution(t *testing.T) {\n\tt.Run(\"\", func(t *testing.T) {\n\t\tstatus := \"Negotiated\"\n\t\tok := e.UpdateContribution(id, status)\n\t\tif !ok {\n\t\t\tt.Error(\"Expected evaluation to update contribution's status successfully but failed\")\n\t\t}\n\t})\n\n\tt.Run(\"Contribution doesn't exist\", func(t *testing.T) {\n\t\tid := 1\n\t\tstatus := \"Negotiated\"\n\t\tok := e.UpdateContribution(id, status)\n\t\tif ok {\n\t\t\tt.Error(\"Expected evaluation to fail but pass\")\n\t\t}\n\t})\n}\n" + }, + { + "name": "points.gno", + "body": "package evaluation\n\n// Points could be converted to rewards\ntype Points struct {\n\ttotal int64\n\tfactors map[string]int32\n}\n\nfunc NewPoints(t int64, f map[string]int32) Points {\n\treturn Points{\n\t\ttotal: t,\n\t\tfactors: f,\n\t}\n}\n" + }, + { + "name": "pull_request.gno", + "body": "package evaluation\n\nvar pullRequestStatus map[string]struct{}\n\ntype PullRequest struct {\n\tid int\n\tname string\n\tdescription string\n\tstatus string // Draft, Review required, Changes requested, Approved\n\tcategory string // bounty, chore, defect, document etc.\n}\n\nfunc init() {\n\tpullRequestStatus = make(map[string]struct{})\n\tpullRequestStatus[\"Draft\"] = struct{}{}\n\tpullRequestStatus[\"Approved\"] = struct{}{}\n\tpullRequestStatus[\"Changes requested\"] = struct{}{}\n\tpullRequestStatus[\"Review required\"] = struct{}{}\n}\n\nfunc NewPullRequest(id int, name string, description string, status string, category string) *PullRequest {\n\tpr := \u0026PullRequest{\n\t\tid: id,\n\t\tname: name,\n\t\tdescription: description,\n\t\tstatus: status,\n\t\tcategory: category,\n\t}\n\treturn pr\n}\n\nfunc (pr PullRequest) Id() int {\n\treturn pr.id\n}\n\nfunc (pr PullRequest) Status() string {\n\treturn pr.status\n}\n\nfunc (pr *PullRequest) UpdateName(name string) {\n\tpr.name = name\n}\n\nfunc (pr *PullRequest) UpdateDescription(description string) {\n\tpr.description = description\n}\n\nfunc (pr *PullRequest) UpdateStatus(status string) bool {\n\tif validateStatus(status) {\n\t\tpr.status = status\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc validateStatus(status string) bool {\n\t_, ok := pullRequestStatus[status]\n\treturn ok\n}\n" + }, + { + "name": "pull_request_test.gno", + "body": "package evaluation\n\nimport \"testing\"\n\nfunc TestPullRequestUpdateStatus(t *testing.T) {\n\tvar (\n\t\tid = 792\n\t\tname = \"Evaluation DAO Kick Off\"\n\t\tdescription = \"The PR is to initiate a discussion regarding the evaluation DAO\"\n\t\tstatus = \"Draft\"\n\t\tcategory = \"feat\"\n\t)\n\n\tvalidPR := NewPullRequest(id, name, description, status, category)\n\n\tt.Run(\"Valid Status Approved\", func(t *testing.T) {\n\t\tstatus := \"Approved\"\n\t\tif !validPR.UpdateStatus(status) {\n\t\t\tt.Error(\"expected validation to pass\")\n\t\t}\n\t})\n\n\tt.Run(\"Valid Status Draft\", func(t *testing.T) {\n\t\tstatus := \"Draft\"\n\t\tif !validPR.UpdateStatus(status) {\n\t\t\tt.Error(\"expected validation to pass\")\n\t\t}\n\t})\n\n\tt.Run(\"Valid Status Changes requested\", func(t *testing.T) {\n\t\tstatus := \"Changes requested\"\n\t\tif !validPR.UpdateStatus(status) {\n\t\t\tt.Error(\"expected validation to pass\")\n\t\t}\n\t})\n\n\tt.Run(\"Valid Status Review required\", func(t *testing.T) {\n\t\tstatus := \"Review required\"\n\t\tif !validPR.UpdateStatus(status) {\n\t\t\tt.Error(\"expected validation to pass\")\n\t\t}\n\t})\n\n\tt.Run(\"Invalid Status\", func(t *testing.T) {\n\t\tstatus := \"Junk\"\n\t\tif validPR.UpdateStatus(status) {\n\t\t\tt.Error(\"expected validation to fail\")\n\t\t}\n\t})\n}\n" + }, + { + "name": "tally.gno", + "body": "package evaluation\n\nimport \"gno.land/p/demo/avl\"\n\ntype TallyResult struct {\n\tresults avl.Tree\n}\n\ntype Tally interface {\n\tTally()\n}\n" + }, + { + "name": "task.gno", + "body": "package evaluation\n\n// Maybe a task in the project management system\ntype Task struct {\n\tid int\n\tname string\n\tdescription string\n\tstatus string\n}\n" + }, + { + "name": "vote.gno", + "body": "package evaluation\n\nimport \"std\"\n\nconst (\n\tVoteYes = \"YES\"\n\tVoteNo = \"NO\"\n)\n\ntype Vote struct {\n\tvoter std.Address\n\toption string\n}\n\nfunc NewVote(voter std.Address, option string) Vote {\n\tv := Vote{\n\t\tvoter: voter,\n\t\toption: option,\n\t}\n\treturn v\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "0BJCGBw14ylBlGFAGwGBdmmjmmEbEtesX4CAl7WbFR14Ua+pVZB5t7k+c7NwAZYfZJG7TugaZFeqerqa7QCQDA==" + } + ], + "memo": "" + } + }, + { + "tx": { + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1pz24uxleckl47xv44xqhrhu0hsv2m202cklegs", + "package": { + "name": "skiptime", + "path": "gno.land/r/x/skip_height_to_skip_time", + "files": [ + { + "name": "skiptime.gno", + "body": "package skiptime\n" + }, + { + "name": "skiptime_test.gno", + "body": "package skiptime\n\nimport (\n\t\"std\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestSkipHeights(t *testing.T) {\n\toldHeight := std.ChainHeight()\n\tshouldEQ(t, oldHeight, 123)\n\n\toldNow := time.Now().Unix()\n\tshouldEQ(t, oldNow, 1234567890)\n\n\t// skip 3 blocks == 15 seconds\n\ttesting.SkipHeights(3)\n\n\tshouldEQ(t, std.ChainHeight()-oldHeight, 3)\n\tshouldEQ(t, time.Now().Unix()-oldNow, 15)\n}\n\nfunc shouldEQ(t *testing.T, got, expected int64) {\n\tif got != expected {\n\t\tt.Fatalf(\"expected %d, got %d.\", expected, got)\n\t}\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "50000", + "gas_fee": "1000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeyEd25519", + "value": "KTARn+R6JwHwGuOlrIY0Z3BTjo8GWU6KpB35FeJEKcI=" + }, + "signature": "geSWZncso0/J42bUh3Omowi765iu/qplUaLc04ENYAp5REW67VC9vgQrDsQ8JFRLp7UFZNEg9fzwPsb5K4VdAQ==" + } + ], + "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 + } + }, + "bank": { + "params": { + "restricted_denoms": [] + } + }, + "vm": { + "params": { + "sysnames_pkgpath": "gno.land/r/sys/names", + "chain_domain": "gno.land" + }, + "realm_params": null + } + } +} \ No newline at end of file From acddec48960ec426edb1d4f212c695adcf59fbd8 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 13:27:22 +0200 Subject: [PATCH 31/67] chore: trigger codecov Signed-off-by: Norman From 9d50381a390b482728df1c86295ccd3092798e5d Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 13:41:02 +0200 Subject: [PATCH 32/67] chore: replace misleading comment Signed-off-by: Norman --- tm2/pkg/bft/backup/v1/reader.go | 2 +- tm2/pkg/bft/backup/v1/writer.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tm2/pkg/bft/backup/v1/reader.go b/tm2/pkg/bft/backup/v1/reader.go index 93cb84fe8e7..f84e320abc2 100644 --- a/tm2/pkg/bft/backup/v1/reader.go +++ b/tm2/pkg/bft/backup/v1/reader.go @@ -17,7 +17,7 @@ import ( type Reader = func(yield func(block *types.Block) error) error // WithReader creates a backup reader and pass it to the provided cb. -// It is process-safe but not thread-safe. +// yield should not be called concurently func WithReader(dir string, startHeight int64, endHeight int64, cb func(reader Reader) error) (resErr error) { dir = filepath.Clean(dir) diff --git a/tm2/pkg/bft/backup/v1/writer.go b/tm2/pkg/bft/backup/v1/writer.go index 91b0ad9aa7b..2b26844e942 100644 --- a/tm2/pkg/bft/backup/v1/writer.go +++ b/tm2/pkg/bft/backup/v1/writer.go @@ -21,7 +21,7 @@ const ( type Writer = func(block *types.Block) error // WithWriter creates a backup writer and pass it to the provided cb. -// It is process-safe but not thread-safe. +// write should not be called concurently func WithWriter(dir string, startHeightReq int64, endHeight int64, logger *zap.Logger, cb func(startHeight int64, write Writer) error) (retErr error) { if startHeightReq < 0 { return errors.New("start height request must be >= 0") From 18864ccb956f50b81a2fde9b0ecd376acbd504d2 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 14:00:56 +0200 Subject: [PATCH 33/67] chore: tm2bacup test Signed-off-by: Norman --- contribs/tm2backup/go.mod | 2 ++ contribs/tm2backup/main_test.go | 51 +++++++++++++++++++++++++++ gno.land/cmd/gnoland/start_test.go | 4 +++ tm2/pkg/bft/backup/backup_svc.go | 14 ++++++-- tm2/pkg/bft/backup/backup_svc_test.go | 5 +-- 5 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 contribs/tm2backup/main_test.go diff --git a/contribs/tm2backup/go.mod b/contribs/tm2backup/go.mod index 1784cf899e4..d34a2b35e23 100644 --- a/contribs/tm2backup/go.mod +++ b/contribs/tm2backup/go.mod @@ -25,8 +25,10 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect google.golang.org/protobuf v1.36.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contribs/tm2backup/main_test.go b/contribs/tm2backup/main_test.go new file mode 100644 index 00000000000..b5d89e75a9a --- /dev/null +++ b/contribs/tm2backup/main_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "net/http/httptest" + "os" + "testing" + + "github.com/gnolang/gno/tm2/pkg/bft/backup" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/require" +) + +func TestBackup(t *testing.T) { + store := &mockBlockStore{height: 5, blocks: map[int64]*types.Block{ + 1: {Header: types.Header{Height: 1}}, + 2: {Header: types.Header{Height: 2}}, + 3: {Header: types.Header{Height: 3}}, + 4: {Header: types.Header{Height: 4}}, + 5: {Header: types.Header{Height: 5}}, + }} + + mux := backup.NewMux(store) + srv := httptest.NewServer(mux) + defer srv.Close() + + io := commands.NewTestIO() + io.SetOut(os.Stdout) + io.SetErr(os.Stderr) + + err := newRootCmd(io).ParseAndRun(context.Background(), []string{ + "--remote", srv.URL, + }) + require.NoError(t, err) +} + +type mockBlockStore struct { + height int64 + blocks map[int64]*types.Block +} + +// Height implements blockStore. +func (m *mockBlockStore) Height() int64 { + return m.height +} + +// LoadBlock implements blockStore. +func (m *mockBlockStore) LoadBlock(height int64) *types.Block { + return m.blocks[height] +} diff --git a/gno.land/cmd/gnoland/start_test.go b/gno.land/cmd/gnoland/start_test.go index 2f620fcd360..c3502aa0c2c 100644 --- a/gno.land/cmd/gnoland/start_test.go +++ b/gno.land/cmd/gnoland/start_test.go @@ -162,3 +162,7 @@ func TestStart_Lazy(t *testing.T) { assert.FileExists(t, validatorStatePath) assert.FileExists(t, nodeKeyPath) } + +func TestCreateNode(t *testing.T) { + +} diff --git a/tm2/pkg/bft/backup/backup_svc.go b/tm2/pkg/bft/backup/backup_svc.go index c4da1a0339e..56817834ea3 100644 --- a/tm2/pkg/bft/backup/backup_svc.go +++ b/tm2/pkg/bft/backup/backup_svc.go @@ -29,11 +29,19 @@ type blockStore interface { LoadBlock(height int64) *types.Block } -func NewServer(conf *Config, store blockStore) *http.Server { +func NewBackupServiceHandler(store blockStore) (string, http.Handler) { backupServ := &backupServer{store: store} + return backuppbconnect.NewBackupServiceHandler(backupServ) +} + +func NewMux(store blockStore) *http.ServeMux { mux := http.NewServeMux() - path, handler := backuppbconnect.NewBackupServiceHandler(backupServ) - mux.Handle(path, handler) + mux.Handle(NewBackupServiceHandler(store)) + return mux +} + +func NewServer(conf *Config, store blockStore) *http.Server { + mux := NewMux(store) return &http.Server{Addr: conf.ListenAddress, Handler: h2c.NewHandler(mux, &http2.Server{}), ReadHeaderTimeout: time.Second * 5} } diff --git a/tm2/pkg/bft/backup/backup_svc_test.go b/tm2/pkg/bft/backup/backup_svc_test.go index b7ad1098588..c7d8a86f9ac 100644 --- a/tm2/pkg/bft/backup/backup_svc_test.go +++ b/tm2/pkg/bft/backup/backup_svc_test.go @@ -2,7 +2,6 @@ package backup import ( "context" - "net/http" "net/http/httptest" "testing" @@ -161,9 +160,7 @@ func TestStreamBlocks(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { store := tc.initStore() - svc := &backupServer{store: store} - mux := http.NewServeMux() - mux.Handle(backuppbconnect.NewBackupServiceHandler(svc)) + mux := NewMux(store) srv := httptest.NewServer(mux) defer srv.Close() httpClient := srv.Client() From 327fab5bc8b3e2f84b5e7d0829406f67607da5bc Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 14:03:46 +0200 Subject: [PATCH 34/67] chore: fmt Signed-off-by: Norman --- contribs/tm2backup/go.mod | 2 +- gno.land/cmd/gnoland/start_test.go | 1 - tm2/pkg/bft/backup/backuppb/backup.pb.go | 5 +++-- .../bft/backup/backuppb/backuppbconnect/backup.connect.go | 5 +++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/contribs/tm2backup/go.mod b/contribs/tm2backup/go.mod index d34a2b35e23..26008b8e6a8 100644 --- a/contribs/tm2backup/go.mod +++ b/contribs/tm2backup/go.mod @@ -9,6 +9,7 @@ replace github.com/gnolang/gno => ../.. require ( connectrpc.com/connect v1.18.1 github.com/gnolang/gno v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.10.0 go.uber.org/zap v1.27.0 ) @@ -21,7 +22,6 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/peterbourgon/ff/v3 v3.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.10.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.32.0 // indirect diff --git a/gno.land/cmd/gnoland/start_test.go b/gno.land/cmd/gnoland/start_test.go index c3502aa0c2c..057366d61ae 100644 --- a/gno.land/cmd/gnoland/start_test.go +++ b/gno.land/cmd/gnoland/start_test.go @@ -164,5 +164,4 @@ func TestStart_Lazy(t *testing.T) { } func TestCreateNode(t *testing.T) { - } diff --git a/tm2/pkg/bft/backup/backuppb/backup.pb.go b/tm2/pkg/bft/backup/backuppb/backup.pb.go index 84c67a40d1d..5b3cbd502cf 100644 --- a/tm2/pkg/bft/backup/backuppb/backup.pb.go +++ b/tm2/pkg/bft/backup/backuppb/backup.pb.go @@ -7,11 +7,12 @@ package backuppb import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go index f0afc1fe508..81ec75cdaa0 100644 --- a/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go +++ b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go @@ -5,12 +5,13 @@ package backuppbconnect import ( - connect "connectrpc.com/connect" context "context" errors "errors" - backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" http "net/http" strings "strings" + + connect "connectrpc.com/connect" + backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" ) // This is a compile-time assertion to ensure that this generated file and the connect package are From 73687c8c97f9331c2ee8d0856e29f98bbaf58abd Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 14:06:00 +0200 Subject: [PATCH 35/67] chore: regen Signed-off-by: Norman --- tm2/pkg/bft/backup/backuppb/backup.pb.go | 5 ++--- .../bft/backup/backuppb/backuppbconnect/backup.connect.go | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/tm2/pkg/bft/backup/backuppb/backup.pb.go b/tm2/pkg/bft/backup/backuppb/backup.pb.go index 5b3cbd502cf..84c67a40d1d 100644 --- a/tm2/pkg/bft/backup/backuppb/backup.pb.go +++ b/tm2/pkg/bft/backup/backuppb/backup.pb.go @@ -7,12 +7,11 @@ package backuppb import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" - - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go index 81ec75cdaa0..f0afc1fe508 100644 --- a/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go +++ b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go @@ -5,13 +5,12 @@ package backuppbconnect import ( + connect "connectrpc.com/connect" context "context" errors "errors" + backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" http "net/http" strings "strings" - - connect "connectrpc.com/connect" - backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" ) // This is a compile-time assertion to ensure that this generated file and the connect package are From 577a37729a28b4fe79e96142cce05e2133b41cf9 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 14:07:08 +0200 Subject: [PATCH 36/67] chore: run goimports on tm2 codegen Signed-off-by: Norman --- tm2/Makefile | 3 ++- tm2/pkg/bft/backup/backuppb/backup.pb.go | 5 +++-- .../bft/backup/backuppb/backuppbconnect/backup.connect.go | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tm2/Makefile b/tm2/Makefile index f2222aa5092..99f048df32a 100644 --- a/tm2/Makefile +++ b/tm2/Makefile @@ -67,4 +67,5 @@ _test.pkg.db:; go test $(GOTEST_FLAGS) ./pkg/db/... ./pkg/iavl/benchmarks/. .PHONY: generate generate: go generate -x ./... - $(MAKE) fmt \ No newline at end of file + $(MAKE) fmt + $(MAKE) imports \ No newline at end of file diff --git a/tm2/pkg/bft/backup/backuppb/backup.pb.go b/tm2/pkg/bft/backup/backuppb/backup.pb.go index 84c67a40d1d..5b3cbd502cf 100644 --- a/tm2/pkg/bft/backup/backuppb/backup.pb.go +++ b/tm2/pkg/bft/backup/backuppb/backup.pb.go @@ -7,11 +7,12 @@ package backuppb import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" unsafe "unsafe" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go index f0afc1fe508..81ec75cdaa0 100644 --- a/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go +++ b/tm2/pkg/bft/backup/backuppb/backuppbconnect/backup.connect.go @@ -5,12 +5,13 @@ package backuppbconnect import ( - connect "connectrpc.com/connect" context "context" errors "errors" - backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" http "net/http" strings "strings" + + connect "connectrpc.com/connect" + backuppb "github.com/gnolang/gno/tm2/pkg/bft/backup/backuppb" ) // This is a compile-time assertion to ensure that this generated file and the connect package are From 80032ba6c1c80ece44b7f0fa403aaa53fc530ebf Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 16:02:52 +0200 Subject: [PATCH 37/67] chore: test create node Signed-off-by: Norman --- gno.land/cmd/gnoland/start_test.go | 100 +++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/gno.land/cmd/gnoland/start_test.go b/gno.land/cmd/gnoland/start_test.go index 057366d61ae..2c59f959ce1 100644 --- a/gno.land/cmd/gnoland/start_test.go +++ b/gno.land/cmd/gnoland/start_test.go @@ -3,11 +3,15 @@ package main import ( "bytes" "context" + "flag" + "os" "path/filepath" + "runtime" "strings" "testing" "time" + "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -164,4 +168,100 @@ func TestStart_Lazy(t *testing.T) { } func TestCreateNode(t *testing.T) { + tcs := []struct { + name string + errContains string + args []string + prepare func(t *testing.T, dataDir string) + }{ + { + name: "lazy", + args: []string{ + "--lazy", + }, + }, + { + name: "err init logger", + errContains: "unable to initialize zap logger", + args: []string{ + "--log-level", "NOTEXIST", + }, + }, + { + name: "err no config", + errContains: "unable to load config", + }, + { + name: "err no genesis", + errContains: "missing genesis.json", + prepare: func(t *testing.T, dataDir string) { + confDir := filepath.Join(dataDir, "gnoland-data", "config") + require.NoError(t, os.MkdirAll(confDir, 0775)) + err := config.WriteConfigFile(filepath.Join(confDir, "config.toml"), config.DefaultConfig()) + require.NoError(t, err) + }, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + testDir := t.TempDir() + chtestdir(t, testDir) + + if tc.prepare != nil { + tc.prepare(t, testDir) + } + + cfg := &nodeCfg{} + + fset := flag.NewFlagSet("test", flag.PanicOnError) + cfg.RegisterFlags(fset) + + require.NoError(t, fset.Parse(tc.args)) + + io := commands.NewTestIO() + io.SetOut(os.Stdout) + io.SetErr(os.Stderr) + _, err := createNode(cfg, io) + if tc.errContains != "" { + require.ErrorContains(t, err, tc.errContains) + return + } + require.NoError(t, err) + }) + } +} + +func chtestdir(t *testing.T, dir string) { + oldwd, err := os.Open(".") + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + // On POSIX platforms, PWD represents “an absolute pathname of the + // current working directory.” Since we are changing the working + // directory, we should also set or update PWD to reflect that. + switch runtime.GOOS { + case "windows", "plan9": + // Windows and Plan 9 do not use the PWD variable. + default: + if !filepath.IsAbs(dir) { + dir, err = os.Getwd() + if err != nil { + t.Fatal(err) + } + } + t.Setenv("PWD", dir) + } + t.Cleanup(func() { + err := oldwd.Chdir() + oldwd.Close() + if err != nil { + // It's not safe to continue with tests if we can't + // get back to the original working directory. Since + // we are holding a dirfd, this is highly unlikely. + panic("testing.Chdir: " + err.Error()) + } + }) } From da9590126bce5589105722ac808f026150515355 Mon Sep 17 00:00:00 2001 From: Norman Date: Wed, 2 Apr 2025 16:05:34 +0200 Subject: [PATCH 38/67] chore: fmt and lint Signed-off-by: Norman --- gno.land/cmd/gnoland/start_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gno.land/cmd/gnoland/start_test.go b/gno.land/cmd/gnoland/start_test.go index 2c59f959ce1..9639b719c4d 100644 --- a/gno.land/cmd/gnoland/start_test.go +++ b/gno.land/cmd/gnoland/start_test.go @@ -195,8 +195,9 @@ func TestCreateNode(t *testing.T) { name: "err no genesis", errContains: "missing genesis.json", prepare: func(t *testing.T, dataDir string) { + t.Helper() confDir := filepath.Join(dataDir, "gnoland-data", "config") - require.NoError(t, os.MkdirAll(confDir, 0775)) + require.NoError(t, os.MkdirAll(confDir, 0o775)) err := config.WriteConfigFile(filepath.Join(confDir, "config.toml"), config.DefaultConfig()) require.NoError(t, err) }, @@ -232,6 +233,8 @@ func TestCreateNode(t *testing.T) { } func chtestdir(t *testing.T, dir string) { + t.Helper() + oldwd, err := os.Open(".") if err != nil { t.Fatal(err) From d476adf85dd82b13b7df55d1a503b42979553c96 Mon Sep 17 00:00:00 2001 From: Norman Date: Mon, 7 Apr 2025 15:28:57 +0200 Subject: [PATCH 39/67] chore: re-run flaky test Signed-off-by: Norman From c98822eacf986418207387c0d94a39cbb3ea71b3 Mon Sep 17 00:00:00 2001 From: Norman Date: Tue, 29 Apr 2025 15:48:35 +0200 Subject: [PATCH 40/67] chore: add reactor restore test in bft/blockchain package Signed-off-by: Norman --- tm2/pkg/bft/blockchain/reactor_test.go | 81 ++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tm2/pkg/bft/blockchain/reactor_test.go b/tm2/pkg/bft/blockchain/reactor_test.go index 5bc76aad3a5..68fbee63b04 100644 --- a/tm2/pkg/bft/blockchain/reactor_test.go +++ b/tm2/pkg/bft/blockchain/reactor_test.go @@ -25,6 +25,7 @@ import ( p2pTypes "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/gnolang/gno/tm2/pkg/testutils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var config *cfg.Config @@ -403,6 +404,86 @@ func TestBcStatusResponseMessageValidateBasic(t *testing.T) { } } +func TestRestore(t *testing.T) { + t.Parallel() + + config, _ = cfg.ResetTestRoot("blockchain_reactor_test") + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + + logger := log.NewNoopLogger() + + reactor := newBlockchainReactor(logger, genDoc, privVals, 0) + + stateDB := memdb.NewMemDB() + state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) + require.NoError(t, err) + + app := &testApp{} + cc := proxy.NewLocalClientCreator(app) + proxyApp := appconn.NewAppConns(cc) + require.NoError(t, proxyApp.Start()) + + // we generate blocks using another executor and then restore using the test reactor that has it's own executor + db := memdb.NewMemDB() + blockExec := sm.NewBlockExecutor(db, logger, proxyApp.Consensus(), mock.Mempool{}) + sm.SaveState(db, state) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var ( + lastBlock *types.Block + lastBlockMeta *types.BlockMeta + blockHeight int64 = 1 + ) + generateBlock := func() *types.Block { + t.Helper() + + lastCommit := types.NewCommit(types.BlockID{}, nil) + if blockHeight > 1 { + vote, err := types.MakeVote(lastBlock.Header.Height, lastBlockMeta.BlockID, state.Validators, privVals[0], lastBlock.Header.ChainID) + require.NoError(t, err) + voteCommitSig := vote.CommitSig() + lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{voteCommitSig}) + } + + thisBlock := makeBlock(blockHeight, state, lastCommit) + + thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) + blockID := types.BlockID{Hash: thisBlock.Hash(), PartsHeader: thisParts.Header()} + + state, err = blockExec.ApplyBlock(state, blockID, thisBlock) + require.NoError(t, err) + + lastBlock = thisBlock + lastBlockMeta = &types.BlockMeta{BlockID: blockID, Header: lastBlock.Header} + + blockHeight++ + + return thisBlock + } + + numBlocks := 50 + + err = reactor.reactor.Restore(ctx, func(yield func(block *types.Block) error) error { + for range numBlocks { + block := generateBlock() + + err := yield(block) + require.NoError(t, err) + + if blockHeight > 2 { + require.NotNil(t, reactor.reactor.store.LoadBlock(blockHeight-2)) + } + + require.Equal(t, int64(blockHeight-2), reactor.reactor.store.Height()) + } + return nil + }) + require.NoError(t, err) +} + // ---------------------------------------------- // utility funcs From faf78daa6ade4441fe1791c45ad76912fb760426 Mon Sep 17 00:00:00 2001 From: Norman Date: Tue, 29 Apr 2025 15:57:02 +0200 Subject: [PATCH 41/67] chore: remove unnecessary conversion Signed-off-by: Norman --- tm2/pkg/bft/blockchain/reactor_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm2/pkg/bft/blockchain/reactor_test.go b/tm2/pkg/bft/blockchain/reactor_test.go index 68fbee63b04..166b3246e71 100644 --- a/tm2/pkg/bft/blockchain/reactor_test.go +++ b/tm2/pkg/bft/blockchain/reactor_test.go @@ -477,7 +477,7 @@ func TestRestore(t *testing.T) { require.NotNil(t, reactor.reactor.store.LoadBlock(blockHeight-2)) } - require.Equal(t, int64(blockHeight-2), reactor.reactor.store.Height()) + require.Equal(t, blockHeight-2, reactor.reactor.store.Height()) } return nil }) From 207a0545ea4739fc2b3909b46fb19f19f5264616 Mon Sep 17 00:00:00 2001 From: Norman Date: Tue, 13 May 2025 21:41:27 +0200 Subject: [PATCH 42/67] chore: tidy Signed-off-by: Norman --- contribs/tm2backup/go.mod | 14 +++++++------- contribs/tm2backup/go.sum | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/contribs/tm2backup/go.mod b/contribs/tm2backup/go.mod index 26008b8e6a8..3cb2fc2f124 100644 --- a/contribs/tm2backup/go.mod +++ b/contribs/tm2backup/go.mod @@ -1,8 +1,8 @@ module github.com/gnolang/gno/contribs/tm2backup -go 1.23 +go 1.23.0 -toolchain go1.23.6 +toolchain go1.24.0 replace github.com/gnolang/gno => ../.. @@ -24,11 +24,11 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/term v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contribs/tm2backup/go.sum b/contribs/tm2backup/go.sum index 7df248a0223..f79492090fc 100644 --- a/contribs/tm2backup/go.sum +++ b/contribs/tm2backup/go.sum @@ -106,15 +106,15 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -125,15 +125,15 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From f6b8ef848d6dea574692ddf06595c378387cc5ef Mon Sep 17 00:00:00 2001 From: Norman Date: Tue, 13 May 2025 21:43:39 +0200 Subject: [PATCH 43/67] chore: tidy Signed-off-by: Norman --- misc/stress-test/stress-test-many-posts/go.mod | 2 +- misc/stress-test/stress-test-many-posts/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/misc/stress-test/stress-test-many-posts/go.mod b/misc/stress-test/stress-test-many-posts/go.mod index 0958adcb26e..a41fd2dd41b 100644 --- a/misc/stress-test/stress-test-many-posts/go.mod +++ b/misc/stress-test/stress-test-many-posts/go.mod @@ -5,7 +5,7 @@ go 1.23.0 toolchain go1.23.8 require ( - connectrpc.com/connect v1.16.2 + connectrpc.com/connect v1.18.1 github.com/gnolang/gnonative v1.8.1 ) diff --git a/misc/stress-test/stress-test-many-posts/go.sum b/misc/stress-test/stress-test-many-posts/go.sum index 5c3e56e98c3..a617eff86c8 100644 --- a/misc/stress-test/stress-test-many-posts/go.sum +++ b/misc/stress-test/stress-test-many-posts/go.sum @@ -1,5 +1,5 @@ -connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= -connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= connectrpc.com/grpchealth v1.3.0 h1:FA3OIwAvuMokQIXQrY5LbIy8IenftksTP/lG4PbYN+E= connectrpc.com/grpchealth v1.3.0/go.mod h1:3vpqmX25/ir0gVgW6RdnCPPZRcR6HvqtXX5RNPmDXHM= connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= From 5a84158863f644ae1ec897afb000c06a8905409f Mon Sep 17 00:00:00 2001 From: Norman Date: Sat, 17 May 2025 21:23:43 +0200 Subject: [PATCH 44/67] fix: update gnoland restore testdata Signed-off-by: Norman --- .../0000000000000000001.tm2blocks.tar.zst | Bin 526 -> 1250 bytes .../testdata/backup-2blocks/genesis.json | 4005 +++++++++-------- .../gnoland/testdata/backup-2blocks/info.json | 2 +- 3 files changed, 2123 insertions(+), 1884 deletions(-) diff --git a/gno.land/cmd/gnoland/testdata/backup-2blocks/0000000000000000001.tm2blocks.tar.zst b/gno.land/cmd/gnoland/testdata/backup-2blocks/0000000000000000001.tm2blocks.tar.zst index 6ce94067a12d57d83578602d69df49e7b90daba7..2f9f2d54db69414dfa8803f2b8e8d87713acdc8b 100644 GIT binary patch delta 1209 zcmV;q1V;Og1mX!UD77#BWB?zfCIECnF#s?yFfcYYG%zqQFfuncGB+^*ATR&`0CjV8 zVR8U43bT60t6xq2#ms@uK4!?LP!cAX*W5gM!rMxvv!Sf&;KU0*)o7N_%xU+Rhv)+R@L?X z5+n!$5+F?Wa*Sy-xLUWG&{W>v8{K!wGLAmJ@UJ&Sf4yR@EdH`eAaeM$W>yN{R0d{O zTu}sZQem!anZJzb2W4h*_8MN{ZE_%7gvm{vxA4j0+;3R2C|3j9G#ByDD_vVZXwF_l zcIx$&0tyny0SE#R0wO9hLO?O;#OS)OE$Y9ny_@_TBeNh*on6Zpbc_#3s1~gHmv7~4 z$uYa9e@X8t5}7TDGSQ2(_XDQHc>W!YWMbGUQ)UAIGcyJv3<#01p_7LLp~wka(KRJE ze(8{RWiMnn>(uvoJ48~>UL3j;n*@;?`4kr}RwJySx$^suh=LzIwI$>04F5LTQma|X z_e6=$N+6)4bBd~Z_j}#ojk$U!Ra($XZ*B>efAJ$w;k9)R9csP)G65%C&Y9Jd*dFjLd`rG&Bez3<#F!_WqRlkkBxupl%A& ze^1JDx`%1{ds0(|P7MZ`q0%h#{ymMc%c)5a!5RxdMfJ+Fq`#71k}xUEN8DU@=(#L= z>KXiFN+3Jy&yuyI*HPT9+S3@gn*08CGAg+S{y@*PshOO8uube|h9R78sg_IuH+#KW zxiG`Cw9B58EA+D-X*v{aUjYUx3pi__e|JxXMX*?*)ms6LH**RQF5@sE{7wua30Wo} z4w=s+kENWC;?nwdT&pmD*+sOy*1&LwocId_xsIoF0W~!WA`A$qnB&}s0({EaEIVa% z_5^I-+8FTsF~QUF==Q4pmYWdmlC$R(@2|zN4ixU*w$VP7L5@qkQMlMH(p)%Ve@gFK z=H4f;^rlK6EG*#98@1qUq(Dc6JC^sYl1zZlNqipBK$xPlS#zK8Fl|5JU95F(of@Bo|Qlrty2r5X%GvG|xySZ8yC9_OwwFZGOUcBl{-ij8Pv?88$ zc7dUQu|Ryt|FnUKS|h!n**1G9e|?#xzA6Y2)uo8^ybTTjN+2Ku0D#aqna>q~^ng%Q zuK{VAGY%dKn#IR#N}>UeUMP|Q685$)k17?^0ijl}0b`nT4;~7d#m8()q5+RyD3SpZ zEf+M8$|@-FqE?{+V}^5&8w!SX$6`vN0FPcMk^vI-wlB}BXh0_br`OXNPe6J?!PVm| zLE;?8HEJlg5}-pxBId{A4QW<5X=GJlj|R|yNoQo5DRUl7h&o8&V?Zz!^#km7!0(Wd XclNZwGq_%ic^vbXd8UsAK?T{Xb`vJ@ delta 479 zcmV<50U-Y3362CYD77#BWB?5X5CD`KF#s?yFfcYYG%zqQFfukb05>u>Hvk|o0001W zb97;H05AXwvXL$pE(-{Ur?kHY5V)4=K3X6vdL)_FUUF(v4 z!RA=f4aDW$BJqFC-OEy!B^cMzPYZG&-7r^OS5WHEeu8F{EAfmLeJ#>%AR=4s54LTd z?e4dWl>!P9$N>lf5CS49GD1N0OPj!mhn!m7)F zduC(3yF+u`L|hbEyP`-#1fT9{B zLX=wxMTe3kn;#E2axy@8AZcVk6~Lnbasvj^fssEznNtc|)H#Y8g9M22W