diff --git a/.benchmarks/README.md b/.benchmarks/README.md deleted file mode 100644 index 75cf1018025..00000000000 --- a/.benchmarks/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Benchmarks - -This folder is where benchmarks are configured to be added on the dashboard generated in [benchmarks](https://gnoland.github.io/benchmarks). - -We are using the [gobenchdata](https://github.com/bobheadxi/gobenchdata) GitHub action to run all our benchmarks and generate the graphs. Use its documentation if you need to do something more complicated than adding some benchmarks from a new package. - -We have two types of benchmarks; slow and fast ones. Slow ones can also be executed as checks on every PR. - -Now let's see how to add your tests to the generated benchmark graphs and also add as checks if they are fast enough on every PR: - -## Add new benchmarks to generated graphs. - -All benchmarks can be added to these graphs to keep track of the performance evolution on different parts of the code. This is done adding new lines on [gobenchdata-web.yml](https://github.com/gnolang/gno/blob/gh-benchmarks/gobenchdata-web.yml) - -This is eventually copied into [benchmark](https://github.com/gnolang/benchmarks/tree/gh-pages) gh-pages branch and it will be rendered [here](https://gnolang.github.io/benchmarks/). - -Things to take into account: - -- All benchmarks on a package will be shown on the same graph. -- The value on `package` and `benchmarks` are regular expressions. -- You have to explicitly add your new package here to make it appears on generated graphs. -- If you have benchmarks on the same package that takes much more time per op than the rest, you should divide it into a separate graph for visibility. In this example we can see how we separated tests from the gnolang package into the ones finishing with `Equality` and `LoopyMain`, because `LoopyMain` is taking an order of magnitude more time per operation than the other tests: -```yaml - - name: Equality benchmarks (gnovm) - benchmarks: [ '.Equality' ] - package: github.com\/gnolang\/gno\/gnovm\/pkg\/gnolang - - name: LoopyMain benchmarks (gnovm) - benchmarks: [ '.LoopyMain' ] - package: github.com\/gnolang\/gno\/gnovm\/pkg\/gnolang -``` - -## Add new checks for PRs - -If we want to add a new package to check all the fast benchmarks on it on every PR, we should have a look into [gobenchdata-checks.yml](./gobenchdata-checks.yml). diff --git a/.benchmarks/gobenchdata-checks.yml b/.benchmarks/gobenchdata-checks.yml deleted file mode 100755 index a0d760d3e4c..00000000000 --- a/.benchmarks/gobenchdata-checks.yml +++ /dev/null @@ -1,9 +0,0 @@ -checks: - - name: Benchmark regression checks on Ns per OP - description: |- - It checks speed per OP performance regressions. - package: . - benchmarks: [ '.' ] - diff: (current.NsPerOp - base.NsPerOp) / base.NsPerOp * 100 - thresholds: - max: 10 \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index a45b7bafa98..3536640b4d7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,9 @@ .dockerignore build/ Dockerfile -misc/ +misc/* +!misc/loop/ +!misc/autocounterd/ docker-compose.yml tests/docker-integration/ diff --git a/.gitattributes b/.gitattributes index c22d136ec50..13825940056 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,9 @@ *.gno linguist-language=Go *.pb.go linguist-generated merge=ours -diff go.sum linguist-generated text -gnovm/stdlibs/native.go linguist-generated -gnovm/tests/stdlibs/native.go linguist-generated +gnovm/stdlibs/generated.go linguist-generated +gnovm/tests/stdlibs/generated.go linguist-generated +*.gen.gno linguist-generated +*.gen_test.gno linguist-generated +*.gen.go linguist-generated +*.gen_test.go linguist-generated \ No newline at end of file diff --git a/.github/.editorconfig b/.github/.editorconfig new file mode 100644 index 00000000000..751cd705457 --- /dev/null +++ b/.github/.editorconfig @@ -0,0 +1,8 @@ +# Make sure this is the top-level editorconfig +# https://editorconfig.org/ +root = true + +# GitHub Actions Workflows +[workflows/**.yml] +indent_style = space +indent_size = 2 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index f13ce49ef45..00000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,95 +0,0 @@ -# CODEOWNERS: https://help.github.com/articles/about-codeowners/ - -# Primary repo maintainers. -* @gnolang/tech-staff - -# Tendermint2. -/tm2/ @jaekwon @moul @piux2 @zivkovicmilos -/tm2/pkg/crypto/ @jaekwon @moul @gnolang/security -/tm2/pkg/crypto/keys/client/ @jaekwon @gnolang/security -/tm2/pkg/db/ @ajnavarro -# TODO: add per package exceptions -# ... - -# Docs & Content. -/docs/ @moul -/docs/**.md @gnolang/devrels -/docs/**.gif @gnolang/devrels -/docs/Makefile @gnolang/devrels -/README.md @moul @gnolang/devrels -/**/README.md @gnolang/devrels -/.gitpod.yml @gnolang/devrels - -# Gno examples and default contracts. -/examples/ @gnolang/tech-staff @gnolang/devrels -/examples/gno.land/p/demo/ @gnolang/tech-staff @gnolang/devrels -/examples/gno.land/p/demo/avl/ @jaekwon -/examples/gno.land/p/demo/bf/ @moul -/examples/gno.land/p/demo/blog/ @gnolang/devrels -/examples/gno.land/p/demo/cford32/ @thehowl -/examples/gno.land/p/demo/memeland/ @leohhhn -/examples/gno.land/p/demo/seqid/ @thehowl -/examples/gno.land/p/demo/ownable/ @leohhhn -/examples/gno.land/p/demo/pausable/ @leohhhn -/examples/gno.land/p/demo/svg/ @moul -/examples/gno.land/p/demo/tamagotchi/ @moul -/examples/gno.land/p/demo/ui/ @moul -/examples/gno.land/r/demo/ @gnolang/tech-staff @gnolang/devrels -/examples/gno.land/r/demo/art/ @moul -/examples/gno.land/r/demo/memeland/ @leohhhn -/examples/gno.land/r/demo/tamagotchi/ @moul -/examples/gno.land/r/demo/userbook/ @leohhhn -/examples/gno.land/r/gnoland/ @moul -/examples/gno.land/r/sys/ @moul -/examples/gno.land/r/jaekwon/ @jaekwon -/examples/gno.land/r/manfred/ @moul - -# Gno.land. -/gno.land/ @moul @zivkovicmilos -/gno.land/cmd/genesis/ @zivkovicmilos -/gno.land/cmd/gnokey/ @jaekwon @moul @gfanton -/gno.land/cmd/gnoland/ @zivkovicmilos @gnolang/devops -/gno.land/cmd/gnoweb/ @gfanton @thehowl -/gno.land/pkg/gnoclient/ @zivkovicmilos @leohhhn @gfanton -/gno.land/pkg/gnoland/ @zivkovicmilos @gfanton -/gno.land/pkg/keyscli/ @jaekwon @moul @gfanton -/gno.land/pkg/log/ @zivkovicmilos @gfanton -/gno.land/pkg/sdk/vm/ @moul @gfanton @thehowl -/gno.land/pkg/integration/ @gfanton -/gno.land/genesis/ @moul -#... - -# GnoVM/Gnolang. -/gnovm/ @jaekwon @moul @piux2 @thehowl -/gnovm/stdlibs/ @thehowl -/gnovm/tests/ @jaekwon @deelawn @thehowl @mvertes -/gnovm/cmd/gno/ @moul @thehowl -/gnovm/pkg/gnolang/ @jaekwon @moul @piux2 @deelawn -/gnovm/pkg/doc/ @thehowl -/gnovm/pkg/repl/ @mvertes @ajnavarro -/gnovm/pkg/gnomod/ @thehowl -/gnovm/pkg/gnoenv/ @gfanton -/gnovm/pkg/transpiler/ @thehowl -/gnovm/pkg/integration/ @gfanton - -# Contribs -/contribs/ @gnolang/tech-staff -/contribs/gnodev/ @gfanton -/contribs/gnokeykc/ @moul -/contribs/gnomd/ @moul - -# Misc -/misc/ @gnolang/tech-staff -/misc/loop/ @moul @gnolang/devops -/misc/deployments/ @moul @gnolang/devops -/misc/genstd/ @thehowl - -# Special files. -/PLAN.md @jaekwon @moul -/PHILOSOPHY.md @jaekwon -/CONTRIBUTING.md @jaekwon @moul @gnolang/tech-staff -/LICENSE.md @jaekwon -/.github/ @moul @gnolang/tech-staff -/.github/workflows @ajnavarro @moul -/.github/CODEOWNERS @jaekwon @moul -/go.mod @gnolang/tech-staff # no unnecessary dependencies diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.md b/.github/ISSUE_TEMPLATE/BUG-REPORT.md index 70a20a4c47e..a63b450d678 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.md @@ -1,6 +1,7 @@ --- name: Bug Report Template about: Create a bug report +labels: "🐞 bug" # NOTE: keep in sync with gnovm/cmd/gno/bug.go --- diff --git a/.github/codecov.yml b/.github/codecov.yml index ea1c701d946..f0cb9583cf2 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -4,7 +4,7 @@ codecov: wait_for_ci: true comment: - require_changes: false + require_changes: true coverage: round: down @@ -13,7 +13,7 @@ coverage: project: default: target: auto - threshold: 10 # Let's decrease this later. + threshold: 5 # Let's decrease this later. base: parent if_no_uploads: error if_not_found: success @@ -22,12 +22,12 @@ coverage: patch: default: target: auto - threshold: 10 # Let's decrease this later. + threshold: 5 # Let's decrease this later. base: auto if_no_uploads: error if_not_found: success if_ci_failed: error - only_pulls: false + only_pulls: true # Only check patch coverage on PRs flag_management: default_rules: @@ -39,3 +39,8 @@ flag_management: - type: patch target: auto # Let's decrease this later. threshold: 10 + +ignore: + - "gnovm/stdlibs/generated.go" + - "gnovm/tests/stdlibs/generated.go" + - "**/*.pb.go" diff --git a/.github/golangci.yml b/.github/golangci.yml index e78d09a582e..afc581d2ec5 100644 --- a/.github/golangci.yml +++ b/.github/golangci.yml @@ -28,6 +28,7 @@ linters: - misspell # Misspelled English words in comments - makezero # Finds slice declarations with non-zero initial length - importas # Enforces consistent import aliases + - govet # same as 'go vet' - gosec # Security problems - gofmt # Whether the code was gofmt-ed - goimports # Unused imports @@ -43,17 +44,21 @@ linters: linters-settings: gofmt: simplify: true + goconst: min-len: 3 min-occurrences: 3 + gosec: excludes: - G204 # Subprocess launched with a potential tainted input or cmd arguments - G306 # Expect WriteFile permissions to be 0600 or less + - G115 # Integer overflow conversion, no solution to check the overflow in time of convert, so linter shouldn't check the overflow. stylecheck: checks: [ "all", "-ST1022", "-ST1003" ] errorlint: asserts: false + gocritic: enabled-tags: - diagnostic @@ -61,6 +66,7 @@ linters-settings: - opinionated - performance - style + forbidigo: forbid: - p: '^regexp\.(Match|MatchString)$' @@ -72,12 +78,14 @@ issues: max-same-issues: 0 new: false fix: false + exclude-rules: - path: _test\.go linters: - gosec # Disabled linting of weak number generators - makezero # Disabled linting of intentional slice appends - goconst # Disabled linting of common mnemonics and test case strings + - unused # Disabled linting of unused mock methods - path: _\.gno linters: - errorlint # Disabled linting of error comparisons, because of lacking std lib support diff --git a/.github/goreleaser-master.yaml b/.github/goreleaser-master.yaml deleted file mode 100644 index bca52615db8..00000000000 --- a/.github/goreleaser-master.yaml +++ /dev/null @@ -1,503 +0,0 @@ -project_name: gno - -before: - hooks: - - go mod tidy - -builds: - - id: gno - main: ./gnovm/cmd/gno - binary: gno - env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - goarch: - - amd64 - - arm64 - - arm - goarm: - - 6 - - 7 - - id: gnoland - main: ./gno.land/cmd/gnoland - binary: gnoland - env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - goarch: - - amd64 - - arm64 - - arm - goarm: - - 6 - - 7 - - id: gnokey - main: ./gno.land/cmd/gnokey - binary: gnokey - env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - goarch: - - amd64 - - arm64 - - arm - goarm: - - 6 - - 7 - - id: gnoweb - main: ./gno.land/cmd/gnoweb - binary: gnoweb - env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - goarch: - - amd64 - - arm64 - - arm - goarm: - - 6 - - 7 -gomod: - proxy: true - -archives: - # https://goreleaser.com/customization/archive/ - - files: - # Standard Release Files - - LICENSE.md - - README.md - -signs: - - cmd: cosign - env: - - COSIGN_EXPERIMENTAL=1 - certificate: "${artifact}.pem" - args: - - sign-blob - - "--output-certificate=${certificate}" - - "--output-signature=${signature}" - - "${artifact}" - - "--yes" # needed on cosign 2.0.0+ - artifacts: checksum - output: true - -dockers: - # https://goreleaser.com/customization/docker/ - - # gno - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: amd64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64" - - "ghcr.io/gnolang/{{ .ProjectName }}:master-amd64" - build_flag_templates: - - "--target=gno" - - "--platform=linux/amd64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gno - extra_files: - - examples - - gnovm/stdlibs - - gnovm/tests/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8" - - "ghcr.io/gnolang/{{ .ProjectName }}:master-arm64v8" - build_flag_templates: - - "--target=gno" - - "--platform=linux/arm64/v8" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gno - extra_files: - - examples - - gnovm/stdlibs - - gnovm/tests/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}:master-armv6" - build_flag_templates: - - "--target=gno" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gno - extra_files: - - examples - - gnovm/stdlibs - - gnovm/tests/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}:master-armv7" - build_flag_templates: - - "--target=gno" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gno - extra_files: - - examples - - gnovm/stdlibs - - gnovm/tests/stdlibs - - # gnoland - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: amd64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-amd64" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:master-amd64" - build_flag_templates: - - "--target=gnoland" - - "--platform=linux/amd64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoland - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-arm64v8" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:master-arm64v8" - build_flag_templates: - - "--target=gnoland" - - "--platform=linux/arm64/v8" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoland - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:master-armv6" - build_flag_templates: - - "--target=gnoland" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoland - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:master-armv7" - build_flag_templates: - - "--target=gnoland" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoland - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - # gnokey - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: amd64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-amd64" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:master-amd64" - build_flag_templates: - - "--target=gnokey" - - "--platform=linux/amd64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnokey - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-arm64v8" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:master-arm64v8" - build_flag_templates: - - "--target=gnokey" - - "--platform=linux/arm64/v8" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnokey - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:master-armv6" - build_flag_templates: - - "--target=gnokey" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnokey - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:master-armv7" - build_flag_templates: - - "--target=gnokey" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnokey - - # gnoweb - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: amd64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-amd64" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:master-amd64" - build_flag_templates: - - "--target=gnoweb" - - "--platform=linux/amd64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoweb - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-arm64v8" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:master-arm64v8" - build_flag_templates: - - "--target=gnoweb" - - "--platform=linux/arm64/v8" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoweb - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:master-armv6" - build_flag_templates: - - "--target=gnoweb" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoweb - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:master-armv7" - build_flag_templates: - - "--target=gnoweb" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoweb - -docker_manifests: - # https://goreleaser.com/customization/docker_manifest/ - - # gno - - name_template: ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv7 - - name_template: ghcr.io/gnolang/{{ .ProjectName }}:master - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}:master-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}:master-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}:master-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}:master-armv7 - - # gnoland - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv7 - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoland:master - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:master-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:master-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:master-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:master-armv7 - - # gnokey - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv7 - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnokey:master - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:master-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:master-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:master-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:master-armv7 - - # gnoweb - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv7 - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:master - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:master-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:master-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:master-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:master-armv7 - -docker_signs: - - cmd: cosign - env: - - COSIGN_EXPERIMENTAL=1 - artifacts: images - output: true - args: - - "sign" - - "${artifact}" - - "--yes" # needed on cosign 2.0.0+ - -checksum: - name_template: "checksums.txt" - -changelog: - sort: asc - -source: - enabled: true - -sboms: - - artifacts: archive - - id: source # Two different sbom configurations need two different IDs - artifacts: source - -release: - draft: true - replace_existing_draft: true - prerelease: auto - make_latest: false - mode: append - footer: | - ### Container Images - - You can find all docker images at: - - https://github.com/orgs/gnolang/packages?repo_name={{ .ProjectName }} - -nightly: - tag_name: master - publish_release: true - keep_single_release: true - name_template: "{{ incpatch .Version }}-{{ .ShortCommit }}-master" \ No newline at end of file diff --git a/.github/goreleaser-nightly.yaml b/.github/goreleaser-nightly.yaml deleted file mode 100644 index 3dac915b7cd..00000000000 --- a/.github/goreleaser-nightly.yaml +++ /dev/null @@ -1,502 +0,0 @@ -project_name: gno - -before: - hooks: - - go mod tidy - -builds: - - id: gno - main: ./gnovm/cmd/gno - binary: gno - env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - goarch: - - amd64 - - arm64 - - arm - goarm: - - 6 - - 7 - - id: gnoland - main: ./gno.land/cmd/gnoland - binary: gnoland - env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - goarch: - - amd64 - - arm64 - - arm - goarm: - - 6 - - 7 - - id: gnokey - main: ./gno.land/cmd/gnokey - binary: gnokey - env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - goarch: - - amd64 - - arm64 - - arm - goarm: - - 6 - - 7 - - id: gnoweb - main: ./gno.land/cmd/gnoweb - binary: gnoweb - env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - goarch: - - amd64 - - arm64 - - arm - goarm: - - 6 - - 7 -gomod: - proxy: true - -archives: - # https://goreleaser.com/customization/archive/ - - files: - # Standard Release Files - - LICENSE.md - - README.md - -signs: - - cmd: cosign - env: - - COSIGN_EXPERIMENTAL=1 - certificate: "${artifact}.pem" - args: - - sign-blob - - "--output-certificate=${certificate}" - - "--output-signature=${signature}" - - "${artifact}" - - "--yes" # needed on cosign 2.0.0+ - artifacts: checksum - output: true - -dockers: - # https://goreleaser.com/customization/docker/ - - # gno - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: amd64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64" - - "ghcr.io/gnolang/{{ .ProjectName }}:nightly-amd64" - build_flag_templates: - - "--target=gno" - - "--platform=linux/amd64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gno - extra_files: - - examples - - gnovm/stdlibs - - gnovm/tests/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8" - - "ghcr.io/gnolang/{{ .ProjectName }}:nightly-arm64v8" - build_flag_templates: - - "--target=gno" - - "--platform=linux/arm64/v8" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gno - extra_files: - - examples - - gnovm/stdlibs - - gnovm/tests/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}:nightly-armv6" - build_flag_templates: - - "--target=gno" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gno - extra_files: - - examples - - gnovm/stdlibs - - gnovm/tests/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}:nightly-armv7" - build_flag_templates: - - "--target=gno" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gno - extra_files: - - examples - - gnovm/stdlibs - - gnovm/tests/stdlibs - - # gnoland - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: amd64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-amd64" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-amd64" - build_flag_templates: - - "--target=gnoland" - - "--platform=linux/amd64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoland - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-arm64v8" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-arm64v8" - build_flag_templates: - - "--target=gnoland" - - "--platform=linux/arm64/v8" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoland - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-armv6" - build_flag_templates: - - "--target=gnoland" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoland - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-armv7" - build_flag_templates: - - "--target=gnoland" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoland" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoland - extra_files: - - gno.land/genesis/genesis_balances.txt - - gno.land/genesis/genesis_txs.jsonl - - examples - - gnovm/stdlibs - # gnokey - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: amd64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-amd64" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-amd64" - build_flag_templates: - - "--target=gnokey" - - "--platform=linux/amd64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnokey - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-arm64v8" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-arm64v8" - build_flag_templates: - - "--target=gnokey" - - "--platform=linux/arm64/v8" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnokey - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-armv6" - build_flag_templates: - - "--target=gnokey" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnokey - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-armv7" - build_flag_templates: - - "--target=gnokey" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnokey" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnokey - - # gnoweb - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: amd64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-amd64" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-amd64" - build_flag_templates: - - "--target=gnoweb" - - "--platform=linux/amd64" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoweb - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm64 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-arm64v8" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-arm64v8" - build_flag_templates: - - "--target=gnoweb" - - "--platform=linux/arm64/v8" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoweb - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-armv6" - build_flag_templates: - - "--target=gnoweb" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoweb - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-armv7" - build_flag_templates: - - "--target=gnoweb" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnoweb" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnoweb - -docker_manifests: - # https://goreleaser.com/customization/docker_manifest/ - - # gno - - name_template: ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Version }}-armv7 - - name_template: ghcr.io/gnolang/{{ .ProjectName }}:nightly - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}:nightly-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}:nightly-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}:nightly-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}:nightly-armv7 - - # gnoland - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Version }}-armv7 - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:nightly-armv7 - - # gnokey - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Version }}-armv7 - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:nightly-armv7 - - # gnoweb - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }} - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }}-armv7 - - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly - image_templates: - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-amd64 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:nightly-armv7 - -docker_signs: - - cmd: cosign - env: - - COSIGN_EXPERIMENTAL=1 - artifacts: images - output: true - args: - - "sign" - - "${artifact}" - - "--yes" # needed on cosign 2.0.0+ - -checksum: - name_template: "checksums.txt" - -changelog: - sort: asc - -source: - enabled: true - -sboms: - - artifacts: archive - - id: source # Two different sbom configurations need two different IDs - artifacts: source - -release: - draft: true - replace_existing_draft: true - prerelease: auto - mode: append - footer: | - ### Container Images - - You can find all docker images at: - - https://github.com/orgs/gnolang/packages?repo_name={{ .ProjectName }} - -nightly: - tag_name: nightly - publish_release: true - keep_single_release: true - name_template: "{{ incpatch .Version }}-{{ .ShortCommit }}-nightly" \ No newline at end of file diff --git a/.github/goreleaser.yaml b/.github/goreleaser.yaml index 1984493d36f..71a8ba98745 100644 --- a/.github/goreleaser.yaml +++ b/.github/goreleaser.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json project_name: gno version: 2 @@ -24,8 +25,8 @@ builds: - arm64 - arm goarm: - - 6 - - 7 + - "6" + - "7" - id: gnoland main: ./gno.land/cmd/gnoland binary: gnoland @@ -39,8 +40,8 @@ builds: - arm64 - arm goarm: - - 6 - - 7 + - "6" + - "7" - id: gnokey main: ./gno.land/cmd/gnokey binary: gnokey @@ -54,8 +55,8 @@ builds: - arm64 - arm goarm: - - 6 - - 7 + - "6" + - "7" - id: gnoweb main: ./gno.land/cmd/gnoweb binary: gnoweb @@ -69,8 +70,8 @@ builds: - arm64 - arm goarm: - - 6 - - 7 + - "6" + - "7" - id: gnofaucet dir: ./contribs/gnofaucet binary: gnofaucet @@ -84,8 +85,40 @@ builds: - arm64 - arm goarm: - - 6 - - 7 + - "6" + - "7" + # Gno Contribs + # NOTE: Contribs binary will be added in a single docker image below: gnocontribs + - id: gnobro + dir: ./contribs/gnodev/cmd/gnobro + binary: gnobro + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - "6" + - "7" + - id: gnogenesis + dir: ./contribs/gnogenesis + binary: gnogenesis + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - "6" + - "7" gomod: proxy: true @@ -285,6 +318,7 @@ dockers: - gno.land/genesis/genesis_txs.jsonl - examples - gnovm/stdlibs + # gnokey - use: buildx dockerfile: Dockerfile.release @@ -489,6 +523,98 @@ dockers: ids: - gnofaucet + # gnocontribs + - use: buildx + dockerfile: Dockerfile.release + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-amd64" + build_flag_templates: + - "--target=gnocontribs" + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + - use: buildx + dockerfile: Dockerfile.release + goos: linux + goarch: arm64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-arm64v8" + build_flag_templates: + - "--target=gnocontribs" + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + - use: buildx + dockerfile: Dockerfile.release + goos: linux + goarch: arm + goarm: 6 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv6" + build_flag_templates: + - "--target=gnocontribs" + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + - use: buildx + dockerfile: Dockerfile.release + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv7" + build_flag_templates: + - "--target=gnocontribs" + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnocontribs" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnobro + - gnogenesis + extra_files: + - gno.land/genesis/genesis_balances.txt + - gno.land/genesis/genesis_txs.jsonl + - examples + - gnovm/stdlibs + docker_manifests: # https://goreleaser.com/customization/docker_manifest/ @@ -533,7 +659,7 @@ docker_manifests: - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }}-arm64v8 - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }}-armv6 - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }}-armv7 - + # gnoweb - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Version }} image_templates: @@ -562,6 +688,20 @@ docker_manifests: - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv6 - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv7 + # gnocontribs + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnocontribs:{{ .Env.TAG_VERSION }}-armv7 + docker_signs: - cmd: cosign env: @@ -606,4 +746,8 @@ nightly: tag_name: nightly publish_release: true keep_single_release: true - name_template: "{{ incpatch .Version }}-{{ .ShortCommit }}-{{ .Env.TAG_VERSION }}" + version_template: "{{ incpatch .Version }}-{{ .ShortCommit }}-{{ .Env.TAG_VERSION }}" + +git: + ignore_tag_prefixes: + - "chain/" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index d76f68cba5d..00000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,12 +0,0 @@ - - -
Contributors' checklist... - -- [ ] Added new tests, or not needed, or not feasible -- [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory -- [ ] Updated the official documentation or not needed -- [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description -- [ ] Added references to related issues and PRs -- [ ] Provided any useful hints for running manual tests -- [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md). -
diff --git a/.github/workflows/auto-author-assign.yml b/.github/workflows/auto-author-assign.yml index 06dfb4ab903..890e70da9ae 100644 --- a/.github/workflows/auto-author-assign.yml +++ b/.github/workflows/auto-author-assign.yml @@ -1,4 +1,4 @@ -name: auto-author-assign +name: Auto Assign PR Author on: pull_request_target: diff --git a/.github/workflows/autocounterd.yml b/.github/workflows/autocounterd.yml index 66aced0d89c..dcba56178bd 100644 --- a/.github/workflows/autocounterd.yml +++ b/.github/workflows/autocounterd.yml @@ -1,15 +1,13 @@ -name: autocounterd +name: Portal Loop - autocounterd on: push: + branches: + - "master" paths: - misc/autocounterd + - misc/loop - .github/workflows/autocounterd.yml - branches: - - "master" - - "misc/autocounterd" - tags: - - "v*" permissions: contents: read @@ -41,7 +39,6 @@ jobs: - name: Build and push uses: docker/build-push-action@v6 with: - context: ./misc/autocounterd push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/benchmark-check.yml b/.github/workflows/benchmark-check.yml deleted file mode 100644 index 9009f23f80e..00000000000 --- a/.github/workflows/benchmark-check.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: run benchmarks on every PR - -on: - pull_request: - -jobs: - check: - uses: ./.github/workflows/benchmark_template.yml - secrets: inherit - with: - publish: false - test-flags: "-short" \ No newline at end of file diff --git a/.github/workflows/benchmark-master-push.yml b/.github/workflows/benchmark-master-push.yml new file mode 100644 index 00000000000..1c054077a3a --- /dev/null +++ b/.github/workflows/benchmark-master-push.yml @@ -0,0 +1,70 @@ +name: Run and Save Benchmarks + +on: + push: + branches: + - master + paths: + - contribs/**/*.go + - gno.land/**/*.go + - gnovm/**/*.go + - tm2/**/*.go + +permissions: + # deployments permission to deploy GitHub pages website + deployments: write + # contents permission to update benchmark contents in gh-pages branch + contents: write + +env: + CGO_ENABLED: 0 + +jobs: + benchmarks: + if: ${{ github.repository == 'gnolang/gno' }} + runs-on: [ self-hosted, Linux, X64, benchmarks ] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run benchmark + # add more benchmarks by adding additional lines for different packages; + # or modify the -bench regexp. + run: | + set -xeuo pipefail && ( + go test ./gnovm/pkg/gnolang -bench='BenchmarkBenchdata' -benchmem -run='^$' -v -cpu=1,2 + ) | tee benchmarks.txt + + - name: Download previous benchmark data + uses: actions/cache@v4 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + + - name: Store benchmark results into `gh-benchmarks` branch + uses: benchmark-action/github-action-benchmark@v1 + # see https://github.com/benchmark-action/github-action-benchmark?tab=readme-ov-file#action-inputs + with: + name: Go Benchmarks + tool: "go" + output-file-path: benchmarks.txt + max-items-in-chart: 100 + # Show alert with commit comment on detecting possible performance regression + alert-threshold: "120%" + fail-on-alert: false + comment-on-alert: true + alert-comment-cc-users: "@ajnavarro,@thehowl,@zivkovicmilos" + # Enable Job Summary for PRs + summary-always: true + github-token: ${{ secrets.GITHUB_TOKEN }} + # NOTE you need to use a separate GITHUB PAT token that has a write access to the specified repository. + # gh-repository: 'github.com/gnolang/benchmarks' # on gh-pages branch + gh-pages-branch: gh-benchmarks + benchmark-data-dir-path: . + auto-push: true diff --git a/.github/workflows/benchmark-publish.yml b/.github/workflows/benchmark-publish.yml deleted file mode 100644 index 8baa4c7889b..00000000000 --- a/.github/workflows/benchmark-publish.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: run benchmarks on main branch every day - -on: - workflow_dispatch: - schedule: - - cron: '0 0 * * *' # run on default branch every day -jobs: - publish: - uses: ./.github/workflows/benchmark_template.yml - secrets: inherit - with: - publish: true - test-flags: "-timeout 50m" \ No newline at end of file diff --git a/.github/workflows/benchmark_template.yml b/.github/workflows/benchmark_template.yml deleted file mode 100644 index bdd3d607ca3..00000000000 --- a/.github/workflows/benchmark_template.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: benchmarks -on: - workflow_call: - inputs: - publish: - required: true - type: boolean - test-flags: - required: true - type: string - -env: - CGO_ENABLED: 0 - -jobs: - benchmarks: - if: ${{ github.repository == 'gnolang/gno' }} - runs-on: [self-hosted, Linux, X64, benchmark-v1] - steps: - - name: checkout - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - uses: actions/setup-go@v5 - with: - go-version: "1.22.x" - - name: "gobenchdata publish: ${{ inputs.publish }}" - run: go run go.bobheadxi.dev/gobenchdata@v1 action - env: - INPUT_PRUNE_COUNT: 30 - INPUT_GO_TEST_FLAGS: "${{ inputs.test-flags }} -run=^$ -cpu 1,2" # executing only using one and two CPUs to not be dependant on the machine cores. - INPUT_PUBLISH: ${{ inputs.publish }} - INPUT_PUBLISH_BRANCH: gh-benchmarks - INPUT_BENCHMARKS_OUT: benchmarks.json - INPUT_CHECKS: ${{ !inputs.publish }} - INPUT_CHECKS_CONFIG: .benchmarks/gobenchdata-checks.yml diff --git a/.github/workflows/bot-proxy.yml b/.github/workflows/bot-proxy.yml new file mode 100644 index 00000000000..9bef0630d32 --- /dev/null +++ b/.github/workflows/bot-proxy.yml @@ -0,0 +1,48 @@ +# This workflow must be kept in sync to some extent with bot.yml +name: GitHub Bot Proxy + +on: + # Watch for any completed run on bot.yml workflow + workflow_run: + workflows: [GitHub Bot] + types: [completed] + +jobs: + # This workflow monitors any run completed on the GitHub Bot workflow and + # checks if the event that triggered it is limited to read-only permissions + # (e.g 'pull_request_review' on a pull request opened from a fork). + # In this case, it reruns the GitHub Bot workflow using a 'workflow_dispatch' + # event, thereby allowing it to run with write permissions. + # + # Complete flow: + # 'pull_request_review' from fork on bot.yml (read-only) -> 'workflow_run' on bot-proxy.yml (write) -> 'workflow_dispatch' on bot.yml (write) + rerun-with-write-perm: + name: Rerun Bot with write permission + # Skip this workflow if the original event is not 'pull_request_review' + if: github.event.workflow_run.event == 'pull_request_review' + runs-on: ubuntu-latest + permissions: + actions: write + + steps: + - name: Download artifact from previous run + uses: actions/download-artifact@v4 + with: + name: pr-number + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + # Even if the artifact doesn't exist, do not mark the workflow as failed + # Useful if the 'pull_request_review' event was emitted by a PR opened + # from a branch on the main repo, so it has already been processed by + # the bot workflow, and no artifact has been uploaded. + continue-on-error: true + id: download + + - name: Send workflow_dispatch event to Github Bot + # Run only if an artifact was downloaded + if: steps.download.outcome == 'success' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.event.workflow_run.repository.full_name }} + run: | + gh workflow run bot.yml -R "$REPO" -f "pull-request-list=$(cat pr-number)" diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml new file mode 100644 index 00000000000..add800fe2bf --- /dev/null +++ b/.github/workflows/bot.yml @@ -0,0 +1,123 @@ +# This workflow must be kept in sync to some extent with bot-proxy.yml +name: GitHub Bot + +on: + # Watch for changes on PR state, assignees, labels, head branch and draft/ready status + pull_request_target: + types: + - assigned + - unassigned + - labeled + - unlabeled + - opened + - reopened + - synchronize # PR head updated + - converted_to_draft + - ready_for_review + + # Watch for changes on PR reviews + pull_request_review: + types: [submitted, edited, dismissed] + + # Watch for changes on PR comment + issue_comment: + types: [created, edited, deleted] + + # Manual run from GitHub Actions interface + workflow_dispatch: + inputs: + pull-request-list: + description: "PR(s) to process: specify 'all' or a comma separated list of PR numbers, e.g. '42,1337,7890'" + required: true + default: all + type: string + +jobs: + # This job creates a matrix of PR numbers based on the inputs from the various + # events that can trigger this workflow so that the process-pr job below can + # handle the parallel processing of the pull-requests + define-prs-matrix: + name: Define PRs matrix + # Skip this workflow if: + # - the bot is retriggering itself + # - the event is emitted by codecov + # - the event is a review on a pull request from a fork (see save-pr-number job below) + if: | + github.actor != vars.GH_BOT_LOGIN && + github.actor != 'codecov[bot]' && + (github.event_name != 'pull_request_review' || github.event.pull_request.base.repo.full_name == github.event.pull_request.head.repo.full_name) + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + pr-numbers: ${{ steps.pr-numbers.outputs.pr-numbers }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: contribs/github-bot/go.mod + + - name: Generate matrix from event + id: pr-numbers + working-directory: contribs/github-bot + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: go run . matrix -matrix-key 'pr-numbers' -verbose + + # This job is executed if an event with read-only permission has triggered this + # workflow (e.g 'pull_request_review' on a pull request opened from a fork). + # In this case, this job persists the PR number in an artifact so that the + # proxy workflow can use it to rerun the current workflow with write permission. + # See bot-proxy.yml for more info. + save-pr-number: + name: Persist PR number for proxy + # Run this job if the event is a review on a pull request opened from a fork + if: github.event_name == 'pull_request_review' && github.event.pull_request.base.repo.full_name != github.event.pull_request.head.repo.full_name + runs-on: ubuntu-latest + + steps: + - name: Write PR number to a file + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: echo $PR_NUMBER > pr-number + + - name: Upload it as an artifact + uses: actions/upload-artifact@v4 + with: + name: pr-number + path: pr-number + + # This job processes each pull request in the matrix individually while ensuring + # that a same PR cannot be processed concurrently by mutliple runners + process-pr: + name: Process PR + needs: define-prs-matrix + # Just skip this job if PR numbers matrix is empty (prevent failed state) + if: needs.define-prs-matrix.outputs.pr-numbers != '[]' && needs.define-prs-matrix.outputs.pr-numbers != '' + runs-on: ubuntu-latest + strategy: + matrix: + # Run one job for each PR to process + pr-number: ${{ fromJSON(needs.define-prs-matrix.outputs.pr-numbers) }} + concurrency: + # Prevent running concurrent jobs for a given PR number + group: ${{ matrix.pr-number }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: contribs/github-bot/go.mod + + - name: Run GitHub Bot + working-directory: contribs/github-bot + env: + GITHUB_TOKEN: ${{ secrets.GH_BOT_PAT }} + run: go run . check -pr-numbers '${{ matrix.pr-number }}' -verbose diff --git a/.github/workflows/build_template.yml b/.github/workflows/build_template.yml index 430aa393a73..a2c96f2d37e 100644 --- a/.github/workflows/build_template.yml +++ b/.github/workflows/build_template.yml @@ -12,14 +12,14 @@ jobs: generated: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} - - name: Checkout code - uses: actions/checkout@v4 - - name: Check generated files are up to date working-directory: ${{ inputs.modulepath }} run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d2eef9d7445..4745788714d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,13 +9,17 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "CodeQL" +name: CodeQL on: push: branches: [ "master", "chain/*" ] pull_request: branches: [ "master", "chain/*" ] + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' schedule: - cron: '22 17 * * 3' @@ -41,8 +45,8 @@ jobs: fail-fast: false matrix: include: - - language: go - build-mode: autobuild + - language: go + build-mode: autobuild # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -52,38 +56,38 @@ jobs: # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/contribs.yml b/.github/workflows/contribs.yml index 3739339f7be..c1de5e78c35 100644 --- a/.github/workflows/contribs.yml +++ b/.github/workflows/contribs.yml @@ -1,30 +1,54 @@ -name: contribs +name: Contribs on: push: branches: - master - workflow_dispatch: pull_request: + paths: + - contribs/** + workflow_dispatch: jobs: setup: runs-on: ubuntu-latest outputs: programs: ${{ steps.set-matrix.outputs.programs }} + go-versions: ${{ steps.get-go-versions.outputs.go-versions }} steps: - uses: actions/checkout@v4 + - id: set-matrix - run: echo "::set-output name=programs::$(ls -d contribs/*/ | cut -d/ -f2 | jq -R -s -c 'split("\n")[:-1]')" + run: | + echo "::set-output name=programs::$(ls -d contribs/*/ | cut -d/ -f2 | jq -R -s -c 'split("\n")[:-1]')" + + - id: get-go-versions + run: | + contribs_programs=$(ls -d contribs/*/ | cut -d/ -f2) + versions_map="{" + + for p in $contribs_programs; do + # Fetch the go version of the contribs entry, and save it + # to a versions map we can reference later in the workflow + go_version=$(grep "^go [0-9]" contribs/$p/go.mod | cut -d ' ' -f2) + versions_map="$versions_map\"$p\":\"$go_version\"," + done + + # Close out the JSON + versions_map="${versions_map%,}" + versions_map="$versions_map}" + echo "::set-output name=go-versions::$versions_map" + main: needs: setup strategy: - fail-fast: false - matrix: - program: ${{ fromJson(needs.setup.outputs.programs) }} + fail-fast: false + matrix: + program: ${{ fromJson(needs.setup.outputs.programs) }} name: Run Main uses: ./.github/workflows/main_template.yml with: modulepath: contribs/${{ matrix.program }} + go-version: ${{ (fromJson(needs.setup.outputs.go-versions))[matrix.program] }} secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/dependabot-tidy.yml b/.github/workflows/dependabot-tidy.yml index 59e9e1c8146..39fed8b0172 100644 --- a/.github/workflows/dependabot-tidy.yml +++ b/.github/workflows/dependabot-tidy.yml @@ -20,7 +20,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version-file: go.mod - name: Tidy all Go mods env: diff --git a/.github/workflows/dependabot-validate.yml b/.github/workflows/dependabot-validate.yml index b1387dc0bb2..3d7b2c315c6 100644 --- a/.github/workflows/dependabot-validate.yml +++ b/.github/workflows/dependabot-validate.yml @@ -1,10 +1,11 @@ -name: dependabot validate +name: Validate Dependabot Config on: pull_request: paths: - '.github/dependabot.yml' - '.github/workflows/dependabot-validate.yml' + jobs: validate: runs-on: ubuntu-latest diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index d800147a498..f180f1679b1 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,4 +1,6 @@ -name: deploy docs on gnolang/docs.gno.land repository +# This workflow triggers a cross-repo workflow call, +# that deploys the monorepo docs on Netlify, using Docusaurus +name: Deploy the Documentation on: push: branches: diff --git a/.github/workflows/docs-linter.yml b/.github/workflows/docs-linter.yml new file mode 100644 index 00000000000..d603d796ae9 --- /dev/null +++ b/.github/workflows/docs-linter.yml @@ -0,0 +1,34 @@ +name: Docs Linter + +on: + push: + branches: + - master + pull_request: + paths: + - "docs/**" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Install dependencies + run: go mod download + + - name: Build docs + run: make -C docs/ build + + - name: Check diff + run: git diff --exit-code || (echo "Some docs files are not formatted, please run 'make build'." && exit 1) + + - name: Run linter + run: make -C docs/ lint diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 262b341276c..00000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: "docs / lint" - -on: - push: - paths: - - master - pull_request: - paths: - - "docs/**" - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.22' - - - name: Install dependencies - run: go mod download - - - name: Build docs - run: make -C docs/ build - - - name: Check diff - run: git diff --exit-code || (echo "Some docs files are not formatted, please run 'make build'." && exit 1) - - - name: Run linter - run: make -C docs/ lint diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 5b3c3c1fbf1..5d606a2a663 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -1,9 +1,14 @@ -name: examples +name: Gno Examples on: - pull_request: push: - branches: [ "master" ] + branches: + - master + pull_request: + paths: + - gnovm/**/*.gno + - examples/**/*.gno + - examples/**/gno.mod concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -47,7 +52,7 @@ jobs: echo "LOG_LEVEL=debug" >> $GITHUB_ENV echo "LOG_PATH_DIR=$LOG_PATH_DIR" >> $GITHUB_ENV - run: go install -v ./gnovm/cmd/gno - - run: go run ./gnovm/cmd/gno test -v ./examples/... + - run: go run ./gnovm/cmd/gno test -v -print-runtime-metrics -print-events ./examples/... lint: strategy: fail-fast: false @@ -66,22 +71,20 @@ jobs: - run: make lint -C ./examples # TODO: consider running lint on every other directories, maybe in "warning" mode? # TODO: track coverage + fmt: - strategy: - fail-fast: false - matrix: - goversion: [ "1.22.x" ] - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.goversion }} - - run: | - make fmt -C ./examples - # Check if there are changes after running make fmt - git diff --exit-code || (echo "Some gno files are not formatted, please run 'make fmt'." && exit 1) + name: Run gno fmt on examples + uses: ./.github/workflows/gnofmt_template.yml + with: + path: "examples/..." + + generate: + name: Check generated files are up to date + uses: ./.github/workflows/build_template.yml + with: + modulepath: "examples" + go-version: "1.22.x" + mod-tidy: strategy: fail-fast: false diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 11f04ca8282..0a94211cb90 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -1,13 +1,19 @@ name: Dependency License Scanning on: - workflow_dispatch: - pull_request: + push: + branches: + - master + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' + pull_request_target: paths: - - ".github/.fossa.yml" - - ".github/workflows/fossa.yml" - schedule: - - cron: '0 0 * * 6' # At 00:00 on saturdays + - '**/*.go' + - 'go.mod' + - 'go.sum' + workflow_dispatch: permissions: contents: read @@ -31,7 +37,7 @@ jobs: uses: coursier/cache-action@v6.4.6 - name: Set up JDK 17 - uses: coursier/setup-action@v1.3.5 + uses: coursier/setup-action@v1.3.9 with: jvm: temurin:1.17 @@ -47,4 +53,3 @@ jobs: run: fossa test env: FOSSA_API_KEY: "${{secrets.FOSSA_API_KEY}}" - diff --git a/.github/workflows/genesis-verify.yml b/.github/workflows/genesis-verify.yml new file mode 100644 index 00000000000..acc41cc99ad --- /dev/null +++ b/.github/workflows/genesis-verify.yml @@ -0,0 +1,58 @@ +name: Deployment genesis.json Verification + +on: + push: + branches: + - master + pull_request: + paths: + - "misc/deployments/**/genesis.json" + - ".github/workflows/genesis-verify.yml" + +jobs: + verify: + strategy: + fail-fast: false + matrix: + testnet: [ "test5.gno.land" ] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v45 + with: + files: "misc/deployments/${{ matrix.testnet }}/genesis.json" + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: contribs/gnogenesis/go.mod + + - name: Build gnogenesis + run: make -C contribs/gnogenesis + + - name: Verify each genesis file + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + echo "Verifying $file" + gnogenesis verify -genesis-path $file + done + + - name: Build gnoland + run: make -C gno.land install.gnoland + + - name: Running latest gnoland with each genesis file + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + echo "Running gnoland with $file" + timeout 60s gnoland start -lazy --genesis $file || exit_code=$? + if [ $exit_code -eq 124 ]; then + echo "Gnoland genesis state generated successfully" + else + echo "Gnoland failed to start with $file" + exit 1 + fi + done diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index a8407f57291..a293469bb5d 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -1,9 +1,11 @@ -# generate docs and publish on gh-pages branch -name: gh-pages +# generate Go docs and publish on gh-pages branch +# Live at: https://gnolang.github.io/gno +name: Go Reference Docs Deployment on: push: - branches: [ "master" ] + branches: + - master workflow_dispatch: permissions: @@ -23,13 +25,20 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod + - run: echo "GOROOT=$(go env GOROOT)" >> $GITHUB_ENV + - run: echo $GOROOT + - run: "cd misc/stdlib_diff && make gen" - run: "cd misc/gendocs && make install gen" + - run: "mkdir -p pages_output/stdlib_diff" + - run: | + cp -r misc/gendocs/godoc/* pages_output/ + cp -r misc/stdlib_diff/stdlib_diff/* pages_output/stdlib_diff/ - uses: actions/configure-pages@v5 id: pages - uses: actions/upload-pages-artifact@v3 with: - path: ./misc/gendocs/godoc + path: ./pages_output deploy: if: ${{ github.repository == 'gnolang/gno' }} # Alternatively, validate based on provided tokens and permissions. diff --git a/.github/workflows/gnofmt_template.yml b/.github/workflows/gnofmt_template.yml index 1ba66d0fbe3..096dbaa1b5d 100644 --- a/.github/workflows/gnofmt_template.yml +++ b/.github/workflows/gnofmt_template.yml @@ -1,12 +1,15 @@ on: workflow_call: - inputs: - path: - required: true - type: string - go-version: - required: true - type: string + inputs: + path: + description: "Path to run gno fmt on" + required: true + type: string + go-version: + description: "Go version to use" + required: false + type: string + default: "1.22.x" jobs: fmt: @@ -16,9 +19,15 @@ jobs: uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} + - name: Checkout code uses: actions/checkout@v4 - - name: Fmt + + - name: Format code with gno fmt env: GNOFMT_PATH: ${{ inputs.path }} run: go run ./gnovm/cmd/gno fmt -v -diff $GNOFMT_PATH + + - name: Check for unformatted code + run: | + git diff --exit-code || (echo "Some gno files are not formatted, please run 'make fmt'." && exit 1) \ No newline at end of file diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 9451d6da3a1..0d3a7a10516 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -4,14 +4,43 @@ on: push: branches: - master - workflow_dispatch: pull_request: + paths: + - gno.land/** + # We trigger the testing workflow for gno.land on the following, + # since there are integration suites that cover the gnovm / tm2 + - gnovm/** + - tm2/** + workflow_dispatch: jobs: main: - name: Run Main + name: Run gno.land suite uses: ./.github/workflows/main_template.yml with: modulepath: "gno.land" + tests-extra-args: "-coverpkg=github.com/gnolang/gno/gno.land/..." secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} + + gnoweb_generate: + strategy: + fail-fast: false + matrix: + go-version: ["1.22.x"] + # unittests: TODO: matrix with contracts + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - uses: actions/setup-node@v4 + with: + node-version: lts/Jod # (22.x) https://github.com/nodejs/Release + - uses: actions/checkout@v4 + - run: | + make -C gno.land/pkg/gnoweb fclean generate + # Check if there are changes after running generate.gnoweb + git diff --exit-code || \ + (echo "\`gnoweb generate\` out of date, please run \`make gnoweb.generate\` within './gno.land'" && exit 1) diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 7e7586b23d9..7a015b74e09 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -1,23 +1,26 @@ -name: gnovm +name: GnoVM on: push: branches: - master - workflow_dispatch: pull_request: + paths: + - gnovm/** + - tm2/** # GnoVM has a dependency on TM2 types + workflow_dispatch: jobs: main: - name: Run Main + name: Run GnoVM suite uses: ./.github/workflows/main_template.yml with: modulepath: "gnovm" + tests-extra-args: "-coverpkg=github.com/gnolang/gno/gnovm/..." secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} fmt: - name: Run Gno Fmt + name: Run gno fmt on stdlibs uses: ./.github/workflows/gnofmt_template.yml with: path: "gnovm/stdlibs/..." - go-version: "1.22.x" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 06b2daa1d3d..56075c31db3 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,6 +1,6 @@ -name: "Pull Request Labeler" +name: Pull Request Labeler on: -- pull_request_target + - pull_request_target jobs: triage: @@ -9,5 +9,5 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/labeler@v5 + - uses: actions/checkout@v4 + - uses: actions/labeler@v5 diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml index 631f764c37f..3c7236b264f 100644 --- a/.github/workflows/lint-pr-title.yml +++ b/.github/workflows/lint-pr-title.yml @@ -1,4 +1,4 @@ -name: "lint-pr-title" +name: PR Title Linter on: pull_request_target: diff --git a/.github/workflows/lint_template.yml b/.github/workflows/lint_template.yml index 65679633240..43246572daa 100644 --- a/.github/workflows/lint_template.yml +++ b/.github/workflows/lint_template.yml @@ -8,21 +8,20 @@ on: required: true type: string - jobs: lint: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} - - name: Checkout code - uses: actions/checkout@v4 - name: Lint uses: golangci/golangci-lint-action@v6 with: working-directory: ${{ inputs.modulepath }} args: --config=${{ github.workspace }}/.github/golangci.yml - version: v1.59 # sync with misc/devdeps \ No newline at end of file + version: v1.62 # sync with misc/devdeps diff --git a/.github/workflows/main_template.yml b/.github/workflows/main_template.yml index 8efb0277816..a463bb330ea 100644 --- a/.github/workflows/main_template.yml +++ b/.github/workflows/main_template.yml @@ -1,37 +1,42 @@ on: - workflow_call: - inputs: - modulepath: - required: true - type: string - secrets: - codecov-token: - required: true - -# TODO: environment variables cannot be sent to reusable workflows: https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations -# env: -# GO_VERSION: "1.22.x" + workflow_call: + inputs: + modulepath: + required: true + type: string + tests-extra-args: + required: false + type: string + go-version: + description: "Go version to use" + required: false + type: string + default: "1.22.x" + secrets: + codecov-token: + required: true jobs: - lint: - name: Go Linter - uses: ./.github/workflows/lint_template.yml - with: - modulepath: ${{ inputs.modulepath }} - go-version: "1.22.x" - build: - name: Go Build - uses: ./.github/workflows/build_template.yml - with: - modulepath: ${{ inputs.modulepath }} - go-version: "1.22.x" - test: - name: Go Test - uses: ./.github/workflows/test_template.yml - with: - modulepath: ${{ inputs.modulepath }} - tests-timeout: "30m" - go-version: "1.22.x" - secrets: - codecov-token: ${{ secrets.codecov-token }} - \ No newline at end of file + lint: + name: Go Lint + uses: ./.github/workflows/lint_template.yml + with: + modulepath: ${{ inputs.modulepath }} + go-version: ${{ inputs.go-version }} + build: + name: Go Build + uses: ./.github/workflows/build_template.yml + with: + modulepath: ${{ inputs.modulepath }} + go-version: ${{ inputs.go-version }} + test: + name: Go Test + uses: ./.github/workflows/test_template.yml + with: + modulepath: ${{ inputs.modulepath }} + tests-timeout: "30m" + go-version: ${{ inputs.go-version }} + tests-extra-args: ${{ inputs.tests-extra-args }} + secrets: + codecov-token: ${{ secrets.codecov-token }} + diff --git a/.github/workflows/misc.yml b/.github/workflows/misc.yml index b824235ca2b..1116a87c300 100644 --- a/.github/workflows/misc.yml +++ b/.github/workflows/misc.yml @@ -6,27 +6,27 @@ on: push: branches: - master - workflow_dispatch: pull_request: + paths: + - misc/** + workflow_dispatch: jobs: main: strategy: - fail-fast: false - matrix: - # fixed list because we have some non go programs on that misc folder - program: - - autocounterd - # - devdeps - - docker-integration - - genproto - - genstd - - goscan - - logos - - loop - name: Run Main + fail-fast: false + matrix: + # fixed list because we have some non go programs on that misc folder + program: + - autocounterd + - genproto + - genstd + - goscan + - loop + name: Run misc suite uses: ./.github/workflows/main_template.yml with: modulepath: misc/${{ matrix.program }} + tests-extra-args: "-coverpkg=github.com/gnolang/gno/misc/..." secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/mod-tidy.yml b/.github/workflows/mod-tidy.yml new file mode 100644 index 00000000000..5b6401b0d13 --- /dev/null +++ b/.github/workflows/mod-tidy.yml @@ -0,0 +1,34 @@ +name: go.mod Tidy Checker + +on: + push: + branches: + - master + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' + pull_request: + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' + workflow_dispatch: + +jobs: + main: + name: Ensure go.mods are tidied + runs-on: ubuntu-latest + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check go.mod files are up to date + working-directory: ${{ inputs.modulepath }} + run: | + make tidy VERIFY_MOD_SUMS=true diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml deleted file mode 100644 index 0110801dc93..00000000000 --- a/.github/workflows/nightlies.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Trigger nightly build - -on: - schedule: - - cron: '0 0 * * 2-6' - workflow_dispatch: - -permissions: - contents: write # needed to write releases - id-token: write # needed for keyless signing - packages: write # needed for ghcr access - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v5 - with: - go-version: "1.22.x" - cache: true - - - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.17.0 - - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - uses: goreleaser/goreleaser-action@v6 - with: - distribution: goreleaser-pro - version: ~> v2 - args: release --clean --nightly --config ./.github/goreleaser.yaml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} - TAG_VERSION: nightly diff --git a/.github/workflows/portal-loop.yml b/.github/workflows/portal-loop.yml index b81957b22db..b5cafa459a7 100644 --- a/.github/workflows/portal-loop.yml +++ b/.github/workflows/portal-loop.yml @@ -1,19 +1,13 @@ -name: portal-loop +name: Portal Loop on: - pull_request: - branches: - - master push: + branches: + - "master" + pull_request: paths: - "misc/loop/**" - ".github/workflows/portal-loop.yml" - branches: - - "master" - # NOTE(albttx): branch name to simplify tests for this workflow - - "ci/portal-loop" - tags: - - "v*" permissions: contents: read @@ -45,7 +39,6 @@ jobs: - name: Build and push uses: docker/build-push-action@v6 with: - context: ./misc/loop target: portalloopd push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} @@ -58,27 +51,26 @@ jobs: - name: "Checkout" uses: actions/checkout@v4 - - name: "Setup the images" + - name: "Setup The portal loop docker compose" run: | cd misc/loop - - docker compose build - docker compose pull - docker compose up -d + echo "Making docker compose happy" + touch .env + make docker.ci - name: "Test1 - Portal loop start gnoland" run: | while block_height=$(curl -s localhost:26657/status | jq -r '.result.sync_info.latest_block_height') echo "Current block height: $block_height" - [[ "$block_height" -lt 10 ]] + [[ "$block_height" -lt 2 ]] do sleep 1 done curl -s localhost:26657/status | jq - - name: "Buid new gnolang/gno image" + - name: "Build new gnolang/gno image" run: | docker build -t ghcr.io/gnolang/gno/gnoland:master -f Dockerfile --target gnoland . @@ -98,7 +90,7 @@ jobs: while block_height=$(curl -s localhost:26657/status | jq -r '.result.sync_info.latest_block_height') echo "Current block height: $block_height" - [[ "$block_height" -lt 10 ]] + [[ "$block_height" -lt 2 ]] do sleep 5 done diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml index 96a622e3272..7c81789b060 100644 --- a/.github/workflows/releaser-master.yml +++ b/.github/workflows/releaser-master.yml @@ -1,9 +1,9 @@ -name: Trigger master build +name: Master Releases on: push: branches: - - "master" + - master workflow_dispatch: permissions: @@ -18,14 +18,16 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Create a dummy tag to avoid goreleaser failure + run: git tag v0.0.0 master - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod cache: true - - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.17.0 + - uses: sigstore/cosign-installer@v3.7.0 + - uses: anchore/sbom-action/download-syft@v0.17.9 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/releaser-nightly.yml b/.github/workflows/releaser-nightly.yml new file mode 100644 index 00000000000..47b6cabb223 --- /dev/null +++ b/.github/workflows/releaser-nightly.yml @@ -0,0 +1,46 @@ +name: Nightly Releases + +on: + schedule: + - cron: "0 0 * * 2-6" + workflow_dispatch: + +permissions: + contents: write # needed to write releases + id-token: write # needed for keyless signing + packages: write # needed for ghcr access + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: sigstore/cosign-installer@v3.7.0 + - uses: anchore/sbom-action/download-syft@v0.17.9 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser-pro + version: ~> v2 + args: release --clean --nightly --snapshot --config ./.github/goreleaser.yaml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + TAG_VERSION: nightly diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml deleted file mode 100644 index f3317419510..00000000000 --- a/.github/workflows/releaser.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Go Releaser - -on: - push: - tags: - - "v*" - -permissions: - contents: write # needed to write releases - id-token: write # needed for keyless signing - packages: write # needed for ghcr access - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v5 - with: - go-version: "1.22.x" - cache: true - - - uses: sigstore/cosign-installer@v3.5.0 - - uses: anchore/sbom-action/download-syft@v0.17.0 - - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - uses: goreleaser/goreleaser-action@v6 - with: - distribution: goreleaser-pro - version: ~> v2 - args: release --clean --config ./.github/goreleaser.yaml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml new file mode 100644 index 00000000000..6eb38ac5728 --- /dev/null +++ b/.github/workflows/stale-bot.yml @@ -0,0 +1,23 @@ +name: Stale PR Bot +on: + schedule: + - cron: "30 1 * * *" +permissions: + pull-requests: write + issues: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + exempt-all-milestones: true + stale-pr-message: "This PR is stale because it has been open 3 months with no activity. Remove stale label or comment or this will be closed in 3 months." + close-pr-message: "This PR was closed because it has been stalled for 3 months with no activity." + days-before-pr-stale: 90 + days-before-pr-close: 90 + stale-issue-message: "This issue is stale because it has been open 6 months with no activity. Remove stale label or comment or this will be closed in 3 months." + close-issue-message: "This issue was closed because it has been stalled for 3 months with no activity." + days-before-issue-stale: 180 + days-before-issue-close: 90 diff --git a/.github/workflows/test_template.yml b/.github/workflows/test_template.yml index 18911415087..a1bc58ecebb 100644 --- a/.github/workflows/test_template.yml +++ b/.github/workflows/test_template.yml @@ -1,78 +1,70 @@ on: - workflow_call: - inputs: - modulepath: - required: true - type: string - tests-timeout: - required: true - type: string - go-version: - required: true - type: string - secrets: - codecov-token: - required: true + workflow_call: + inputs: + modulepath: + required: true + type: string + tests-timeout: + required: true + type: string + go-version: + required: true + type: string + tests-extra-args: + required: false + type: string + secrets: + codecov-token: + required: true jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: ${{ inputs.go-version }} - - name: Checkout code - uses: actions/checkout@v4 - - name: Go test - working-directory: ${{ inputs.modulepath }} - env: - TXTARCOVERDIR: /tmp/txtarcoverdir # txtar cover output - GOCOVERDIR: /tmp/gocoverdir # go cover output - COVERDIR: /tmp/coverdir # final output - run: | - set -x # print commands + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + - name: Go test + working-directory: ${{ inputs.modulepath }} + env: + TXTARCOVERDIR: /tmp/txtarcoverdir # txtar cover output + GOCOVERDIR: /tmp/gocoverdir # go cover output + COVERDIR: /tmp/coverdir # final output + run: | + set -x # print commands + + mkdir -p "$GOCOVERDIR" "$TXTARCOVERDIR" "$COVERDIR" + + # Craft a filter flag based on the module path to avoid expanding coverage on unrelated tags. + export filter="-pkg=github.com/gnolang/gno/${{ inputs.modulepath }}/..." + + # codecov only supports "boolean" coverage (whether a line is + # covered or not); so using -covermode=count or atomic would be + # pointless here. + # XXX: Simplify coverage of txtar - the current setup is a bit + # confusing and meticulous. There will be some improvements in Go + # 1.23 regarding coverage, so we can use this as a workaround until + # then. + go test -covermode=set -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR + + # Print results + (set +x; echo 'go coverage results:') + go tool covdata percent $filter -i=$GOCOVERDIR + (set +x; echo 'txtar coverage results:') + go tool covdata percent $filter -i=$TXTARCOVERDIR + + # Generate final coverage output + go tool covdata textfmt -v 1 $filter -i=$GOCOVERDIR,$TXTARCOVERDIR -o gocoverage.out - mkdir -p "$GOCOVERDIR" "$TXTARCOVERDIR" "$COVERDIR" - - # Craft a filter flag based on the module path to avoid expanding coverage on unrelated tags. - export filter="-pkg=github.com/gnolang/gno/${{ inputs.modulepath }}/..." - - # XXX: Simplify coverage of txtar - the current setup is a bit - # confusing and meticulous. There will be some improvements in Go - # 1.23 regarding coverage, so we can use this as a workaround until - # then. - go test -covermode=atomic -timeout ${{ inputs.tests-timeout }} -v ./... -test.gocoverdir=$GOCOVERDIR - - # Print results - (set +x; echo 'go coverage results:') - go tool covdata percent $filter -i=$GOCOVERDIR - (set +x; echo 'txtar coverage results:') - go tool covdata percent $filter -i=$TXTARCOVERDIR - - # Generate final coverage output - go tool covdata textfmt -v 1 $filter -i=$GOCOVERDIR,$TXTARCOVERDIR -o gocoverage.out - - - name: Upload go coverage to Codecov - uses: codecov/codecov-action@v4 - with: - disable_search: true - fail_ci_if_error: true - file: ${{ inputs.modulepath }}/gocoverage.out - flags: ${{ inputs.modulepath }} - token: ${{ secrets.codecov-token }} - verbose: true # keep this enable as it help debugging when coverage fail randomly on the CI - - # TODO: We have to fix race conditions before running this job - # test-with-race: - # runs-on: ubuntu-latest - # steps: - # - name: Install Go - # uses: actions/setup-go@v5 - # with: - # go-version: ${{ inputs.go-version }} - # - name: Checkout code - # uses: actions/checkout@v4 - # - name: Go race test - # run: go test -race -timeout ${{ inputs.tests-timeout }} ./... - # working-directory: ${{ inputs.modulepath }} + - name: Upload go coverage to Codecov + uses: codecov/codecov-action@v5 + with: + disable_search: true + fail_ci_if_error: true + files: ${{ inputs.modulepath }}/gocoverage.out + flags: ${{ inputs.modulepath }} + token: ${{ secrets.codecov-token }} + verbose: true # keep this enable as it help debugging when coverage fails randomly on the CI diff --git a/.github/workflows/tm2.yml b/.github/workflows/tm2.yml index 57e84793c94..757391eab8c 100644 --- a/.github/workflows/tm2.yml +++ b/.github/workflows/tm2.yml @@ -1,17 +1,20 @@ -name: tm2 +name: TM2 on: push: branches: - master - workflow_dispatch: pull_request: + paths: + - tm2/** + workflow_dispatch: jobs: main: - name: Run Main + name: Run TM2 suite uses: ./.github/workflows/main_template.yml with: modulepath: "tm2" + tests-extra-args: "-coverpkg=github.com/gnolang/gno/tm2/..." secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc125a6da73..b58d63c6c75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -469,6 +469,18 @@ Resources for idiomatic Go docs: - [godoc](https://go.dev/blog/godoc) - [Go Doc Comments](https://tip.golang.org/doc/comment) +## Avoding Unhelpful Contributions + +While we welcome all contributions to the Gno project, it's important to ensure that your changes provide meaningful value or improve the quality of the codebase. Contributions that fail to meet these criteria may not be accepted. Examples of unhelpful contributions include (but not limited to): + +- Airdrop farming & karma farming: Making minimal, superficial changes, with the goal of becoming eligible for airdrops and GovDAO participation. +- Incomplete submissions: Changes that lack adequate context, link to a related issue, documentation, or test coverage. + +Before submitting a pull request, ask yourself: +- Does this change solve a specific problem or add clear value? +- Is the implementation aligned with the gno.land's goals and style guide? +- Have I tested my changes and included relevant documentation? + ## Additional Notes ### Issue and Pull Request Labels @@ -502,3 +514,4 @@ automatic label management. | info needed | Issue is lacking information needed for resolving | | investigating | Issue is still being investigated by the team | | question | Issue starts a discussion or raises a question | + diff --git a/Dockerfile b/Dockerfile index fa5a9e47270..effc30ca32f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,11 +10,20 @@ RUN --mount=type=cache,target=/root/.cache/go-build go build -o ./ RUN --mount=type=cache,target=/root/.cache/go-build go build -o ./build/gnoweb ./gno.land/cmd/gnoweb RUN --mount=type=cache,target=/root/.cache/go-build go build -o ./build/gno ./gnovm/cmd/gno +# build misc binaries +FROM golang:1.22-alpine AS build-misc +RUN go env -w GOMODCACHE=/root/.cache/go-build +WORKDIR /gnoroot +ENV GNOROOT="/gnoroot" +COPY . ./ +RUN --mount=type=cache,target=/root/.cache/go-build go build -C ./misc/loop -o /gnoroot/build/portalloopd ./cmd +RUN --mount=type=cache,target=/root/.cache/go-build go build -C ./misc/autocounterd -o /gnoroot/build/autocounterd ./cmd + # Base image FROM alpine:3.17 AS base WORKDIR /gnoroot ENV GNOROOT="/gnoroot" -RUN apk add ca-certificates +RUN apk add --no-cache ca-certificates CMD [ "" ] # alpine images @@ -43,10 +52,24 @@ ENTRYPOINT ["/usr/bin/gno"] # gnoweb FROM base AS gnoweb COPY --from=build-gno /gnoroot/build/gnoweb /usr/bin/gnoweb -COPY --from=build-gno /opt/gno/src/gno.land/cmd/gnoweb /opt/gno/src/gnoweb EXPOSE 8888 ENTRYPOINT ["/usr/bin/gnoweb"] +# misc/loop +FROM docker AS portalloopd +WORKDIR /gnoroot +ENV GNOROOT="/gnoroot" +RUN apk add --no-cache ca-certificates bash curl jq +COPY --from=build-misc /gnoroot/build/portalloopd /usr/bin/portalloopd +ENTRYPOINT ["/usr/bin/portalloopd"] +CMD ["serve"] + +# misc/autocounterd +FROM base AS autocounterd +COPY --from=build-misc /gnoroot/build/autocounterd /usr/bin/autocounterd +ENTRYPOINT ["/usr/bin/autocounterd"] +CMD ["start"] + # all, contains everything. FROM base AS all COPY --from=build-gno /gnoroot/build/* /usr/bin/ diff --git a/Dockerfile.release b/Dockerfile.release index 644f8cb5de9..c7bb1b582ed 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -1,3 +1,6 @@ +# This file is similar to Dockerfile, but assumes that the binaries have +# already been created, and as such doesn't `go build` them. + FROM alpine AS base ENV GNOROOT="/gnoroot/" @@ -8,17 +11,16 @@ CMD [ "" ] ## ghcr.io/gnolang/gno/gnoland FROM base as gnoland -COPY ./gnoland /usr/bin/gnoland -COPY ./examples /gnoroot/examples/ -COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ -COPY ./gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt -COPY ./gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl +COPY ./gnoland /usr/bin/gnoland +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt +COPY ./gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl EXPOSE 26656 26657 ENTRYPOINT [ "/usr/bin/gnoland" ] - # ## ghcr.io/gnolang/gno/gnokey FROM base as gnokey @@ -26,7 +28,6 @@ FROM base as gnokey COPY ./gnokey /usr/bin/gnokey ENTRYPOINT [ "/usr/bin/gnokey" ] - # ## ghcr.io/gnolang/gno/gnoweb FROM base as gnoweb @@ -47,9 +48,23 @@ ENTRYPOINT [ "/usr/bin/gnofaucet" ] ## ghcr.io/gnolang/gno FROM base as gno -COPY ./gno /usr/bin/gno -COPY ./examples /gnoroot/examples/ -COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ -COPY ./gnovm/tests/stdlibs /gnoroot/gnovm/tests/stdlibs/ +COPY ./gno /usr/bin/gno +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gnovm/tests/stdlibs /gnoroot/gnovm/tests/stdlibs/ ENTRYPOINT [ "/usr/bin/gno" ] + +# +## ghcr.io/gnolang/gnocontribs +FROM base as gnocontribs + +COPY ./gnobro /usr/bin/gnobro +COPY ./gnogenesis /usr/bin/gnogenesis +COPY ./examples /gnoroot/examples/ +COPY ./gnovm/stdlibs /gnoroot/gnovm/stdlibs/ +COPY ./gno.land/genesis/genesis_balances.txt /gnoroot/gno.land/genesis/genesis_balances.txt +COPY ./gno.land/genesis/genesis_txs.jsonl /gnoroot/gno.land/genesis/genesis_txs.jsonl +EXPOSE 22 + +ENTRYPOINT [ "/bin/sh", "-c" ] diff --git a/Makefile b/Makefile index fe862d52893..bd67020f236 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,7 @@ install.gnodev: $(MAKE) --no-print-directory -C ./contribs/gnodev install @printf "\033[0;32m[+] 'gnodev' has been installed. Read more in ./contribs/gnodev/\033[0m\n" + # old aliases .PHONY: install_gnokey install_gnokey: install.gnokey @@ -53,7 +54,7 @@ install_gnokey: install.gnokey install_gno: install.gno .PHONY: test -test: test.components test.docker +test: test.components .PHONY: test.components test.components: @@ -63,14 +64,6 @@ test.components: $(MAKE) --no-print-directory -C examples test $(MAKE) --no-print-directory -C misc test -.PHONY: test.docker -test.docker: - @if hash docker 2>/dev/null; then \ - go test --tags=docker -count=1 -v ./misc/docker-integration; \ - else \ - echo "[-] 'docker' is missing, skipping ./misc/docker-integration tests."; \ - fi - .PHONY: fmt fmt: $(MAKE) --no-print-directory -C tm2 fmt imports diff --git a/README.md b/README.md index 19ac161e790..89bfd96d74f 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ If you haven't already, take a moment to check out our [website](https://gno.lan > The website is a deployment of our [gnoweb](./gno.land/cmd/gnoweb) frontend; you > can use it to check out -> [some](https://test3.gno.land/r/demo/boards) -> [example](https://test3.gno.land/r/gnoland/blog) -> [contracts](https://test3.gno.land/r/demo/users). +> [some](https://gno.land/r/demo/boards) +> [example](https://gno.land/r/gnoland/blog) +> [contracts](https://gno.land/r/demo/users). > > Use the `[source]` button in the header to inspect the program's source; use > the `[help]` button to view how you can use [`gnokey`](./gno.land/cmd/gnokey) @@ -53,7 +53,7 @@ repository offers more resources to dig into. We are eager to see your first PR! * [examples](./examples) - Smart-contract examples and guides for new Gno developers. * [gnovm](./gnovm) - GnoVM and Gnolang. -* [gno.land](./gno.land) - Gno.land blockchain and tools. +* [gno.land](./gno.land) - gno.land blockchain and tools. * [tm2](./tm2) - Tendermint2. * [docs](./docs) - Official documentation, deployed under [docs.gno.land](https://docs.gno.land). * [contribs](./contribs) - Collection of enhanced tools for Gno. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..409c3867e57 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Reporting a Vulnerability +If you've identified a vulnerability, please **DO NOT** open a new public issue. Instead, report it through one of the following venues: + +* Submit an advisory through GitHub: https://github.com/gnolang/gno/security/advisories/new +* Email security [at-symbol] tendermint [dot] com. If you are concerned about confidentiality e.g. because of a high-severity issue, you may email us for PGP or Signal contact details. If you’ve found multiple vulnerabilities, please submit one per email. +* A security bug bounty platform for gno.land will be available Soonᵀᴹ. You will need to report via our bug bounty platform in order to be eligible for rewards. + +We will respond within 3 business days to all received reports. + +Thank you for helping to keep our ecosystem safe! diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md new file mode 100644 index 00000000000..639901c52ee --- /dev/null +++ b/contribs/github-bot/README.md @@ -0,0 +1,58 @@ +# GitHub Bot + +## Overview + +The GitHub Bot is designed to automate and streamline the process of managing pull requests. It can automate certain tasks such as requesting reviews, assigning users or applying labels, but it also ensures that certain requirements are satisfied before allowing a pull request to be merged. Interaction with the bot occurs through a comment on the pull request, providing all the information to the user and allowing them to check boxes for the manual validation of certain rules. + +## How It Works + +### Configuration + +The bot operates by defining a set of rules that are evaluated against each pull request passed as parameter. These rules are categorized into automatic and manual checks: + +- **Automatic Checks**: These are rules that the bot evaluates automatically. If a pull request meets the conditions specified in the rule, then the corresponding requirements are executed. For example, ensuring that changes to specific directories are reviewed by specific team members. +- **Manual Checks**: These require human intervention. If a pull request meets the conditions specified in the rule, then a checkbox that can be checked only by specified teams is displayed on the bot comment. For example, determining if infrastructure needs to be updated based on changes to specific files. + +The bot configuration is defined in Go and is located in the file [config.go](./internal/config/config.go). + +### GitHub Token + +For the bot to make requests to the GitHub API, it needs a Personal Access Token. The fine-grained permissions to assign to the token for the bot to function are: + +#### Repository permissions + +- `pull_requests` scope to read is the bare minimum to run the bot in dry-run mode +- `pull_requests` scope to write to be able to update bot comment, assign user, apply label and request review +- `contents` scope to read to be able to check if the head branch is up to date with another one +- `commit_statuses` scope to write to be able to update pull request bot status check + +#### Organization permissions + +- `members` scope to read to be able to list the members of a team + +#### Bot account role + +For the bot to create a commit status on a repo - and only for this feature at the time of writing this - the GitHub account of the bot must either: + +- have the `write` role on the repo +- have the `owner` role in the organization that owns the repo + +## Usage + +```bash +> github-bot check --help +USAGE + github-bot check [flags] + +This tool checks if the requirements for a pull request to be merged are satisfied (defined in ./internal/config/config.go) and displays PR status checks accordingly. +A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable. + +FLAGS + -dry-run=false print if pull request requirements are satisfied without updating anything on GitHub + -owner ... owner of the repo to process, if empty, will be retrieved from GitHub Actions context + -pr-all=false process all opened pull requests + -pr-numbers ... pull request(s) to process, must be a comma separated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrieved from GitHub Actions context + -repo ... repo to process, if empty, will be retrieved from GitHub Actions context + -timeout 0s timeout after which the bot execution is interrupted + -verbose=false set logging level to debug +``` diff --git a/contribs/github-bot/go.mod b/contribs/github-bot/go.mod new file mode 100644 index 00000000000..8df55e3f282 --- /dev/null +++ b/contribs/github-bot/go.mod @@ -0,0 +1,28 @@ +module github.com/gnolang/gno/contribs/github-bot + +go 1.22 + +toolchain go1.22.2 + +replace github.com/gnolang/gno => ../.. + +require ( + github.com/gnolang/gno v0.0.0-00010101000000-000000000000 + github.com/google/go-github/v64 v64.0.0 + github.com/migueleliasweb/go-github-mock v1.0.1 + github.com/sethvargo/go-githubactions v1.3.0 + github.com/stretchr/testify v1.9.0 + github.com/xlab/treeprint v1.2.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/time v0.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/contribs/github-bot/go.sum b/contribs/github-bot/go.sum new file mode 100644 index 00000000000..2dae4e83e72 --- /dev/null +++ b/contribs/github-bot/go.sum @@ -0,0 +1,38 @@ +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/google/go-cmp v0.5.2/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/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg= +github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/migueleliasweb/go-github-mock v1.0.1 h1:amLEECVny28RCD1ElALUpQxrAimamznkg9rN2O7t934= +github.com/migueleliasweb/go-github-mock v1.0.1/go.mod h1:8PJ7MpMoIiCBBNpuNmvndHm0QicjsE+hjex1yMGmjYQ= +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/sethvargo/go-githubactions v1.3.0 h1:Kg633LIUV2IrJsqy2MfveiED/Ouo+H2P0itWS0eLh8A= +github.com/sethvargo/go-githubactions v1.3.0/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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/github-bot/internal/check/check.go b/contribs/github-bot/internal/check/check.go new file mode 100644 index 00000000000..cb1848b757c --- /dev/null +++ b/contribs/github-bot/internal/check/check.go @@ -0,0 +1,250 @@ +package check + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + "sync/atomic" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/config" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/sethvargo/go-githubactions" + "github.com/xlab/treeprint" +) + +func execCheck(flags *checkFlags) error { + // Create context with timeout if specified in the parameters. + ctx := context.Background() + if flags.Timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), flags.Timeout) + defer cancel() + } + + // Init GitHub API client. + gh, err := client.New(ctx, &client.Config{ + Owner: flags.Owner, + Repo: flags.Repo, + Verbose: *flags.Verbose, + DryRun: flags.DryRun, + }) + if err != nil { + return fmt.Errorf("comment update handling failed: %w", err) + } + + // Get GitHub Actions context to retrieve comment update. + actionCtx, err := githubactions.Context() + if err != nil { + gh.Logger.Debugf("Unable to retrieve GitHub Actions context: %v", err) + return nil + } + + // Handle comment update, if any. + if err := handleCommentUpdate(gh, actionCtx); errors.Is(err, errTriggeredByBot) { + return nil // Ignore if this run was triggered by a previous run. + } else if err != nil { + return fmt.Errorf("comment update handling failed: %w", err) + } + + // Retrieve a slice of pull requests to process. + var prs []*github.PullRequest + + // If requested, retrieve all open pull requests. + if flags.PRAll { + prs, err = gh.ListPR(utils.PRStateOpen) + if err != nil { + return fmt.Errorf("unable to list all PR: %w", err) + } + } else { + // Otherwise, retrieve only specified pull request(s) + // (flag or GitHub Action context). + prs = make([]*github.PullRequest, len(flags.PRNums)) + for i, prNum := range flags.PRNums { + pr, err := gh.GetOpenedPullRequest(prNum) + if err != nil { + return fmt.Errorf("unable to process PR list: %w", err) + } + prs[i] = pr + } + } + + return processPRList(gh, prs) +} + +func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { + if len(prs) > 1 { + prNums := make([]int, len(prs)) + for i, pr := range prs { + prNums[i] = pr.GetNumber() + } + + gh.Logger.Infof("%d pull requests to process: %v\n", len(prNums), prNums) + } + + // Process all pull requests in parallel. + autoRules, manualRules := config.Config(gh) + var wg sync.WaitGroup + + // Used in dry-run mode to log cleanly from different goroutines. + logMutex := sync.Mutex{} + + // Used in regular-run mode to return an error if one PR processing failed. + var failed atomic.Bool + + for _, pr := range prs { + wg.Add(1) + go func(pr *github.PullRequest) { + defer wg.Done() + commentContent := CommentContent{} + commentContent.AutoAllSatisfied = true + commentContent.ManualAllSatisfied = true + + // Iterate over all automatic rules in config. + for _, autoRule := range autoRules { + ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success)) + + // Check if conditions of this rule are met by this PR. + if !autoRule.If.IsMet(pr, ifDetails) { + continue + } + + c := AutoContent{Description: autoRule.Description, Satisfied: false} + thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.Fail)) + + // Check if requirements of this rule are satisfied by this PR. + if autoRule.Then.IsSatisfied(pr, thenDetails) { + thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success)) + c.Satisfied = true + } else { + commentContent.AutoAllSatisfied = false + } + + c.ConditionDetails = ifDetails.String() + c.RequirementDetails = thenDetails.String() + commentContent.AutoRules = append(commentContent.AutoRules, c) + } + + // Retrieve manual check states. + checks := make(map[string]manualCheckDetails) + if comment, err := gh.GetBotComment(pr.GetNumber()); err == nil { + checks = getCommentManualChecks(comment.GetBody()) + } + + // Iterate over all manual rules in config. + for _, manualRule := range manualRules { + ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success)) + + // Check if conditions of this rule are met by this PR. + if !manualRule.If.IsMet(pr, ifDetails) { + continue + } + + // Get check status from current comment, if any. + checkedBy := "" + check, ok := checks[manualRule.Description] + if ok { + checkedBy = check.checkedBy + } + + commentContent.ManualRules = append( + commentContent.ManualRules, + ManualContent{ + Description: manualRule.Description, + ConditionDetails: ifDetails.String(), + CheckedBy: checkedBy, + Teams: manualRule.Teams, + }, + ) + + // If this check is the special one, store its state in the dedicated var. + if manualRule.Description == config.ForceSkipDescription { + if checkedBy != "" { + commentContent.ForceSkip = true + } + } else if checkedBy == "" { + // Or if its a normal check, just verify if it was checked by someone. + commentContent.ManualAllSatisfied = false + } + } + + // Logs results or write them in bot PR comment. + if gh.DryRun { + logMutex.Lock() + logResults(gh.Logger, pr.GetNumber(), commentContent) + logMutex.Unlock() + } else { + if err := updatePullRequest(gh, pr, commentContent); err != nil { + gh.Logger.Errorf("unable to update pull request: %v", err) + failed.Store(true) + } + } + }(pr) + } + wg.Wait() + + if failed.Load() { + return errors.New("error occurred while processing pull requests") + } + + return nil +} + +// logResults is called in dry-run mode and outputs the status of each check +// and a conclusion. +func logResults(logger logger.Logger, prNum int, commentContent CommentContent) { + logger.Infof("Pull request #%d requirements", prNum) + if len(commentContent.AutoRules) > 0 { + logger.Infof("Automated Checks:") + } + + for _, rule := range commentContent.AutoRules { + status := utils.Fail + if rule.Satisfied { + status = utils.Success + } + logger.Infof("%s %s", status, rule.Description) + logger.Debugf("If:\n%s", rule.ConditionDetails) + logger.Debugf("Then:\n%s", rule.RequirementDetails) + } + + if len(commentContent.ManualRules) > 0 { + logger.Infof("Manual Checks:") + } + + for _, rule := range commentContent.ManualRules { + status := utils.Fail + checker := "any user with comment edit permission" + if rule.CheckedBy != "" { + status = utils.Success + } + if len(rule.Teams) == 0 { + checker = fmt.Sprintf("a member of one of these teams: %s", strings.Join(rule.Teams, ", ")) + } + logger.Infof("%s %s", status, rule.Description) + logger.Debugf("If:\n%s", rule.ConditionDetails) + logger.Debugf("Can be checked by %s", checker) + } + + logger.Infof("Conclusion:") + + if commentContent.AutoAllSatisfied { + logger.Infof("%s All automated checks are satisfied", utils.Success) + } else { + logger.Infof("%s Some automated checks are not satisfied", utils.Fail) + } + + if commentContent.ManualAllSatisfied { + logger.Infof("%s All manual checks are satisfied\n", utils.Success) + } else { + logger.Infof("%s Some manual checks are not satisfied\n", utils.Fail) + } + + if commentContent.ForceSkip { + logger.Infof("%s Bot checks are force skipped\n", utils.Success) + } +} diff --git a/contribs/github-bot/internal/check/cmd.go b/contribs/github-bot/internal/check/cmd.go new file mode 100644 index 00000000000..7ea6c02795b --- /dev/null +++ b/contribs/github-bot/internal/check/cmd.go @@ -0,0 +1,131 @@ +package check + +import ( + "context" + "flag" + "fmt" + "os" + "time" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/sethvargo/go-githubactions" +) + +type checkFlags struct { + Owner string + Repo string + PRAll bool + PRNums utils.PRList + Verbose *bool + DryRun bool + Timeout time.Duration + flagSet *flag.FlagSet +} + +func NewCheckCmd(verbose *bool) *commands.Command { + flags := &checkFlags{Verbose: verbose} + + return commands.NewCommand( + commands.Metadata{ + Name: "check", + ShortUsage: "github-bot check [flags]", + ShortHelp: "checks requirements for a pull request to be merged", + LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in ./internal/config/config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.", + }, + flags, + func(_ context.Context, _ []string) error { + flags.validateFlags() + return execCheck(flags) + }, + ) +} + +func (flags *checkFlags) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &flags.Owner, + "owner", + "", + "owner of the repo to process, if empty, will be retrieved from GitHub Actions context", + ) + + fs.StringVar( + &flags.Repo, + "repo", + "", + "repo to process, if empty, will be retrieved from GitHub Actions context", + ) + + fs.BoolVar( + &flags.PRAll, + "pr-all", + false, + "process all opened pull requests", + ) + + fs.TextVar( + &flags.PRNums, + "pr-numbers", + utils.PRList(nil), + "pull request(s) to process, must be a comma separated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrieved from GitHub Actions context", + ) + + fs.BoolVar( + &flags.DryRun, + "dry-run", + false, + "print if pull request requirements are satisfied without updating anything on GitHub", + ) + + fs.DurationVar( + &flags.Timeout, + "timeout", + 0, + "timeout after which the bot execution is interrupted", + ) + + flags.flagSet = fs +} + +func (flags *checkFlags) validateFlags() { + // Helper to display an error + usage message before exiting. + errorUsage := func(err string) { + fmt.Fprintf(flags.flagSet.Output(), "Error: %s\n\n", err) + flags.flagSet.Usage() + os.Exit(1) + } + + // Check if flags are coherent. + if flags.PRAll && len(flags.PRNums) != 0 { + errorUsage("You can specify only one of the '-pr-all' and '-pr-numbers' flags.") + } + + // If one of these values is empty, it must be retrieved + // from GitHub Actions context. + if flags.Owner == "" || flags.Repo == "" || (len(flags.PRNums) == 0 && !flags.PRAll) { + actionCtx, err := githubactions.Context() + if err != nil { + errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v.", err)) + } + + if flags.Owner == "" { + if flags.Owner, _ = actionCtx.Repo(); flags.Owner == "" { + errorUsage("Unable to retrieve owner from GitHub Actions context, you may want to set it using -onwer flag.") + } + } + if flags.Repo == "" { + if _, flags.Repo = actionCtx.Repo(); flags.Repo == "" { + errorUsage("Unable to retrieve repo from GitHub Actions context, you may want to set it using -repo flag.") + } + } + + if len(flags.PRNums) == 0 && !flags.PRAll { + prNum, err := utils.GetPRNumFromActionsCtx(actionCtx) + if err != nil { + errorUsage(fmt.Sprintf("Unable to retrieve pull request number from GitHub Actions context: %s\nYou may want to set it using -pr-numbers flag.", err.Error())) + } + + flags.PRNums = utils.PRList{prNum} + } + } +} diff --git a/contribs/github-bot/internal/check/comment.go b/contribs/github-bot/internal/check/comment.go new file mode 100644 index 00000000000..d2b386cfa2e --- /dev/null +++ b/contribs/github-bot/internal/check/comment.go @@ -0,0 +1,295 @@ +package check + +import ( + "bytes" + _ "embed" + "errors" + "fmt" + "regexp" + "strings" + "text/template" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/config" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/sethvargo/go-githubactions" +) + +//go:embed comment.tmpl +var tmplString string // Embed template used for comment generation. + +var errTriggeredByBot = errors.New("event triggered by bot") + +// Compile regex only once. +var ( + // Regex for capturing the entire line of a manual check. + manualCheckLine = regexp.MustCompile(`(?m:^- \[([ xX])\] (.+?)(?: \(checked by @([A-Za-z0-9-]+)\))?$)`) + // Regex for capturing only the checkboxes. + checkboxes = regexp.MustCompile(`(?m:^- \[[ xX]\])`) + // Regex used to capture markdown links. + markdownLink = regexp.MustCompile(`\[(.*)\]\([^)]*\)`) +) + +// These structures contain the necessary information to generate +// the bot's comment from the template file. +type AutoContent struct { + Description string + Satisfied bool + ConditionDetails string + RequirementDetails string +} +type ManualContent struct { + Description string + CheckedBy string + ConditionDetails string + Teams []string +} +type CommentContent struct { + AutoRules []AutoContent + ManualRules []ManualContent + AutoAllSatisfied bool + ManualAllSatisfied bool + ForceSkip bool +} + +type manualCheckDetails struct { + status string + checkedBy string +} + +// getCommentManualChecks parses the bot comment to get the checkbox status, +// the check description and the username who checked it. +func getCommentManualChecks(commentBody string) map[string]manualCheckDetails { + checks := make(map[string]manualCheckDetails) + + // For each line that matches the "Manual check" regex. + for _, match := range manualCheckLine.FindAllStringSubmatch(commentBody, -1) { + description := match[2] + status := strings.ToLower(match[1]) // if X captured, convert it to x. + checkedBy := "" + if len(match) > 3 { + checkedBy = match[3] + } + + checks[description] = manualCheckDetails{status: status, checkedBy: checkedBy} + } + + return checks +} + +// handleCommentUpdate checks if: +// - the current run was triggered by GitHub Actions +// - the triggering event is an edit of the bot comment +// - the comment was not edited by the bot itself (prevent infinite loop) +// - the comment change is only a checkbox being checked or unckecked (or restore it) +// - the actor / comment editor has permission to modify this checkbox (or restore it) +func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubContext) error { + // Ignore if it's not a comment related event. + if actionCtx.EventName != utils.EventIssueComment { + gh.Logger.Debugf("Event is not issue comment related (%s)", actionCtx.EventName) + return nil + } + + // Ignore if the action type is not deleted or edited. + actionType, ok := actionCtx.Event["action"].(string) + if !ok { + return errors.New("unable to get type on issue comment event") + } + + if actionType != "deleted" && actionType != "edited" { + return nil + } + + // Get PR number from GitHub Actions context. + prNumFloat, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64) + if !ok || prNumFloat <= 0 { + return errors.New("unable to get issue number on issue comment event") + } + prNum := int(prNumFloat) + + // Ignore if this comment update is not related to an opened PR. + if _, err := gh.GetOpenedPullRequest(prNum); err != nil { + return nil // May come from an issue or a closed PR + } + + // Return if comment was edited by bot (current authenticated user). + authUser, _, err := gh.Client.Users.Get(gh.Ctx, "") + if err != nil { + return fmt.Errorf("unable to get authenticated user: %w", err) + } + + if actionCtx.Actor == authUser.GetLogin() { + gh.Logger.Debugf("Prevent infinite loop if the bot comment was edited by the bot itself") + return errTriggeredByBot + } + + // Get login of the author of the edited comment. + login, ok := utils.IndexMap(actionCtx.Event, "comment", "user", "login").(string) + if !ok { + return errors.New("unable to get comment user login on issue comment event") + } + + // If the author is not the bot, return. + if login != authUser.GetLogin() { + return nil + } + + // Get comment updated body. + current, ok := utils.IndexMap(actionCtx.Event, "comment", "body").(string) + if !ok { + return errors.New("unable to get comment body on issue comment event") + } + + // Get comment previous body. + previous, ok := utils.IndexMap(actionCtx.Event, "changes", "body", "from").(string) + if !ok { + return errors.New("unable to get changes body content on issue comment event") + } + + // Check if change is only a checkbox being checked or unckecked. + if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") { + // If not, restore previous comment body. + if !gh.DryRun { + gh.SetBotComment(previous, prNum) + } + return errors.New("bot comment edited outside of checkboxes") + } + + // Check if actor / comment editor has permission to modify changed boxes. + currentChecks := getCommentManualChecks(current) + previousChecks := getCommentManualChecks(previous) + edited := "" + for key := range currentChecks { + // If there is no diff for this check, ignore it. + if currentChecks[key].status == previousChecks[key].status { + continue + } + + // Get teams allowed to edit this box from config. + var teams []string + found := false + _, manualRules := config.Config(gh) + + for _, manualRule := range manualRules { + if manualRule.Description == key { + found = true + teams = manualRule.Teams + } + } + + // If rule were not found, return to reprocess the bot comment entirely + // (maybe bot config was updated since last run?). + if !found { + gh.Logger.Debugf("Updated rule not found in config: %s", key) + return nil + } + + // If teams specified in rule, check if actor is a member of one of them. + if len(teams) > 0 { + if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed to check the boxes. + if !gh.DryRun { + gh.SetBotComment(previous, prNum) // Then restore previous state. + } + return errors.New("checkbox edited by a user not allowed to") + } + } + + // This regex capture only the line of the current check. + specificManualCheck := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key].status, regexp.QuoteMeta(key))) + + // If the box is checked, append the username of the user who checked it. + if strings.TrimSpace(currentChecks[key].status) == "x" { + replacement := fmt.Sprintf("- [%s] %s (checked by @%s)", currentChecks[key].status, key, actionCtx.Actor) + edited = specificManualCheck.ReplaceAllString(current, replacement) + } else { + // Else, remove the username of the user. + replacement := fmt.Sprintf("- [%s] %s", currentChecks[key].status, key) + edited = specificManualCheck.ReplaceAllString(current, replacement) + } + } + + // Update comment with username. + if edited != "" && !gh.DryRun { + gh.SetBotComment(edited, prNum) + gh.Logger.Debugf("Comment manual checks updated successfully") + } + + return nil +} + +// generateComment generates a comment using the template file and the +// content passed as parameter. +func generateComment(content CommentContent) (string, error) { + // Custom function to strip markdown links. + funcMap := template.FuncMap{ + "stripLinks": func(input string) string { + return markdownLink.ReplaceAllString(input, "$1") + }, + } + + // Bind markdown stripping function to template generator. + tmpl, err := template.New("comment").Funcs(funcMap).Parse(tmplString) + if err != nil { + return "", fmt.Errorf("unable to init template: %w", err) + } + + // Generate bot comment using template file. + var commentBytes bytes.Buffer + if err := tmpl.Execute(&commentBytes, content); err != nil { + return "", fmt.Errorf("unable to execute template: %w", err) + } + + return commentBytes.String(), nil +} + +// updatePullRequest updates or creates both the bot comment and the commit status. +func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content CommentContent) error { + // Generate comment text content. + commentText, err := generateComment(content) + if err != nil { + return fmt.Errorf("unable to generate comment on PR %d: %w", pr.GetNumber(), err) + } + + // Update comment on pull request. + comment, err := gh.SetBotComment(commentText, pr.GetNumber()) + if err != nil { + return fmt.Errorf("unable to update comment on PR %d: %w", pr.GetNumber(), err) + } else { + gh.Logger.Infof("Comment successfully updated on PR %d", pr.GetNumber()) + } + + // Prepare commit status content. + var ( + context = "Merge Requirements" + targetURL = comment.GetHTMLURL() + state = "success" + description = "All requirements are satisfied." + ) + + if content.ForceSkip { + description = "Bot checks are skipped for this PR." + } else if !content.AutoAllSatisfied || !content.ManualAllSatisfied { + state = "failure" + description = "Some requirements are not satisfied yet. See bot comment." + } + + // Update or create commit status. + if _, _, err := gh.Client.Repositories.CreateStatus( + gh.Ctx, + gh.Owner, + gh.Repo, + pr.GetHead().GetSHA(), + &github.RepoStatus{ + Context: &context, + State: &state, + TargetURL: &targetURL, + Description: &description, + }); err != nil { + return fmt.Errorf("unable to create status on PR %d: %w", pr.GetNumber(), err) + } else { + gh.Logger.Infof("Commit status successfully updated on PR %d", pr.GetNumber()) + } + + return nil +} diff --git a/contribs/github-bot/internal/check/comment.tmpl b/contribs/github-bot/internal/check/comment.tmpl new file mode 100644 index 00000000000..d9b633a69d5 --- /dev/null +++ b/contribs/github-bot/internal/check/comment.tmpl @@ -0,0 +1,70 @@ +#### 🛠 PR Checks Summary +{{ if and .AutoRules (not .AutoAllSatisfied) }}{{ range .AutoRules }}{{ if not .Satisfied }} 🔴 {{ .Description }} +{{end}}{{end}}{{ else }}All **Automated Checks** passed. ✅{{end}} + +##### Manual Checks (for Reviewers): +{{ if .ManualRules }}{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }} +{{ end }}{{ else }}*No manual checks match this pull request.*{{ end }} + +
Read More + +🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers. + +##### ✅ Automated Checks (for Contributors): +{{ if .AutoRules }}{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }} +{{ end }}{{ else }}*No automated checks match this pull request.*{{ end }} + +##### ☑️ Contributor Actions: +1. Fix any issues flagged by automated checks. +2. Follow the Contributor Checklist to ensure your PR is ready for review. + - Add new tests, or document why they are unnecessary. + - Provide clear examples/screenshots, if necessary. + - Update documentation, if required. + - Ensure no breaking changes, or include `BREAKING CHANGE` notes. + - Link related issues/PRs, where applicable. + +##### ☑️ Reviewer Actions: +1. Complete manual checks for the PR, including the guidelines and additional checks if applicable. + +##### 📚 Resources: +- [Report a bug with the bot](https://github.com/gnolang/gno/issues/3238). +- [View the bot’s configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go). + +{{ if or .AutoRules .ManualRules }}
Debug
+{{ if .AutoRules }}
Automated Checks
+{{ range .AutoRules }} +
{{ .Description | stripLinks }}
+ +### If +``` +{{ .ConditionDetails | stripLinks }} +``` +### Then +``` +{{ .RequirementDetails | stripLinks }} +``` +
+{{ end }} +
+{{ end }} + +{{ if .ManualRules }}
Manual Checks
+{{ range .ManualRules }} +
{{ .Description | stripLinks }}
+ +### If +``` +{{ .ConditionDetails }} +``` +### Can be checked by +{{range $item := .Teams }} - team {{ $item | stripLinks }} +{{ else }} +- Any user with comment edit permission +{{end}} +
+{{ end }} +
+{{ end }} +
+{{ end }} +
diff --git a/contribs/github-bot/internal/check/comment_test.go b/contribs/github-bot/internal/check/comment_test.go new file mode 100644 index 00000000000..29886f80f43 --- /dev/null +++ b/contribs/github-bot/internal/check/comment_test.go @@ -0,0 +1,188 @@ +package check + +import ( + "context" + "fmt" + "regexp" + "strings" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/sethvargo/go-githubactions" + "github.com/stretchr/testify/assert" +) + +func TestGeneratedComment(t *testing.T) { + t.Parallel() + + autoCheckSuccessLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.Success)) + autoCheckFailLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.Fail)) + + content := CommentContent{} + autoRules := []AutoContent{ + {Description: "Test automatic 1", Satisfied: false}, + {Description: "Test automatic 2", Satisfied: false}, + {Description: "Test automatic 3", Satisfied: true}, + {Description: "Test automatic 4", Satisfied: true}, + {Description: "Test automatic 5", Satisfied: false}, + } + manualRules := []ManualContent{ + {Description: "Test manual 1", CheckedBy: "user-1"}, + {Description: "Test manual 2", CheckedBy: ""}, + {Description: "Test manual 3", CheckedBy: ""}, + {Description: "Test manual 4", CheckedBy: "user-4"}, + {Description: "Test manual 5", CheckedBy: "user-5"}, + } + + commentText, err := generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) + assert.True(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should contains automated check placeholder") + assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.True(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") + + content.AutoRules = autoRules + content.AutoAllSatisfied = true + commentText, err = generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) + assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") + assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.True(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") + assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check") + assert.Equal(t, 3, len(autoCheckFailLine.FindAllStringSubmatch(commentText, -1)), "wrong number of failed automatic check") + + content.AutoAllSatisfied = false + commentText, err = generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) + assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") + assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.False(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") + assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check") + assert.Equal(t, 3+3, len(autoCheckFailLine.FindAllStringSubmatch(commentText, -1)), "wrong number of failed automatic check") + + content.ManualRules = manualRules + commentText, err = generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) + assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") + assert.False(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should not contains manual check placeholder") + assert.False(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") + + manualChecks := getCommentManualChecks(commentText) + assert.Equal(t, len(manualChecks), len(manualRules), "wrong number of manual checks found") + for _, rule := range manualRules { + val, ok := manualChecks[rule.Description] + assert.True(t, ok, "manual check should exist") + if rule.CheckedBy == "" { + assert.Equal(t, " ", val.status, "manual rule should not be checked") + } else { + assert.Equal(t, "x", val.status, "manual rule should be checked") + } + assert.Equal(t, rule.CheckedBy, val.checkedBy, "invalid username found for CheckedBy") + } +} + +func setValue(t *testing.T, m map[string]any, value any, keys ...string) map[string]any { + t.Helper() + + if len(keys) > 1 { + currMap, ok := m[keys[0]].(map[string]any) + if !ok { + currMap = map[string]any{} + } + m[keys[0]] = setValue(t, currMap, value, keys[1:]...) + } else if len(keys) == 1 { + m[keys[0]] = value + } + + return m +} + +func TestCommentUpdateHandler(t *testing.T) { + t.Parallel() + + const ( + user = "user" + bot = "bot" + ) + actionCtx := &githubactions.GitHubContext{ + Event: make(map[string]any), + } + + mockOptions := []mock.MockBackendOption{} + newGHClient := func() *client.GitHub { + return &client.GitHub{ + Client: github.NewClient(mock.NewMockedHTTPClient(mockOptions...)), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + } + gh := newGHClient() + + // Exit without error because EventName is empty. + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.EventName = utils.EventIssueComment + + // Exit with error because Event.action is not set. + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event["action"] = "" + + // Exit without error because Event.action is set but not 'deleted'. + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event["action"] = "deleted" + + // Exit with error because Event.issue.number is not set. + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, float64(42), "issue", "number") + + // Exit without error can't get open pull request associated with PR num. + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + mockOptions = append(mockOptions, mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/42", + Method: "GET", + }, + github.PullRequest{Number: github.Int(42), State: github.String(utils.PRStateOpen)}, + )) + gh = newGHClient() + + // Exit with error because mock not setup to return authUser. + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + mockOptions = append(mockOptions, mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/user", + Method: "GET", + }, + github.User{Login: github.String(bot)}, + )) + gh = newGHClient() + actionCtx.Actor = bot + + // Exit with error because authUser and action actor is the same user. + assert.ErrorIs(t, handleCommentUpdate(gh, actionCtx), errTriggeredByBot) + actionCtx.Actor = user + + // Exit with error because Event.comment.user.login is not set. + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, user, "comment", "user", "login") + + // Exit without error because comment author is not the bot. + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, bot, "comment", "user", "login") + + // Exit with error because Event.comment.body is not set. + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "comment", "body") + + // Exit with error because Event.changes.body.from is not set. + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, "updated_body", "changes", "body", "from") + + // Exit with error because checkboxes are differents. + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "changes", "body", "from") + + assert.Nil(t, handleCommentUpdate(gh, actionCtx)) +} diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go new file mode 100644 index 00000000000..a5c875e0d22 --- /dev/null +++ b/contribs/github-bot/internal/client/client.go @@ -0,0 +1,315 @@ +package client + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" +) + +// PageSize is the number of items to load for each iteration when fetching a list. +const PageSize = 100 + +var ErrBotCommentNotFound = errors.New("bot comment not found") + +// GitHub contains everything necessary to interact with the GitHub API, +// including the client, a context (which must be passed with each request), +// a logger, etc. This object will be passed to each condition or requirement +// that requires fetching additional information or modifying things on GitHub. +// The object also provides methods for performing more complex operations than +// a simple API call. +type GitHub struct { + Client *github.Client + Ctx context.Context + DryRun bool + Logger logger.Logger + Owner string + Repo string +} + +type Config struct { + Owner string + Repo string + Verbose bool + DryRun bool +} + +// GetBotComment retrieves the bot's (current user) comment on provided PR number. +func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { + // List existing comments. + const ( + sort = "created" + direction = "desc" + ) + + // Get current user (bot). + currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") + if err != nil { + return nil, fmt.Errorf("unable to get current user: %w", err) + } + + // Pagination option. + opts := &github.IssueListCommentsOptions{ + Sort: github.String(sort), + Direction: github.String(direction), + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + + for { + comments, response, err := gh.Client.Issues.ListComments( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + return nil, fmt.Errorf("unable to list comments for PR %d: %w", prNum, err) + } + + // Get the comment created by current user. + for _, comment := range comments { + if comment.GetUser().GetLogin() == currentUser.GetLogin() { + return comment, nil + } + } + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return nil, ErrBotCommentNotFound +} + +// SetBotComment creates a bot's comment on the provided PR number +// or updates it if it already exists. +func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, error) { + // Prevent updating anything in dry run mode. + if gh.DryRun { + return nil, errors.New("should not write bot comment in dry run mode") + } + + // Create bot comment if it does not already exist. + comment, err := gh.GetBotComment(prNum) + if errors.Is(err, ErrBotCommentNotFound) { + newComment, _, err := gh.Client.Issues.CreateComment( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + &github.IssueComment{Body: &body}, + ) + if err != nil { + return nil, fmt.Errorf("unable to create bot comment for PR %d: %w", prNum, err) + } + return newComment, nil + } else if err != nil { + return nil, fmt.Errorf("unable to get bot comment: %w", err) + } + + comment.Body = &body + editComment, _, err := gh.Client.Issues.EditComment( + gh.Ctx, + gh.Owner, + gh.Repo, + comment.GetID(), + comment, + ) + if err != nil { + return nil, fmt.Errorf("unable to edit bot comment with ID %d: %w", comment.GetID(), err) + } + + return editComment, nil +} + +func (gh *GitHub) GetOpenedPullRequest(prNum int) (*github.PullRequest, error) { + pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum) + if err != nil { + return nil, fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err) + } else if pr.GetState() != utils.PRStateOpen { + return nil, fmt.Errorf("pull request %d is not opened, actual state: %s", prNum, pr.GetState()) + } + + return pr, nil +} + +// ListTeamMembers lists the members of the specified team. +func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) { + var ( + allMembers []*github.User + opts = &github.TeamListTeamMembersOptions{ + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + ) + + for { + members, response, err := gh.Client.Teams.ListTeamMembersBySlug( + gh.Ctx, + gh.Owner, + team, + opts, + ) + if err != nil { + return nil, fmt.Errorf("unable to list members for team %s: %w", team, err) + } + + allMembers = append(allMembers, members...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allMembers, nil +} + +// IsUserInTeams checks if the specified user is a member of any of the +// provided teams. +func (gh *GitHub) IsUserInTeams(user string, teams []string) bool { + for _, team := range teams { + teamMembers, err := gh.ListTeamMembers(team) + if err != nil { + gh.Logger.Errorf("unable to check if user %s in team %s", user, team) + continue + } + + for _, member := range teamMembers { + if member.GetLogin() == user { + return true + } + } + } + + return false +} + +// ListPRReviewers returns the list of reviewers for the specified PR number. +func (gh *GitHub) ListPRReviewers(prNum int) (*github.Reviewers, error) { + var ( + allReviewers = &github.Reviewers{} + opts = &github.ListOptions{ + PerPage: PageSize, + } + ) + + for { + reviewers, response, err := gh.Client.PullRequests.ListReviewers( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + return nil, fmt.Errorf("unable to list reviewers for PR %d: %w", prNum, err) + } + + allReviewers.Teams = append(allReviewers.Teams, reviewers.Teams...) + allReviewers.Users = append(allReviewers.Users, reviewers.Users...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allReviewers, nil +} + +// ListPRReviewers returns the list of reviews for the specified PR number. +func (gh *GitHub) ListPRReviews(prNum int) ([]*github.PullRequestReview, error) { + var ( + allReviews []*github.PullRequestReview + opts = &github.ListOptions{ + PerPage: PageSize, + } + ) + + for { + reviews, response, err := gh.Client.PullRequests.ListReviews( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + return nil, fmt.Errorf("unable to list reviews for PR %d: %w", prNum, err) + } + + allReviews = append(allReviews, reviews...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allReviews, nil +} + +// ListPR returns the list of pull requests in the specified state. +func (gh *GitHub) ListPR(state string) ([]*github.PullRequest, error) { + var prs []*github.PullRequest + + opts := &github.PullRequestListOptions{ + State: state, + Sort: "updated", + Direction: "desc", + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + + for { + prsPage, response, err := gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) + if err != nil { + return nil, fmt.Errorf("unable to list pull requests with state %s: %w", state, err) + } + + prs = append(prs, prsPage...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return prs, nil +} + +// New initializes the API client, the logger, and creates an instance of GitHub. +func New(ctx context.Context, cfg *Config) (*GitHub, error) { + gh := &GitHub{ + Ctx: ctx, + Owner: cfg.Owner, + Repo: cfg.Repo, + DryRun: cfg.DryRun, + } + + // Detect if the current process was launched by a GitHub Action and return + // a logger suitable for terminal output or the GitHub Actions web interface. + gh.Logger = logger.NewLogger(cfg.Verbose) + + // Retrieve GitHub API token from env. + token, set := os.LookupEnv("GITHUB_TOKEN") + if !set { + return nil, errors.New("GITHUB_TOKEN is not set in env") + } + + // Init GitHub API client using token. + gh.Client = github.NewClient(nil).WithAuthToken(token) + + return gh, nil +} diff --git a/contribs/github-bot/internal/conditions/assignee.go b/contribs/github-bot/internal/conditions/assignee.go new file mode 100644 index 00000000000..7024259909c --- /dev/null +++ b/contribs/github-bot/internal/conditions/assignee.go @@ -0,0 +1,66 @@ +package conditions + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Assignee Condition. +type assignee struct { + user string +} + +var _ Condition = &assignee{} + +func (a *assignee) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A pull request assignee is user: %s", a.user) + + for _, assignee := range pr.Assignees { + if a.user == assignee.GetLogin() { + return utils.AddStatusNode(true, detail, details) + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func Assignee(user string) Condition { + return &assignee{user: user} +} + +// AssigneeInTeam Condition. +type assigneeInTeam struct { + gh *client.GitHub + team string +} + +var _ Condition = &assigneeInTeam{} + +func (a *assigneeInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A pull request assignee is a member of the team: %s", a.team) + + teamMembers, err := a.gh.ListTeamMembers(a.team) + if err != nil { + a.gh.Logger.Errorf("unable to check if assignee is in team %s: %v", a.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, member := range teamMembers { + for _, assignee := range pr.Assignees { + if member.GetLogin() == assignee.GetLogin() { + return utils.AddStatusNode(true, fmt.Sprintf("%s (member: %s)", detail, member.GetLogin()), details) + } + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func AssigneeInTeam(gh *client.GitHub, team string) Condition { + return &assigneeInTeam{gh: gh, team: team} +} diff --git a/contribs/github-bot/internal/conditions/assignee_test.go b/contribs/github-bot/internal/conditions/assignee_test.go new file mode 100644 index 00000000000..9207e4604b7 --- /dev/null +++ b/contribs/github-bot/internal/conditions/assignee_test.go @@ -0,0 +1,100 @@ +package conditions + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestAssignee(t *testing.T) { + t.Parallel() + + assignees := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + assignees []*github.User + isMet bool + }{ + {"empty assignee list", "user", []*github.User{}, false}, + {"assignee list contains user", "user", assignees, true}, + {"assignee list doesn't contain user", "user2", assignees, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{Assignees: testCase.assignees} + details := treeprint.New() + condition := Assignee(testCase.user) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} + +func TestAssigneeInTeam(t *testing.T) { + t.Parallel() + + members := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + members []*github.User + isMet bool + }{ + {"empty assignee list", "user", []*github.User{}, false}, + {"assignee list contains user", "user", members, true}, + {"assignee list doesn't contain user", "user2", members, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/orgs/teams/team/members", + Method: "GET", + }, + testCase.members, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{ + Assignees: []*github.User{ + {Login: github.String(testCase.user)}, + }, + } + details := treeprint.New() + condition := AssigneeInTeam(gh, "team") + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/author.go b/contribs/github-bot/internal/conditions/author.go new file mode 100644 index 00000000000..9052f781bd5 --- /dev/null +++ b/contribs/github-bot/internal/conditions/author.go @@ -0,0 +1,60 @@ +package conditions + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Author Condition. +type author struct { + user string +} + +var _ Condition = &author{} + +func (a *author) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + a.user == pr.GetUser().GetLogin(), + fmt.Sprintf("Pull request author is user: %v", a.user), + details, + ) +} + +func Author(user string) Condition { + return &author{user: user} +} + +// AuthorInTeam Condition. +type authorInTeam struct { + gh *client.GitHub + team string +} + +var _ Condition = &authorInTeam{} + +func (a *authorInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("Pull request author is a member of the team: %s", a.team) + + teamMembers, err := a.gh.ListTeamMembers(a.team) + if err != nil { + a.gh.Logger.Errorf("unable to check if author is in team %s: %v", a.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, member := range teamMembers { + if member.GetLogin() == pr.GetUser().GetLogin() { + return utils.AddStatusNode(true, detail, details) + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func AuthorInTeam(gh *client.GitHub, team string) Condition { + return &authorInTeam{gh: gh, team: team} +} diff --git a/contribs/github-bot/internal/conditions/author_test.go b/contribs/github-bot/internal/conditions/author_test.go new file mode 100644 index 00000000000..c5836f1ea76 --- /dev/null +++ b/contribs/github-bot/internal/conditions/author_test.go @@ -0,0 +1,93 @@ +package conditions + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestAuthor(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + user string + author string + isMet bool + }{ + {"author match", "user", "user", true}, + {"author doesn't match", "user", "author", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.author)}, + } + details := treeprint.New() + condition := Author(testCase.user) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} + +func TestAuthorInTeam(t *testing.T) { + t.Parallel() + + members := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + members []*github.User + isMet bool + }{ + {"empty member list", "user", []*github.User{}, false}, + {"member list contains user", "user", members, true}, + {"member list doesn't contain user", "user2", members, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/orgs/teams/team/members", + Method: "GET", + }, + testCase.members, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.user)}, + } + details := treeprint.New() + condition := AuthorInTeam(gh, "team") + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/boolean.go b/contribs/github-bot/internal/conditions/boolean.go new file mode 100644 index 00000000000..2fa3a25f7ac --- /dev/null +++ b/contribs/github-bot/internal/conditions/boolean.go @@ -0,0 +1,98 @@ +package conditions + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// And Condition. +type and struct { + conditions []Condition +} + +var _ Condition = &and{} + +func (a *and) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + met := utils.Success + branch := details.AddBranch("") + + for _, condition := range a.conditions { + if !condition.IsMet(pr, branch) { + met = utils.Fail + // We don't break here because we need to call IsMet on all conditions + // to populate the details tree. + } + } + + branch.SetValue(fmt.Sprintf("%s And", met)) + + return (met == utils.Success) +} + +func And(conditions ...Condition) Condition { + if len(conditions) < 2 { + panic("You should pass at least 2 conditions to And()") + } + + return &and{conditions} +} + +// Or Condition. +type or struct { + conditions []Condition +} + +var _ Condition = &or{} + +func (o *or) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + met := utils.Fail + branch := details.AddBranch("") + + for _, condition := range o.conditions { + if condition.IsMet(pr, branch) { + met = utils.Success + // We don't break here because we need to call IsMet on all conditions + // to populate the details tree. + } + } + + branch.SetValue(fmt.Sprintf("%s Or", met)) + + return (met == utils.Success) +} + +func Or(conditions ...Condition) Condition { + if len(conditions) < 2 { + panic("You should pass at least 2 conditions to Or()") + } + + return &or{conditions} +} + +// Not Condition. +type not struct { + cond Condition +} + +var _ Condition = ¬{} + +func (n *not) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + met := n.cond.IsMet(pr, details) + node := details.FindLastNode() + + if met { + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Fail, node.(*treeprint.Node).Value.(string))) + } else { + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Success, node.(*treeprint.Node).Value.(string))) + } + + return !met +} + +func Not(cond Condition) Condition { + return ¬{cond} +} diff --git a/contribs/github-bot/internal/conditions/boolean_test.go b/contribs/github-bot/internal/conditions/boolean_test.go new file mode 100644 index 00000000000..52f028cf2b4 --- /dev/null +++ b/contribs/github-bot/internal/conditions/boolean_test.go @@ -0,0 +1,96 @@ +package conditions + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestAnd(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + conditions []Condition + isMet bool + }{ + {"and is true", []Condition{Always(), Always()}, true}, + {"and is false", []Condition{Always(), Always(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + condition := And(testCase.conditions...) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} + +func TestAndPanic(t *testing.T) { + t.Parallel() + + assert.Panics(t, func() { And(Always()) }, "and constructor should panic if less than 2 conditions are provided") +} + +func TestOr(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + conditions []Condition + isMet bool + }{ + {"or is true", []Condition{Never(), Always()}, true}, + {"or is false", []Condition{Never(), Never(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + condition := Or(testCase.conditions...) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} + +func TestOrPanic(t *testing.T) { + t.Parallel() + + assert.Panics(t, func() { Or(Always()) }, "or constructor should panic if less than 2 conditions are provided") +} + +func TestNot(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + condition Condition + isMet bool + }{ + {"not is true", Never(), true}, + {"not is false", Always(), false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + condition := Not(testCase.condition) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/branch.go b/contribs/github-bot/internal/conditions/branch.go new file mode 100644 index 00000000000..6977d633d98 --- /dev/null +++ b/contribs/github-bot/internal/conditions/branch.go @@ -0,0 +1,49 @@ +package conditions + +import ( + "fmt" + "regexp" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// BaseBranch Condition. +type baseBranch struct { + pattern *regexp.Regexp +} + +var _ Condition = &baseBranch{} + +func (b *baseBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + b.pattern.MatchString(pr.GetBase().GetRef()), + fmt.Sprintf("The base branch matches this pattern: %s", b.pattern.String()), + details, + ) +} + +func BaseBranch(pattern string) Condition { + return &baseBranch{pattern: regexp.MustCompile(pattern)} +} + +// HeadBranch Condition. +type headBranch struct { + pattern *regexp.Regexp +} + +var _ Condition = &headBranch{} + +func (h *headBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + h.pattern.MatchString(pr.GetHead().GetRef()), + fmt.Sprintf("The head branch matches this pattern: %s", h.pattern.String()), + details, + ) +} + +func HeadBranch(pattern string) Condition { + return &headBranch{pattern: regexp.MustCompile(pattern)} +} diff --git a/contribs/github-bot/internal/conditions/branch_test.go b/contribs/github-bot/internal/conditions/branch_test.go new file mode 100644 index 00000000000..81ed96f8314 --- /dev/null +++ b/contribs/github-bot/internal/conditions/branch_test.go @@ -0,0 +1,49 @@ +package conditions + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestHeadBaseBranch(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + pattern string + base string + isMet bool + }{ + {"perfectly match", "base", "base", true}, + {"prefix match", "^dev/", "dev/test-bot", true}, + {"prefix doesn't match", "^/test-bot", "dev/test-bot", false}, + {"suffix match", "/test-bot$", "dev/test-bot", true}, + {"suffix doesn't match", "dev/$", "dev/test-bot", false}, + {"doesn't match", "base", "notatall", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{ + Base: &github.PullRequestBranch{Ref: github.String(testCase.base)}, + Head: &github.PullRequestBranch{Ref: github.String(testCase.base)}, + } + conditions := []Condition{ + BaseBranch(testCase.pattern), + HeadBranch(testCase.pattern), + } + + for _, condition := range conditions { + details := treeprint.New() + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + } + }) + } +} diff --git a/contribs/github-bot/internal/conditions/condition.go b/contribs/github-bot/internal/conditions/condition.go new file mode 100644 index 00000000000..8c2fa5a2948 --- /dev/null +++ b/contribs/github-bot/internal/conditions/condition.go @@ -0,0 +1,12 @@ +package conditions + +import ( + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +type Condition interface { + // Check if the Condition is met and add the details + // to the tree passed as a parameter. + IsMet(pr *github.PullRequest, details treeprint.Tree) bool +} diff --git a/contribs/github-bot/internal/conditions/constant.go b/contribs/github-bot/internal/conditions/constant.go new file mode 100644 index 00000000000..26bbe9e8110 --- /dev/null +++ b/contribs/github-bot/internal/conditions/constant.go @@ -0,0 +1,34 @@ +package conditions + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Always Condition. +type always struct{} + +var _ Condition = &always{} + +func (*always) IsMet(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(true, "On every pull request", details) +} + +func Always() Condition { + return &always{} +} + +// Never Condition. +type never struct{} + +var _ Condition = &never{} + +func (*never) IsMet(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(false, "On no pull request", details) +} + +func Never() Condition { + return &never{} +} diff --git a/contribs/github-bot/internal/conditions/constant_test.go b/contribs/github-bot/internal/conditions/constant_test.go new file mode 100644 index 00000000000..92bbe9b318a --- /dev/null +++ b/contribs/github-bot/internal/conditions/constant_test.go @@ -0,0 +1,25 @@ +package conditions + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + "github.com/xlab/treeprint" +) + +func TestAlways(t *testing.T) { + t.Parallel() + + details := treeprint.New() + assert.True(t, Always().IsMet(nil, details), "condition should have a met status: true") + assert.True(t, utils.TestLastNodeStatus(t, true, details), "condition details should have a status: true") +} + +func TestNever(t *testing.T) { + t.Parallel() + + details := treeprint.New() + assert.False(t, Never().IsMet(nil, details), "condition should have a met status: false") + assert.True(t, utils.TestLastNodeStatus(t, false, details), "condition details should have a status: false") +} diff --git a/contribs/github-bot/internal/conditions/draft.go b/contribs/github-bot/internal/conditions/draft.go new file mode 100644 index 00000000000..2c263f2ae75 --- /dev/null +++ b/contribs/github-bot/internal/conditions/draft.go @@ -0,0 +1,21 @@ +package conditions + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Draft Condition. +type draft struct{} + +var _ Condition = &baseBranch{} + +func (*draft) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(pr.GetDraft(), "This pull request is a draft", details) +} + +func Draft() Condition { + return &draft{} +} diff --git a/contribs/github-bot/internal/conditions/draft_test.go b/contribs/github-bot/internal/conditions/draft_test.go new file mode 100644 index 00000000000..a31b4eaca4c --- /dev/null +++ b/contribs/github-bot/internal/conditions/draft_test.go @@ -0,0 +1,34 @@ +package conditions + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/stretchr/testify/assert" + "github.com/xlab/treeprint" +) + +func TestDraft(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + isMet bool + }{ + {"draft is true", true}, + {"draft is false", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{Draft: &testCase.isMet} + details := treeprint.New() + condition := Draft() + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/file.go b/contribs/github-bot/internal/conditions/file.go new file mode 100644 index 00000000000..e3854a7734a --- /dev/null +++ b/contribs/github-bot/internal/conditions/file.go @@ -0,0 +1,58 @@ +package conditions + +import ( + "fmt" + "regexp" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// FileChanged Condition. +type fileChanged struct { + gh *client.GitHub + pattern *regexp.Regexp +} + +var _ Condition = &fileChanged{} + +func (fc *fileChanged) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A changed file matches this pattern: %s", fc.pattern.String()) + opts := &github.ListOptions{ + PerPage: client.PageSize, + } + + for { + files, response, err := fc.gh.Client.PullRequests.ListFiles( + fc.gh.Ctx, + fc.gh.Owner, + fc.gh.Repo, + pr.GetNumber(), + opts, + ) + if err != nil { + fc.gh.Logger.Errorf("Unable to list changed files for PR %d: %v", pr.GetNumber(), err) + break + } + + for _, file := range files { + if fc.pattern.MatchString(file.GetFilename()) { + return utils.AddStatusNode(true, fmt.Sprintf("%s (filename: %s)", detail, file.GetFilename()), details) + } + } + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return utils.AddStatusNode(false, detail, details) +} + +func FileChanged(gh *client.GitHub, pattern string) Condition { + return &fileChanged{gh: gh, pattern: regexp.MustCompile(pattern)} +} diff --git a/contribs/github-bot/internal/conditions/file_test.go b/contribs/github-bot/internal/conditions/file_test.go new file mode 100644 index 00000000000..8571ffea7d0 --- /dev/null +++ b/contribs/github-bot/internal/conditions/file_test.go @@ -0,0 +1,68 @@ +package conditions + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestFileChanged(t *testing.T) { + t.Parallel() + + filenames := []*github.CommitFile{ + {Filename: github.String("foo")}, + {Filename: github.String("bar")}, + {Filename: github.String("baz")}, + } + + for _, testCase := range []struct { + name string + pattern string + files []*github.CommitFile + isMet bool + }{ + {"empty file list", "foo", []*github.CommitFile{}, false}, + {"file list contains exact match", "foo", filenames, true}, + {"file list contains prefix match", "^fo", filenames, true}, + {"file list contains prefix doesn't match", "^oo", filenames, false}, + {"file list contains suffix match", "oo$", filenames, true}, + {"file list contains suffix doesn't match", "fo$", filenames, false}, + {"file list doesn't contains match", "foobar", filenames, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/files", + Method: "GET", + }, + testCase.files, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + condition := FileChanged(gh, testCase.pattern) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/conditions/fork.go b/contribs/github-bot/internal/conditions/fork.go new file mode 100644 index 00000000000..72cbae12004 --- /dev/null +++ b/contribs/github-bot/internal/conditions/fork.go @@ -0,0 +1,27 @@ +package conditions + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// CreatedFromFork Condition. +type createdFromFork struct{} + +var _ Condition = &createdFromFork{} + +func (b *createdFromFork) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + pr.GetHead().GetRepo().GetFullName() != pr.GetBase().GetRepo().GetFullName(), + fmt.Sprintf("The pull request was created from a fork (head branch repo: %s)", pr.GetHead().GetRepo().GetFullName()), + details, + ) +} + +func CreatedFromFork() Condition { + return &createdFromFork{} +} diff --git a/contribs/github-bot/internal/conditions/fork_test.go b/contribs/github-bot/internal/conditions/fork_test.go new file mode 100644 index 00000000000..fe7e9a95bf1 --- /dev/null +++ b/contribs/github-bot/internal/conditions/fork_test.go @@ -0,0 +1,31 @@ +package conditions + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestCreatedFromFork(t *testing.T) { + t.Parallel() + + var ( + repo = &github.PullRequestBranch{Repo: &github.Repository{Owner: &github.User{Login: github.String("main")}, Name: github.String("repo"), FullName: github.String("main/repo")}} + fork = &github.PullRequestBranch{Repo: &github.Repository{Owner: &github.User{Login: github.String("fork")}, Name: github.String("repo"), FullName: github.String("fork/repo")}} + ) + + prFromMain := &github.PullRequest{Base: repo, Head: repo} + prFromFork := &github.PullRequest{Base: repo, Head: fork} + + details := treeprint.New() + assert.False(t, CreatedFromFork().IsMet(prFromMain, details)) + assert.True(t, utils.TestLastNodeStatus(t, false, details), "condition details should have a status: false") + + details = treeprint.New() + assert.True(t, CreatedFromFork().IsMet(prFromFork, details)) + assert.True(t, utils.TestLastNodeStatus(t, true, details), "condition details should have a status: true") +} diff --git a/contribs/github-bot/internal/conditions/label.go b/contribs/github-bot/internal/conditions/label.go new file mode 100644 index 00000000000..ace94ed436c --- /dev/null +++ b/contribs/github-bot/internal/conditions/label.go @@ -0,0 +1,34 @@ +package conditions + +import ( + "fmt" + "regexp" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Label Condition. +type label struct { + pattern *regexp.Regexp +} + +var _ Condition = &label{} + +func (l *label) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A label matches this pattern: %s", l.pattern.String()) + + for _, label := range pr.Labels { + if l.pattern.MatchString(label.GetName()) { + return utils.AddStatusNode(true, fmt.Sprintf("%s (label: %s)", detail, label.GetName()), details) + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func Label(pattern string) Condition { + return &label{pattern: regexp.MustCompile(pattern)} +} diff --git a/contribs/github-bot/internal/conditions/label_test.go b/contribs/github-bot/internal/conditions/label_test.go new file mode 100644 index 00000000000..00a3a8e3457 --- /dev/null +++ b/contribs/github-bot/internal/conditions/label_test.go @@ -0,0 +1,48 @@ +package conditions + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestLabel(t *testing.T) { + t.Parallel() + + labels := []*github.Label{ + {Name: github.String("notTheRightOne")}, + {Name: github.String("label")}, + {Name: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + pattern string + labels []*github.Label + isMet bool + }{ + {"empty label list", "label", []*github.Label{}, false}, + {"label list contains exact match", "label", labels, true}, + {"label list contains prefix match", "^lab", labels, true}, + {"label list contains prefix doesn't match", "^bel", labels, false}, + {"label list contains suffix match", "bel$", labels, true}, + {"label list contains suffix doesn't match", "lab$", labels, false}, + {"label list doesn't contains match", "baleb", labels, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{Labels: testCase.labels} + details := treeprint.New() + condition := Label(testCase.pattern) + + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) + }) + } +} diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go new file mode 100644 index 00000000000..f80fc86cb11 --- /dev/null +++ b/contribs/github-bot/internal/config/config.go @@ -0,0 +1,99 @@ +package config + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/client" + c "github.com/gnolang/gno/contribs/github-bot/internal/conditions" + r "github.com/gnolang/gno/contribs/github-bot/internal/requirements" +) + +type Teams []string + +// Automatic check that will be performed by the bot. +type AutomaticCheck struct { + Description string + If c.Condition // If the condition is met, the rule is displayed and the requirement is executed. + Then r.Requirement // If the requirement is satisfied, the check passes. +} + +// Manual check that will be performed by users. +type ManualCheck struct { + Description string + If c.Condition // If the condition is met, a checkbox will be displayed on bot comment. + Teams Teams // Members of these teams can check the checkbox to make the check pass. +} + +// This is the description for a persistent rule with a non-standard behavior +// that allow maintainer to force the "success" state of the CI check +const ForceSkipDescription = "**IGNORE** the bot requirements for this PR (force green CI check)" + +// This function returns the configuration of the bot consisting of automatic and manual checks +// in which the GitHub client is injected. +func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { + auto := []AutomaticCheck{ + { + Description: "Maintainers must be able to edit this pull request ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork))", + If: c.CreatedFromFork(), + Then: r.MaintainerCanModify(), + }, + { + Description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff", + If: c.FileChanged(gh, "^docs/"), + Then: r.And( + r.Or( + r.AuthorInTeam(gh, "tech-staff"), + r.ReviewByTeamMembers(gh, "tech-staff", 1), + ), + r.Or( + r.AuthorInTeam(gh, "devrels"), + r.ReviewByTeamMembers(gh, "devrels", 1), + ), + ), + }, + { + Description: "Must not contain the \"don't merge\" label", + If: c.Label("don't merge"), + Then: r.Never(), + }, + } + + manual := []ManualCheck{ + { + // WARN: Do not edit this special rule which must remain persistent. + Description: ForceSkipDescription, + If: c.Always(), + }, + { + Description: "The pull request description provides enough details", + If: c.And( + c.Not(c.AuthorInTeam(gh, "core-contributors")), + c.Not(c.Author("dependabot[bot]")), + ), + Teams: Teams{"core-contributors"}, + }, + { + Description: "Determine if infra needs to be updated before merging", + If: c.And( + c.BaseBranch("master"), + c.Or( + c.FileChanged(gh, `Dockerfile`), + c.FileChanged(gh, `^misc/deployments`), + c.FileChanged(gh, `^misc/docker-`), + c.FileChanged(gh, `^.github/workflows/releaser.*\.yml$`), + c.FileChanged(gh, `^.github/workflows/portal-loop\.yml$`), + ), + ), + Teams: Teams{"devops"}, + }, + } + + // Check for duplicates in manual rule descriptions (needs to be unique for the bot operations). + unique := make(map[string]struct{}) + for _, rule := range manual { + if _, exists := unique[rule.Description]; exists { + gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.Description) + } + unique[rule.Description] = struct{}{} + } + + return auto, manual +} diff --git a/contribs/github-bot/internal/logger/action.go b/contribs/github-bot/internal/logger/action.go new file mode 100644 index 00000000000..c6d10429e62 --- /dev/null +++ b/contribs/github-bot/internal/logger/action.go @@ -0,0 +1,43 @@ +package logger + +import ( + "github.com/sethvargo/go-githubactions" +) + +type actionLogger struct{} + +var _ Logger = &actionLogger{} + +// Debugf implements Logger. +func (a *actionLogger) Debugf(msg string, args ...any) { + githubactions.Debugf(msg, args...) +} + +// Errorf implements Logger. +func (a *actionLogger) Errorf(msg string, args ...any) { + githubactions.Errorf(msg, args...) +} + +// Fatalf implements Logger. +func (a *actionLogger) Fatalf(msg string, args ...any) { + githubactions.Fatalf(msg, args...) +} + +// Infof implements Logger. +func (a *actionLogger) Infof(msg string, args ...any) { + githubactions.Infof(msg, args...) +} + +// Noticef implements Logger. +func (a *actionLogger) Noticef(msg string, args ...any) { + githubactions.Noticef(msg, args...) +} + +// Warningf implements Logger. +func (a *actionLogger) Warningf(msg string, args ...any) { + githubactions.Warningf(msg, args...) +} + +func newActionLogger() Logger { + return &actionLogger{} +} diff --git a/contribs/github-bot/internal/logger/logger.go b/contribs/github-bot/internal/logger/logger.go new file mode 100644 index 00000000000..570ca027e5c --- /dev/null +++ b/contribs/github-bot/internal/logger/logger.go @@ -0,0 +1,40 @@ +package logger + +import ( + "os" +) + +// All Logger methods follow the standard fmt.Printf convention. +type Logger interface { + // Debugf prints a debug-level message. + Debugf(msg string, args ...any) + + // Noticef prints a notice-level message. + Noticef(msg string, args ...any) + + // Warningf prints a warning-level message. + Warningf(msg string, args ...any) + + // Errorf prints a error-level message. + Errorf(msg string, args ...any) + + // Fatalf prints a error-level message and exits. + Fatalf(msg string, args ...any) + + // Infof prints message to stdout without any level annotations. + Infof(msg string, args ...any) +} + +// Returns a logger suitable for Github Actions or terminal output. +func NewLogger(verbose bool) Logger { + if _, isAction := os.LookupEnv("GITHUB_ACTION"); isAction { + return newActionLogger() + } + + return newTermLogger(verbose) +} + +// NewNoopLogger returns a logger that does not log anything. +func NewNoopLogger() Logger { + return newNoopLogger() +} diff --git a/contribs/github-bot/internal/logger/noop.go b/contribs/github-bot/internal/logger/noop.go new file mode 100644 index 00000000000..629ed9d52d9 --- /dev/null +++ b/contribs/github-bot/internal/logger/noop.go @@ -0,0 +1,27 @@ +package logger + +type noopLogger struct{} + +var _ Logger = &noopLogger{} + +// Debugf implements Logger. +func (*noopLogger) Debugf(_ string, _ ...any) {} + +// Errorf implements Logger. +func (*noopLogger) Errorf(_ string, _ ...any) {} + +// Fatalf implements Logger. +func (*noopLogger) Fatalf(_ string, _ ...any) {} + +// Infof implements Logger. +func (*noopLogger) Infof(_ string, _ ...any) {} + +// Noticef implements Logger. +func (*noopLogger) Noticef(_ string, _ ...any) {} + +// Warningf implements Logger. +func (*noopLogger) Warningf(_ string, _ ...any) {} + +func newNoopLogger() Logger { + return &noopLogger{} +} diff --git a/contribs/github-bot/internal/logger/terminal.go b/contribs/github-bot/internal/logger/terminal.go new file mode 100644 index 00000000000..d0e5671a3c8 --- /dev/null +++ b/contribs/github-bot/internal/logger/terminal.go @@ -0,0 +1,55 @@ +package logger + +import ( + "fmt" + "log/slog" + "os" +) + +type termLogger struct{} + +var _ Logger = &termLogger{} + +// Debugf implements Logger. +func (s *termLogger) Debugf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Debug(fmt.Sprintf(msg, args...)) +} + +// Errorf implements Logger. +func (s *termLogger) Errorf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Error(fmt.Sprintf(msg, args...)) +} + +// Fatalf implements Logger. +func (s *termLogger) Fatalf(msg string, args ...any) { + s.Errorf(msg, args...) + os.Exit(1) +} + +// Infof implements Logger. +func (s *termLogger) Infof(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Info(fmt.Sprintf(msg, args...)) +} + +// Noticef implements Logger. +func (s *termLogger) Noticef(msg string, args ...any) { + // Alias to info on terminal since notice level only exists on GitHub Actions. + s.Infof(msg, args...) +} + +// Warningf implements Logger. +func (s *termLogger) Warningf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Warn(fmt.Sprintf(msg, args...)) +} + +func newTermLogger(verbose bool) Logger { + if verbose { + slog.SetLogLoggerLevel(slog.LevelDebug) + } + + return &termLogger{} +} diff --git a/contribs/github-bot/internal/matrix/cmd.go b/contribs/github-bot/internal/matrix/cmd.go new file mode 100644 index 00000000000..8bcc3a34424 --- /dev/null +++ b/contribs/github-bot/internal/matrix/cmd.go @@ -0,0 +1,53 @@ +package matrix + +import ( + "context" + "flag" + "fmt" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type matrixFlags struct { + verbose *bool + matrixKey string + flagSet *flag.FlagSet +} + +func NewMatrixCmd(verbose *bool) *commands.Command { + flags := &matrixFlags{verbose: verbose} + + return commands.NewCommand( + commands.Metadata{ + Name: "matrix", + ShortUsage: "github-bot matrix [flags]", + ShortHelp: "parses GitHub Actions event and defines matrix accordingly", + LongHelp: "This tool retrieves the GitHub Actions context, parses the attached event, and defines the matrix with the pull request numbers to be processed accordingly", + }, + flags, + func(_ context.Context, _ []string) error { + flags.validateFlags() + return execMatrix(flags) + }, + ) +} + +func (flags *matrixFlags) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &flags.matrixKey, + "matrix-key", + "", + "key of the matrix to set in Github Actions output (required)", + ) + + flags.flagSet = fs +} + +func (flags *matrixFlags) validateFlags() { + if flags.matrixKey == "" { + fmt.Fprintf(flags.flagSet.Output(), "Error: no matrix-key provided\n\n") + flags.flagSet.Usage() + os.Exit(1) + } +} diff --git a/contribs/github-bot/internal/matrix/matrix.go b/contribs/github-bot/internal/matrix/matrix.go new file mode 100644 index 00000000000..02840721c80 --- /dev/null +++ b/contribs/github-bot/internal/matrix/matrix.go @@ -0,0 +1,139 @@ +package matrix + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "strings" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/sethvargo/go-githubactions" +) + +func execMatrix(flags *matrixFlags) error { + // Get GitHub Actions context to retrieve event. + actionCtx, err := githubactions.Context() + if err != nil { + return fmt.Errorf("unable to get GitHub Actions context: %w", err) + } + + // If verbose is set, print the Github Actions event for debugging purpose. + if *flags.verbose { + jsonBytes, err := json.MarshalIndent(actionCtx.Event, "", " ") + if err != nil { + return fmt.Errorf("unable to marshal event to json: %w", err) + } + fmt.Println("Event:", string(jsonBytes)) + } + + // Init Github client using only GitHub Actions context. + owner, repo := actionCtx.Repo() + gh, err := client.New(context.Background(), &client.Config{ + Owner: owner, + Repo: repo, + Verbose: *flags.verbose, + DryRun: true, + }) + if err != nil { + return fmt.Errorf("unable to init GitHub client: %w", err) + } + + // Retrieve PR list from GitHub Actions event. + prList, err := getPRListFromEvent(gh, actionCtx) + if err != nil { + return err + } + + // Format PR list for GitHub Actions matrix definition. + bytes, err := prList.MarshalText() + if err != nil { + return fmt.Errorf("unable to marshal PR list: %w", err) + } + matrix := fmt.Sprintf("%s=[%s]", flags.matrixKey, string(bytes)) + + // If verbose is set, print the matrix for debugging purpose. + if *flags.verbose { + fmt.Printf("Matrix: %s\n", matrix) + } + + // Get the path of the GitHub Actions environment file used for output. + output, ok := os.LookupEnv("GITHUB_OUTPUT") + if !ok { + return errors.New("unable to get GITHUB_OUTPUT var") + } + + // Open GitHub Actions output file + file, err := os.OpenFile(output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return fmt.Errorf("unable to open GitHub Actions output file: %w", err) + } + defer file.Close() + + // Append matrix to GitHub Actions output file + if _, err := fmt.Fprintf(file, "%s\n", matrix); err != nil { + return fmt.Errorf("unable to write matrix in GitHub Actions output file: %w", err) + } + + return nil +} + +func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContext) (utils.PRList, error) { + var prList utils.PRList + + switch actionCtx.EventName { + // Event triggered from GitHub Actions user interface. + case utils.EventWorkflowDispatch: + // Get input entered by the user. + rawInput, ok := utils.IndexMap(actionCtx.Event, "inputs", "pull-request-list").(string) + if !ok { + return nil, errors.New("unable to get workflow dispatch input") + } + input := strings.TrimSpace(rawInput) + + // If all PR are requested, list them from GitHub API. + if input == "all" { + prs, err := gh.ListPR(utils.PRStateOpen) + if err != nil { + return nil, fmt.Errorf("unable to list all PR: %w", err) + } + + prList = make(utils.PRList, len(prs)) + for i := range prs { + prList[i] = prs[i].GetNumber() + } + } else { + // If a PR list is provided, parse it. + if err := prList.UnmarshalText([]byte(input)); err != nil { + return nil, fmt.Errorf("invalid PR list provided as input: %w", err) + } + } + + // Event triggered by an issue / PR comment being created / edited / deleted + // or any update on a PR. + case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestReview, utils.EventPullRequestTarget: + // For these events, retrieve the number of the associated PR from the context. + prNum, err := utils.GetPRNumFromActionsCtx(actionCtx) + if err != nil { + return nil, fmt.Errorf("unable to retrieve PR number from GitHub Actions context: %w", err) + } + prList = utils.PRList{prNum} + + default: + return nil, fmt.Errorf("unsupported event type: %s", actionCtx.EventName) + } + + // Then only keep provided PR that are opened. + var openedPRList utils.PRList = nil + for _, prNum := range prList { + if _, err := gh.GetOpenedPullRequest(prNum); err != nil { + gh.Logger.Warningf("Can't get PR from event: %v", err) + } else { + openedPRList = append(openedPRList, prNum) + } + } + + return openedPRList, nil +} diff --git a/contribs/github-bot/internal/matrix/matrix_test.go b/contribs/github-bot/internal/matrix/matrix_test.go new file mode 100644 index 00000000000..f6b34f16c24 --- /dev/null +++ b/contribs/github-bot/internal/matrix/matrix_test.go @@ -0,0 +1,256 @@ +package matrix + +import ( + "context" + "net/http" + "strconv" + "strings" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/sethvargo/go-githubactions" + "github.com/stretchr/testify/assert" +) + +func TestProcessEvent(t *testing.T) { + t.Parallel() + + prs := []*github.PullRequest{ + {Number: github.Int(1), State: github.String(utils.PRStateOpen)}, + {Number: github.Int(2), State: github.String(utils.PRStateOpen)}, + {Number: github.Int(3), State: github.String(utils.PRStateOpen)}, + {Number: github.Int(4), State: github.String(utils.PRStateClosed)}, + {Number: github.Int(5), State: github.String(utils.PRStateClosed)}, + {Number: github.Int(6), State: github.String(utils.PRStateClosed)}, + } + openPRs := prs[:3] + + for _, testCase := range []struct { + name string + gaCtx *githubactions.GitHubContext + prs []*github.PullRequest + expectedPRList utils.PRList + expectedError bool + }{ + { + "valid issue_comment event", + &githubactions.GitHubContext{ + EventName: utils.EventIssueComment, + Event: map[string]any{"issue": map[string]any{"number": 1.}}, + }, + prs, + utils.PRList{1}, + false, + }, { + "valid pull_request event", + &githubactions.GitHubContext{ + EventName: utils.EventPullRequest, + Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, + }, + prs, + utils.PRList{1}, + false, + }, { + "valid pull_request_review event", + &githubactions.GitHubContext{ + EventName: utils.EventPullRequestReview, + Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, + }, + prs, + utils.PRList{1}, + false, + }, { + "valid pull_request_target event", + &githubactions.GitHubContext{ + EventName: utils.EventPullRequestTarget, + Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, + }, + prs, + utils.PRList{1}, + false, + }, { + "invalid event (PR number not set)", + &githubactions.GitHubContext{ + EventName: utils.EventIssueComment, + Event: map[string]any{"issue": nil}, + }, + prs, + utils.PRList(nil), + true, + }, { + "invalid event name", + &githubactions.GitHubContext{ + EventName: "invalid_event", + Event: map[string]any{"issue": map[string]any{"number": 1.}}, + }, + prs, + utils.PRList(nil), + true, + }, { + "valid workflow_dispatch all", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}}, + }, + openPRs, + utils.PRList{1, 2, 3}, + false, + }, { + "valid workflow_dispatch all (no prs)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}}, + }, + nil, + utils.PRList(nil), + false, + }, { + "valid workflow_dispatch list", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3"}}, + }, + prs, + utils.PRList{1, 2, 3}, + false, + }, { + "valid workflow_dispatch list with spaces", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": " 1, 2 ,3 "}}, + }, + prs, + utils.PRList{1, 2, 3}, + false, + }, { + "invalid workflow_dispatch list (1 closed)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3,4"}}, + }, + prs, + utils.PRList{1, 2, 3}, + false, + }, { + "invalid workflow_dispatch list (1 doesn't exist)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "42"}}, + }, + prs, + utils.PRList(nil), + false, + }, { + "invalid workflow_dispatch list (all closed)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "4,5,6"}}, + }, + prs, + utils.PRList(nil), + false, + }, { + "invalid workflow_dispatch list (empty)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": ""}}, + }, + prs, + utils.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (unset)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": ""}, + }, + prs, + utils.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (not a number list)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "foo"}}, + }, + prs, + utils.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (number list with invalid elem)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,foo"}}, + }, + prs, + utils.PRList(nil), + true, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if testCase.expectedPRList != nil { + w.Write(mock.MustMarshal(testCase.prs)) + } + }), + ), + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls/{number}", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var ( + err error + prNum int + parts = strings.Split(req.RequestURI, "/") + ) + + if len(parts) > 0 { + prNumStr := parts[len(parts)-1] + prNum, err = strconv.Atoi(prNumStr) + if err != nil { + panic(err) // Should never happen. + } + } + + for _, pr := range prs { + if pr.GetNumber() == prNum { + w.Write(mock.MustMarshal(pr)) + return + } + } + + w.Write(mock.MustMarshal(nil)) + }), + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + prList, err := getPRListFromEvent(gh, testCase.gaCtx) + if testCase.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, testCase.expectedPRList, prList) + }) + } +} diff --git a/contribs/github-bot/internal/requirements/assignee.go b/contribs/github-bot/internal/requirements/assignee.go new file mode 100644 index 00000000000..9a2723ad18f --- /dev/null +++ b/contribs/github-bot/internal/requirements/assignee.go @@ -0,0 +1,53 @@ +package requirements + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Assignee Requirement. +type assignee struct { + gh *client.GitHub + user string +} + +var _ Requirement = &assignee{} + +func (a *assignee) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("This user is assigned to pull request: %s", a.user) + + // Check if user was already assigned to PR. + for _, assignee := range pr.Assignees { + if a.user == assignee.GetLogin() { + return utils.AddStatusNode(true, detail, details) + } + } + + // If in a dry run, skip assigning the user. + if a.gh.DryRun { + return utils.AddStatusNode(false, detail, details) + } + + // If user not already assigned, assign it. + if _, _, err := a.gh.Client.Issues.AddAssignees( + a.gh.Ctx, + a.gh.Owner, + a.gh.Repo, + pr.GetNumber(), + []string{a.user}, + ); err != nil { + a.gh.Logger.Errorf("Unable to assign user %s to PR %d: %v", a.user, pr.GetNumber(), err) + return utils.AddStatusNode(false, detail, details) + } + + return utils.AddStatusNode(true, detail, details) +} + +func Assignee(gh *client.GitHub, user string) Requirement { + return &assignee{gh: gh, user: user} +} diff --git a/contribs/github-bot/internal/requirements/assignee_test.go b/contribs/github-bot/internal/requirements/assignee_test.go new file mode 100644 index 00000000000..aa86fb0054d --- /dev/null +++ b/contribs/github-bot/internal/requirements/assignee_test.go @@ -0,0 +1,72 @@ +package requirements + +import ( + "context" + "net/http" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestAssignee(t *testing.T) { + t.Parallel() + + assignees := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + assignees []*github.User + dryRun bool + exists bool + }{ + {"empty assignee list", "user", []*github.User{}, false, false}, + {"empty assignee list with dry-run", "user", []*github.User{}, true, false}, + {"assignee list contains user", "user", assignees, false, true}, + {"assignee list doesn't contain user", "user2", assignees, false, false}, + {"assignee list doesn't contain user with dry-run", "user2", assignees, true, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/issues/0/assignees", + Method: "GET", // It looks like this mock package doesn't support mocking POST requests. + }, + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + requested = true + }), + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + DryRun: testCase.dryRun, + } + + pr := &github.PullRequest{Assignees: testCase.assignees} + details := treeprint.New() + requirement := Assignee(gh, testCase.user) + + assert.True(t, requirement.IsSatisfied(pr, details) || testCase.dryRun, "requirement should have a satisfied status: true") + assert.True(t, utils.TestLastNodeStatus(t, true, details) || testCase.dryRun, "requirement details should have a status: true") + assert.True(t, testCase.exists || requested || testCase.dryRun, "requirement should have requested to create item") + }) + } +} diff --git a/contribs/github-bot/internal/requirements/author.go b/contribs/github-bot/internal/requirements/author.go new file mode 100644 index 00000000000..eed2c510b97 --- /dev/null +++ b/contribs/github-bot/internal/requirements/author.go @@ -0,0 +1,39 @@ +package requirements + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/conditions" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Author Requirement. +type author struct { + c conditions.Condition // Alias Author requirement to identical condition. +} + +var _ Requirement = &author{} + +func (a *author) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + return a.c.IsMet(pr, details) +} + +func Author(user string) Requirement { + return &author{conditions.Author(user)} +} + +// AuthorInTeam Requirement. +type authorInTeam struct { + c conditions.Condition // Alias AuthorInTeam requirement to identical condition. +} + +var _ Requirement = &authorInTeam{} + +func (a *authorInTeam) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + return a.c.IsMet(pr, details) +} + +func AuthorInTeam(gh *client.GitHub, team string) Requirement { + return &authorInTeam{conditions.AuthorInTeam(gh, team)} +} diff --git a/contribs/github-bot/internal/requirements/author_test.go b/contribs/github-bot/internal/requirements/author_test.go new file mode 100644 index 00000000000..768ca44f24e --- /dev/null +++ b/contribs/github-bot/internal/requirements/author_test.go @@ -0,0 +1,93 @@ +package requirements + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestAuthor(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + user string + author string + isSatisfied bool + }{ + {"author match", "user", "user", true}, + {"author doesn't match", "user", "author", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.author)}, + } + details := treeprint.New() + requirement := Author(testCase.user) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} + +func TestAuthorInTeam(t *testing.T) { + t.Parallel() + + members := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + members []*github.User + isSatisfied bool + }{ + {"empty member list", "user", []*github.User{}, false}, + {"member list contains user", "user", members, true}, + {"member list doesn't contain user", "user2", members, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/orgs/teams/team/members", + Method: "GET", + }, + testCase.members, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.user)}, + } + details := treeprint.New() + requirement := AuthorInTeam(gh, "team") + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} diff --git a/contribs/github-bot/internal/requirements/boolean.go b/contribs/github-bot/internal/requirements/boolean.go new file mode 100644 index 00000000000..6b441c92f80 --- /dev/null +++ b/contribs/github-bot/internal/requirements/boolean.go @@ -0,0 +1,98 @@ +package requirements + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// And Requirement. +type and struct { + requirements []Requirement +} + +var _ Requirement = &and{} + +func (a *and) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + satisfied := utils.Success + branch := details.AddBranch("") + + for _, requirement := range a.requirements { + if !requirement.IsSatisfied(pr, branch) { + satisfied = utils.Fail + // We don't break here because we need to call IsSatisfied on all + // requirements to populate the details tree. + } + } + + branch.SetValue(fmt.Sprintf("%s And", satisfied)) + + return (satisfied == utils.Success) +} + +func And(requirements ...Requirement) Requirement { + if len(requirements) < 2 { + panic("You should pass at least 2 requirements to And()") + } + + return &and{requirements} +} + +// Or Requirement. +type or struct { + requirements []Requirement +} + +var _ Requirement = &or{} + +func (o *or) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + satisfied := utils.Fail + branch := details.AddBranch("") + + for _, requirement := range o.requirements { + if requirement.IsSatisfied(pr, branch) { + satisfied = utils.Success + // We don't break here because we need to call IsSatisfied on all + // requirements to populate the details tree. + } + } + + branch.SetValue(fmt.Sprintf("%s Or", satisfied)) + + return (satisfied == utils.Success) +} + +func Or(requirements ...Requirement) Requirement { + if len(requirements) < 2 { + panic("You should pass at least 2 requirements to Or()") + } + + return &or{requirements} +} + +// Not Requirement. +type not struct { + req Requirement +} + +var _ Requirement = ¬{} + +func (n *not) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + satisfied := n.req.IsSatisfied(pr, details) + node := details.FindLastNode() + + if satisfied { + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Fail, node.(*treeprint.Node).Value.(string))) + } else { + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Success, node.(*treeprint.Node).Value.(string))) + } + + return !satisfied +} + +func Not(req Requirement) Requirement { + return ¬{req} +} diff --git a/contribs/github-bot/internal/requirements/boolean_test.go b/contribs/github-bot/internal/requirements/boolean_test.go new file mode 100644 index 00000000000..0043a44985c --- /dev/null +++ b/contribs/github-bot/internal/requirements/boolean_test.go @@ -0,0 +1,96 @@ +package requirements + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestAnd(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + requirements []Requirement + isSatisfied bool + }{ + {"and is true", []Requirement{Always(), Always()}, true}, + {"and is false", []Requirement{Always(), Always(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := And(testCase.requirements...) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} + +func TestAndPanic(t *testing.T) { + t.Parallel() + + assert.Panics(t, func() { And(Always()) }, "and constructor should panic if less than 2 conditions are provided") +} + +func TestOr(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + requirements []Requirement + isSatisfied bool + }{ + {"or is true", []Requirement{Never(), Always()}, true}, + {"or is false", []Requirement{Never(), Never(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := Or(testCase.requirements...) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} + +func TestOrPanic(t *testing.T) { + t.Parallel() + + assert.Panics(t, func() { Or(Always()) }, "or constructor should panic if less than 2 conditions are provided") +} + +func TestNot(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + requirement Requirement + isSatisfied bool + }{ + {"not is true", Never(), true}, + {"not is false", Always(), false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := Not(testCase.requirement) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} diff --git a/contribs/github-bot/internal/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go new file mode 100644 index 00000000000..6481285ae82 --- /dev/null +++ b/contribs/github-bot/internal/requirements/branch.go @@ -0,0 +1,58 @@ +package requirements + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Pass this to UpToDateWith constructor to check the PR head branch +// against its base branch. +const PR_BASE = "PR_BASE" + +// UpToDateWith Requirement. +type upToDateWith struct { + gh *client.GitHub + base string +} + +var _ Requirement = &author{} + +func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + base := u.base + if u.base == PR_BASE { + base = pr.GetBase().GetRef() + } + + head := pr.GetHead().GetRef() + // If pull request is open from a fork, prepend head ref with fork owner login. + if pr.GetHead().GetRepo().GetFullName() != pr.GetBase().GetRepo().GetFullName() { + head = fmt.Sprintf("%s:%s", pr.GetHead().GetRepo().GetOwner().GetLogin(), pr.GetHead().GetRef()) + } + + cmp, _, err := u.gh.Client.Repositories.CompareCommits(u.gh.Ctx, u.gh.Owner, u.gh.Repo, base, head, nil) + if err != nil { + u.gh.Logger.Errorf("Unable to compare head branch (%s) and base (%s): %v", head, base, err) + return false + } + + return utils.AddStatusNode( + cmp.GetBehindBy() == 0, + fmt.Sprintf( + "Head branch (%s) is up to date with base (%s): behind by %d / ahead by %d", + head, + base, + cmp.GetBehindBy(), + cmp.GetAheadBy(), + ), + details, + ) +} + +func UpToDateWith(gh *client.GitHub, base string) Requirement { + return &upToDateWith{gh, base} +} diff --git a/contribs/github-bot/internal/requirements/branch_test.go b/contribs/github-bot/internal/requirements/branch_test.go new file mode 100644 index 00000000000..54387beb605 --- /dev/null +++ b/contribs/github-bot/internal/requirements/branch_test.go @@ -0,0 +1,62 @@ +package requirements + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestUpToDateWith(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + behind int + ahead int + isSatisfied bool + }{ + {"up-to-date without commit ahead", 0, 0, true}, + {"up-to-date with commits ahead", 0, 3, true}, + {"not up-to-date with commits behind", 3, 0, false}, + {"not up-to-date with commits behind and ahead", 3, 3, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/compare/base...", + Method: "GET", + }, + github.CommitsComparison{ + AheadBy: &testCase.ahead, + BehindBy: &testCase.behind, + }, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := UpToDateWith(gh, "base") + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} diff --git a/contribs/github-bot/internal/requirements/constant.go b/contribs/github-bot/internal/requirements/constant.go new file mode 100644 index 00000000000..cbe932da830 --- /dev/null +++ b/contribs/github-bot/internal/requirements/constant.go @@ -0,0 +1,34 @@ +package requirements + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Always Requirement. +type always struct{} + +var _ Requirement = &always{} + +func (*always) IsSatisfied(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(true, "On every pull request", details) +} + +func Always() Requirement { + return &always{} +} + +// Never Requirement. +type never struct{} + +var _ Requirement = &never{} + +func (*never) IsSatisfied(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(false, "On no pull request", details) +} + +func Never() Requirement { + return &never{} +} diff --git a/contribs/github-bot/internal/requirements/constant_test.go b/contribs/github-bot/internal/requirements/constant_test.go new file mode 100644 index 00000000000..b04addcb672 --- /dev/null +++ b/contribs/github-bot/internal/requirements/constant_test.go @@ -0,0 +1,25 @@ +package requirements + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + "github.com/xlab/treeprint" +) + +func TestAlways(t *testing.T) { + t.Parallel() + + details := treeprint.New() + assert.True(t, Always().IsSatisfied(nil, details), "requirement should have a satisfied status: true") + assert.True(t, utils.TestLastNodeStatus(t, true, details), "requirement details should have a status: true") +} + +func TestNever(t *testing.T) { + t.Parallel() + + details := treeprint.New() + assert.False(t, Never().IsSatisfied(nil, details), "requirement should have a satisfied status: false") + assert.True(t, utils.TestLastNodeStatus(t, false, details), "requirement details should have a status: false") +} diff --git a/contribs/github-bot/internal/requirements/label.go b/contribs/github-bot/internal/requirements/label.go new file mode 100644 index 00000000000..d1ee475db92 --- /dev/null +++ b/contribs/github-bot/internal/requirements/label.go @@ -0,0 +1,53 @@ +package requirements + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Label Requirement. +type label struct { + gh *client.GitHub + name string +} + +var _ Requirement = &label{} + +func (l *label) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("This label is applied to pull request: %s", l.name) + + // Check if label was already applied to PR. + for _, label := range pr.Labels { + if l.name == label.GetName() { + return utils.AddStatusNode(true, detail, details) + } + } + + // If in a dry run, skip applying the label. + if l.gh.DryRun { + return utils.AddStatusNode(false, detail, details) + } + + // If label not already applied, apply it. + if _, _, err := l.gh.Client.Issues.AddLabelsToIssue( + l.gh.Ctx, + l.gh.Owner, + l.gh.Repo, + pr.GetNumber(), + []string{l.name}, + ); err != nil { + l.gh.Logger.Errorf("Unable to add label %s to PR %d: %v", l.name, pr.GetNumber(), err) + return utils.AddStatusNode(false, detail, details) + } + + return utils.AddStatusNode(true, detail, details) +} + +func Label(gh *client.GitHub, name string) Requirement { + return &label{gh, name} +} diff --git a/contribs/github-bot/internal/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go new file mode 100644 index 00000000000..631bff9e64b --- /dev/null +++ b/contribs/github-bot/internal/requirements/label_test.go @@ -0,0 +1,72 @@ +package requirements + +import ( + "context" + "net/http" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestLabel(t *testing.T) { + t.Parallel() + + labels := []*github.Label{ + {Name: github.String("notTheRightOne")}, + {Name: github.String("label")}, + {Name: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + pattern string + labels []*github.Label + dryRun bool + exists bool + }{ + {"empty label list", "label", []*github.Label{}, false, false}, + {"empty label list with dry-run", "user", []*github.Label{}, true, false}, + {"label list contains label", "label", labels, false, true}, + {"label list doesn't contain label", "label2", labels, false, false}, + {"label list doesn't contain label with dry-run", "label", labels, true, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/issues/0/labels", + Method: "GET", // It looks like this mock package doesn't support mocking POST requests. + }, + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + requested = true + }), + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + DryRun: testCase.dryRun, + } + + pr := &github.PullRequest{Labels: testCase.labels} + details := treeprint.New() + requirement := Label(gh, testCase.pattern) + + assert.True(t, requirement.IsSatisfied(pr, details) || testCase.dryRun, "requirement should have a satisfied status: true") + assert.True(t, utils.TestLastNodeStatus(t, true, details) || testCase.dryRun, "requirement details should have a status: true") + assert.True(t, testCase.exists || requested || testCase.dryRun, "requirement should have requested to create item") + }) + } +} diff --git a/contribs/github-bot/internal/requirements/maintainer.go b/contribs/github-bot/internal/requirements/maintainer.go new file mode 100644 index 00000000000..8e3f356bebf --- /dev/null +++ b/contribs/github-bot/internal/requirements/maintainer.go @@ -0,0 +1,25 @@ +package requirements + +import ( + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// MaintainerCanModify Requirement. +type maintainerCanModify struct{} + +var _ Requirement = &maintainerCanModify{} + +func (a *maintainerCanModify) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + pr.GetMaintainerCanModify(), + "Maintainer can modify this pull request", + details, + ) +} + +func MaintainerCanModify() Requirement { + return &maintainerCanModify{} +} diff --git a/contribs/github-bot/internal/requirements/maintener_test.go b/contribs/github-bot/internal/requirements/maintener_test.go new file mode 100644 index 00000000000..5b71803b468 --- /dev/null +++ b/contribs/github-bot/internal/requirements/maintener_test.go @@ -0,0 +1,34 @@ +package requirements + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/google/go-github/v64/github" + "github.com/stretchr/testify/assert" + "github.com/xlab/treeprint" +) + +func TestMaintenerCanModify(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + isSatisfied bool + }{ + {"modify is true", true}, + {"modify is false", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{MaintainerCanModify: &testCase.isSatisfied} + details := treeprint.New() + requirement := MaintainerCanModify() + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + }) + } +} diff --git a/contribs/github-bot/internal/requirements/requirement.go b/contribs/github-bot/internal/requirements/requirement.go new file mode 100644 index 00000000000..296c4a1461d --- /dev/null +++ b/contribs/github-bot/internal/requirements/requirement.go @@ -0,0 +1,12 @@ +package requirements + +import ( + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +type Requirement interface { + // Check if the Requirement is satisfied and add the detail + // to the tree passed as a parameter. + IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool +} diff --git a/contribs/github-bot/internal/requirements/reviewer.go b/contribs/github-bot/internal/requirements/reviewer.go new file mode 100644 index 00000000000..aa3914d4c4a --- /dev/null +++ b/contribs/github-bot/internal/requirements/reviewer.go @@ -0,0 +1,156 @@ +package requirements + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// Reviewer Requirement. +type reviewByUser struct { + gh *client.GitHub + user string +} + +var _ Requirement = &reviewByUser{} + +func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("This user approved pull request: %s", r.user) + + // If not a dry run, make the user a reviewer if he's not already. + if !r.gh.DryRun { + requested := false + reviewers, err := r.gh.ListPRReviewers(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if user %s review is already requested: %v", r.user, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, user := range reviewers.Users { + if user.GetLogin() == r.user { + requested = true + break + } + } + + if requested { + r.gh.Logger.Debugf("Review of user %s already requested on PR %d", r.user, pr.GetNumber()) + } else { + r.gh.Logger.Debugf("Requesting review from user %s on PR %d", r.user, pr.GetNumber()) + if _, _, err := r.gh.Client.PullRequests.RequestReviewers( + r.gh.Ctx, + r.gh.Owner, + r.gh.Repo, + pr.GetNumber(), + github.ReviewersRequest{ + Reviewers: []string{r.user}, + }, + ); err != nil { + r.gh.Logger.Errorf("Unable to request review from user %s on PR %d: %v", r.user, pr.GetNumber(), err) + } + } + } + + // Check if user already approved this PR. + reviews, err := r.gh.ListPRReviews(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if user %s already approved this PR: %v", r.user, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, review := range reviews { + if review.GetUser().GetLogin() == r.user { + r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState()) + return utils.AddStatusNode(review.GetState() == "APPROVED", detail, details) + } + } + r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber()) + + return utils.AddStatusNode(false, detail, details) +} + +func ReviewByUser(gh *client.GitHub, user string) Requirement { + return &reviewByUser{gh, user} +} + +// Reviewer Requirement. +type reviewByTeamMembers struct { + gh *client.GitHub + team string + count uint +} + +var _ Requirement = &reviewByTeamMembers{} + +func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("At least %d user(s) of the team %s approved pull request", r.count, r.team) + + // If not a dry run, make the user a reviewer if he's not already. + if !r.gh.DryRun { + requested := false + reviewers, err := r.gh.ListPRReviewers(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if team %s review is already requested: %v", r.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, team := range reviewers.Teams { + if team.GetSlug() == r.team { + requested = true + break + } + } + + if requested { + r.gh.Logger.Debugf("Review of team %s already requested on PR %d", r.team, pr.GetNumber()) + } else { + r.gh.Logger.Debugf("Requesting review from team %s on PR %d", r.team, pr.GetNumber()) + if _, _, err := r.gh.Client.PullRequests.RequestReviewers( + r.gh.Ctx, + r.gh.Owner, + r.gh.Repo, + pr.GetNumber(), + github.ReviewersRequest{ + TeamReviewers: []string{r.team}, + }, + ); err != nil { + r.gh.Logger.Errorf("Unable to request review from team %s on PR %d: %v", r.team, pr.GetNumber(), err) + } + } + } + + // Check how many members of this team already approved this PR. + approved := uint(0) + reviews, err := r.gh.ListPRReviews(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if a member of team %s already approved this PR: %v", r.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, review := range reviews { + teamMembers, err := r.gh.ListTeamMembers(r.team) + if err != nil { + r.gh.Logger.Errorf(err.Error()) + continue + } + + for _, member := range teamMembers { + if review.GetUser().GetLogin() == member.GetLogin() { + if review.GetState() == "APPROVED" { + approved += 1 + } + r.gh.Logger.Debugf("Member %s from team %s already reviewed PR %d with state %s (%d/%d required approval(s))", member.GetLogin(), r.team, pr.GetNumber(), review.GetState(), approved, r.count) + } + } + } + + return utils.AddStatusNode(approved >= r.count, detail, details) +} + +func ReviewByTeamMembers(gh *client.GitHub, team string, count uint) Requirement { + return &reviewByTeamMembers{gh, team, count} +} diff --git a/contribs/github-bot/internal/requirements/reviewer_test.go b/contribs/github-bot/internal/requirements/reviewer_test.go new file mode 100644 index 00000000000..16c50e13743 --- /dev/null +++ b/contribs/github-bot/internal/requirements/reviewer_test.go @@ -0,0 +1,215 @@ +package requirements + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestReviewByUser(t *testing.T) { + t.Parallel() + + reviewers := github.Reviewers{ + Users: []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + }, + } + + reviews := []*github.PullRequestReview{ + { + User: &github.User{Login: github.String("notTheRightOne")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("anotherOne")}, + State: github.String("REQUEST_CHANGES"), + }, + } + + for _, testCase := range []struct { + name string + user string + isSatisfied bool + create bool + }{ + {"reviewer matches", "user", true, false}, + {"reviewer matches without approval", "anotherOne", false, false}, + {"reviewer doesn't match", "user2", false, true}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + firstRequest := true + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/requested_reviewers", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if firstRequest { + w.Write(mock.MustMarshal(reviewers)) + firstRequest = false + } else { + requested = true + } + }), + ), + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/reviews", + Method: "GET", + }, + reviews, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := ReviewByUser(gh, testCase.user) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + assert.Equal(t, testCase.create, requested, fmt.Sprintf("requirement should have requested to create item: %t", testCase.create)) + }) + } +} + +func TestReviewByTeamMembers(t *testing.T) { + t.Parallel() + + reviewers := github.Reviewers{ + Teams: []*github.Team{ + {Slug: github.String("team1")}, + {Slug: github.String("team2")}, + {Slug: github.String("team3")}, + }, + } + + members := map[string][]*github.User{ + "team1": { + {Login: github.String("user1")}, + {Login: github.String("user2")}, + {Login: github.String("user3")}, + }, + "team2": { + {Login: github.String("user3")}, + {Login: github.String("user4")}, + {Login: github.String("user5")}, + }, + "team3": { + {Login: github.String("user4")}, + {Login: github.String("user5")}, + }, + } + + reviews := []*github.PullRequestReview{ + { + User: &github.User{Login: github.String("user1")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user2")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user3")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user4")}, + State: github.String("REQUEST_CHANGES"), + }, { + User: &github.User{Login: github.String("user5")}, + State: github.String("REQUEST_CHANGES"), + }, + } + + for _, testCase := range []struct { + name string + team string + count uint + isSatisfied bool + testRequest bool + }{ + {"3/3 team members approved;", "team1", 3, true, false}, + {"1/1 team member approved", "team2", 1, true, false}, + {"1/2 team member approved", "team2", 2, false, false}, + {"0/1 team member approved", "team3", 1, false, false}, + {"0/1 team member approved with request", "team3", 1, false, true}, + {"team doesn't exist with request", "team4", 1, false, true}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + firstRequest := true + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/requested_reviewers", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if firstRequest { + if testCase.testRequest { + w.Write(mock.MustMarshal(github.Reviewers{})) + } else { + w.Write(mock.MustMarshal(reviewers)) + } + firstRequest = false + } else { + requested = true + } + }), + ), + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: fmt.Sprintf("/orgs/teams/%s/members", testCase.team), + Method: "GET", + }, + members[testCase.team], + ), + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/reviews", + Method: "GET", + }, + reviews, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := ReviewByTeamMembers(gh, testCase.team, testCase.count) + + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + assert.Equal(t, testCase.testRequest, requested, fmt.Sprintf("requirement should have requested to create item: %t", testCase.testRequest)) + }) + } +} diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go new file mode 100644 index 00000000000..0686e8c29c5 --- /dev/null +++ b/contribs/github-bot/internal/utils/actions.go @@ -0,0 +1,45 @@ +package utils + +import ( + "fmt" + + "github.com/sethvargo/go-githubactions" +) + +// Recursively search for nested values using the keys provided. +func IndexMap(m map[string]any, keys ...string) any { + if len(keys) == 0 { + return m + } + + if val, ok := m[keys[0]]; ok { + if keys = keys[1:]; len(keys) == 0 { + return val + } + subMap, _ := val.(map[string]any) + return IndexMap(subMap, keys...) + } + + return nil +} + +// Retrieve PR number from GitHub Actions context. +func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) { + firstKey := "" + + switch actionCtx.EventName { + case EventIssueComment: + firstKey = "issue" + case EventPullRequest, EventPullRequestReview, EventPullRequestTarget: + firstKey = "pull_request" + default: + return 0, fmt.Errorf("unsupported event: %s", actionCtx.EventName) + } + + num, ok := IndexMap(actionCtx.Event, firstKey, "number").(float64) + if !ok || num <= 0 { + return 0, fmt.Errorf("invalid value: %d", int(num)) + } + + return int(num), nil +} diff --git a/contribs/github-bot/internal/utils/actions_test.go b/contribs/github-bot/internal/utils/actions_test.go new file mode 100644 index 00000000000..3114bb8a061 --- /dev/null +++ b/contribs/github-bot/internal/utils/actions_test.go @@ -0,0 +1,43 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIndexMap(t *testing.T) { + t.Parallel() + + m := map[string]any{ + "Key1": map[string]any{ + "Key2": map[string]any{ + "Key3": 1, + }, + }, + } + + test := IndexMap(m) + assert.NotNil(t, test, "should return m") + _, ok := test.(map[string]any) + assert.True(t, ok, "returned m should be a map") + + test = IndexMap(m, "Key1") + assert.NotNil(t, test, "should return Key1 value") + _, ok = test.(map[string]any) + assert.True(t, ok, "Key1 value type should be a map") + + test = IndexMap(m, "Key1", "Key2") + assert.NotNil(t, test, "should return Key2 value") + _, ok = test.(map[string]any) + assert.True(t, ok, "Key2 value type should be a map") + + test = IndexMap(m, "Key1", "Key2", "Key3") + assert.NotNil(t, test, "should return Key3 value") + val, ok := test.(int) + assert.True(t, ok, "Key3 value type should be an int") + assert.Equal(t, 1, val, "Key3 value should be a 1") + + test = IndexMap(m, "Key1", "Key2", "Key3", "Key4") + assert.Nil(t, test, "Key4 value should not exist") +} diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go new file mode 100644 index 00000000000..f030d9365f7 --- /dev/null +++ b/contribs/github-bot/internal/utils/github_const.go @@ -0,0 +1,15 @@ +package utils + +// GitHub API const. +const ( + // GitHub Actions Event Names. + EventIssueComment = "issue_comment" + EventPullRequest = "pull_request" + EventPullRequestReview = "pull_request_review" + EventPullRequestTarget = "pull_request_target" + EventWorkflowDispatch = "workflow_dispatch" + + // Pull Request States. + PRStateOpen = "open" + PRStateClosed = "closed" +) diff --git a/contribs/github-bot/internal/utils/prlist.go b/contribs/github-bot/internal/utils/prlist.go new file mode 100644 index 00000000000..2893bf802b5 --- /dev/null +++ b/contribs/github-bot/internal/utils/prlist.go @@ -0,0 +1,50 @@ +package utils + +import ( + "encoding" + "fmt" + "strconv" + "strings" +) + +// Type used to (un)marshal input/output for check and matrix subcommands. +type PRList []int + +// PRList is both a TextMarshaler and a TextUnmarshaler. +var ( + _ encoding.TextMarshaler = PRList{} + _ encoding.TextUnmarshaler = &PRList{} +) + +// MarshalText implements encoding.TextMarshaler. +func (p PRList) MarshalText() (text []byte, err error) { + prNumsStr := make([]string, len(p)) + + for i, prNum := range p { + prNumsStr[i] = strconv.Itoa(prNum) + } + + return []byte(strings.Join(prNumsStr, ", ")), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (p *PRList) UnmarshalText(text []byte) error { + prNumsStr := strings.Split(string(text), ",") + prNums := make([]int, len(prNumsStr)) + + for i := range prNumsStr { + prNum, err := strconv.Atoi(strings.TrimSpace(prNumsStr[i])) + if err != nil { + return err + } + + if prNum <= 0 { + return fmt.Errorf("invalid pull request number (<= 0): original(%s) parsed(%d)", prNumsStr[i], prNum) + } + + prNums[i] = prNum + } + *p = prNums + + return nil +} diff --git a/contribs/github-bot/internal/utils/testing.go b/contribs/github-bot/internal/utils/testing.go new file mode 100644 index 00000000000..3c7f7bfef88 --- /dev/null +++ b/contribs/github-bot/internal/utils/testing.go @@ -0,0 +1,21 @@ +package utils + +import ( + "strings" + "testing" + + "github.com/xlab/treeprint" +) + +func TestLastNodeStatus(t *testing.T, success bool, details treeprint.Tree) bool { + t.Helper() + + detail := details.FindLastNode().(*treeprint.Node).Value.(string) + status := Fail + + if success { + status = Success + } + + return strings.HasPrefix(detail, string(status)) +} diff --git a/contribs/github-bot/internal/utils/tree.go b/contribs/github-bot/internal/utils/tree.go new file mode 100644 index 00000000000..c6ff57bcd99 --- /dev/null +++ b/contribs/github-bot/internal/utils/tree.go @@ -0,0 +1,24 @@ +package utils + +import ( + "fmt" + + "github.com/xlab/treeprint" +) + +type Status string + +const ( + Success Status = "🟢" + Fail Status = "🔴" +) + +func AddStatusNode(b bool, desc string, details treeprint.Tree) bool { + if b { + details.AddNode(fmt.Sprintf("%s %s", Success, desc)) + } else { + details.AddNode(fmt.Sprintf("%s %s", Fail, desc)) + } + + return b +} diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go new file mode 100644 index 00000000000..e11fe6ffd78 --- /dev/null +++ b/contribs/github-bot/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "flag" + "os" + + "github.com/gnolang/gno/contribs/github-bot/internal/check" + "github.com/gnolang/gno/contribs/github-bot/internal/matrix" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type rootFlags struct { + verbose bool +} + +func main() { + flags := &rootFlags{} + + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: "github-bot [flags]", + LongHelp: "Bot that allows for advanced management of GitHub pull requests.", + }, + flags, + commands.HelpExec, + ) + + cmd.AddSubCommands( + check.NewCheckCmd(&flags.verbose), + matrix.NewMatrixCmd(&flags.verbose), + ) + + cmd.Execute(context.Background(), os.Args[1:]) +} + +func (flags *rootFlags) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &flags.verbose, + "verbose", + false, + "set logging level to debug", + ) +} diff --git a/contribs/gnodev/cmd/gnobro/main.go b/contribs/gnodev/cmd/gnobro/main.go index 6bb6bfc2396..91713d6c6d8 100644 --- a/contribs/gnodev/cmd/gnobro/main.go +++ b/contribs/gnodev/cmd/gnobro/main.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "fmt" + "io" "log/slog" "net" "net/url" @@ -21,7 +22,6 @@ import ( "github.com/charmbracelet/wish" "github.com/charmbracelet/wish/activeterm" "github.com/charmbracelet/wish/bubbletea" - "github.com/charmbracelet/wish/logging" "golang.org/x/sync/errgroup" "github.com/gnolang/gno/contribs/gnodev/pkg/browser" @@ -47,6 +47,7 @@ type broCfg struct { sshListener string sshHostKeyPath string banner bool + jsonlog bool } var defaultBroOptions = broCfg{ @@ -152,6 +153,13 @@ func (c *broCfg) RegisterFlags(fs *flag.FlagSet) { defaultBroOptions.readonly, "readonly mode, no commands allowed", ) + + fs.BoolVar( + &c.jsonlog, + "jsonlog", + defaultBroOptions.jsonlog, + "display server log as json format", + ) } func execBrowser(cfg *broCfg, args []string, cio commands.IO) error { @@ -277,9 +285,7 @@ func runLocal(ctx context.Context, gnocl *gnoclient.Client, cfg *broCfg, bcfg br func runServer(ctx context.Context, gnocl *gnoclient.Client, cfg *broCfg, bcfg browser.Config, io commands.IO) error { // setup logger - charmlogger := charmlog.New(io.Out()) - charmlogger.SetLevel(charmlog.DebugLevel) - logger := slog.New(charmlogger) + logger := newLogger(io.Out(), cfg.jsonlog) teaHandler := func(s ssh.Session) (tea.Model, []tea.ProgramOption) { shortid := fmt.Sprintf("%.10s", s.Context().SessionID()) @@ -326,8 +332,8 @@ func runServer(ctx context.Context, gnocl *gnoclient.Client, cfg *broCfg, bcfg b bubbletea.Middleware(teaHandler), activeterm.Middleware(), // ensure PTY ValidatePathCommandMiddleware(bcfg.URLPrefix), - logging.StructuredMiddlewareWithLogger( - charmlogger, charmlog.DebugLevel, + StructuredMiddlewareWithLogger( + ctx, logger, slog.LevelInfo, ), // XXX: add ip throttler ), @@ -358,7 +364,9 @@ func runServer(ctx context.Context, gnocl *gnoclient.Client, cfg *broCfg, bcfg b return err } - io.Println("Bye!") + if !cfg.jsonlog { + io.Println("Bye!") + } return nil } @@ -421,14 +429,14 @@ func getSignerForAccount(io commands.IO, address string, kb keys.Keybase, cfg *b } // try empty password first - if _, err := kb.ExportPrivKeyUnsafe(address, ""); err != nil { + if _, err := kb.ExportPrivKey(address, ""); err != nil { prompt := fmt.Sprintf("[%.10s] Enter password:", address) signer.Password, err = io.GetPassword(prompt, true) if err != nil { return nil, fmt.Errorf("error while reading password: %w", err) } - if _, err := kb.ExportPrivKeyUnsafe(address, signer.Password); err != nil { + if _, err := kb.ExportPrivKey(address, signer.Password); err != nil { return nil, fmt.Errorf("invalid password: %w", err) } } @@ -460,3 +468,47 @@ func ValidatePathCommandMiddleware(pathPrefix string) wish.Middleware { } } } + +func StructuredMiddlewareWithLogger(ctx context.Context, logger *slog.Logger, level slog.Level) wish.Middleware { + return func(next ssh.Handler) ssh.Handler { + return func(sess ssh.Session) { + ct := time.Now() + hpk := sess.PublicKey() != nil + pty, _, _ := sess.Pty() + logger.Log( + ctx, + level, + "connect", + "user", sess.User(), + "remote-addr", sess.RemoteAddr().String(), + "public-key", hpk, + "command", sess.Command(), + "term", pty.Term, + "width", pty.Window.Width, + "height", pty.Window.Height, + "client-version", sess.Context().ClientVersion(), + ) + next(sess) + logger.Log( + ctx, + level, + "disconnect", + "user", sess.User(), + "remote-addr", sess.RemoteAddr().String(), + "duration", time.Since(ct), + ) + } + } +} + +func newLogger(out io.Writer, json bool) *slog.Logger { + if json { + return slog.New(slog.NewJSONHandler(out, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + } + + charmlogger := charmlog.New(out) + charmlogger.SetLevel(charmlog.DebugLevel) + return slog.New(charmlogger) +} diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 2c694b608bb..95f1d95e0a6 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -57,22 +57,26 @@ type devCfg struct { txsFile string // Web Configuration + noWeb bool + webHTML bool webListenerAddr string webRemoteHelperAddr string // Node Configuration - minimal bool - verbose bool - noWatch bool - noReplay bool - maxGas int64 - chainId string - serverMode bool - unsafeAPI bool + minimal bool + verbose bool + noWatch bool + noReplay bool + maxGas int64 + chainId string + chainDomain string + serverMode bool + unsafeAPI bool } var defaultDevOptions = &devCfg{ chainId: "dev", + chainDomain: "gno.land", maxGas: 10_000_000_000, webListenerAddr: "127.0.0.1:8888", nodeRPCListenerAddr: "127.0.0.1:26657", @@ -120,18 +124,32 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "gno root directory", ) + fs.BoolVar( + &c.noWeb, + "no-web", + defaultDevOptions.noWeb, + "disable gnoweb", + ) + + fs.BoolVar( + &c.webHTML, + "web-html", + defaultDevOptions.webHTML, + "gnoweb: enable unsafe HTML parsing in markdown rendering", + ) + fs.StringVar( &c.webListenerAddr, "web-listener", defaultDevOptions.webListenerAddr, - "web server listener address", + "gnoweb: web server listener address", ) fs.StringVar( &c.webRemoteHelperAddr, "web-help-remote", defaultDevOptions.webRemoteHelperAddr, - "web server help page's remote addr (default to )", + "gnoweb: web server help page's remote addr (default to )", ) fs.StringVar( @@ -203,6 +221,13 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { "set node ChainID", ) + fs.StringVar( + &c.chainDomain, + "chain-domain", + defaultDevOptions.chainDomain, + "set node ChainDomain", + ) + fs.BoolVar( &c.noWatch, "no-watch", @@ -306,7 +331,10 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { defer server.Close() // Setup gnoweb - webhandler := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) + webhandler, err := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) + if err != nil { + return fmt.Errorf("unable to setup gnoweb server: %w", err) + } // Setup unsafe APIs if enabled if cfg.unsafeAPI { @@ -334,14 +362,17 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { mux.Handle("/", webhandler) } - go func() { - err := server.ListenAndServe() - cancel(err) - }() + // Serve gnoweb + if !cfg.noWeb { + go func() { + err := server.ListenAndServe() + cancel(err) + }() - logger.WithGroup(WebLogName). - Info("gnoweb started", - "lisn", fmt.Sprintf("http://%s", server.Addr)) + logger.WithGroup(WebLogName). + Info("gnoweb started", + "lisn", fmt.Sprintf("http://%s", server.Addr)) + } watcher, err := watcher.NewPackageWatcher(loggerEvents, emitterServer) if err != nil { @@ -360,7 +391,7 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { return runEventLoop(ctx, logger, book, rt, devNode, watcher) } -var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: +var helper string = `For more in-depth documentation, visit the GNO Tooling CLI documentation: https://docs.gno.land/gno-tooling/cli/gno-tooling-gnodev P Previous TX - Go to the previous tx diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index 578cf525751..eaeb89b7e95 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -23,7 +23,7 @@ func setupDevNode( if devCfg.txsFile != "" { // Load txs files var err error - nodeConfig.InitialTxs, err = parseTxs(devCfg.txsFile) + nodeConfig.InitialTxs, err = gnoland.ReadGenesisTxs(ctx, devCfg.txsFile) if err != nil { return nil, fmt.Errorf("unable to load transactions: %w", err) } @@ -35,9 +35,15 @@ func setupDevNode( // Override balances and txs nodeConfig.BalancesList = state.Balances - nodeConfig.InitialTxs = state.Txs - logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(nodeConfig.InitialTxs)) + stateTxs := state.Txs + nodeConfig.InitialTxs = make([]gnoland.TxWithMetadata, len(stateTxs)) + + for index, nodeTx := range stateTxs { + nodeConfig.InitialTxs[index] = nodeTx + } + + logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(stateTxs)) } return gnodev.NewDevNode(ctx, nodeConfig) @@ -51,7 +57,7 @@ func setupDevNodeConfig( balances gnoland.Balances, pkgspath []gnodev.PackagePath, ) *gnodev.NodeConfig { - config := gnodev.DefaultNodeConfig(cfg.root) + config := gnodev.DefaultNodeConfig(cfg.root, cfg.chainDomain) config.Logger = logger config.Emitter = emitter diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index 635c27af19d..e509768d2a1 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log/slog" "net/http" @@ -9,18 +10,25 @@ import ( ) // setupGnowebServer initializes and starts the Gnoweb server. -func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) http.Handler { - webConfig := gnoweb.NewDefaultConfig() +func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) (http.Handler, error) { + if cfg.noWeb { + return http.HandlerFunc(http.NotFound), nil + } + + remote := dnode.GetRemoteAddress() - webConfig.HelpChainID = cfg.chainId - webConfig.RemoteAddr = dnode.GetRemoteAddress() - webConfig.HelpRemote = cfg.webRemoteHelperAddr + appcfg := gnoweb.NewDefaultAppConfig() + appcfg.UnsafeHTML = cfg.webHTML + appcfg.NodeRemote = remote + appcfg.ChainID = cfg.chainId + if cfg.webRemoteHelperAddr != "" { + appcfg.RemoteHelp = cfg.webRemoteHelperAddr + } - // If `HelpRemote` is empty default it to `RemoteAddr` - if webConfig.HelpRemote == "" { - webConfig.HelpRemote = webConfig.RemoteAddr + router, err := gnoweb.NewRouter(logger, appcfg) + if err != nil { + return nil, fmt.Errorf("unable to create router app: %w", err) } - app := gnoweb.MakeApp(logger, webConfig) - return app.Router + return router, nil } diff --git a/contribs/gnodev/cmd/gnodev/txs.go b/contribs/gnodev/cmd/gnodev/txs.go deleted file mode 100644 index 0be33b68702..00000000000 --- a/contribs/gnodev/cmd/gnodev/txs.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - - "github.com/gnolang/gno/tm2/pkg/std" -) - -func parseTxs(txFile string) ([]std.Tx, error) { - if txFile == "" { - return nil, nil - } - - file, loadErr := os.Open(txFile) - if loadErr != nil { - return nil, fmt.Errorf("unable to open tx file %s: %w", txFile, loadErr) - } - defer file.Close() - - return std.ParseTxs(context.Background(), file) -} diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index 80e9867ab27..92d8494fa40 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -2,6 +2,8 @@ module github.com/gnolang/gno/contribs/gnodev go 1.22 +toolchain go1.22.4 + replace github.com/gnolang/gno => ../.. require ( @@ -21,19 +23,19 @@ require ( github.com/sahilm/fuzzy v0.1.1 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 - golang.org/x/sync v0.7.0 - golang.org/x/term v0.22.0 + golang.org/x/sync v0.8.0 + golang.org/x/term v0.23.0 ) require ( - dario.cat/mergo v1.0.0 // indirect - github.com/alecthomas/chroma/v2 v2.8.0 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect - github.com/btcsuite/btcd/btcutil v1.1.5 // indirect + github.com/btcsuite/btcd/btcutil v1.1.6 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/charmbracelet/keygen v0.5.0 // indirect github.com/charmbracelet/x/ansi v0.1.2 // indirect @@ -48,9 +50,8 @@ require ( github.com/creack/pty v1.1.21 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -58,11 +59,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/securecookie v1.1.1 // indirect - github.com/gorilla/sessions v1.2.1 // indirect - github.com/gotuna/gotuna v0.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -78,35 +75,37 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/rs/cors v1.11.0 // indirect - github.com/rs/xid v1.5.0 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/goldmark v1.5.4 // indirect + github.com/yuin/goldmark v1.7.2 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect - go.etcd.io/bbolt v1.3.10 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.etcd.io/bbolt v1.3.11 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap/exp v0.2.0 // indirect - golang.org/x/crypto v0.25.0 // indirect + go.uber.org/zap/exp v0.3.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect - golang.org/x/mod v0.19.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/tools v0.23.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/tools v0.24.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index 19fb24d2ebb..3f22e4f2f00 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -1,12 +1,14 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +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= -github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= -github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= -github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= -github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= -github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= -github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= +github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= +github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -17,16 +19,18 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 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 h1:js1gPwhcFflTZ7Nzl7WHaOTlTr5hIrR4n1NM4v9n4Kw= 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 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= 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= @@ -89,8 +93,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC 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/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -99,8 +105,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -128,23 +132,13 @@ 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/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/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gotuna/gotuna v0.6.0 h1:N1lQKXEi/lwRp8u3sccTYLhzOffA4QasExz/1M5Riws= -github.com/gotuna/gotuna v0.6.0/go.mod h1:F/ecRt29ChB6Ycy1AFIBpBiMNK0j7Heq+gFbLWquhjc= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= 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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -213,14 +207,21 @@ 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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= -github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= @@ -228,30 +229,33 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= -github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.2 h1:NjGd7lO7zrUn/A7eKwn5PEOt4ONYGqpxSEeZuduvgxc= +github.com/yuin/goldmark v1.7.2/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= -go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= -go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 h1:aLmmtjRke7LPDQ3lvpFz+kNEH43faFhzW7v8BFIEydg= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0/go.mod h1:TC1pyCt6G9Sjb4bQpShH+P5R53pO6ZuGnHuuln9xMeE= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= -go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 h1:k6fQVDQexDE+3jG2SfCQjnHS7OamcP73YMoxEVq5B6k= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0/go.mod h1:t4BrYLHU450Zo9fnydWlIuswB1bm7rM8havDpWOJeDo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 h1:xvhQxJ/C9+RTnAj5DpTg7LSM1vbbMTiXt7e9hsfqHNw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0/go.mod h1:Fcvs2Bz1jkDM+Wf5/ozBGmi3tQ/c9zPKLnsipnfhGAo= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -260,27 +264,27 @@ 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.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= -go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= +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-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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= @@ -292,25 +296,25 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -319,8 +323,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ 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.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/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= diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 5b7c4fe08da..12a88490515 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -4,9 +4,11 @@ import ( "context" "fmt" "log/slog" + "os" "path/filepath" "strings" "sync" + "time" "unicode" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" @@ -35,15 +37,16 @@ type NodeConfig struct { BalancesList []gnoland.Balance PackagesPathList []PackagePath Emitter emitter.Emitter - InitialTxs []std.Tx + InitialTxs []gnoland.TxWithMetadata TMConfig *tmcfg.Config SkipFailingGenesisTxs bool NoReplay bool MaxGasPerBlock int64 ChainID string + ChainDomain string } -func DefaultNodeConfig(rootdir string) *NodeConfig { +func DefaultNodeConfig(rootdir, domain string) *NodeConfig { tmc := gnoland.NewDefaultTMConfig(rootdir) tmc.Consensus.SkipTimeoutCommit = false // avoid time drifting, see issue #1507 tmc.Consensus.WALDisabled = true @@ -63,6 +66,7 @@ func DefaultNodeConfig(rootdir string) *NodeConfig { DefaultDeployer: defaultDeployer, BalancesList: balances, ChainID: tmc.ChainID(), + ChainDomain: domain, TMConfig: tmc, SkipFailingGenesisTxs: true, MaxGasPerBlock: 10_000_000_000, @@ -83,8 +87,11 @@ type Node struct { // keep track of number of loaded package to be able to skip them on restore loadedPackages int + // track starting time for genesis + startTime time.Time + // state - initialState, state []std.Tx + initialState, state []gnoland.TxWithMetadata currentStateIndex int } @@ -96,7 +103,8 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { return nil, fmt.Errorf("unable map pkgs list: %w", err) } - pkgsTxs, err := mpkgs.Load(DefaultFee) + startTime := time.Now() + pkgsTxs, err := mpkgs.Load(DefaultFee, startTime) if err != nil { return nil, fmt.Errorf("unable to load genesis packages: %w", err) } @@ -109,16 +117,14 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { pkgs: mpkgs, logger: cfg.Logger, loadedPackages: len(pkgsTxs), + startTime: startTime, state: cfg.InitialTxs, initialState: cfg.InitialTxs, currentStateIndex: len(cfg.InitialTxs), } - - // generate genesis state - genesis := gnoland.GnoGenesisState{ - Balances: cfg.BalancesList, - Txs: append(pkgsTxs, cfg.InitialTxs...), - } + genesis := gnoland.DefaultGenState() + genesis.Balances = cfg.BalancesList + genesis.Txs = append(pkgsTxs, cfg.InitialTxs...) if err := devnode.rebuildNode(ctx, genesis); err != nil { return nil, fmt.Errorf("unable to initialize the node: %w", err) @@ -154,7 +160,7 @@ func (n *Node) GetRemoteAddress() string { // GetBlockTransactions returns the transactions contained // within the specified block, if any -func (n *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) { +func (n *Node) GetBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) { n.muNode.RLock() defer n.muNode.RUnlock() @@ -163,21 +169,27 @@ func (n *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) { // GetBlockTransactions returns the transactions contained // within the specified block, if any -func (n *Node) getBlockTransactions(blockNum uint64) ([]std.Tx, error) { +func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) { int64BlockNum := int64(blockNum) b, err := n.client.Block(&int64BlockNum) if err != nil { - return []std.Tx{}, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) // nothing to see here + return []gnoland.TxWithMetadata{}, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) // nothing to see here } - txs := make([]std.Tx, len(b.Block.Data.Txs)) + txs := make([]gnoland.TxWithMetadata, len(b.Block.Data.Txs)) for i, encodedTx := range b.Block.Data.Txs { + // fallback on std tx var tx std.Tx if unmarshalErr := amino.Unmarshal(encodedTx, &tx); unmarshalErr != nil { - return nil, fmt.Errorf("unable to unmarshal amino tx, %w", unmarshalErr) + return nil, fmt.Errorf("unable to unmarshal tx: %w", unmarshalErr) } - txs[i] = tx + txs[i] = gnoland.TxWithMetadata{ + Tx: tx, + Metadata: &gnoland.GnoTxMetadata{ + Timestamp: b.BlockMeta.Header.Time.Unix(), + }, + } } return txs, nil @@ -262,18 +274,20 @@ func (n *Node) Reset(ctx context.Context) error { return fmt.Errorf("unable to stop the node: %w", err) } + // Reset starting time + startTime := time.Now() + // Generate a new genesis state based on the current packages - pkgsTxs, err := n.pkgs.Load(DefaultFee) + pkgsTxs, err := n.pkgs.Load(DefaultFee, startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } // Append initialTxs txs := append(pkgsTxs, n.initialState...) - genesis := gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, - Txs: txs, - } + genesis := gnoland.DefaultGenState() + genesis.Balances = n.config.BalancesList + genesis.Txs = txs // Reset the node with the new genesis state. err = n.rebuildNode(ctx, genesis) @@ -283,6 +297,7 @@ func (n *Node) Reset(ctx context.Context) error { n.loadedPackages = len(pkgsTxs) n.currentStateIndex = len(n.initialState) + n.startTime = startTime n.emitter.Emit(&events.Reset{}) return nil } @@ -347,11 +362,13 @@ func (n *Node) SendTransaction(tx *std.Tx) error { return nil } -func (n *Node) getBlockStoreState(ctx context.Context) ([]std.Tx, error) { +func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata, error) { // get current genesis state genesis := n.GenesisDoc().AppState.(gnoland.GnoGenesisState) - state := genesis.Txs[n.loadedPackages:] // ignore previously loaded packages + initialTxs := genesis.Txs[n.loadedPackages:] // ignore previously loaded packages + state := append([]gnoland.TxWithMetadata{}, initialTxs...) + lastBlock := n.getLatestBlockNumber() var blocnum uint64 = 1 for ; blocnum <= lastBlock; blocnum++ { @@ -388,14 +405,14 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { // If NoReplay is true, simply reset the node to its initial state n.logger.Warn("replay disabled") - txs, err := n.pkgs.Load(DefaultFee) + txs, err := n.pkgs.Load(DefaultFee, n.startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } - - return n.rebuildNode(ctx, gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, Txs: txs, - }) + genesis := gnoland.DefaultGenState() + genesis.Balances = n.config.BalancesList + genesis.Txs = txs + return n.rebuildNode(ctx, genesis) } state, err := n.getBlockStoreState(ctx) @@ -404,16 +421,15 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee) + pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } // Create genesis with loaded pkgs + previous state - genesis := gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, - Txs: append(pkgsTxs, state...), - } + genesis := gnoland.DefaultGenState() + genesis.Balances = n.config.BalancesList + genesis.Txs = append(pkgsTxs, state...) // Reset the node with the new genesis state. err = n.rebuildNode(ctx, genesis) @@ -468,9 +484,13 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) } // Setup node config - nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, genesis) - nodeConfig.GenesisTxHandler = n.genesisTxHandler + nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, n.config.ChainDomain, genesis) + nodeConfig.GenesisTxResultHandler = n.genesisTxResultHandler + // Speed up stdlib loading after first start (saves about 2-3 seconds on each reload). + nodeConfig.CacheStdlibLoad = true nodeConfig.Genesis.ConsensusParams.Block.MaxGas = n.config.MaxGasPerBlock + // Genesis verification is always false with Gnodev + nodeConfig.SkipGenesisVerification = true // recoverFromError handles panics and converts them to errors. recoverFromError := func() { @@ -512,7 +532,7 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) return nil } -func (n *Node) genesisTxHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { +func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { if !res.IsErr() { return } @@ -545,10 +565,10 @@ func (n *Node) genesisTxHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { return } -func newNodeConfig(tmc *tmcfg.Config, chainid string, appstate gnoland.GnoGenesisState) *gnoland.InMemoryNodeConfig { +func newNodeConfig(tmc *tmcfg.Config, chainid, chaindomain string, appstate gnoland.GnoGenesisState) *gnoland.InMemoryNodeConfig { // Create Mocked Identity pv := gnoland.NewMockedPrivValidator() - genesis := gnoland.NewDefaultGenesisConfig(chainid) + genesis := gnoland.NewDefaultGenesisConfig(chainid, chaindomain) genesis.AppState = appstate // Add self as validator @@ -562,10 +582,11 @@ func newNodeConfig(tmc *tmcfg.Config, chainid string, appstate gnoland.GnoGenesi }, } - return &gnoland.InMemoryNodeConfig{ - PrivValidator: pv, - TMConfig: tmc, - Genesis: genesis, - GenesisMaxVMCycles: 100_000_000, + cfg := &gnoland.InMemoryNodeConfig{ + PrivValidator: pv, + TMConfig: tmc, + Genesis: genesis, + VMOutput: os.Stdout, } + return cfg } diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 846c4857784..3f996bc7716 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -8,7 +8,6 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/gno.land/pkg/gnoland" bft "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/std" ) var ErrEmptyState = errors.New("empty state") @@ -29,7 +28,7 @@ func (n *Node) SaveCurrentState(ctx context.Context) error { } // Export the current state as list of txs -func (n *Node) ExportCurrentState(ctx context.Context) ([]std.Tx, error) { +func (n *Node) ExportCurrentState(ctx context.Context) ([]gnoland.TxWithMetadata, error) { n.muNode.RLock() defer n.muNode.RUnlock() @@ -42,7 +41,7 @@ func (n *Node) ExportCurrentState(ctx context.Context) ([]std.Tx, error) { return state[:n.currentStateIndex], nil } -func (n *Node) getState(ctx context.Context) ([]std.Tx, error) { +func (n *Node) getState(ctx context.Context) ([]gnoland.TxWithMetadata, error) { if n.state == nil { var err error n.state, err = n.getBlockStoreState(ctx) @@ -85,7 +84,7 @@ func (n *Node) MoveBy(ctx context.Context, x int) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee) + pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -93,10 +92,9 @@ func (n *Node) MoveBy(ctx context.Context, x int) error { newState := n.state[:newIndex] // Create genesis with loaded pkgs + previous state - genesis := gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, - Txs: append(pkgsTxs, newState...), - } + genesis := gnoland.DefaultGenState() + genesis.Balances = n.config.BalancesList + genesis.Txs = append(pkgsTxs, newState...) // Reset the node with the new genesis state. if err = n.rebuildNode(ctx, genesis); err != nil { @@ -133,10 +131,11 @@ func (n *Node) ExportStateAsGenesis(ctx context.Context) (*bft.GenesisDoc, error // Get current blockstore state doc := *n.Node.GenesisDoc() // copy doc - doc.AppState = gnoland.GnoGenesisState{ - Balances: n.config.BalancesList, - Txs: state, - } + + genState := doc.AppState.(gnoland.GnoGenesisState) + genState.Balances = n.config.BalancesList + genState.Txs = state + doc.AppState = genState return &doc, nil } diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 11b0a2090d7..38fab0a3360 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -2,9 +2,11 @@ package dev import ( "context" + "encoding/json" "os" "path/filepath" "testing" + "time" mock "github.com/gnolang/gno/contribs/gnodev/internal/mock" @@ -15,8 +17,10 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" + tm2events "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -34,7 +38,7 @@ func TestNewNode_NoPackages(t *testing.T) { logger := log.NewTestingLogger(t) // Call NewDevNode with no package should work - cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.Logger = logger node, err := NewDevNode(ctx, cfg) require.NoError(t, err) @@ -62,7 +66,7 @@ func Render(_ string) string { return "foo" } logger := log.NewTestingLogger(t) // Call NewDevNode with no package should work - cfg := DefaultNodeConfig(gnoenv.RootDir()) + cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") cfg.PackagesPathList = []PackagePath{pkgpath} cfg.Logger = logger node, err := NewDevNode(ctx, cfg) @@ -221,6 +225,191 @@ func Render(_ string) string { return str } assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) } +func TestTxTimestampRecover(t *testing.T) { + const ( + // foo package + foobarGnoMod = "module gno.land/r/dev/foo\n" + fooFile = `package foo +import ( + "strconv" + "strings" + "time" +) + +var times = []time.Time{ + time.Now(), // Evaluate at genesis +} + +func SpanTime() { + times = append(times, time.Now()) +} + +func Render(_ string) string { + var strs strings.Builder + + strs.WriteRune('[') + for i, t := range times { + if i > 0 { + strs.WriteRune(',') + } + strs.WriteString(strconv.Itoa(int(t.UnixNano()))) + } + strs.WriteRune(']') + + return strs.String() +} +` + ) + + // Add a hard deadline of 20 seconds to avoid potential deadlock and fail early + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + parseJSONTimesList := func(t *testing.T, render string) []time.Time { + t.Helper() + + var times []time.Time + var nanos []int64 + + err := json.Unmarshal([]byte(render), &nanos) + require.NoError(t, err) + + for _, nano := range nanos { + sec, nsec := nano/int64(time.Second), nano%int64(time.Second) + times = append(times, time.Unix(sec, nsec)) + } + + return times + } + + // Generate package foo + foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) + + // Call NewDevNode with no package should work + cfg := createDefaultTestingNodeConfig(foopkg) + + // XXX(gfanton): Setting this to `false` somehow makes the time block + // drift from the time spanned by the VM. + cfg.TMConfig.Consensus.SkipTimeoutCommit = false + cfg.TMConfig.Consensus.TimeoutCommit = 500 * time.Millisecond + cfg.TMConfig.Consensus.TimeoutPropose = 100 * time.Millisecond + cfg.TMConfig.Consensus.CreateEmptyBlocks = true + + node, emitter := newTestingDevNodeWithConfig(t, cfg) + + // We need to make sure that blocks are separated by at least 1 second + // (minimal time between blocks). We can ensure this by listening for + // new blocks and comparing timestamps + cc := make(chan types.EventNewBlock) + node.Node.EventSwitch().AddListener("test-timestamp", func(evt tm2events.Event) { + newBlock, ok := evt.(types.EventNewBlock) + if !ok { + return + } + + select { + case cc <- newBlock: + default: + } + }) + + // wait for first block for reference + var refHeight, refTimestamp int64 + + select { + case <-ctx.Done(): + require.FailNow(t, ctx.Err().Error()) + case res := <-cc: + refTimestamp = res.Block.Time.Unix() + refHeight = res.Block.Height + } + + // number of span to process + const nevents = 3 + + // Span multiple time + for i := 0; i < nevents; i++ { + t.Logf("waiting for a bock greater than height(%d) and unix(%d)", refHeight, refTimestamp) + for { + var block types.EventNewBlock + select { + case <-ctx.Done(): + require.FailNow(t, ctx.Err().Error()) + case block = <-cc: + } + + t.Logf("got a block height(%d) and unix(%d)", + block.Block.Height, block.Block.Time.Unix()) + + // Ensure we consume every block before tx block + if refHeight >= block.Block.Height { + continue + } + + // Ensure new block timestamp is before previous reference timestamp + if newRefTimestamp := block.Block.Time.Unix(); newRefTimestamp > refTimestamp { + refTimestamp = newRefTimestamp + break // break the loop + } + } + + t.Logf("found a valid block(%d)! continue", refHeight) + + // Span a new time + msg := vm.MsgCall{ + PkgPath: "gno.land/r/dev/foo", + Func: "SpanTime", + } + + res, err := testingCallRealm(t, node, msg) + + require.NoError(t, err) + require.NoError(t, res.CheckTx.Error) + require.NoError(t, res.DeliverTx.Error) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) + + // Set the new height from the tx as reference + refHeight = res.Height + } + + // Render JSON times list + render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + + // Parse times list + timesList1 := parseJSONTimesList(t, render) + t.Logf("list of times: %+v", timesList1) + + // Ensure times are correctly expending. + for i, t2 := range timesList1 { + if i == 0 { + continue + } + + t1 := timesList1[i-1] + require.Greater(t, t2.UnixNano(), t1.UnixNano()) + } + + // Reload the node + err = node.Reload(context.Background()) + require.NoError(t, err) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) + + // Fetch time list again from render + render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + + timesList2 := parseJSONTimesList(t, render) + + // Times list should be identical from the orignal list + require.Len(t, timesList2, len(timesList1)) + for i := 0; i < len(timesList1); i++ { + t1nsec, t2nsec := timesList1[i].UnixNano(), timesList2[i].UnixNano() + assert.Equal(t, t1nsec, t2nsec, + "comparing times1[%d](%d) == times2[%d](%d)", i, t1nsec, i, t2nsec) + } +} + func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error) { t.Helper() @@ -249,7 +438,7 @@ func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types txcfg := gnoclient.BaseTxCfg{ GasFee: ugnot.ValueString(1000000), // Gas fee - GasWanted: 2_000_000, // Gas wanted + GasWanted: 3_000_000, // Gas wanted } // Set Caller in the msgs @@ -285,25 +474,37 @@ func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath { } } +func createDefaultTestingNodeConfig(pkgslist ...PackagePath) *NodeConfig { + cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") + cfg.PackagesPathList = pkgslist + return cfg +} + func newTestingDevNode(t *testing.T, pkgslist ...PackagePath) (*Node, *mock.ServerEmitter) { t.Helper() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - logger := log.NewTestingLogger(t) + cfg := createDefaultTestingNodeConfig(pkgslist...) + return newTestingDevNodeWithConfig(t, cfg) +} + +func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.ServerEmitter) { + t.Helper() + ctx, cancel := context.WithCancel(context.Background()) + logger := log.NewTestingLogger(t) emitter := &mock.ServerEmitter{} - // Call NewDevNode with no package should work - cfg := DefaultNodeConfig(gnoenv.RootDir()) - cfg.PackagesPathList = pkgslist cfg.Emitter = emitter cfg.Logger = logger + node, err := NewDevNode(ctx, cfg) require.NoError(t, err) - assert.Len(t, node.ListPkgs(), len(pkgslist)) + assert.Len(t, node.ListPkgs(), len(cfg.PackagesPathList)) - t.Cleanup(func() { node.Close() }) + t.Cleanup(func() { + node.Close() + cancel() + }) return node, emitter } diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go index 7b560c21e09..62c1907b8c9 100644 --- a/contribs/gnodev/pkg/dev/packages.go +++ b/contribs/gnodev/pkg/dev/packages.go @@ -5,8 +5,10 @@ import ( "fmt" "net/url" "path/filepath" + "time" "github.com/gnolang/gno/contribs/gnodev/pkg/address" + "github.com/gnolang/gno/gno.land/pkg/gnoland" vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" @@ -118,7 +120,7 @@ func (pm PackagesMap) toList() gnomod.PkgList { return list } -func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) { +func (pm PackagesMap) Load(fee std.Fee, start time.Time) ([]gnoland.TxWithMetadata, error) { pkgs := pm.toList() sorted, err := pkgs.Sort() @@ -127,7 +129,8 @@ func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) { } nonDraft := sorted.GetNonDraftPkgs() - txs := []std.Tx{} + + metatxs := make([]gnoland.TxWithMetadata, 0, len(nonDraft)) for _, modPkg := range nonDraft { pkg := pm[modPkg.Dir] if pkg.Creator.IsZero() { @@ -135,7 +138,7 @@ func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) { } // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(modPkg.Dir, modPkg.Name) + memPkg := gno.MustReadMemPackage(modPkg.Dir, modPkg.Name) if err := memPkg.Validate(); err != nil { return nil, fmt.Errorf("invalid package: %w", err) } @@ -153,8 +156,15 @@ func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) { } tx.Signatures = make([]std.Signature, len(tx.GetSigners())) - txs = append(txs, tx) + metatx := gnoland.TxWithMetadata{ + Tx: tx, + Metadata: &gnoland.GnoTxMetadata{ + Timestamp: start.Unix(), + }, + } + + metatxs = append(metatxs, metatx) } - return txs, nil + return metatxs, nil } diff --git a/contribs/gnofaucet/README.md b/contribs/gnofaucet/README.md new file mode 100644 index 00000000000..eefa41a8c6f --- /dev/null +++ b/contribs/gnofaucet/README.md @@ -0,0 +1,25 @@ +# Start a local faucet + +## Step1: + +Make sure you have started gnoland + + ../../gno.land/build/gnoland start -lazy + +## Step2: + +Start the faucet. + + ./build/gnofaucet serve -chain-id dev -mnemonic "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" + +By default, the faucet sends out 10,000,000ugnot (10gnot) per request. + +## Step3: + +Make sure you have started website + + ../../gno.land/build/gnoweb + +Request testing tokens from following URL, Have fun! + + http://localhost:8888/faucet \ No newline at end of file diff --git a/contribs/gnofaucet/go.mod b/contribs/gnofaucet/go.mod index c56c0b7d425..3d1e5f54c54 100644 --- a/contribs/gnofaucet/go.mod +++ b/contribs/gnofaucet/go.mod @@ -6,51 +6,53 @@ toolchain go1.22.4 require ( github.com/gnolang/faucet v0.3.2 - github.com/gnolang/gno v0.1.1 + github.com/gnolang/gno v0.1.0-nightly.20240627 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 golang.org/x/time v0.5.0 ) +replace github.com/gnolang/gno => ../.. + require ( - github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect - github.com/btcsuite/btcd/btcutil v1.1.5 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/btcsuite/btcd/btcutil v1.1.6 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/peterbourgon/ff/v3 v3.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rs/cors v1.11.0 // indirect - github.com/rs/xid v1.5.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap/exp v0.2.0 // indirect - golang.org/x/crypto v0.25.0 // indirect + go.uber.org/zap/exp v0.3.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect - golang.org/x/mod v0.19.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum index 1508cdae1e6..10e2c19b408 100644 --- a/contribs/gnofaucet/go.sum +++ b/contribs/gnofaucet/go.sum @@ -1,18 +1,20 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +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= 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 h1:js1gPwhcFflTZ7Nzl7WHaOTlTr5hIrR4n1NM4v9n4Kw= 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.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0= -github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +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 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= 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= @@ -47,10 +49,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gnolang/faucet v0.3.2 h1:3QBrdmnQszRaAZbxgO5xDDm3czNa0L/RFmhnCkbxy5I= github.com/gnolang/faucet v0.3.2/go.mod h1:/wbw9h4ooMzzyNBuM0X+ol7CiPH2OFjAFF3bYAXqA7U= -github.com/gnolang/gno v0.1.1 h1:t41S0SWIUa3syI7XpRAuCneCgRc8gOJ2g8DkUedF72U= -github.com/gnolang/gno v0.1.1/go.mod h1:BTaBNeaoY/W95NN6QA4RCoQ6Z7mi8M+Zb1I1wMWGg2w= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -77,10 +75,10 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ 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/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= 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= @@ -109,32 +107,39 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= -github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.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= -go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= -go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 h1:aLmmtjRke7LPDQ3lvpFz+kNEH43faFhzW7v8BFIEydg= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0/go.mod h1:TC1pyCt6G9Sjb4bQpShH+P5R53pO6ZuGnHuuln9xMeE= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= -go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 h1:k6fQVDQexDE+3jG2SfCQjnHS7OamcP73YMoxEVq5B6k= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0/go.mod h1:t4BrYLHU450Zo9fnydWlIuswB1bm7rM8havDpWOJeDo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 h1:xvhQxJ/C9+RTnAj5DpTg7LSM1vbbMTiXt7e9hsfqHNw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0/go.mod h1:Fcvs2Bz1jkDM+Wf5/ozBGmi3tQ/c9zPKLnsipnfhGAo= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -143,27 +148,27 @@ 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.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= -go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= +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-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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= @@ -173,24 +178,24 @@ 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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -199,8 +204,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ 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.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/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= diff --git a/contribs/gnogenesis/Makefile b/contribs/gnogenesis/Makefile new file mode 100644 index 00000000000..20f234e7e36 --- /dev/null +++ b/contribs/gnogenesis/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/gnogenesis . + +lint: + $(golangci_lint) --config ../../.github/golangci.yml run ./... + +test: + go test $(GOTEST_FLAGS) -v ./... + diff --git a/contribs/gnogenesis/README.md b/contribs/gnogenesis/README.md new file mode 100644 index 00000000000..25c82992f8f --- /dev/null +++ b/contribs/gnogenesis/README.md @@ -0,0 +1,185 @@ +## Overview + +`gnogenesis` is a CLI tool for managing the Gnoland blockchain's `genesis.json` file. It provides +subcommands for setting up and manipulating the genesis file, from generating a new genesis configuration to managing +initial validators, balances, and transactions. + +Refer to specific command help options (`--help`) for further customization options. + +## Installation + +To install gnogenesis, clone the repository and build the tool: + +```shell +git clone https://github.com/gnoland/gno.git +cd gno/contribs/gnogenesis +make install +``` + +This will compile and install `gnogenesis` to your system path, allowing you to run commands directly. + +## Features + +### Generate a `genesis.json` + +To create a new genesis.json, use the `generate` subcommand. You can specify parameters such as chain ID, block limits, +and more: + +```shell +gnogenesis generate --chain-id gno-dev --block-max-gas 100000000 --output-path ./genesis.json +``` + +This command generates a genesis.json file with custom parameters, defining the chain’s identity, block limits, and +more. By default, the genesis-time is set to the current timestamp, or you can specify a future time for scheduled chain +launches. + +Keep in mind the `genesis.json` is generated with an empty validator set, and you will need to manually add the initial +validators. + +### Manage initial validators + +The `validator` subcommands allow you to add or remove validators directly in the genesis file. + +#### Add a validator + +To add a validator, specify their `address`, `name`, and `pub-key`: + +```shell +gnogenesis validator add --address g1rzuwh5frve732k4futyw45y78rzuty4626zy6h --name validator1 --pub-key gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zplmcmggxyxyrch0zcyg684yxmerullv3l6hmau58sk4eyxskmny9h7lsnz +``` + +This command will add the validator with the specified details in the genesis file. + +The `address` and `pub-key` values need to be in bech32 format. They can be fetched using `gnoland secrets get`. + +#### Remove a validator + +If you need to remove a validator, specify their address: + +```shell +gnogenesis validator remove --address g1rzuwh5frve732k4futyw45y78rzuty4626zy6h +``` + +This will remove the specified validator from the validator set in `genesis.json`, if it is present. + +### Verify the `genesis.json` + +The `verify` subcommand is helpful to confirm the integrity of a `genesis.json` file: + +```shell +gnogenesis verify --genesis-path ./genesis.json +``` + +This validation checks for proper structure, account balance totals, and ensures validators are correctly configured, +preventing common genesis setup issues. It is advised to always run this verification step when dealing with an external +`genesis.json`. + +### Manage account balances + +Balances can be added or removed through the balances subcommand, either individually or using a balance sheet file. + +The format for individual balance entries is `
=ugnot`. + +#### Add Account Balances + +Add a single balance directly: + +```shell +gnogenesis balances add --single g1rzuwh5frve732k4futyw45y78rzuty4626zy6h=100ugnot +``` + +Alternatively, load multiple accounts with a balance sheet file: + +```shell +gnogenesis balances add --balance-sheet ./balances.txt +``` + +The format of the balance sheet file is the same as with individual entries, for example: + +```text +# Test accounts. +g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5=10000000000000ugnot # test1 +g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj=10000000000000ugnot # test2 + +# Faucet accounts. +g1f4v282mwyhu29afke4vq5r2xzcm6z3ftnugcnv=1000000000000ugnot # faucet0 (jae) +g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa=1000000000000ugnot # faucet1 (moul) +g1q6jrp203fq0239pv38sdq3y3urvd6vt5azacpv=1000000000000ugnot # faucet2 (devx) +g13d7jc32adhc39erm5me38w5v7ej7lpvlnqjk73=1000000000000ugnot # faucet3 (devx) +g18l9us6trqaljw39j94wzf5ftxmd9qqkvrxghd2=1000000000000ugnot # faucet4 (adena) +``` + +This will update `genesis.json` with the provided accounts and balances. + +#### Remove account balances + +To remove an account’s balance from `genesis.json`, use: + +```shell +gnogenesis balances remove --address g1rzuwh5frve732k4futyw45y78rzuty4626zy6h +``` + +This deletes the balance entry for the specified address, if present. + +### Handle genesis transactions + +The `txs` subcommand allows you to manage initial transactions. + +It is a bit more robust than the `balances` command suite, in the sense that it supports: + +- adding transactions from transaction sheets +- generating and adding deploy transactions from a directory (ex. like `examples`) + +The format for transactions in the transaction sheet is the following: + +- Transaction (`std.Tx`) is encoded in Amino JSON +- Transactions are saved single-line, 1 line 1 tx +- File format of the transaction sheet file is `jsonl` + +#### Add genesis transactions + +To add genesis transactions from a file: + +```shell +gnogenesis txs add sheets ./txs.json +``` + +This outputs the initial transaction count. + +An example transaction sheet: + +```json lines +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj:10\ng1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s:1\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8:1\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q:1\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj:1\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0:1\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz:1\ng187982000zsc493znqt828s90cmp6hcp2erhu6m:1\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl:1\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037:1\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5:1\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr:1\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz:1\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w:1\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz:1\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3:1\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0:1\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n:1\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac:1\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap:1\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv:1\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv:1\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq:1\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6:1\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q:1\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7:1\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k:1\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll:1\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd:1\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64:1\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw:1\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a:1\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc:1\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6:1\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6:1\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9:1\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea:1\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3:1\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp:1\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5:1\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf:1\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g:1\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r:1\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su:1\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69:1\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6:1\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"S8iMMzlOMK8dmox78R9Z8+pSsS8YaTCXrIcaHDpiOgkOy7gqoQJ0oftM0zf8zAz4xpezK8Lzg8Q0fCdXJxV76w=="}],"memo":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1thlf3yct7n7ex70k0p62user0kn6mj6d3s0cg3\ng1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"njczE6xYdp01+CaUU/8/v0YC/NuZD06+qLind+ZZEEMNaRe/4Ln+4z7dG6HYlaWUMsyI1KCoB6NIehoE0PZ44Q=="}],"memo":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz\ng187982000zsc493znqt828s90cmp6hcp2erhu6m\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6\ng1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t\n"]}],"fee":{"gas_wanted":"4000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"7AmlhZhsVkxCUl0bbpvpPMnIKihwtG7A5IFR6Tg4xStWLgaUr05XmWRKlO2xjstTtwbVKQT5mFL4h5wyX4SQzw=="}],"memo":""} +``` + +To add genesis (deploy) transactions from a directory: + +```shell +gnogenesis txs add packages ./examples +``` + +This will generate `MsgAddPkg` transactions, and add them to the given `genesis.json`. + +#### Remove genesis transactions + +To clear specific transactions, use the transaction hash: + +```shell +gnogenesis txs remove "5HuU9LN8WUa2NsjiNxp8Xii9n0zlSGXc9UqzLHB+DPs=" +``` +To specify a deployer address (package creator) on add packages command +```shell +gnogenesis txs add packages ./examples --deployer-address=SOME_ADDRESS +``` + +The transaction hash is the base64 encoding of the Amino-Binary encoded `std.Tx` transaction hash. + +The steps to get this sort of hash are: + +- get the `std.Tx` +- marshal it using `amino.Marshal` +- cast the result to `types.Tx` (`bft`) +- call `Hash` on the `types.Tx` +- encode the result into base64 diff --git a/contribs/gnogenesis/genesis.go b/contribs/gnogenesis/genesis.go new file mode 100644 index 00000000000..839e5fbe653 --- /dev/null +++ b/contribs/gnogenesis/genesis.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/gnolang/contribs/gnogenesis/internal/balances" + "github.com/gnolang/contribs/gnogenesis/internal/generate" + "github.com/gnolang/contribs/gnogenesis/internal/txs" + "github.com/gnolang/contribs/gnogenesis/internal/validator" + "github.com/gnolang/contribs/gnogenesis/internal/verify" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +func newGenesisCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags] [...]", + ShortHelp: "gno genesis manipulation suite", + LongHelp: "Gno genesis.json manipulation suite, for managing genesis parameters", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + generate.NewGenerateCmd(io), + validator.NewValidatorCmd(io), + verify.NewVerifyCmd(io), + balances.NewBalancesCmd(io), + txs.NewTxsCmd(io), + ) + + return cmd +} diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod new file mode 100644 index 00000000000..3056af1d4cc --- /dev/null +++ b/contribs/gnogenesis/go.mod @@ -0,0 +1,62 @@ +module github.com/gnolang/contribs/gnogenesis + +go 1.22 + +require ( + github.com/gnolang/gno v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.9.0 +) + +replace github.com/gnolang/gno => ../.. + +require ( + 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 + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect + github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/zondax/hid v0.9.2 // indirect + github.com/zondax/ledger-go v0.14.3 // indirect + go.etcd.io/bbolt v1.3.11 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum new file mode 100644 index 00000000000..7e4a683cad1 --- /dev/null +++ b/contribs/gnogenesis/go.sum @@ -0,0 +1,228 @@ +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= +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/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/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/cosmos/ledger-cosmos-go v0.13.3 h1:7ehuBGuyIytsXbd4MP43mLeoN2LTOEnk5nvue4rK+yM= +github.com/cosmos/ledger-cosmos-go v0.13.3/go.mod h1:HENcEP+VtahZFw38HZ3+LS3Iv5XV6svsnkk9vdJtLr8= +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/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +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/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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +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/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/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +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/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/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +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 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +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 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +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/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= +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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.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/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= +github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= +github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 h1:k6fQVDQexDE+3jG2SfCQjnHS7OamcP73YMoxEVq5B6k= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0/go.mod h1:t4BrYLHU450Zo9fnydWlIuswB1bm7rM8havDpWOJeDo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 h1:xvhQxJ/C9+RTnAj5DpTg7LSM1vbbMTiXt7e9hsfqHNw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0/go.mod h1:Fcvs2Bz1jkDM+Wf5/ozBGmi3tQ/c9zPKLnsipnfhGAo= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +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.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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/gnogenesis/internal/balances/balances.go b/contribs/gnogenesis/internal/balances/balances.go new file mode 100644 index 00000000000..bdfa5aa38d0 --- /dev/null +++ b/contribs/gnogenesis/internal/balances/balances.go @@ -0,0 +1,40 @@ +package balances + +import ( + "flag" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type balancesCfg struct { + common.Cfg +} + +// NewBalancesCmd creates the genesis balances subcommand +func NewBalancesCmd(io commands.IO) *commands.Command { + cfg := &balancesCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "balances", + ShortUsage: " [flags]", + ShortHelp: "manages genesis.json account balances", + LongHelp: "Manipulates the initial genesis.json account balances (pre-mines)", + }, + cfg, + commands.HelpExec, + ) + + cmd.AddSubCommands( + newBalancesAddCmd(cfg, io), + newBalancesRemoveCmd(cfg, io), + newBalancesExportCmd(cfg, io), + ) + + return cmd +} + +func (c *balancesCfg) RegisterFlags(fs *flag.FlagSet) { + c.Cfg.RegisterFlags(fs) +} diff --git a/contribs/gnogenesis/internal/balances/balances_add.go b/contribs/gnogenesis/internal/balances/balances_add.go new file mode 100644 index 00000000000..a17a13f8bc8 --- /dev/null +++ b/contribs/gnogenesis/internal/balances/balances_add.go @@ -0,0 +1,298 @@ +package balances + +import ( + "bufio" + "context" + "errors" + "flag" + "fmt" + "io" + "os" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + + _ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" +) + +var ( + errNoBalanceSource = errors.New("at least one balance source must be set") + errBalanceParsingAborted = errors.New("balance parsing aborted") + errInvalidAddress = errors.New("invalid address encountered") +) + +type balancesAddCfg struct { + rootCfg *balancesCfg + + balanceSheet string + singleEntries commands.StringArr + parseExport string +} + +// newBalancesAddCmd creates the genesis balances add subcommand +func newBalancesAddCmd(rootCfg *balancesCfg, io commands.IO) *commands.Command { + cfg := &balancesAddCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "add", + ShortUsage: "balances add [flags]", + ShortHelp: "adds balances to the genesis.json", + }, + cfg, + func(ctx context.Context, _ []string) error { + return execBalancesAdd(ctx, cfg, io) + }, + ) +} + +func (c *balancesAddCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.balanceSheet, + "balance-sheet", + "", + "the path to the balance file containing addresses in the format
="+ugnot.Denom, + ) + + fs.Var( + &c.singleEntries, + "single", + "the direct balance addition in the format
="+ugnot.Denom, + ) + + fs.StringVar( + &c.parseExport, + "parse-export", + "", + "the path to the transaction export containing a list of transactions (JSONL)", + ) +} + +func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.GenesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Validate the source is set correctly + var ( + singleEntriesSet = len(cfg.singleEntries) != 0 + balanceSheetSet = cfg.balanceSheet != "" + txFileSet = cfg.parseExport != "" + ) + + if !singleEntriesSet && !balanceSheetSet && !txFileSet { + return errNoBalanceSource + } + + finalBalances := gnoland.NewBalances() + + // Get the balance sheet from the source + if singleEntriesSet { + balances, err := gnoland.GetBalancesFromEntries(cfg.singleEntries...) + if err != nil { + return fmt.Errorf("unable to get balances from entries, %w", err) + } + + finalBalances.LeftMerge(balances) + } + + if balanceSheetSet { + // Open the balance sheet + file, loadErr := os.Open(cfg.balanceSheet) + if loadErr != nil { + return fmt.Errorf("unable to open balance sheet, %w", loadErr) + } + + balances, err := gnoland.GetBalancesFromSheet(file) + if err != nil { + return fmt.Errorf("unable to get balances from balance sheet, %w", err) + } + + finalBalances.LeftMerge(balances) + } + + if txFileSet { + // Open the transactions file + file, loadErr := os.Open(cfg.parseExport) + if loadErr != nil { + return fmt.Errorf("unable to open transactions file, %w", loadErr) + } + + balances, err := getBalancesFromTransactions(ctx, io, file) + if err != nil { + return fmt.Errorf("unable to get balances from tx file, %w", err) + } + + finalBalances.LeftMerge(balances) + } + + // Initialize genesis app state if it is not initialized already + if genesis.AppState == nil { + genesis.AppState = gnoland.GnoGenesisState{} + } + + // Construct the initial genesis balance sheet + state := genesis.AppState.(gnoland.GnoGenesisState) + genesisBalances, err := mapGenesisBalancesFromState(state) + if err != nil { + return err + } + + // Merge the two balance sheets, with the input + // having precedence over the genesis balances + finalBalances.LeftMerge(genesisBalances) + + // Save the balances + state.Balances = finalBalances.List() + genesis.AppState = state + + // Save the updated genesis + if err := genesis.SaveAs(cfg.rootCfg.GenesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "%d pre-mines saved", + len(finalBalances), + ) + + io.Println() + + for address, balance := range finalBalances { + io.Printfln("%s:%d%s", address.String(), balance, ugnot.Denom) + } + + return nil +} + +// getBalancesFromTransactions constructs a balance map based on MsgSend messages. +// This way of determining the final balance sheet is not valid, since it doesn't take into +// account different message types (ex. MsgCall) that can initialize accounts with some balance values. +// The right way to do this sort of initialization is to spin up an in-memory node +// and execute the entire transaction history to determine touched accounts and final balances, +// and construct a balance sheet based off of this information +func getBalancesFromTransactions( + ctx context.Context, + io commands.IO, + reader io.Reader, +) (gnoland.Balances, error) { + balances := gnoland.NewBalances() + + scanner := bufio.NewScanner(reader) + + for scanner.Scan() { + select { + case <-ctx.Done(): + return nil, errBalanceParsingAborted + default: + // Parse the amino JSON + var tx std.Tx + + line := scanner.Bytes() + + if err := amino.UnmarshalJSON(line, &tx); err != nil { + io.ErrPrintfln( + "invalid amino JSON encountered: %q", + string(line), + ) + + continue + } + + feeAmount := std.NewCoins(tx.Fee.GasFee) + if feeAmount.AmountOf(ugnot.Denom) <= 0 { + io.ErrPrintfln( + "invalid gas fee amount encountered: %q", + tx.Fee.GasFee.String(), + ) + } + + for _, msg := range tx.Msgs { + if msg.Type() != "send" { + continue + } + + msgSend := msg.(bank.MsgSend) + + sendAmount := msgSend.Amount + if sendAmount.AmountOf(ugnot.Denom) <= 0 { + io.ErrPrintfln( + "invalid send amount encountered: %s", + msgSend.Amount.String(), + ) + continue + } + + // This way of determining final account balances is not really valid, + // because we take into account only the ugnot transfer messages (MsgSend) + // and not other message types (like MsgCall), that can also + // initialize accounts with some gnoland. Because of this, + // we can run into a situation where a message send amount or fee + // causes an accounts balance to go < 0. In these cases, + // we initialize the account (it is present in the balance sheet), but + // with the balance of 0 + + from := balances[msgSend.FromAddress].Amount + to := balances[msgSend.ToAddress].Amount + + to = to.Add(sendAmount) + + if from.IsAllLT(sendAmount) || from.IsAllLT(feeAmount) { + // Account cannot cover send amount / fee + // (see message above) + from = std.NewCoins(std.NewCoin(ugnot.Denom, 0)) + } + + if from.IsAllGT(sendAmount) { + from = from.Sub(sendAmount) + } + + if from.IsAllGT(feeAmount) { + from = from.Sub(feeAmount) + } + + // Set new balance + balances[msgSend.FromAddress] = gnoland.Balance{ + Address: msgSend.FromAddress, + Amount: from, + } + balances[msgSend.ToAddress] = gnoland.Balance{ + Address: msgSend.ToAddress, + Amount: to, + } + } + } + } + + // Check for scanning errors + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf( + "error encountered while reading file, %w", + err, + ) + } + + return balances, nil +} + +// mapGenesisBalancesFromState extracts the initial account balances from the +// genesis app state +func mapGenesisBalancesFromState(state gnoland.GnoGenesisState) (gnoland.Balances, error) { + // Construct the initial genesis balance sheet + genesisBalances := gnoland.NewBalances() + + for _, balance := range state.Balances { + genesisBalances[balance.Address] = balance + } + + return genesisBalances, nil +} diff --git a/contribs/gnogenesis/internal/balances/balances_add_test.go b/contribs/gnogenesis/internal/balances/balances_add_test.go new file mode 100644 index 00000000000..29ffe19d95a --- /dev/null +++ b/contribs/gnogenesis/internal/balances/balances_add_test.go @@ -0,0 +1,568 @@ +package balances + +import ( + "bytes" + "context" + "fmt" + "strings" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Balances_Add(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis", func(t *testing.T) { + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.ErrorContains(t, cmdErr, common.ErrUnableToLoadGenesis.Error()) + }) + + t.Run("no sources selected", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoBalanceSource.Error()) + }) + + t.Run("invalid genesis path", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrUnableToLoadGenesis.Error()) + }) + + t.Run("balances from entries", func(t *testing.T) { + t.Parallel() + + dummyKeys := common.GetDummyKeys(t, 2) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + } + + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + + for _, dummyKey := range dummyKeys { + args = append(args, "--single") + args = append( + args, + fmt.Sprintf( + "%s=%s", + dummyKey.Address().String(), + ugnot.ValueString(amount.AmountOf(ugnot.Denom)), + ), + ) + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the genesis was updated + genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, loadErr) + + require.NotNil(t, genesis.AppState) + + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + + require.Equal(t, len(dummyKeys), len(state.Balances)) + + for _, balance := range state.Balances { + // Find the appropriate key + // (the genesis is saved with randomized balance order) + found := false + for _, dummyKey := range dummyKeys { + if dummyKey.Address().String() == balance.Address.String() { + assert.Equal(t, amount, balance.Amount) + + found = true + break + } + } + + if !found { + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) + } + } + }) + + t.Run("balances from sheet", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + dummyKeys := common.GetDummyKeys(t, 10) + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + + balances := make([]string, len(dummyKeys)) + + // Add a random comment to the balances file output + balances = append(balances, "#comment\n") + + for index, key := range dummyKeys { + balances[index] = fmt.Sprintf( + "%s=%s", + key.Address().String(), + ugnot.ValueString(amount.AmountOf(ugnot.Denom)), + ) + } + + // Write the balance sheet to a file + balanceSheet, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + _, err := balanceSheet.WriteString(strings.Join(balances, "\n")) + require.NoError(t, err) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + "--balance-sheet", + balanceSheet.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the genesis was updated + genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, loadErr) + + require.NotNil(t, genesis.AppState) + + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + + require.Equal(t, len(dummyKeys), len(state.Balances)) + + for _, balance := range state.Balances { + // Find the appropriate key + // (the genesis is saved with randomized balance order) + found := false + for _, dummyKey := range dummyKeys { + if dummyKey.Address().String() == balance.Address.String() { + assert.Equal(t, amount, balance.Amount) + + found = true + break + } + } + + if !found { + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) + } + } + }) + + t.Run("balances from transactions", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + var ( + dummyKeys = common.GetDummyKeys(t, 10) + amount = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + gasFee = std.NewCoin(ugnot.Denom, 1000000) + txs = make([]std.Tx, 0) + ) + + sender := dummyKeys[0] + for _, dummyKey := range dummyKeys[1:] { + tx := std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: sender.Address(), + ToAddress: dummyKey.Address(), + Amount: amountCoins, + }, + }, + Fee: std.Fee{ + GasWanted: 10, + GasFee: gasFee, + }, + Signatures: make([]std.Signature, 0), + } + + txs = append(txs, tx) + } + + // Marshal the transactions into amino JSON + marshalledTxs := make([]string, 0, len(txs)) + + for _, tx := range txs { + marshalledTx, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + marshalledTxs = append(marshalledTxs, string(marshalledTx)) + } + + // Write the transactions to a file + txsFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + _, err := txsFile.WriteString(strings.Join(marshalledTxs, "\n")) + require.NoError(t, err) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + "--parse-export", + txsFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the genesis was updated + genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, loadErr) + + require.NotNil(t, genesis.AppState) + + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + + require.Equal(t, len(dummyKeys), len(state.Balances)) + + for _, balance := range state.Balances { + // Find the appropriate key + // (the genesis is saved with randomized balance order) + found := false + for index, dummyKey := range dummyKeys { + checkAmount := amount + if index == 0 { + // the first address should + // have a balance of 0 + checkAmount = std.NewCoins(std.NewCoin(ugnot.Denom, 0)) + } + + if dummyKey.Address().String() == balance.Address.String() { + assert.True(t, balance.Amount.IsEqual(checkAmount)) + + found = true + break + } + } + + if !found { + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) + } + } + }) + + t.Run("balances overwrite", func(t *testing.T) { + t.Parallel() + + dummyKeys := common.GetDummyKeys(t, 10) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + state := gnoland.GnoGenesisState{ + // Set an initial balance value + Balances: []gnoland.Balance{ + { + Address: dummyKeys[0].Address(), + Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 100)), + }, + }, + } + genesis.AppState = state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + } + + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + + for _, dummyKey := range dummyKeys { + args = append(args, "--single") + args = append( + args, + fmt.Sprintf( + "%s=%s", + dummyKey.Address().String(), + ugnot.ValueString(amount.AmountOf(ugnot.Denom)), + ), + ) + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the genesis was updated + genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, loadErr) + + require.NotNil(t, genesis.AppState) + + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + + require.Equal(t, len(dummyKeys), len(state.Balances)) + + for _, balance := range state.Balances { + // Find the appropriate key + // (the genesis is saved with randomized balance order) + found := false + for _, dummyKey := range dummyKeys { + if dummyKey.Address().String() == balance.Address.String() { + assert.Equal(t, amount, balance.Amount) + + found = true + break + } + } + + if !found { + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) + } + } + }) +} + +func TestBalances_GetBalancesFromTransactions(t *testing.T) { + t.Parallel() + + t.Run("valid transactions", func(t *testing.T) { + t.Parallel() + + var ( + dummyKeys = common.GetDummyKeys(t, 10) + amount = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + gasFee = std.NewCoin(ugnot.Denom, 1000000) + txs = make([]std.Tx, 0) + ) + + sender := dummyKeys[0] + for _, dummyKey := range dummyKeys[1:] { + tx := std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: sender.Address(), + ToAddress: dummyKey.Address(), + Amount: amountCoins, + }, + }, + Fee: std.Fee{ + GasWanted: 10, + GasFee: gasFee, + }, + Signatures: make([]std.Signature, 0), + } + + txs = append(txs, tx) + } + + // Marshal the transactions into amino JSON + marshalledTxs := make([]string, 0, len(txs)) + + for _, tx := range txs { + marshalledTx, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + marshalledTxs = append(marshalledTxs, string(marshalledTx)) + } + + mockErr := new(bytes.Buffer) + io := commands.NewTestIO() + io.SetErr(commands.WriteNopCloser(mockErr)) + + reader := strings.NewReader(strings.Join(marshalledTxs, "\n")) + balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader) + require.NoError(t, err) + + // Validate the balance map + assert.Len(t, balanceMap, len(dummyKeys)) + for _, key := range dummyKeys[1:] { + assert.Equal(t, amount, balanceMap[key.Address()].Amount) + } + + assert.Equal(t, std.Coins{}, balanceMap[sender.Address()].Amount) + }) + + t.Run("malformed transaction, invalid fee amount", func(t *testing.T) { + t.Parallel() + + var ( + dummyKeys = common.GetDummyKeys(t, 10) + amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + gasFee = std.NewCoin("gnos", 1) // invalid fee + txs = make([]std.Tx, 0) + ) + + sender := dummyKeys[0] + for _, dummyKey := range dummyKeys[1:] { + tx := std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: sender.Address(), + ToAddress: dummyKey.Address(), + Amount: amountCoins, + }, + }, + Fee: std.Fee{ + GasWanted: 10, + GasFee: gasFee, + }, + Signatures: make([]std.Signature, 0), + } + + txs = append(txs, tx) + } + + // Marshal the transactions into amino JSON + marshalledTxs := make([]string, 0, len(txs)) + + for _, tx := range txs { + marshalledTx, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + marshalledTxs = append(marshalledTxs, string(marshalledTx)) + } + + mockErr := new(bytes.Buffer) + io := commands.NewTestIO() + io.SetErr(commands.WriteNopCloser(mockErr)) + + reader := strings.NewReader(strings.Join(marshalledTxs, "\n")) + balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader) + require.NoError(t, err) + + assert.NotNil(t, balanceMap) + assert.Contains(t, mockErr.String(), "invalid gas fee amount") + }) + + t.Run("malformed transaction, invalid send amount", func(t *testing.T) { + t.Parallel() + + var ( + dummyKeys = common.GetDummyKeys(t, 10) + amountCoins = std.NewCoins(std.NewCoin("gnogno", 10)) // invalid send amount + gasFee = std.NewCoin(ugnot.Denom, 1) + txs = make([]std.Tx, 0) + ) + + sender := dummyKeys[0] + for _, dummyKey := range dummyKeys[1:] { + tx := std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: sender.Address(), + ToAddress: dummyKey.Address(), + Amount: amountCoins, + }, + }, + Fee: std.Fee{ + GasWanted: 10, + GasFee: gasFee, + }, + Signatures: make([]std.Signature, 0), + } + + txs = append(txs, tx) + } + + // Marshal the transactions into amino JSON + marshalledTxs := make([]string, 0, len(txs)) + + for _, tx := range txs { + marshalledTx, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + marshalledTxs = append(marshalledTxs, string(marshalledTx)) + } + + mockErr := new(bytes.Buffer) + io := commands.NewTestIO() + io.SetErr(commands.WriteNopCloser(mockErr)) + + reader := strings.NewReader(strings.Join(marshalledTxs, "\n")) + balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader) + require.NoError(t, err) + + assert.NotNil(t, balanceMap) + assert.Contains(t, mockErr.String(), "invalid send amount") + }) +} diff --git a/contribs/gnogenesis/internal/balances/balances_export.go b/contribs/gnogenesis/internal/balances/balances_export.go new file mode 100644 index 00000000000..1970e348b1a --- /dev/null +++ b/contribs/gnogenesis/internal/balances/balances_export.go @@ -0,0 +1,80 @@ +package balances + +import ( + "context" + "fmt" + "os" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// newBalancesExportCmd creates the genesis balances export subcommand +func newBalancesExportCmd(balancesCfg *balancesCfg, io commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "export", + ShortUsage: "balances export [flags] ", + ShortHelp: "exports the balances from the genesis.json", + LongHelp: "Exports the balances from the genesis.json to an output file", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execBalancesExport(balancesCfg, io, args) + }, + ) +} + +func execBalancesExport(cfg *balancesCfg, io commands.IO, args []string) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.GenesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Load the genesis state + if genesis.AppState == nil { + return common.ErrAppStateNotSet + } + + state := genesis.AppState.(gnoland.GnoGenesisState) + if len(state.Balances) == 0 { + io.Println("No genesis balances to export") + + return nil + } + + // Make sure the output file path is specified + if len(args) == 0 { + return common.ErrNoOutputFile + } + + // Open output file + outputFile, err := os.OpenFile( + args[0], + os.O_RDWR|os.O_CREATE|os.O_APPEND, + 0o755, + ) + if err != nil { + return fmt.Errorf("unable to create output file, %w", err) + } + defer outputFile.Close() + + // Save the balances + for _, balance := range state.Balances { + if _, err = outputFile.WriteString( + fmt.Sprintf("%s\n", balance), + ); err != nil { + return fmt.Errorf("unable to write to output, %w", err) + } + } + + io.Printfln( + "Exported %d balances", + len(state.Balances), + ) + + return nil +} diff --git a/contribs/gnogenesis/internal/balances/balances_export_test.go b/contribs/gnogenesis/internal/balances/balances_export_test.go new file mode 100644 index 00000000000..d4f4723df15 --- /dev/null +++ b/contribs/gnogenesis/internal/balances/balances_export_test.go @@ -0,0 +1,156 @@ +package balances + +import ( + "bufio" + "context" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// getDummyBalances generates dummy balance lines +func getDummyBalances(t *testing.T, count int) []gnoland.Balance { + t.Helper() + + dummyKeys := common.GetDummyKeys(t, count) + amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) + + balances := make([]gnoland.Balance, len(dummyKeys)) + + for index, key := range dummyKeys { + balances[index] = gnoland.Balance{ + Address: key.Address(), + Amount: amount, + } + } + + return balances +} + +func TestGenesis_Balances_Export(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "export", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrUnableToLoadGenesis.Error()) + }) + + t.Run("invalid genesis app state", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + genesis.AppState = nil // no app state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "export", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrAppStateNotSet.Error()) + }) + + t.Run("no output file specified", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Balances: getDummyBalances(t, 1), + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "export", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrNoOutputFile.Error()) + }) + + t.Run("valid balances export", func(t *testing.T) { + t.Parallel() + + // Generate dummy balances + balances := getDummyBalances(t, 10) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Balances: balances, + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Prepare the output file + outputFile, outputCleanup := testutils.NewTestFile(t) + t.Cleanup(outputCleanup) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "export", + "--genesis-path", + tempGenesis.Name(), + outputFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + scanner := bufio.NewScanner(outputFile) + + outputBalances := make([]gnoland.Balance, 0) + for scanner.Scan() { + var balance gnoland.Balance + err := balance.Parse(scanner.Text()) + require.NoError(t, err) + + outputBalances = append(outputBalances, balance) + } + + require.NoError(t, scanner.Err()) + + assert.Len(t, outputBalances, len(balances)) + + for index, balance := range outputBalances { + assert.Equal(t, balances[index], balance) + } + }) +} diff --git a/contribs/gnogenesis/internal/balances/balances_remove.go b/contribs/gnogenesis/internal/balances/balances_remove.go new file mode 100644 index 00000000000..ea2aefda5cc --- /dev/null +++ b/contribs/gnogenesis/internal/balances/balances_remove.go @@ -0,0 +1,101 @@ +package balances + +import ( + "context" + "errors" + "flag" + "fmt" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" +) + +var errBalanceNotFound = errors.New("genesis balances entry does not exist") + +type balancesRemoveCfg struct { + rootCfg *balancesCfg + + address string +} + +// newBalancesRemoveCmd creates the genesis balances remove subcommand +func newBalancesRemoveCmd(rootCfg *balancesCfg, io commands.IO) *commands.Command { + cfg := &balancesRemoveCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "remove", + ShortUsage: "balances remove [flags]", + ShortHelp: "removes the balance information of a specific account", + }, + cfg, + func(_ context.Context, _ []string) error { + return execBalancesRemove(cfg, io) + }, + ) +} + +func (c *balancesRemoveCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.address, + "address", + "", + "the address of the account whose balance information should be removed from genesis.json", + ) +} + +func execBalancesRemove(cfg *balancesRemoveCfg, io commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.GenesisPath) + if loadErr != nil { + return fmt.Errorf("%w, %w", common.ErrUnableToLoadGenesis, loadErr) + } + + // Validate the address + address, err := crypto.AddressFromString(cfg.address) + if err != nil { + return fmt.Errorf("%w, %w", errInvalidAddress, err) + } + + // Check if the genesis state is set at all + if genesis.AppState == nil { + return common.ErrAppStateNotSet + } + + // Construct the initial genesis balance sheet + state := genesis.AppState.(gnoland.GnoGenesisState) + genesisBalances, err := mapGenesisBalancesFromState(state) + if err != nil { + return err + } + + // Check if the genesis balance for the account is present + _, exists := genesisBalances[address] + if !exists { + return errBalanceNotFound + } + + // Drop the account pre-mine + delete(genesisBalances, address) + + // Save the balances + state.Balances = genesisBalances.List() + genesis.AppState = state + + // Save the updated genesis + if err := genesis.SaveAs(cfg.rootCfg.GenesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "Pre-mine information for address %s removed", + address.String(), + ) + + return nil +} diff --git a/contribs/gnogenesis/internal/balances/balances_remove_test.go b/contribs/gnogenesis/internal/balances/balances_remove_test.go new file mode 100644 index 00000000000..ab99a31c0a9 --- /dev/null +++ b/contribs/gnogenesis/internal/balances/balances_remove_test.go @@ -0,0 +1,138 @@ +package balances + +import ( + "context" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Balances_Remove(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis", func(t *testing.T) { + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.ErrorContains(t, cmdErr, common.ErrUnableToLoadGenesis.Error()) + }) + + t.Run("genesis app state not set", func(t *testing.T) { + t.Parallel() + + dummyKey := common.GetDummyKey(t) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + genesis.AppState = nil // not set + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKey.Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.ErrorContains(t, cmdErr, common.ErrAppStateNotSet.Error()) + }) + + t.Run("address is present", func(t *testing.T) { + t.Parallel() + + dummyKey := common.GetDummyKey(t) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + state := gnoland.GnoGenesisState{ + // Set an initial balance value + Balances: []gnoland.Balance{ + { + Address: dummyKey.Address(), + Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 100)), + }, + }, + } + genesis.AppState = state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKey.Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the genesis was updated + genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, loadErr) + + require.NotNil(t, genesis.AppState) + + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + + assert.Len(t, state.Balances, 0) + }) + + t.Run("address not present", func(t *testing.T) { + t.Parallel() + + dummyKey := common.GetDummyKey(t) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + state := gnoland.GnoGenesisState{ + Balances: []gnoland.Balance{}, // Empty initial balance + } + genesis.AppState = state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewBalancesCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKey.Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.ErrorContains(t, cmdErr, errBalanceNotFound.Error()) + }) +} diff --git a/contribs/gnogenesis/internal/common/config.go b/contribs/gnogenesis/internal/common/config.go new file mode 100644 index 00000000000..99278b77764 --- /dev/null +++ b/contribs/gnogenesis/internal/common/config.go @@ -0,0 +1,35 @@ +package common + +import ( + "flag" + "time" + + "github.com/gnolang/gno/tm2/pkg/bft/types" +) + +const DefaultChainID = "dev" + +// Cfg is the common +// configuration for genesis commands +// that require a genesis.json +type Cfg struct { + GenesisPath string +} + +func (c *Cfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.GenesisPath, + "genesis-path", + "./genesis.json", + "the path to the genesis.json", + ) +} + +// GetDefaultGenesis returns the default genesis config +func GetDefaultGenesis() *types.GenesisDoc { + return &types.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: DefaultChainID, + ConsensusParams: types.DefaultConsensusParams(), + } +} diff --git a/contribs/gnogenesis/internal/common/errors.go b/contribs/gnogenesis/internal/common/errors.go new file mode 100644 index 00000000000..6eff43e9dc7 --- /dev/null +++ b/contribs/gnogenesis/internal/common/errors.go @@ -0,0 +1,9 @@ +package common + +import "errors" + +var ( + ErrAppStateNotSet = errors.New("genesis app state not set") + ErrNoOutputFile = errors.New("no output file path specified") + ErrUnableToLoadGenesis = errors.New("unable to load genesis") +) diff --git a/contribs/gnogenesis/internal/common/helpers.go b/contribs/gnogenesis/internal/common/helpers.go new file mode 100644 index 00000000000..2b1f473aed1 --- /dev/null +++ b/contribs/gnogenesis/internal/common/helpers.go @@ -0,0 +1,52 @@ +package common + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" + "github.com/gnolang/gno/tm2/pkg/crypto/hd" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/stretchr/testify/require" +) + +// GetDummyKey generates a random public key, +// and returns the key info +func GetDummyKey(t *testing.T) crypto.PubKey { + t.Helper() + + mnemonic, err := client.GenerateMnemonic(256) + require.NoError(t, err) + + seed := bip39.NewSeed(mnemonic, "") + + return generateKeyFromSeed(seed, 0).PubKey() +} + +// generateKeyFromSeed generates a private key from +// the provided seed and index +func generateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey { + pathParams := hd.NewFundraiserParams(0, crypto.CoinType, index) + + masterPriv, ch := hd.ComputeMastersFromSeed(seed) + + //nolint:errcheck // This derivation can never error out, since the path params + // are always going to be valid + derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, pathParams.String()) + + return secp256k1.PrivKeySecp256k1(derivedPriv) +} + +// GetDummyKeys generates random keys for testing +func GetDummyKeys(t *testing.T, count int) []crypto.PubKey { + t.Helper() + + dummyKeys := make([]crypto.PubKey, count) + + for i := 0; i < count; i++ { + dummyKeys[i] = GetDummyKey(t) + } + + return dummyKeys +} diff --git a/contribs/gnogenesis/internal/generate/generate.go b/contribs/gnogenesis/internal/generate/generate.go new file mode 100644 index 00000000000..729b904d548 --- /dev/null +++ b/contribs/gnogenesis/internal/generate/generate.go @@ -0,0 +1,143 @@ +package generate + +import ( + "context" + "flag" + "fmt" + "time" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type generateCfg struct { + outputPath string + chainID string + genesisTime int64 + blockMaxTxBytes int64 + blockMaxDataBytes int64 + blockMaxGas int64 + blockTimeIota int64 +} + +// NewGenerateCmd creates the genesis generate subcommand +func NewGenerateCmd(io commands.IO) *commands.Command { + cfg := &generateCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "generate", + ShortUsage: "[flags]", + ShortHelp: "generates a fresh genesis.json", + LongHelp: "Generates a node's genesis.json based on specified parameters", + }, + cfg, + func(_ context.Context, _ []string) error { + return execGenerate(cfg, io) + }, + ) +} + +func (c *generateCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.outputPath, + "output-path", + "./genesis.json", + "the output path for the genesis.json", + ) + + fs.Int64Var( + &c.genesisTime, + "genesis-time", + time.Now().Unix(), + "the genesis creation time. Defaults to current time", + ) + + fs.StringVar( + &c.chainID, + "chain-id", + common.DefaultChainID, + "the ID of the chain", + ) + + fs.Int64Var( + &c.blockMaxTxBytes, + "block-max-tx-bytes", + types.MaxBlockTxBytes, + "the max size of the block transaction", + ) + + fs.Int64Var( + &c.blockMaxDataBytes, + "block-max-data-bytes", + types.MaxBlockDataBytes, + "the max size of the block data", + ) + + fs.Int64Var( + &c.blockMaxGas, + "block-max-gas", + types.MaxBlockMaxGas, + "the max gas limit for the block", + ) + + fs.Int64Var( + &c.blockTimeIota, + "block-time-iota", + types.BlockTimeIotaMS, + "the block time iota (in ms)", + ) +} + +func execGenerate(cfg *generateCfg, io commands.IO) error { + // Start with the default configuration + genesis := common.GetDefaultGenesis() + + // Set the genesis time + if cfg.genesisTime > 0 { + genesis.GenesisTime = time.Unix(cfg.genesisTime, 0) + } + + // Set the chain ID + if cfg.chainID != "" { + genesis.ChainID = cfg.chainID + } + + // Set the max tx bytes + if cfg.blockMaxTxBytes > 0 { + genesis.ConsensusParams.Block.MaxTxBytes = cfg.blockMaxTxBytes + } + + // Set the max data bytes + if cfg.blockMaxDataBytes > 0 { + genesis.ConsensusParams.Block.MaxDataBytes = cfg.blockMaxDataBytes + } + + // Set the max block gas + if cfg.blockMaxGas > 0 { + genesis.ConsensusParams.Block.MaxGas = cfg.blockMaxGas + } + + // Set the block time IOTA + if cfg.blockTimeIota > 0 { + genesis.ConsensusParams.Block.TimeIotaMS = cfg.blockTimeIota + } + + // Validate the genesis + if validateErr := genesis.ValidateAndComplete(); validateErr != nil { + return fmt.Errorf("unable to validate genesis, %w", validateErr) + } + + // Save the genesis file to disk + if saveErr := genesis.SaveAs(cfg.outputPath); saveErr != nil { + return fmt.Errorf("unable to save genesis, %w", saveErr) + } + + io.Printfln("Genesis successfully generated at %s\n", cfg.outputPath) + + // Log the empty validator set warning + io.Printfln("WARN: Genesis is generated with an empty validator set") + + return nil +} diff --git a/contribs/gnogenesis/internal/generate/generate_test.go b/contribs/gnogenesis/internal/generate/generate_test.go new file mode 100644 index 00000000000..7ac02169d77 --- /dev/null +++ b/contribs/gnogenesis/internal/generate/generate_test.go @@ -0,0 +1,239 @@ +package generate + +import ( + "context" + "fmt" + "path/filepath" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Generate(t *testing.T) { + t.Parallel() + + t.Run("default genesis", func(t *testing.T) { + t.Parallel() + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := NewGenerateCmd(commands.NewTestIO()) + args := []string{ + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + // Make sure the default configuration is set + defaultGenesis := common.GetDefaultGenesis() + defaultGenesis.GenesisTime = genesis.GenesisTime + + assert.Equal(t, defaultGenesis, genesis) + }) + + t.Run("set chain ID", func(t *testing.T) { + t.Parallel() + + chainID := "example-chain-ID" + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := NewGenerateCmd(commands.NewTestIO()) + args := []string{ + "--chain-id", + chainID, + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + assert.Equal(t, genesis.ChainID, chainID) + }) + + t.Run("set block max tx bytes", func(t *testing.T) { + t.Parallel() + + blockMaxTxBytes := int64(100) + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := NewGenerateCmd(commands.NewTestIO()) + args := []string{ + "--block-max-tx-bytes", + fmt.Sprintf("%d", blockMaxTxBytes), + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + assert.Equal( + t, + genesis.ConsensusParams.Block.MaxTxBytes, + blockMaxTxBytes, + ) + }) + + t.Run("set block max data bytes", func(t *testing.T) { + t.Parallel() + + blockMaxDataBytes := int64(100) + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := NewGenerateCmd(commands.NewTestIO()) + args := []string{ + "--block-max-data-bytes", + fmt.Sprintf("%d", blockMaxDataBytes), + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + assert.Equal( + t, + genesis.ConsensusParams.Block.MaxDataBytes, + blockMaxDataBytes, + ) + }) + + t.Run("set block max gas", func(t *testing.T) { + t.Parallel() + + blockMaxGas := int64(100) + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := NewGenerateCmd(commands.NewTestIO()) + args := []string{ + "--block-max-gas", + fmt.Sprintf("%d", blockMaxGas), + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + assert.Equal( + t, + genesis.ConsensusParams.Block.MaxGas, + blockMaxGas, + ) + }) + + t.Run("set block time iota", func(t *testing.T) { + t.Parallel() + + blockTimeIota := int64(10) + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := NewGenerateCmd(commands.NewTestIO()) + args := []string{ + "--block-time-iota", + fmt.Sprintf("%d", blockTimeIota), + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + assert.Equal( + t, + genesis.ConsensusParams.Block.TimeIotaMS, + blockTimeIota, + ) + }) + + t.Run("invalid genesis config (chain ID)", func(t *testing.T) { + t.Parallel() + + invalidChainID := "thischainidisunusuallylongsoitwillcausethetesttofail" + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := NewGenerateCmd(commands.NewTestIO()) + args := []string{ + "--chain-id", + invalidChainID, + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.Error(t, cmdErr) + }) +} diff --git a/contribs/gnogenesis/internal/txs/txs.go b/contribs/gnogenesis/internal/txs/txs.go new file mode 100644 index 00000000000..fbf4c6ea3c7 --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs.go @@ -0,0 +1,108 @@ +package txs + +import ( + "errors" + "flag" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type txsCfg struct { + common.Cfg +} + +var errInvalidGenesisStateType = errors.New("invalid genesis state type") + +// NewTxsCmd creates the genesis txs subcommand +func NewTxsCmd(io commands.IO) *commands.Command { + cfg := &txsCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "txs", + ShortUsage: " [flags]", + ShortHelp: "manages the initial genesis transactions", + LongHelp: "Manages genesis transactions through input files", + }, + cfg, + commands.HelpExec, + ) + + cmd.AddSubCommands( + newTxsAddCmd(cfg, io), + newTxsRemoveCmd(cfg, io), + newTxsExportCmd(cfg, io), + newTxsListCmd(cfg, io), + ) + + return cmd +} + +func (c *txsCfg) RegisterFlags(fs *flag.FlagSet) { + c.Cfg.RegisterFlags(fs) +} + +// appendGenesisTxs saves the given transactions to the genesis doc +func appendGenesisTxs(genesis *types.GenesisDoc, txs []gnoland.TxWithMetadata) error { + // Initialize the app state if it's not present + if genesis.AppState == nil { + genesis.AppState = gnoland.GnoGenesisState{} + } + + // Make sure the app state is the Gno genesis state + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + if !ok { + return errInvalidGenesisStateType + } + + // Left merge the transactions + fileTxStore := txStore(txs) + genesisTxStore := txStore(state.Txs) + + // The genesis transactions have preference with the order + // in the genesis.json + if err := genesisTxStore.leftMerge(fileTxStore); err != nil { + return err + } + + // Save the state + state.Txs = genesisTxStore + genesis.AppState = state + + return nil +} + +// txStore is a wrapper for TM2 transactions +type txStore []gnoland.TxWithMetadata + +// leftMerge merges the two tx stores, with +// preference to the left +func (i *txStore) leftMerge(b txStore) error { + // Build out the tx hash map + txHashMap := make(map[string]struct{}, len(*i)) + + for _, tx := range *i { + txHash, err := getTxHash(tx.Tx) + if err != nil { + return err + } + + txHashMap[txHash] = struct{}{} + } + + for _, tx := range b { + txHash, err := getTxHash(tx.Tx) + if err != nil { + return err + } + + if _, exists := txHashMap[txHash]; !exists { + *i = append(*i, tx) + } + } + + return nil +} diff --git a/contribs/gnogenesis/internal/txs/txs_add.go b/contribs/gnogenesis/internal/txs/txs_add.go new file mode 100644 index 00000000000..22b3b1b966a --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_add.go @@ -0,0 +1,26 @@ +package txs + +import ( + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// newTxsAddCmd creates the genesis txs add subcommand +func newTxsAddCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "add", + ShortUsage: "txs add [flags] [...]", + ShortHelp: "adds transactions into the genesis.json", + LongHelp: "Adds initial transactions to the genesis.json", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newTxsAddSheetCmd(txsCfg, io), + newTxsAddPackagesCmd(txsCfg, io), + ) + + return cmd +} diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages.go b/contribs/gnogenesis/internal/txs/txs_add_packages.go new file mode 100644 index 00000000000..53c0bb4b686 --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_add_packages.go @@ -0,0 +1,178 @@ +package txs + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" +) + +const ( + defaultAccount_Name = "test1" + defaultAccount_Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" + defaultAccount_publicKey = "gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj" +) + +var errInvalidPackageDir = errors.New("invalid package directory") + +// Keep in sync with gno.land/cmd/start.go +var genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) + +type addPkgCfg struct { + txsCfg *txsCfg + keyName string + gnoHome string // default GNOHOME env var, just here to ease testing with parallel tests + insecurePasswordStdin bool +} + +func (c *addPkgCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.keyName, + "key-name", + "", + "The package deployer key name or address contained on gnokey", + ) + + fs.StringVar( + &c.gnoHome, + "gno-home", + os.Getenv("GNOHOME"), + "the gno home directory", + ) + + fs.BoolVar( + &c.insecurePasswordStdin, + "insecure-password-stdin", + false, + "the gno home directory", + ) +} + +// newTxsAddPackagesCmd creates the genesis txs add packages subcommand +func newTxsAddPackagesCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { + cfg := &addPkgCfg{ + txsCfg: txsCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "packages", + ShortUsage: "txs add packages ", + ShortHelp: "imports transactions from the given packages into the genesis.json", + LongHelp: "Imports the transactions from a given package directory recursively to the genesis.json", + }, + cfg, + func(_ context.Context, args []string) error { + return execTxsAddPackages(cfg, io, args) + }, + ) +} + +func execTxsAddPackages( + cfg *addPkgCfg, + io commands.IO, + args []string, +) error { + var ( + keyname = defaultAccount_Name + keybase keys.Keybase + pass string + ) + // Load the genesis + genesis, err := types.GenesisDocFromFile(cfg.txsCfg.GenesisPath) + if err != nil { + return fmt.Errorf("unable to load genesis, %w", err) + } + + // Make sure the package dir is set + if len(args) == 0 { + return errInvalidPackageDir + } + + if cfg.keyName != "" { + keyname = cfg.keyName + keybase, err = keys.NewKeyBaseFromDir(cfg.gnoHome) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + pass, err = io.GetPassword("Enter password.", cfg.insecurePasswordStdin) + if err != nil { + return fmt.Errorf("cannot read password: %w", err) + } + } else { + keybase = keys.NewInMemory() + _, err := keybase.CreateAccount(defaultAccount_Name, defaultAccount_Seed, "", "", 0, 0) + if err != nil { + return fmt.Errorf("unable to create account: %w", err) + } + } + + info, err := keybase.GetByNameOrAddress(keyname) + if err != nil { + return fmt.Errorf("unable to find key in keybase: %w", err) + } + + creator := info.GetAddress() + parsedTxs := make([]gnoland.TxWithMetadata, 0) + for _, path := range args { + // Generate transactions from the packages (recursively) + txs, err := gnoland.LoadPackagesFromDir(path, creator, genesisDeployFee) + if err != nil { + return fmt.Errorf("unable to load txs from directory, %w", err) + } + + if err := signTxs(txs, keybase, genesis.ChainID, keyname, pass); err != nil { + return fmt.Errorf("unable to sign txs, %w", err) + } + + parsedTxs = append(parsedTxs, txs...) + } + + // Save the txs to the genesis.json + if err := appendGenesisTxs(genesis, parsedTxs); err != nil { + return fmt.Errorf("unable to append genesis transactions, %w", err) + } + + // Save the updated genesis + if err := genesis.SaveAs(cfg.txsCfg.GenesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "Saved %d transactions to genesis.json", + len(parsedTxs), + ) + + return nil +} + +func signTxs(txs []gnoland.TxWithMetadata, keybase keys.Keybase, chainID, keyname string, password string) error { + for index, tx := range txs { + // Here accountNumber and sequenceNumber are set to 0 because they are considered as 0 on genesis transactions. + signBytes, err := tx.Tx.GetSignBytes(chainID, 0, 0) + if err != nil { + return fmt.Errorf("unable to load txs from directory, %w", err) + } + signature, publicKey, err := keybase.Sign(keyname, password, signBytes) + if err != nil { + return fmt.Errorf("unable sign tx %w", err) + } + txs[index].Tx.Signatures = []std.Signature{ + { + PubKey: publicKey, + Signature: signature, + }, + } + } + + return nil +} diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go new file mode 100644 index 00000000000..38d930401e8 --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go @@ -0,0 +1,355 @@ +package txs + +import ( + "context" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Txs_Add_Packages(t *testing.T) { + t.Parallel() + const addPkgExpectedSignature = "cfe5a15d8def04cbdaf9d08e2511db7928152b26419c4577cbfa282c83118852411f3de5d045ce934555572c21bda8042ce5c64b793a01748e49cf2cff7c2983" + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "packages", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrUnableToLoadGenesis.Error()) + }) + + t.Run("invalid package dir", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errInvalidPackageDir.Error()) + }) + + t.Run("non existent key", func(t *testing.T) { + t.Parallel() + keybaseDir := t.TempDir() + keyname := "beep-boop" + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + io := commands.NewTestIO() + io.SetIn( + strings.NewReader( + fmt.Sprintf( + "%s\n", + "password", + ), + ), + ) + // Create the command + cmd := NewTxsCmd(io) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + t.TempDir(), // package dir + "--key-name", + keyname, // non-existent key name + "--gno-home", + keybaseDir, // temporaryDir for keybase + "--insecure-password-stdin", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "Key "+keyname+" not found") + }) + + t.Run("existent key wrong password", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + // Prepare the package + var ( + packagePath = "gno.land/p/demo/cuttlas" + dir = t.TempDir() + keybaseDir = t.TempDir() + keyname = "beep-boop" + password = "somepass" + ) + createValidFile(t, dir, packagePath) + // Create key + kb, err := keys.NewKeyBaseFromDir(keybaseDir) + require.NoError(t, err) + mnemonic, err := client.GenerateMnemonic(256) + require.NoError(t, err) + _, err = kb.CreateAccount(keyname, mnemonic, "", password+"wrong", 0, 0) + require.NoError(t, err) + + io := commands.NewTestIO() + io.SetIn( + strings.NewReader( + fmt.Sprintf( + "%s\n", + password, + ), + ), + ) + + // Create the command + cmd := NewTxsCmd(io) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + "--key-name", + keyname, // non-existent key name + "--gno-home", + keybaseDir, // temporaryDir for keybase + "--insecure-password-stdin", + dir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "unable to sign txs") + }) + + t.Run("existent key correct password", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + // Prepare the package + var ( + packagePath = "gno.land/p/demo/cuttlas" + dir = t.TempDir() + keybaseDir = t.TempDir() + keyname = "beep-boop" + password = "somepass" + ) + createValidFile(t, dir, packagePath) + // Create key + kb, err := keys.NewKeyBaseFromDir(keybaseDir) + require.NoError(t, err) + info, err := kb.CreateAccount(keyname, defaultAccount_Seed, "", password, 0, 0) + require.NoError(t, err) + + io := commands.NewTestIO() + io.SetIn( + strings.NewReader( + fmt.Sprintf( + "%s\n", + password, + ), + ), + ) + + // Create the command + cmd := NewTxsCmd(io) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + "--key-name", + keyname, // non-existent key name + "--gno-home", + keybaseDir, // temporaryDir for keybase + "--insecure-password-stdin", + dir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + require.Equal(t, 1, len(state.Txs)) + require.Equal(t, 1, len(state.Txs[0].Tx.Msgs)) + + msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage) + require.True(t, ok) + require.Equal(t, info.GetPubKey(), state.Txs[0].Tx.Signatures[0].PubKey) + require.Equal(t, addPkgExpectedSignature, hex.EncodeToString(state.Txs[0].Tx.Signatures[0].Signature)) + + assert.Equal(t, packagePath, msgAddPkg.Package.Path) + }) + + t.Run("ok default key", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + // Prepare the package + var ( + packagePath = "gno.land/p/demo/cuttlas" + dir = t.TempDir() + keybaseDir = t.TempDir() + ) + createValidFile(t, dir, packagePath) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + "--gno-home", + keybaseDir, // temporaryDir for keybase + dir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + require.Equal(t, 1, len(state.Txs)) + require.Equal(t, 1, len(state.Txs[0].Tx.Msgs)) + + msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage) + require.True(t, ok) + require.Equal(t, defaultAccount_publicKey, state.Txs[0].Tx.Signatures[0].PubKey.String()) + require.Equal(t, addPkgExpectedSignature, hex.EncodeToString(state.Txs[0].Tx.Signatures[0].Signature)) + + assert.Equal(t, packagePath, msgAddPkg.Package.Path) + }) + + t.Run("valid package", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + // Prepare the package + var ( + packagePath = "gno.land/p/demo/cuttlas" + dir = t.TempDir() + ) + createValidFile(t, dir, packagePath) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + dir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + require.Equal(t, 1, len(state.Txs)) + require.Equal(t, 1, len(state.Txs[0].Tx.Msgs)) + + msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage) + require.True(t, ok) + require.Equal(t, defaultAccount_publicKey, state.Txs[0].Tx.Signatures[0].PubKey.String()) + require.Equal(t, addPkgExpectedSignature, hex.EncodeToString(state.Txs[0].Tx.Signatures[0].Signature)) + + assert.Equal(t, packagePath, msgAddPkg.Package.Path) + }) +} + +func createValidFile(t *testing.T, dir string, packagePath string) { + t.Helper() + createFile := func(path, data string) { + file, err := os.Create(path) + require.NoError(t, err) + + _, err = file.WriteString(data) + require.NoError(t, err) + } + + // Create the gno.mod file + createFile( + filepath.Join(dir, "gno.mod"), + fmt.Sprintf("module %s\n", packagePath), + ) + + // Create a simple main.gno + createFile( + filepath.Join(dir, "main.gno"), + "package cuttlas\n\nfunc Example() string {\nreturn \"Manos arriba!\"\n}", + ) +} diff --git a/contribs/gnogenesis/internal/txs/txs_add_sheet.go b/contribs/gnogenesis/internal/txs/txs_add_sheet.go new file mode 100644 index 00000000000..0bbd4b578cc --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_add_sheet.go @@ -0,0 +1,74 @@ +package txs + +import ( + "context" + "errors" + "fmt" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var errNoTxsFileSpecified = errors.New("no txs file specified") + +// newTxsAddSheetCmd creates the genesis txs add sheet subcommand +func newTxsAddSheetCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "sheets", + ShortUsage: "txs add sheets ", + ShortHelp: "imports transactions from the given sheets into the genesis.json", + LongHelp: "Imports the transactions from a given transactions sheet to the genesis.json", + }, + commands.NewEmptyConfig(), + func(ctx context.Context, args []string) error { + return execTxsAddSheet(ctx, txsCfg, io, args) + }, + ) +} + +func execTxsAddSheet( + ctx context.Context, + cfg *txsCfg, + io commands.IO, + args []string, +) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.GenesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Open the transactions files + if len(args) == 0 { + return errNoTxsFileSpecified + } + + parsedTxs := make([]gnoland.TxWithMetadata, 0) + for _, file := range args { + txs, err := gnoland.ReadGenesisTxs(ctx, file) + if err != nil { + return fmt.Errorf("unable to parse file, %w", err) + } + + parsedTxs = append(parsedTxs, txs...) + } + + // Save the txs to the genesis.json + if err := appendGenesisTxs(genesis, parsedTxs); err != nil { + return fmt.Errorf("unable to append genesis transactions, %w", err) + } + + // Save the updated genesis + if err := genesis.SaveAs(cfg.GenesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "Saved %d transactions to genesis.json", + len(parsedTxs), + ) + + return nil +} diff --git a/contribs/gnogenesis/internal/txs/txs_add_sheet_test.go b/contribs/gnogenesis/internal/txs/txs_add_sheet_test.go new file mode 100644 index 00000000000..6da3faea6ed --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_add_sheet_test.go @@ -0,0 +1,269 @@ +package txs + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateDummyTxs generates dummy transactions +func generateDummyTxs(t *testing.T, count int) []gnoland.TxWithMetadata { + t.Helper() + + txs := make([]gnoland.TxWithMetadata, count) + + for i := 0; i < count; i++ { + txs[i] = gnoland.TxWithMetadata{ + Tx: std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: crypto.Address{byte(i)}, + ToAddress: crypto.Address{byte((i + 1) % count)}, + Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 1)), + }, + }, + Fee: std.Fee{ + GasWanted: 1, + GasFee: std.NewCoin(ugnot.Denom, 1000000), + }, + Memo: fmt.Sprintf("tx %d", i), + }, + } + } + + return txs +} + +// encodeDummyTxs encodes the transactions into amino JSON +func encodeDummyTxs(t *testing.T, txs []gnoland.TxWithMetadata) []string { + t.Helper() + + encodedTxs := make([]string, 0, len(txs)) + + for _, tx := range txs { + encodedTx, err := amino.MarshalJSON(tx) + if err != nil { + t.Fatalf("unable to marshal tx, %v", err) + } + + encodedTxs = append(encodedTxs, string(encodedTx)) + } + + return encodedTxs +} + +func TestGenesis_Txs_Add_Sheets(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "sheets", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrUnableToLoadGenesis.Error()) + }) + + t.Run("invalid txs file", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "sheets", + "--genesis-path", + tempGenesis.Name(), + "dummy-tx-file", + } + + // Run the command + assert.Error(t, cmd.ParseAndRun(context.Background(), args)) + }) + + t.Run("no txs file", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "sheets", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoTxsFileSpecified.Error()) + }) + + t.Run("malformed txs file", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "sheets", + "--genesis-path", + tempGenesis.Name(), + tempGenesis.Name(), // invalid txs file + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "unable to parse file") + }) + + t.Run("valid txs file", func(t *testing.T) { + t.Parallel() + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Prepare the transactions file + txsFile, txsCleanup := testutils.NewTestFile(t) + t.Cleanup(txsCleanup) + + _, err := txsFile.WriteString( + strings.Join( + encodeDummyTxs(t, txs), + "\n", + ), + ) + require.NoError(t, err) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "sheets", + "--genesis-path", + tempGenesis.Name(), + txsFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + assert.Len(t, state.Txs, len(txs)) + + for index, tx := range state.Txs { + assert.Equal(t, txs[index], tx) + } + }) + + t.Run("existing genesis txs", func(t *testing.T) { + t.Parallel() + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + genesisState := gnoland.GnoGenesisState{ + Txs: txs[0 : len(txs)/2], + } + + genesis.AppState = genesisState + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Prepare the transactions file + txsFile, txsCleanup := testutils.NewTestFile(t) + t.Cleanup(txsCleanup) + + _, err := txsFile.WriteString( + strings.Join( + encodeDummyTxs(t, txs), + "\n", + ), + ) + require.NoError(t, err) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "sheets", + "--genesis-path", + tempGenesis.Name(), + txsFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + assert.Len(t, state.Txs, len(txs)) + + for index, tx := range state.Txs { + assert.Equal(t, txs[index], tx) + } + }) +} diff --git a/contribs/gnogenesis/internal/txs/txs_export.go b/contribs/gnogenesis/internal/txs/txs_export.go new file mode 100644 index 00000000000..0409f1fd0ac --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_export.go @@ -0,0 +1,90 @@ +package txs + +import ( + "context" + "fmt" + "os" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// newTxsExportCmd creates the genesis txs export subcommand +func newTxsExportCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "export", + ShortUsage: "txs export [flags] ", + ShortHelp: "exports the transactions from the genesis.json", + LongHelp: "Exports the transactions from the genesis.json to an output file", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execTxsExport(txsCfg, io, args) + }, + ) +} + +func execTxsExport(cfg *txsCfg, io commands.IO, args []string) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.GenesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Load the genesis state + if genesis.AppState == nil { + return errAppStateNotSet + } + + state := genesis.AppState.(gnoland.GnoGenesisState) + if len(state.Txs) == 0 { + io.Println("No genesis transactions to export") + + return nil + } + + // Make sure the output file path is specified + if len(args) == 0 { + return common.ErrNoOutputFile + } + + // Open output file + outputFile, err := os.OpenFile( + args[0], + os.O_RDWR|os.O_CREATE|os.O_APPEND, + 0o755, + ) + if err != nil { + return fmt.Errorf("unable to create output file, %w", err) + } + + // Save the transactions + for _, tx := range state.Txs { + // Marshal tx individual tx into JSON + jsonData, err := amino.MarshalJSON(tx) + if err != nil { + return fmt.Errorf("unable to marshal JSON data, %w", err) + } + + // Write the JSON data as a line to the file + if _, err = outputFile.Write(jsonData); err != nil { + return fmt.Errorf("unable to write to output, %w", err) + } + + // Write a newline character to separate JSON objects + if _, err = outputFile.WriteString("\n"); err != nil { + return fmt.Errorf("unable to write newline output, %w", err) + } + } + + io.Printfln( + "Exported %d transactions", + len(state.Txs), + ) + + return nil +} diff --git a/contribs/gnogenesis/internal/txs/txs_export_test.go b/contribs/gnogenesis/internal/txs/txs_export_test.go new file mode 100644 index 00000000000..ad738cd95f7 --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_export_test.go @@ -0,0 +1,136 @@ +package txs + +import ( + "bufio" + "context" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Txs_Export(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "export", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrUnableToLoadGenesis.Error()) + }) + + t.Run("invalid genesis app state", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + genesis.AppState = nil // no app state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "export", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error()) + }) + + t.Run("no output file specified", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Txs: generateDummyTxs(t, 1), + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "export", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrNoOutputFile.Error()) + }) + + t.Run("valid txs export", func(t *testing.T) { + t.Parallel() + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Txs: txs, + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Prepare the output file + outputFile, outputCleanup := testutils.NewTestFile(t) + t.Cleanup(outputCleanup) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "export", + "--genesis-path", + tempGenesis.Name(), + outputFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + scanner := bufio.NewScanner(outputFile) + + outputTxs := make([]gnoland.TxWithMetadata, 0) + for scanner.Scan() { + var tx gnoland.TxWithMetadata + + require.NoError(t, amino.UnmarshalJSON(scanner.Bytes(), &tx)) + + outputTxs = append(outputTxs, tx) + } + + require.NoError(t, scanner.Err()) + + assert.Len(t, outputTxs, len(txs)) + + for index, tx := range outputTxs { + assert.Equal(t, txs[index], tx) + } + }) +} diff --git a/contribs/gnogenesis/internal/txs/txs_list.go b/contribs/gnogenesis/internal/txs/txs_list.go new file mode 100644 index 00000000000..c7867da5027 --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_list.go @@ -0,0 +1,56 @@ +package txs + +import ( + "bytes" + "context" + "errors" + "fmt" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var ErrWrongGenesisType = errors.New("genesis state is not using the correct Gno Genesis type") + +// newTxsListCmd list all transactions on the specified genesis file +func newTxsListCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "list", + ShortUsage: "txs list [flags] [...]", + ShortHelp: "lists transactions existing on genesis.json", + LongHelp: "Lists transactions existing on genesis.json", + }, + commands.NewEmptyConfig(), + func(ctx context.Context, args []string) error { + return execTxsListCmd(io, txsCfg) + }, + ) + + return cmd +} + +func execTxsListCmd(io commands.IO, cfg *txsCfg) error { + genesis, err := types.GenesisDocFromFile(cfg.GenesisPath) + if err != nil { + return fmt.Errorf("%w, %w", common.ErrUnableToLoadGenesis, err) + } + + gs, ok := genesis.AppState.(gnoland.GnoGenesisState) + if !ok { + return ErrWrongGenesisType + } + + b, err := amino.MarshalJSONIndent(gs.Txs, "", " ") + if err != nil { + return errors.New("error marshalling data to amino JSON") + } + + buf := bytes.NewBuffer(b) + _, err = buf.WriteTo(io.Out()) + + return err +} diff --git a/contribs/gnogenesis/internal/txs/txs_list_test.go b/contribs/gnogenesis/internal/txs/txs_list_test.go new file mode 100644 index 00000000000..d4d9f4d7ba8 --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_list_test.go @@ -0,0 +1,68 @@ +package txs + +import ( + "bytes" + "context" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/testutils" +) + +func TestGenesis_List_All(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis path", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "list", + "--genesis-path", + "", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, common.ErrUnableToLoadGenesis) + }) + + t.Run("list all txs", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + genesis := common.GetDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Txs: txs, + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + cio := commands.NewTestIO() + buf := bytes.NewBuffer(nil) + cio.SetOut(commands.WriteNopCloser(buf)) + + cmd := NewTxsCmd(cio) + args := []string{ + "list", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + require.Len(t, buf.String(), 5262) + }) +} diff --git a/contribs/gnogenesis/internal/txs/txs_remove.go b/contribs/gnogenesis/internal/txs/txs_remove.go new file mode 100644 index 00000000000..dbfc90fb1dc --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_remove.go @@ -0,0 +1,108 @@ +package txs + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" +) + +var ( + errAppStateNotSet = errors.New("genesis app state not set") + errNoTxHashSpecified = errors.New("no transaction hashes specified") + errTxNotFound = errors.New("transaction not present in genesis.json") +) + +// newTxsRemoveCmd creates the genesis txs remove subcommand +func newTxsRemoveCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "remove", + ShortUsage: "txs remove ", + ShortHelp: "removes the transactions from the genesis.json", + LongHelp: "Removes the transactions using the transaction hash", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execTxsRemove(txsCfg, io, args) + }, + ) +} + +func execTxsRemove(cfg *txsCfg, io commands.IO, args []string) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.GenesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Check if the genesis state is set at all + if genesis.AppState == nil { + return errAppStateNotSet + } + + // Make sure the transaction hashes are set + if len(args) == 0 { + return errNoTxHashSpecified + } + + state := genesis.AppState.(gnoland.GnoGenesisState) + + for _, inputHash := range args { + index := -1 + + for indx, tx := range state.Txs { + // Find the hash of the transaction + hash, err := getTxHash(tx.Tx) + if err != nil { + return fmt.Errorf("unable to generate tx hash, %w", err) + } + + // Check if the hashes match + if strings.ToLower(hash) == strings.ToLower(inputHash) { + index = indx + + break + } + } + + if index < 0 { + return errTxNotFound + } + + state.Txs = append(state.Txs[:index], state.Txs[index+1:]...) + + io.Printfln( + "Transaction %s removed from genesis.json", + inputHash, + ) + } + + genesis.AppState = state + + // Save the updated genesis + if err := genesis.SaveAs(cfg.GenesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + return nil +} + +// getTxHash returns the hex hash representation of +// the transaction (Amino encoded) +func getTxHash(tx std.Tx) (string, error) { + encodedTx, err := amino.Marshal(tx) + if err != nil { + return "", fmt.Errorf("unable to marshal transaction, %w", err) + } + + txHash := types.Tx(encodedTx).Hash() + + return fmt.Sprintf("%X", txHash), nil +} diff --git a/contribs/gnogenesis/internal/txs/txs_remove_test.go b/contribs/gnogenesis/internal/txs/txs_remove_test.go new file mode 100644 index 00000000000..16edbb83e3c --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_remove_test.go @@ -0,0 +1,133 @@ +package txs + +import ( + "context" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Txs_Remove(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrUnableToLoadGenesis.Error()) + }) + + t.Run("invalid genesis app state", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + genesis.AppState = nil // no app state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error()) + }) + t.Run("no transaction hash specified", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + genesis := common.GetDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Txs: txs, + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoTxHashSpecified.Error()) + }) + + t.Run("transaction removed", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + genesis := common.GetDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Txs: txs, + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + txHash, err := getTxHash(txs[0].Tx) + require.NoError(t, err) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + tempGenesis.Name(), + txHash, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transaction was removed + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + assert.Len(t, state.Txs, len(txs)-1) + + for _, tx := range state.Txs { + genesisTxHash, err := getTxHash(tx.Tx) + require.NoError(t, err) + + assert.NotEqual(t, txHash, genesisTxHash) + } + }) +} diff --git a/contribs/gnogenesis/internal/validator/validator.go b/contribs/gnogenesis/internal/validator/validator.go new file mode 100644 index 00000000000..8cd84f5c9bf --- /dev/null +++ b/contribs/gnogenesis/internal/validator/validator.go @@ -0,0 +1,50 @@ +package validator + +import ( + "flag" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type validatorCfg struct { + common.Cfg + + address string +} + +// NewValidatorCmd creates the genesis validator subcommand +func NewValidatorCmd(io commands.IO) *commands.Command { + cfg := &validatorCfg{ + Cfg: common.Cfg{}, + } + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "validator", + ShortUsage: " [flags]", + ShortHelp: "validator set management in genesis.json", + LongHelp: "Manipulates the genesis.json validator set", + }, + cfg, + commands.HelpExec, + ) + + cmd.AddSubCommands( + newValidatorAddCmd(cfg, io), + newValidatorRemoveCmd(cfg, io), + ) + + return cmd +} + +func (c *validatorCfg) RegisterFlags(fs *flag.FlagSet) { + c.Cfg.RegisterFlags(fs) + + fs.StringVar( + &c.address, + "address", + "", + "the gno bech32 address of the validator", + ) +} diff --git a/contribs/gnogenesis/internal/validator/validator_add.go b/contribs/gnogenesis/internal/validator/validator_add.go new file mode 100644 index 00000000000..45744f98e82 --- /dev/null +++ b/contribs/gnogenesis/internal/validator/validator_add.go @@ -0,0 +1,137 @@ +package validator + +import ( + "context" + "errors" + "flag" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + _ "github.com/gnolang/gno/tm2/pkg/crypto/keys" +) + +var ( + errInvalidPower = errors.New("invalid validator power") + errInvalidName = errors.New("invalid validator name") + errPublicKeyAddressMismatch = errors.New("provided public key and address do not match") + errAddressPresent = errors.New("validator with same address already present in genesis.json") +) + +type validatorAddCfg struct { + rootCfg *validatorCfg + + pubKey string + name string + power int64 +} + +// newValidatorAddCmd creates the genesis validator add subcommand +func newValidatorAddCmd(validatorCfg *validatorCfg, io commands.IO) *commands.Command { + cfg := &validatorAddCfg{ + rootCfg: validatorCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "add", + ShortUsage: "validator add [flags]", + ShortHelp: "adds a new validator to the genesis.json", + }, + cfg, + func(_ context.Context, _ []string) error { + return execValidatorAdd(cfg, io) + }, + ) +} + +func (c *validatorAddCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.pubKey, + "pub-key", + "", + "the bech32 string representation of the validator's public key", + ) + + fs.StringVar( + &c.name, + "name", + "", + "the name of the validator (must be unique)", + ) + + fs.Int64Var( + &c.power, + "power", + 1, + "the voting power of the validator (must be > 0)", + ) +} + +func execValidatorAdd(cfg *validatorAddCfg, io commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.GenesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Check the validator address + address, err := crypto.AddressFromString(cfg.rootCfg.address) + if err != nil { + return fmt.Errorf("invalid validator address, %w", err) + } + + // Check the voting power + if cfg.power < 1 { + return errInvalidPower + } + + // Check the name + if cfg.name == "" { + return errInvalidName + } + + // Check the public key + pubKey, err := crypto.PubKeyFromBech32(cfg.pubKey) + if err != nil { + return fmt.Errorf("invalid validator public key, %w", err) + } + + // Check the public key matches the address + if pubKey.Address() != address { + return errPublicKeyAddressMismatch + } + + validator := types.GenesisValidator{ + Address: address, + PubKey: pubKey, + Power: cfg.power, + Name: cfg.name, + } + + // Check if the validator exists + for _, genesisValidator := range genesis.Validators { + // There is no need to check if the public keys match + // since the address is derived from it, and the derivation + // is checked already + if validator.Address == genesisValidator.Address { + return errAddressPresent + } + } + + // Add the validator + genesis.Validators = append(genesis.Validators, validator) + + // Save the updated genesis + if err := genesis.SaveAs(cfg.rootCfg.GenesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "Validator with address %s added to genesis file", + cfg.rootCfg.address, + ) + + return nil +} diff --git a/contribs/gnogenesis/internal/validator/validator_add_test.go b/contribs/gnogenesis/internal/validator/validator_add_test.go new file mode 100644 index 00000000000..4e6155137a3 --- /dev/null +++ b/contribs/gnogenesis/internal/validator/validator_add_test.go @@ -0,0 +1,242 @@ +package validator + +import ( + "context" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Validator_Add(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrUnableToLoadGenesis.Error()) + }) + + t.Run("invalid validator address", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + "dummyaddress", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "invalid validator address") + }) + + t.Run("invalid voting power", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + key := common.GetDummyKey(t) + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + key.Address().String(), + "--power", + "-1", // invalid voting power + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidPower) + }) + + t.Run("invalid validator name", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + key := common.GetDummyKey(t) + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + key.Address().String(), + "--name", + "", // invalid validator name + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errInvalidName.Error()) + }) + + t.Run("invalid public key", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + key := common.GetDummyKey(t) + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + key.Address().String(), + "--name", + "example", + "--pub-key", + "invalidkey", // invalid pub key + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "invalid validator public key") + }) + + t.Run("public key address mismatch", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + dummyKeys := common.GetDummyKeys(t, 2) + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKeys[0].Address().String(), + "--name", + "example", + "--pub-key", + crypto.PubKeyToBech32(dummyKeys[1]), // another key + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errPublicKeyAddressMismatch.Error()) + }) + + t.Run("validator with same address exists", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + dummyKeys := common.GetDummyKeys(t, 2) + genesis := common.GetDefaultGenesis() + + // Set an existing validator + genesis.Validators = append(genesis.Validators, types.GenesisValidator{ + Address: dummyKeys[0].Address(), + PubKey: dummyKeys[0], + Power: 1, + Name: "example", + }) + + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKeys[0].Address().String(), + "--name", + "example", + "--pub-key", + crypto.PubKeyToBech32(dummyKeys[0]), // another key + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errAddressPresent.Error()) + }) + + t.Run("valid genesis validator", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + key := common.GetDummyKey(t) + genesis := common.GetDefaultGenesis() + + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + key.Address().String(), + "--name", + "example", + "--pub-key", + crypto.PubKeyToBech32(key), // another key + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + }) +} diff --git a/contribs/gnogenesis/internal/validator/validator_remove.go b/contribs/gnogenesis/internal/validator/validator_remove.go new file mode 100644 index 00000000000..0206fe7d58d --- /dev/null +++ b/contribs/gnogenesis/internal/validator/validator_remove.go @@ -0,0 +1,71 @@ +package validator + +import ( + "context" + "errors" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" +) + +var errValidatorNotPresent = errors.New("validator not present in genesis.json") + +// newValidatorRemoveCmd creates the genesis validator remove subcommand +func newValidatorRemoveCmd(rootCfg *validatorCfg, io commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "remove", + ShortUsage: "validator remove [flags]", + ShortHelp: "removes a validator from the genesis.json", + }, + commands.NewEmptyConfig(), + func(_ context.Context, _ []string) error { + return execValidatorRemove(rootCfg, io) + }, + ) +} + +func execValidatorRemove(cfg *validatorCfg, io commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.GenesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Check the validator address + address, err := crypto.AddressFromString(cfg.address) + if err != nil { + return fmt.Errorf("invalid validator address, %w", err) + } + + index := -1 + + for indx, validator := range genesis.Validators { + if validator.Address == address { + index = indx + + break + } + } + + if index < 0 { + return errors.New("validator not present in genesis.json") + } + + // Drop the validator + genesis.Validators = append(genesis.Validators[:index], genesis.Validators[index+1:]...) + + // Save the updated genesis + if err := genesis.SaveAs(cfg.GenesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "Validator with address %s removed from genesis file", + cfg.address, + ) + + return nil +} diff --git a/contribs/gnogenesis/internal/validator/validator_remove_test.go b/contribs/gnogenesis/internal/validator/validator_remove_test.go new file mode 100644 index 00000000000..78821f4abee --- /dev/null +++ b/contribs/gnogenesis/internal/validator/validator_remove_test.go @@ -0,0 +1,126 @@ +package validator + +import ( + "context" + "testing" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Validator_Remove(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, common.ErrUnableToLoadGenesis.Error()) + }) + + t.Run("invalid validator address", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + "dummyaddress", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "invalid validator address") + }) + + t.Run("validator not found", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + dummyKeys := common.GetDummyKeys(t, 2) + genesis := common.GetDefaultGenesis() + + // Set an existing validator + genesis.Validators = append(genesis.Validators, types.GenesisValidator{ + Address: dummyKeys[0].Address(), + PubKey: dummyKeys[0], + Power: 1, + Name: "example", + }) + + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKeys[1].Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errValidatorNotPresent.Error()) + }) + + t.Run("validator removed", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + dummyKey := common.GetDummyKey(t) + + genesis := common.GetDefaultGenesis() + + // Set an existing validator + genesis.Validators = append(genesis.Validators, types.GenesisValidator{ + Address: dummyKey.Address(), + PubKey: dummyKey, + Power: 1, + Name: "example", + }) + + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := NewValidatorCmd(commands.NewTestIO()) + args := []string{ + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKey.Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.NoError(t, cmdErr) + }) +} diff --git a/contribs/gnogenesis/internal/verify/verify.go b/contribs/gnogenesis/internal/verify/verify.go new file mode 100644 index 00000000000..9022711ce49 --- /dev/null +++ b/contribs/gnogenesis/internal/verify/verify.go @@ -0,0 +1,80 @@ +package verify + +import ( + "context" + "errors" + "flag" + "fmt" + + "github.com/gnolang/contribs/gnogenesis/internal/common" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var errInvalidGenesisState = errors.New("invalid genesis state type") + +type verifyCfg struct { + common.Cfg +} + +// NewVerifyCmd creates the genesis verify subcommand +func NewVerifyCmd(io commands.IO) *commands.Command { + cfg := &verifyCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "verify", + ShortUsage: "[flags]", + ShortHelp: "verifies a genesis.json", + LongHelp: "Verifies a node's genesis.json", + }, + cfg, + func(_ context.Context, _ []string) error { + return execVerify(cfg, io) + }, + ) +} + +func (c *verifyCfg) RegisterFlags(fs *flag.FlagSet) { + c.Cfg.RegisterFlags(fs) +} + +func execVerify(cfg *verifyCfg, io commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.GenesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Verify it + if validateErr := genesis.Validate(); validateErr != nil { + return fmt.Errorf("unable to verify genesis, %w", validateErr) + } + + // Validate the genesis state + if genesis.AppState != nil { + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + if !ok { + return errInvalidGenesisState + } + + // Validate the initial transactions + for _, tx := range state.Txs { + if validateErr := tx.Tx.ValidateBasic(); validateErr != nil { + return fmt.Errorf("invalid transacton, %w", validateErr) + } + } + + // Validate the initial balances + for _, balance := range state.Balances { + if err := balance.Verify(); err != nil { + return fmt.Errorf("invalid balance: %w", err) + } + } + } + + io.Printfln("Genesis at %s is valid", cfg.GenesisPath) + + return nil +} diff --git a/contribs/gnogenesis/internal/verify/verify_test.go b/contribs/gnogenesis/internal/verify/verify_test.go new file mode 100644 index 00000000000..130bd5e09bc --- /dev/null +++ b/contribs/gnogenesis/internal/verify/verify_test.go @@ -0,0 +1,163 @@ +package verify + +import ( + "context" + "testing" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/mock" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Verify(t *testing.T) { + t.Parallel() + + getValidTestGenesis := func() *types.GenesisDoc { + key := mock.GenPrivKey().PubKey() + + return &types.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: "valid-chain-id", + ConsensusParams: types.DefaultConsensusParams(), + Validators: []types.GenesisValidator{ + { + Address: key.Address(), + PubKey: key, + Power: 1, + Name: "valid validator", + }, + }, + } + } + + t.Run("invalid txs", func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + g := getValidTestGenesis() + + g.AppState = gnoland.GnoGenesisState{ + Balances: []gnoland.Balance{}, + Txs: []gnoland.TxWithMetadata{ + {}, + }, + } + + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := NewVerifyCmd(commands.NewTestIO()) + args := []string{ + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.Error(t, cmdErr) + }) + + t.Run("invalid balances", func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + g := getValidTestGenesis() + + g.AppState = gnoland.GnoGenesisState{ + Balances: []gnoland.Balance{ + {}, + }, + Txs: []gnoland.TxWithMetadata{}, + } + + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := NewVerifyCmd(commands.NewTestIO()) + args := []string{ + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.Error(t, cmdErr) + }) + + t.Run("valid genesis", func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + g := getValidTestGenesis() + g.AppState = gnoland.GnoGenesisState{ + Balances: []gnoland.Balance{}, + Txs: []gnoland.TxWithMetadata{}, + } + + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := NewVerifyCmd(commands.NewTestIO()) + args := []string{ + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + }) + + t.Run("valid genesis, no state", func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + g := getValidTestGenesis() + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := NewVerifyCmd(commands.NewTestIO()) + args := []string{ + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + }) + + t.Run("invalid genesis state", func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + g := getValidTestGenesis() + g.AppState = "Totally invalid state" + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := NewVerifyCmd(commands.NewTestIO()) + args := []string{ + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.Error(t, cmdErr) + }) +} diff --git a/contribs/gnogenesis/main.go b/contribs/gnogenesis/main.go new file mode 100644 index 00000000000..a5beb2518dd --- /dev/null +++ b/contribs/gnogenesis/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "context" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +func main() { + cmd := newGenesisCmd(commands.NewDefaultIO()) + + cmd.Execute(context.Background(), os.Args[1:]) +} diff --git a/contribs/gnohealth/Makefile b/contribs/gnohealth/Makefile new file mode 100644 index 00000000000..61c6e8c79ea --- /dev/null +++ b/contribs/gnohealth/Makefile @@ -0,0 +1,17 @@ +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/gnohealth . + +lint: + $(golangci_lint) --config ../../.github/golangci.yml run ./... + +test: + @echo "XXX: add tests" diff --git a/contribs/gnohealth/go.mod b/contribs/gnohealth/go.mod new file mode 100644 index 00000000000..4a3f6392804 --- /dev/null +++ b/contribs/gnohealth/go.mod @@ -0,0 +1,47 @@ +module github.com/gnolang/gno/contribs/gnohealth + +go 1.22.4 + +replace github.com/gnolang/gno => ../.. + +require github.com/gnolang/gno v0.0.0-00010101000000-000000000000 + +require ( + github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect + github.com/btcsuite/btcd/btcutil v1.1.6 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect + github.com/stretchr/testify v1.9.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/contribs/gnohealth/go.sum b/contribs/gnohealth/go.sum new file mode 100644 index 00000000000..02e8893406a --- /dev/null +++ b/contribs/gnohealth/go.sum @@ -0,0 +1,201 @@ +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= +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/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/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/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +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/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/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 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +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/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/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +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/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/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +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/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= +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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.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= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 h1:k6fQVDQexDE+3jG2SfCQjnHS7OamcP73YMoxEVq5B6k= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0/go.mod h1:t4BrYLHU450Zo9fnydWlIuswB1bm7rM8havDpWOJeDo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 h1:xvhQxJ/C9+RTnAj5DpTg7LSM1vbbMTiXt7e9hsfqHNw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0/go.mod h1:Fcvs2Bz1jkDM+Wf5/ozBGmi3tQ/c9zPKLnsipnfhGAo= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +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/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +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.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/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/gnohealth/health.go b/contribs/gnohealth/health.go new file mode 100644 index 00000000000..5118cac5fa5 --- /dev/null +++ b/contribs/gnohealth/health.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/gnolang/gno/contribs/gnohealth/internal/timestamp" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +func newHealthCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags] [...]", + ShortHelp: "gno health check suite", + LongHelp: "Gno health check suite, to verify that different parts of Gno are working correctly", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + timestamp.NewTimestampCmd(io), + ) + + return cmd +} diff --git a/contribs/gnohealth/internal/timestamp/timestamp.go b/contribs/gnohealth/internal/timestamp/timestamp.go new file mode 100644 index 00000000000..50521b9130f --- /dev/null +++ b/contribs/gnohealth/internal/timestamp/timestamp.go @@ -0,0 +1,166 @@ +package timestamp + +import ( + "context" + "flag" + "fmt" + "time" + + rpcClient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +const ( + defaultRemoteAddress = "http://127.0.0.1:26657" + defaultWebSocket = true + defaultCheckDuration = 30 * time.Second + defaultCheckInterval = 50 * time.Millisecond + defaultMaxDelta = 10 * time.Second + defaultVerbose = false +) + +type timestampCfg struct { + remoteAddress string + webSocket bool + checkDuration time.Duration + checkInterval time.Duration + maxDelta time.Duration + verbose bool +} + +// NewTimestampCmd creates the gnohealth timestamp subcommand +func NewTimestampCmd(io commands.IO) *commands.Command { + cfg := ×tampCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "timestamp", + ShortUsage: "[flags]", + ShortHelp: "check if block timestamps are drifting", + LongHelp: "This command checks if block timestamps are drifting on a blockchain by connecting to a specified node via RPC.", + }, + cfg, + func(_ context.Context, _ []string) error { + return execTimestamp(cfg, io) + }, + ) +} + +// RegisterFlags registers command-line flags for the timestamp command +func (c *timestampCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.remoteAddress, + "remote", + defaultRemoteAddress, + "the remote address of the node to connect to via RPC", + ) + + fs.BoolVar( + &c.webSocket, + "ws", + defaultWebSocket, + "flag indicating whether to use the WebSocket protocol for RPC", + ) + + fs.DurationVar( + &c.checkDuration, + "duration", + defaultCheckDuration, + "duration for which checks should be performed", + ) + + fs.DurationVar( + &c.checkInterval, + "interval", + defaultCheckInterval, + "interval between consecutive checks", + ) + + fs.DurationVar( + &c.maxDelta, + "max-delta", + defaultMaxDelta, + "maximum allowable time difference between the current time and the last block time", + ) + + fs.BoolVar( + &c.verbose, + "verbose", + defaultVerbose, + "flag indicating whether to enable verbose logging", + ) +} + +func execTimestamp(cfg *timestampCfg, io commands.IO) error { + var ( + client *rpcClient.RPCClient + err error + lastChecked int64 + count uint64 + totalDelta time.Duration + ) + + // Init RPC client + if cfg.webSocket { + if client, err = rpcClient.NewWSClient(cfg.remoteAddress); err != nil { + return fmt.Errorf("unable to create WS client: %w", err) + } + } else { + if client, err = rpcClient.NewHTTPClient(cfg.remoteAddress); err != nil { + return fmt.Errorf("unable to create HTTP client: %w", err) + } + } + + // Create a ticker for check interval + ticker := time.NewTicker(cfg.checkInterval) + defer ticker.Stop() + + // Create a context that will stop this check when specified duration is elapsed + ctx, cancel := context.WithTimeout(context.Background(), cfg.checkDuration) + defer cancel() + + for { + select { + case <-ctx.Done(): + average := totalDelta / time.Duration(count) + io.Printf("no timestamp drifted beyond the maximum delta (average %s)\n", average) + return nil + + case <-ticker.C: + // Fetch the latest block number from the chain + status, err := client.Status() + if err != nil { + return fmt.Errorf("unable to fetch latest block number: %w", err) + } + + latest := status.SyncInfo.LatestBlockHeight + + // Check if there have been blocks since the last check + if lastChecked == latest { + continue + } + + // Fetch the latest block from the chain + lastBlock, err := client.Block(&latest) + if err != nil { + return fmt.Errorf("unable to fetch latest block content: %w", err) + } + + // Check if the last block timestamp is not drifting + delta := time.Until(lastBlock.Block.Time).Abs() + if delta > cfg.maxDelta { + return fmt.Errorf("block %d drifted of %s (max %s): KO", latest, delta, cfg.maxDelta) + } + + // Increment counters to calculate average on exit + count += 1 + totalDelta += delta + + // Update the last checked block number + lastChecked = latest + if cfg.verbose { + io.Printf("block %d drifted of %s (max %s): OK\n", latest, delta, cfg.maxDelta) + } + } + } +} diff --git a/contribs/gnohealth/main.go b/contribs/gnohealth/main.go new file mode 100644 index 00000000000..4325c657976 --- /dev/null +++ b/contribs/gnohealth/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "context" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +func main() { + cmd := newHealthCmd(commands.NewDefaultIO()) + + cmd.Execute(context.Background(), os.Args[1:]) +} diff --git a/contribs/gnokeykc/go.mod b/contribs/gnokeykc/go.mod index 9531b94a628..157b5585828 100644 --- a/contribs/gnokeykc/go.mod +++ b/contribs/gnokeykc/go.mod @@ -14,14 +14,13 @@ require ( require ( github.com/alessio/shellescape v1.4.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect - github.com/btcsuite/btcd/btcutil v1.1.5 // indirect + github.com/btcsuite/btcd/btcutil v1.1.6 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect @@ -29,36 +28,38 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/peterbourgon/ff/v3 v3.4.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rs/xid v1.5.0 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.25.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect - golang.org/x/mod v0.19.0 // indirect - golang.org/x/net v0.27.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum index 20c847e59f8..7aac05b84a0 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -1,20 +1,22 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +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= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= 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 h1:js1gPwhcFflTZ7Nzl7WHaOTlTr5hIrR4n1NM4v9n4Kw= 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 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= 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= @@ -52,8 +54,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= 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= @@ -84,8 +84,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= 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= @@ -122,12 +122,19 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= 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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= @@ -138,22 +145,22 @@ github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= -go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= -go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 h1:U2guen0GhqH8o/G2un8f/aG/y++OuW6MyCo6hT9prXk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0/go.mod h1:yeGZANgEcpdx/WK0IvvRFC+2oLiMS2u4L/0Rj2M2Qr0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0 h1:aLmmtjRke7LPDQ3lvpFz+kNEH43faFhzW7v8BFIEydg= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.28.0/go.mod h1:TC1pyCt6G9Sjb4bQpShH+P5R53pO6ZuGnHuuln9xMeE= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= -go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 h1:k6fQVDQexDE+3jG2SfCQjnHS7OamcP73YMoxEVq5B6k= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0/go.mod h1:t4BrYLHU450Zo9fnydWlIuswB1bm7rM8havDpWOJeDo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 h1:xvhQxJ/C9+RTnAj5DpTg7LSM1vbbMTiXt7e9hsfqHNw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0/go.mod h1:Fcvs2Bz1jkDM+Wf5/ozBGmi3tQ/c9zPKLnsipnfhGAo= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -161,20 +168,22 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= @@ -184,23 +193,23 @@ 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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -209,8 +218,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ 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.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/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= diff --git a/contribs/gnomd/go.mod b/contribs/gnomd/go.mod index 8bc352d4848..57c07621324 100644 --- a/contribs/gnomd/go.mod +++ b/contribs/gnomd/go.mod @@ -21,7 +21,7 @@ require ( github.com/mattn/go-isatty v0.0.11 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect github.com/rivo/uniseg v0.1.0 // indirect - golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 // indirect - golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect - golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect + golang.org/x/image v0.18.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect ) diff --git a/contribs/gnomd/go.sum b/contribs/gnomd/go.sum index b4ad4f5c9bf..3d4666530b1 100644 --- a/contribs/gnomd/go.sum +++ b/contribs/gnomd/go.sum @@ -55,15 +55,18 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 h1:gQ6GUSD102fPgli+Yb4cR/cGaHF7tNBt+GYoRCpGC7s= golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/contribs/gnomigrate/Makefile b/contribs/gnomigrate/Makefile new file mode 100644 index 00000000000..155fc997012 --- /dev/null +++ b/contribs/gnomigrate/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/contribs/gnomigrate/README.md b/contribs/gnomigrate/README.md new file mode 100644 index 00000000000..2b4f5ecf831 --- /dev/null +++ b/contribs/gnomigrate/README.md @@ -0,0 +1,59 @@ +## Overview + +`gnomigrate` is a CLI tool designed to migrate Gno legacy data formats to the new standard formats used in Gno +blockchain. + +## Features + +- **Transaction Migration**: Converts legacy `std.Tx` transactions to the new `gnoland.TxWithMetadata` format. + +## Installation + +### Clone the repository + +```shell +git clone https://github.com/gnolang/gno.git +``` + +### Navigate to the project directory + +```shell +cd contribs/gnomigrate +``` + +### Build the binary + +```shell +make build +``` + +### Install the binary + +```shell +make install +``` + +## Migrating legacy transactions + +The `gnomigrate` tool provides the `txs` subcommand to manage the migration of legacy transaction sheets. + +```shell +gnomigrate txs --input-dir --output-dir +``` + +### Flags + +- `--input-dir`: Specifies the directory containing the legacy transaction sheets to migrate. +- `--output-dir`: Specifies the directory where the migrated transaction sheets will be saved. + +### Example + +```shell +gnomigrate txs --input-dir ./legacy_txs --output-dir ./migrated_txs +``` + +This command will: + +- Read all `.jsonl` files from the ./legacy_txs directory, that are Amino-JSON encoded `std.Tx`s. +- Migrate each transaction from `std.Tx` to `gnoland.TxWithMetadata` (no metadata). +- Save the migrated transactions to the `./migrated_txs` directory, preserving the original directory structure. diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod new file mode 100644 index 00000000000..49f40eb79af --- /dev/null +++ b/contribs/gnomigrate/go.mod @@ -0,0 +1,57 @@ +module github.com/gnolang/gnomigrate + +go 1.22 + +require ( + github.com/gnolang/gno v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.9.0 +) + +replace github.com/gnolang/gno => ../.. + +require ( + 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 + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect + github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + go.etcd.io/bbolt v1.3.11 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/sdk v1.29.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum new file mode 100644 index 00000000000..7e4a683cad1 --- /dev/null +++ b/contribs/gnomigrate/go.sum @@ -0,0 +1,228 @@ +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= +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/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/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/cosmos/ledger-cosmos-go v0.13.3 h1:7ehuBGuyIytsXbd4MP43mLeoN2LTOEnk5nvue4rK+yM= +github.com/cosmos/ledger-cosmos-go v0.13.3/go.mod h1:HENcEP+VtahZFw38HZ3+LS3Iv5XV6svsnkk9vdJtLr8= +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/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +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/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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +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/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/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +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/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/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +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 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +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 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +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/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= +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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.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/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= +github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= +github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 h1:k6fQVDQexDE+3jG2SfCQjnHS7OamcP73YMoxEVq5B6k= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0/go.mod h1:t4BrYLHU450Zo9fnydWlIuswB1bm7rM8havDpWOJeDo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 h1:xvhQxJ/C9+RTnAj5DpTg7LSM1vbbMTiXt7e9hsfqHNw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0/go.mod h1:Fcvs2Bz1jkDM+Wf5/ozBGmi3tQ/c9zPKLnsipnfhGAo= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/sdk/metric v1.29.0 h1:K2CfmJohnRgvZ9UAj2/FhIf/okdWcNdBwe1m8xFXiSY= +go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= +google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +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.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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/gnomigrate/internal/txs/txs.go b/contribs/gnomigrate/internal/txs/txs.go new file mode 100644 index 00000000000..231428d5064 --- /dev/null +++ b/contribs/gnomigrate/internal/txs/txs.go @@ -0,0 +1,199 @@ +package txs + +import ( + "bufio" + "context" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" +) + +var ( + errInvalidInputDir = errors.New("invalid input directory") + errInvalidOutputDir = errors.New("invalid output directory") +) + +type txsCfg struct { + inputDir string + outputDir string +} + +// NewTxsCmd creates the migrate txs subcommand +func NewTxsCmd(io commands.IO) *commands.Command { + cfg := &txsCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "txs", + ShortUsage: " [flags]", + ShortHelp: "manages the legacy transaction sheet migrations", + LongHelp: "Manages legacy transaction migrations through sheet input files", + }, + cfg, + func(ctx context.Context, _ []string) error { + return cfg.execMigrate(ctx, io) + }, + ) + + return cmd +} + +func (c *txsCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.inputDir, + "input-dir", + "", + "the input directory for the legacy transaction sheets", + ) + + fs.StringVar( + &c.outputDir, + "output-dir", + "", + "the output directory for the standard transaction sheets", + ) +} + +func (c *txsCfg) execMigrate(ctx context.Context, io commands.IO) error { + // Make sure the dirs are set + if c.inputDir == "" { + return errInvalidInputDir + } + + if c.outputDir == "" { + return errInvalidOutputDir + } + + // Make sure the output dir is present + if err := os.MkdirAll(c.outputDir, os.ModePerm); err != nil { + return fmt.Errorf("unable to create output dir, %w", err) + } + + return migrateDir(ctx, io, c.inputDir, c.outputDir) +} + +// migrateDir migrates the transaction sheet directory +func migrateDir( + ctx context.Context, + io commands.IO, + sourceDir string, + outputDir string, +) error { + // Read the sheet directory + entries, err := os.ReadDir(sourceDir) + if err != nil { + return fmt.Errorf("error reading directory %s, %w", sourceDir, err) + } + + for _, entry := range entries { + select { + case <-ctx.Done(): + return nil + default: + var ( + srcPath = filepath.Join(sourceDir, entry.Name()) + destPath = filepath.Join(outputDir, entry.Name()) + ) + + // Check if a dir is encountered + if !entry.IsDir() { + // Make sure the file type is valid + if !strings.HasSuffix(entry.Name(), ".jsonl") { + continue + } + + // Process the tx sheet + io.Printfln("Migrating %s -> %s", srcPath, destPath) + + if err := processFile(ctx, io, srcPath, destPath); err != nil { + io.ErrPrintfln("unable to process file %s, %w", srcPath, err) + } + + continue + } + + // Ensure destination directory exists + if err = os.MkdirAll(destPath, os.ModePerm); err != nil { + return fmt.Errorf("error creating directory %s, %w", destPath, err) + } + + // Recursively process the directory + if err = migrateDir(ctx, io, srcPath, destPath); err != nil { + io.ErrPrintfln("unable migrate directory %s, %w", srcPath, err) + } + } + } + + return nil +} + +// processFile processes the old legacy std.Tx sheet into the new standard gnoland.TxWithMetadata +func processFile(ctx context.Context, io commands.IO, source, destination string) error { + file, err := os.Open(source) + if err != nil { + return fmt.Errorf("unable to open file, %w", err) + } + defer file.Close() + + // Create the destination file + outputFile, err := os.Create(destination) + if err != nil { + return fmt.Errorf("unable to create file, %w", err) + } + defer outputFile.Close() + + scanner := bufio.NewScanner(file) + + scanner.Buffer(make([]byte, 1_000_000), 2_000_000) + + for scanner.Scan() { + select { + case <-ctx.Done(): + return nil + default: + var ( + tx std.Tx + txWithMetadata gnoland.TxWithMetadata + ) + + if err = amino.UnmarshalJSON(scanner.Bytes(), &tx); err != nil { + io.ErrPrintfln("unable to read line, %s", err) + + continue + } + + // Convert the std.Tx -> gnoland.TxWithMetadata + txWithMetadata = gnoland.TxWithMetadata{ + Tx: tx, + Metadata: nil, // not set + } + + // Save the new transaction with metadata + marshaledData, err := amino.MarshalJSON(txWithMetadata) + if err != nil { + io.ErrPrintfln("unable to marshal tx, %s", err) + + continue + } + + if _, err = fmt.Fprintf(outputFile, "%s\n", marshaledData); err != nil { + io.ErrPrintfln("unable to save to output file, %s", err) + } + } + } + + // Check if there were any scanner errors + if err := scanner.Err(); err != nil { + return fmt.Errorf("error encountered during scan, %w", err) + } + + return nil +} diff --git a/contribs/gnomigrate/internal/txs/txs_test.go b/contribs/gnomigrate/internal/txs/txs_test.go new file mode 100644 index 00000000000..edc8addf213 --- /dev/null +++ b/contribs/gnomigrate/internal/txs/txs_test.go @@ -0,0 +1,157 @@ +package txs + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateDummyTxs generates dummy transactions +func generateDummyTxs(t *testing.T, count int) []std.Tx { + t.Helper() + + txs := make([]std.Tx, count) + + for i := 0; i < count; i++ { + txs[i] = std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: crypto.Address{byte(i)}, + ToAddress: crypto.Address{byte((i + 1) % count)}, + Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 1)), + }, + }, + Fee: std.Fee{ + GasWanted: 1, + GasFee: std.NewCoin(ugnot.Denom, 1000000), + }, + Memo: fmt.Sprintf("tx %d", i), + } + } + + return txs +} + +func TestMigrate_Txs(t *testing.T) { + t.Parallel() + + t.Run("invalid input dir", func(t *testing.T) { + t.Parallel() + + // Perform the migration + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "--input-dir", + "", + "--output-dir", + t.TempDir(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidInputDir) + }) + + t.Run("invalid output dir", func(t *testing.T) { + t.Parallel() + + // Perform the migration + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "--input-dir", + t.TempDir(), + "--output-dir", + "", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidOutputDir) + }) + + t.Run("valid tx sheet migration", func(t *testing.T) { + t.Parallel() + + var ( + inputDir = t.TempDir() + outputDir = t.TempDir() + + txs = generateDummyTxs(t, 10000) + + chunks = 4 + chunkSize = len(txs) / chunks + ) + + getSheetPath := func(dir string, index int) string { + return filepath.Join(dir, fmt.Sprintf("transactions-sheet-%d.jsonl", index)) + } + + // Generate the initial sheet files + files := make([]*os.File, 0, chunks) + for i := 0; i < chunks; i++ { + f, err := os.Create(getSheetPath(inputDir, i)) + require.NoError(t, err) + + files = append(files, f) + } + + for i := 0; i < chunks; i++ { + var ( + start = i * chunkSize + end = start + chunkSize + ) + + if end > len(txs) { + end = len(txs) + } + + tx := txs[start:end] + + f := files[i] + + jsonData, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + _, err = f.WriteString(fmt.Sprintf("%s\n", jsonData)) + require.NoError(t, err) + } + + // Perform the migration + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "--input-dir", + inputDir, + "--output-dir", + outputDir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + metadataTxs := make([]gnoland.TxWithMetadata, 0, len(txs)) + for i := 0; i < chunks; i++ { + readTxs, err := gnoland.ReadGenesisTxs(context.Background(), getSheetPath(outputDir, i)) + require.NoError(t, err) + + metadataTxs = append(metadataTxs, readTxs...) + } + + // Make sure the metadata txs match + for index, tx := range metadataTxs { + assert.Equal(t, txs[index], tx.Tx) + } + }) +} diff --git a/contribs/gnomigrate/main.go b/contribs/gnomigrate/main.go new file mode 100644 index 00000000000..ea7e2561e8b --- /dev/null +++ b/contribs/gnomigrate/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "context" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +func main() { + cmd := newMigrateCmd(commands.NewDefaultIO()) + + cmd.Execute(context.Background(), os.Args[1:]) +} diff --git a/contribs/gnomigrate/migrate.go b/contribs/gnomigrate/migrate.go new file mode 100644 index 00000000000..6c8667a5f58 --- /dev/null +++ b/contribs/gnomigrate/migrate.go @@ -0,0 +1,24 @@ +package main + +import ( + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gnomigrate/internal/txs" +) + +func newMigrateCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags] [...]", + ShortHelp: "gno migration suite", + LongHelp: "Gno state migration suite, for managing legacy headaches", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + txs.NewTxsCmd(io), + ) + + return cmd +} diff --git a/docs/concepts/gnovm.md b/docs/concepts/gnovm.md index 16e43cb0d42..13e55defb71 100644 --- a/docs/concepts/gnovm.md +++ b/docs/concepts/gnovm.md @@ -8,7 +8,7 @@ GnoVM is a virtual machine that interprets Gno, a custom version of Go optimized It works with Tendermint2 and enables smarter, more modular, and transparent appchains with embedded smart-contracts. It can be adapted for use in TendermintCore, forks, and non-Cosmos blockchains. -Read the ["Intro to Gnoland"](https://test3.gno.land/r/gnoland/blog:p/intro) blogpost. +Read the ["Intro to Gnoland"](https://gno.land/r/gnoland/blog:p/intro) blogpost. This folder focuses on the VM, language, stdlibs, tests, and tools, independent of the blockchain. This enables non-web3 developers to contribute without requiring an understanding of the broader context. diff --git a/docs/concepts/namespaces.md b/docs/concepts/namespaces.md index 0f9176bcbf1..c7f03ec1f0a 100644 --- a/docs/concepts/namespaces.md +++ b/docs/concepts/namespaces.md @@ -28,7 +28,7 @@ Here's a breakdown of the structure of a package path: - `p/`: [Package](packages.md) - `r/`: [Realm](realms.md) - Namespace: A namespace can be included after the type (e.g., user or organization name). Namespaces are a - way to group related packages or realms, but currently ownership cannot be claimed. (see + way to group related packages or realms, but currently ownership cannot be claimed. (see [Issue#1107](https://github.com/gnolang/gno/issues/1107) for more info) - Remaining Path: The remaining part of the path. - Can only contain alphanumeric characters (letters and numbers) and underscores. @@ -74,8 +74,8 @@ After successful registration, you can add a package under the registered namesp ## Anonymous Namespace -Gno.land offers the ability to add a package without having a registered namespace. -You can do this by using your own address as a namespace. This is formatted as `{p,r}/{std.Address}/**`. +gno.land offers the ability to add a package without having a registered namespace. +You can do this by using your own address as a namespace. This is formatted as `{p,r}/{std.Address}/**`. > ex: with `test1` user adding a package `microblog` using his own address as namespace ```bash diff --git a/docs/concepts/portal-loop.md b/docs/concepts/portal-loop.md index d96738bdddf..adc341e3ae4 100644 --- a/docs/concepts/portal-loop.md +++ b/docs/concepts/portal-loop.md @@ -67,3 +67,20 @@ has some drawbacks: Gno will fail to be replayed, meaning **data will be lost**. - Since transactions are archived and replayed during genesis, block height & timestamp cannot be relied upon. + +### Deploying to the Portal Loop + +There are two ways to deploy code to the Portal Loop: + +1. *automatic* - all packages in found in the `examples/gno.land/{p,r}/` directory in the [Gno monorepo](https://github.com/gnolang/gno) get added to the + new genesis each cycle, +2. *permissionless* - this includes replayed transactions with `addpkg`, and + new transactions you can issue with `gnokey maketx addpkg`. + +Since the packages in `examples/gno.land/{p,r}` are deployed first, +permissionless deployments get superseded when packages with identical `pkgpath` +get merged into `examples/`. + +The above mechanism is also how the `examples/` on the Portal Loop +get collaboratively iterated upon, which is its main mission. + diff --git a/docs/concepts/testnets.md b/docs/concepts/testnets.md index 730795d3742..b5286eaec57 100644 --- a/docs/concepts/testnets.md +++ b/docs/concepts/testnets.md @@ -5,10 +5,10 @@ id: testnets # Gno Testnets This page documents all gno.land testnets, what their properties are, and how -they are meant to be used. For testnet configuration, visit the +they are meant to be used. For testnet configuration, visit the [reference section](../reference/network-config.md). -Gno.land testnets are categorized by 4 main points: +gno.land testnets are categorized by 4 main points: - **Persistence of state** - Is the state and transaction history persisted? - **Timeliness of code** @@ -21,30 +21,51 @@ Gno.land testnets are categorized by 4 main points: Below you can find a breakdown of each existing testnet by these categories. ## Portal Loop -Portal Loop is an always up-to-date rolling testnet. It is meant to be used as + +Portal Loop is an always up-to-date rolling testnet. It is meant to be used as a nightly build of the Gno tech stack. The home page of [gno.land](https://gno.land) -is the `gnoweb` render of the Portal Loop testnet. +is the `gnoweb` render of the Portal Loop testnet. - **Persistence of state:** - - State is kept on a best-effort basis + - State is kept on a best-effort basis - Transactions that are affected by breaking changes will be discarded - **Timeliness of code:** - Packages & realms which are available in the `examples/` folder on the [Gno -monorepo](https://github.com/gnolang/gno) exist on the Portal Loop in matching +monorepo](https://github.com/gnolang/gno) exist on the Portal Loop in matching state - they are refreshed with every new commit to the `master` branch. - **Intended purpose** - Providing access the latest version of Gno for fast development & demoing - **Versioning strategy**: - Portal Loop infrastructure is managed within the -[`misc/loop`](https://github.com/gnolang/gno/tree/master/misc/loop) folder in the +[`misc/loop`](https://github.com/gnolang/gno/tree/master/misc/loop) folder in the monorepo -For more information on the Portal Loop, and how it can be best utilized, +For more information on the Portal Loop, and how it can be best utilized, check out the [Portal Loop concept page](./portal-loop.md). Also, you can find the Portal Loop faucet on [`gno.land/faucet`](https://gno.land/faucet). +## Test5 + +Test5 a permanent multi-node testnet. It bumped the validator set from 7 to 17 +nodes, introduced GovDAO V2, and added lots of bug fixes and quality of life +improvements. + +Test5 was launched in November 2024. + +- **Persistence of state:** + - State is fully persisted unless there are breaking changes in a new release, + where persistence partly depends on implementing a migration strategy +- **Timeliness of code:** + - Pre-deployed packages and realms are at monorepo commit [2e9f5ce](https://github.com/gnolang/gno/tree/2e9f5ce8ecc90ee81eb3ae41c06bab30ab926150) +- **Intended purpose** + - Running a full node, testing validator coordination, deploying stable Gno + dApps, creating tools that require persisted state & transaction history +- **Versioning strategy**: + - Test5 is to be release-based, following releases of the Gno tech stack. + ## Test4 -Test4 a permanent multi-node testnet. + +Test4 is the first permanent multi-node testnet, launched in July 2024. - **Persistence of state:** - State is fully persisted unless there are breaking changes in a new release, @@ -59,6 +80,7 @@ Test4 a permanent multi-node testnet. of the Gno tech stack. ## Staging + Staging is a testnet that is reset once every 60 minutes. - **Persistence of state:** @@ -73,40 +95,26 @@ Staging is a testnet that is reset once every 60 minutes. - Staging is reset every 60 minutes to match the latest monorepo commit ## TestX -These testnets are deprecated and currently serve as archives of previous progress. - -### Test3 -Test3 is the most recent persistent Gno testnet. It is still being used, but -most packages, such as the AVL package, are outdated. -- **Persistence of state:** - - State is fully preserved -- **Timeliness of code:** - - Test3 is at commit [1ca2d97](https://github.com/gnolang/gno/commit/1ca2d973817b174b5b06eb9da011e1fcd2cca575) -of Gno, and it can contain new on-chain code -- **Intended purpose** - - Running a full node, building an indexer, showing demos, persisting history -- **Versioning strategy**: - - There is no versioning strategy for test3. It will stay the way it is, until -the team chooses to shut it down. +These testnets are deprecated and currently serve as archives of previous progress. -Since gno.land is designed with open-source in mind, anyone can see currently -available code by browsing the [test3 homepage](https://test3.gno.land/). +### Test3 (archive) -Test3 is a single-node testnet, ran by the Gno core team. There is no plan to -upgrade test3 to a multi-node testnet. +The third Gno testnet. Archived data for test3 can be found [here](https://github.com/gnolang/tx-exports/tree/main/test3.gno.land). -Launch date: November 4th 2022 +Launch date: November 4th 2022 Release commit: [1ca2d97](https://github.com/gnolang/gno/commit/1ca2d973817b174b5b06eb9da011e1fcd2cca575) ### Test2 (archive) + The second Gno testnet. Find archive data [here](https://github.com/gnolang/tx-exports/tree/main/test2.gno.land). -Launch date: July 10th 2022 -Release commit: [652dc7a](https://github.com/gnolang/gno/commit/652dc7a3a62ee0438093d598d123a8c357bf2499) +Launch date: July 10th 2022 +Release commit: [652dc7a](https://github.com/gnolang/gno/commit/652dc7a3a62ee0438093d598d123a8c357bf2499) ### Test1 (archive) + The first Gno testnet. Find archive data [here](https://github.com/gnolang/tx-exports/tree/main/test1.gno.land). -Launch date: May 6th 2022 +Launch date: May 6th 2022 Release commit: [797c7a1](https://github.com/gnolang/gno/commit/797c7a132d65534df373c63b837cf94b7831ac6e) diff --git a/docs/getting-started/local-setup/creating-a-keypair.md b/docs/getting-started/local-setup/creating-a-keypair.md new file mode 100644 index 00000000000..983d732a0fd --- /dev/null +++ b/docs/getting-started/local-setup/creating-a-keypair.md @@ -0,0 +1,77 @@ +--- +id: creating-a-keypair +--- + +# Creating a Keypair + +## Overview + +In this tutorial, you will learn how to create your Gno keypair using +[`gnokey`](../../gno-tooling/cli/gnokey/gnokey.md). + +Keypairs are the foundation of how users interact with blockchains; and Gno is +no exception. By using a 12-word or 24-word [mnemonic phrase](https://www.zimperium.com/glossary/mnemonic-seed/) +as a source of randomness, users can derive a private and a public key. +These two keys can then be used further; a public key derives an address which is +a unique identifier of a user on the blockchain, while a private key is used for +signing messages and transactions for the aforementioned address, proving a user +has ownership over it. + +Let's see how we can use `gnokey` to generate a Gno keypair locally. + +## Generating a keypair + +The `gnokey add` command allows you to generate a new keypair locally. Simply +run the command, while adding a name for your keypair: + +```bash +gnokey add MyKey +``` + +![gnokey-add-random](../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-add-random.gif) + +After running the command, `gnokey` will ask you to enter a password that will be +used to encrypt your keypair to the disk. Then, it will show you the following +information: +- Your public key, as well as the Gno address derived from it, starting with `g1...`, +- Your randomly generated 12-word mnemonic phrase which was used to derive the keypair. + +:::warning Safeguard your mnemonic phrase! + +A **mnemonic phrase** is like your master password; you can use it over and over +to derive the same keypairs. This is why it is crucial to store it in a safe, +offline place - writing the phrase on a piece of paper and hiding it is highly +recommended. **If it gets lost, it is unrecoverable.** + +::: + +`gnokey` will generate a keybase in which it will store information about your +keypairs. The keybase directory path is stored under the `-home` flag in `gnokey`. + +### Gno addresses + +Your **Gno address** is like your unique identifier on the network; an address +is visible in the caller stack of an application, it is included in each +transaction you create with your keypair, and anyone who knows your address can +send you [coins](../../concepts/stdlibs/coin.md), etc. + +## Conclusion + +That's it 🎉 + +You've successfully created your first Gno keypair. Check out +[Browsing gno.land](./browsing-gnoland.md) and +[Interacting with gno.land](./interacting-with-gnoland.md) to see how you can +use it. + +If you wish to learn more about `gnokey` specifically, check out the +[gnokey section](../../gno-tooling/cli/gnokey/gnokey.md). + + + + + + + + + diff --git a/docs/getting-started/local-setup/installation.md b/docs/getting-started/local-setup/installation.md index 58f71f93026..e05c2f9b205 100644 --- a/docs/getting-started/local-setup/installation.md +++ b/docs/getting-started/local-setup/installation.md @@ -35,7 +35,7 @@ git clone https://github.com/gnolang/gno.git There are three tools that should be used for getting started with Gno development: - `gno` - the GnoVM binary - `gnodev` - the Gno [development helper](../../gno-tooling/cli/gnodev.md) -- `gnokey` - the Gno [keypair manager](working-with-key-pairs.md) +- `gnokey` - the Gno [keypair manager](../../gno-tooling/cli/gnokey/working-with-key-pairs.md) To install all three tools, simply run the following in the root of the repo: ```bash @@ -69,7 +69,7 @@ go run ./cmd/gno --help ### `gnodev` `gnodev` is the go-to Gno development helper tool - it comes with a built in -Gno.land node, a `gnoweb` server to display the state of your smart contracts +gno.land node, a `gnoweb` server to display the state of your smart contracts (realms), and a watcher system to actively track changes in your code. Read more about `gnodev` [here](../../gno-tooling/cli/gnodev.md). @@ -87,7 +87,7 @@ You should get the following output: `gnokey` is the gno.land keypair management CLI tool. It allows you to create keypairs, sign transactions, and broadcast them to gno.land chains. Read more -about `gnokey` [here](../../gno-tooling/cli/gnokey.md). +about `gnokey` [here](../../gno-tooling/cli/gnokey/gnokey.md). To verify that the `gnokey` binary is installed system-wide, you can run: @@ -106,5 +106,5 @@ That's it 🎉 You have successfully built out and installed the necessary tools for Gno development! -In further documents, you will gain a better understanding on how they are used +In further documents, you will gain a better understanding of how they are used to make Gno work. diff --git a/docs/getting-started/local-setup/interacting-with-gnoland.md b/docs/getting-started/local-setup/interacting-with-gnoland.md index e07c839d691..6b4b8213228 100644 --- a/docs/getting-started/local-setup/interacting-with-gnoland.md +++ b/docs/getting-started/local-setup/interacting-with-gnoland.md @@ -10,10 +10,10 @@ You will understand how to use your keypair to send transactions to realms and packages, send native coins, and more. ## Prerequisites + - **`gnokey` installed.** Reference the -[Local Setup](installation.md#3-installing-other-gno-tools) guide for steps -- **A keypair in `gnokey`.** Reference the -[Working with Key Pairs](working-with-key-pairs.md#adding-a-private-key-using-a-mnemonic) guide for steps +[Local Setup](installation.md) guide for steps +- **A keypair in `gnokey`.** Reference the [Creating a key pair](creating-a-keypair.md) guide for steps ## 1. Get testnet GNOTs For interacting with any gno.land chain, you will need a certain amount of GNOTs @@ -21,8 +21,7 @@ to pay gas fees with. For this example, we will use the [Portal Loop](../../concepts/testnets.md#portal-loop) testnet. We can access the Portal Loop faucet through the -[Gno Faucet Hub](https://faucet.gno.land), or by accessing the faucet directly at -[gno.land/faucet](https://gno.land/faucet). +[Gno Faucet Hub](https://faucet.gno.land). ![faucet-hub](../../assets/getting-started/local-setup/interacting-with-gnoland/faucet-hub.png) @@ -35,7 +34,7 @@ After inputting your address and solving the captcha, you can check if you have following `gnokey` command: ```bash -gnokey query bank/balances/ --remote "https://rpc.gno.land:443" +gnokey query bank/balances/ --remote "https://rpc.gno.land:443" ``` If the faucet request was successful, you should see something similar to the @@ -48,6 +47,7 @@ data: "10000000ugnot" ``` ## 2. Visit a realm + For this example, we will use the [Userbook realm](https://gno.land/r/demo/userbook). The Userbook realm is a simple app that allows users to sign up, and keeps track of when they signed up. It also displays the currently signed-up users and the block @@ -55,8 +55,8 @@ height at which they have signed up. ![userbook-default](../../assets/getting-started/local-setup/interacting-with-gnoland/userbook-default.png) -> Note: block heights are not correct because of the way the Portal Loop testnet -> works. +> Note: block heights in this case are unreliable because of the way the Portal Loop +> network works. > Read more [here](../../concepts/portal-loop.md). To see what functions are available to call on the Userbook realm, click @@ -67,7 +67,7 @@ the `[help]` button. By choosing one of the two `gnokey` commands and inputting your address (or keypair name) in the top bar, you will have a ready command to paste into your terminal. For example, the following command will call the `SignUp` function with the -keypair `MyKeypair`: +keypair `MyKey`: ``` gnokey maketx call \ @@ -79,11 +79,11 @@ gnokey maketx call \ -broadcast \ -chainid "portal-loop" \ -remote "https://rpc.gno.land:443" \ -MyKeypair +MyKey ``` -To see what each option and flag in this command does, read the `gnokey` -[reference page](../../gno-tooling/cli/gnokey.md). +To see what each option and flag in this command does, check out `gnokey` in the +[tooling section](../../gno-tooling/cli/gnokey/gnokey.md). ## Conclusion @@ -92,6 +92,6 @@ That's it! Congratulations on executing your first transaction on a Gno network! If the previous transaction was successful, you should be able to see your address on the main page of the Userbook realm. -This concludes the "Local Setup" tutorial. For next steps, see the +This concludes the "Local Setup" section. For next steps, see the [How-to guides section](../../how-to-guides/how-to-guides.md), where you will learn how to write your first realm, package, and much more. diff --git a/docs/getting-started/local-setup/working-with-key-pairs.md b/docs/getting-started/local-setup/working-with-key-pairs.md deleted file mode 100644 index 23516c44e6c..00000000000 --- a/docs/getting-started/local-setup/working-with-key-pairs.md +++ /dev/null @@ -1,198 +0,0 @@ ---- -id: working-with-key-pairs ---- - -# Working with Key Pairs - -## Overview -In this tutorial, you will learn how to manage private user keys, which are -required for interacting with the gno.land blockchain. You will understand what -mnemonics are, how they are used, and how you can make interaction seamless with -Gno. - -## Prerequisites -- **`gnokey` installed.** Reference the -[Local Setup](installation.md#2-installing-the-required-tools-) guide for steps - -## Listing available keys -`gnokey` works by creating a local directory in the filesystem for storing -(encrypted!) user private keys. - -You can find this repository by checking the value of the `--home` flag when -running the following command: - -```bash -gnokey --help -``` - -Example output: - -```bash -USAGE - [flags] [...] - -Manages private keys for the node - -SUBCOMMANDS - add Adds key to the keybase - delete Deletes a key from the keybase - generate Generates a bip39 mnemonic - export Exports private key armor - import Imports encrypted private key armor - list Lists all keys in the keybase - sign Signs the document - verify Verifies the document signature - query Makes an ABCI query - broadcast Broadcasts a signed document - maketx Composes a tx document to sign - -FLAGS - -config ... config file (optional) - -home $XDG_CONFIG/gno home directory - -insecure-password-stdin=false WARNING! take password from stdin - -quiet=false suppress output during execution - -remote 127.0.0.1:26657 remote node URL -``` - -In this example, the directory where `gnokey` will store working data -is `/Users/zmilos/Library/Application Support/gno`. - -Keep note of this directory, in case you need to reset the keystore, or migrate -it for some reason. -You can provide a specific `gnokey` working directory using the `--home` flag. - -To list keys currently present in the keystore, we can run: - -```bash -gnokey list -``` - -In case there are no keys present in the keystore, the command will simply -return an empty response. -Otherwise, it will return the list of keys and their accompanying metadata as a -list, for example: - -```bash -0. Manfred (local) - addr: g15uk9d6feap7z078ttcnwc94k60ullrvhmynxjt pub: gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqvn87u43scec4zfgn4la3nt237nehzydzayqxe43fx63lq6rty9c5almet4, path: -1. Milos (local) - addr: g15lppu0tuxets0c0t80tncs4enqzgxt7v4eftcj pub: gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqw2kkzujprgrfg7vumg85mccsf790n5ep6htpygkuwedwuumf2g7ydm4vqf, path: -``` - -The key response consists of a few pieces of information: - -- The name of the private key -- The derived address (`addr`) -- The public key (`pub`) - -Using these pieces of information, we can interact with gno.land tools and write -blockchain applications. - -## Generating a BIP39 mnemonic - -Using `gnokey`, we can generate a [mnemonic phrase](https://en.bitcoin.it/wiki/Seed_phrase) based on -the [BIP39 standard](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki). - -To generate the mnemonic phrase in the console, you can run: - -```bash -gnokey generate -``` - -![gnokey generate](../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-generate.gif) - -## Adding a random private key -If we wanted to add a new private key to the keystore, we can run the following -command: - -```bash -gnokey add MyKey -``` - -Of course, you can replace `MyKey` with whatever name you want for your key. - -The `gnokey` tool will prompt you to enter a password to encrypt the key on disk -(don't forget this!). -After you enter the password, the `gnokey` tool will add the key to the keystore, -and return the accompanying [mnemonic phrase](https://en.bitcoin.it/wiki/Seed_phrase), which you should remember -somewhere if you want to recover the key at a future point in time. - -![gnokey add random](../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-add-random.gif) - -You can check that the key was indeed added to the keystore, by listing available -keys: - -```bash -gnokey list -``` - -![gnokey list](../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-list.gif) - -## Adding a private key using a mnemonic -To add a private key to the `gnokey` keystore [using an existing mnemonic](#generating-a-bip39-mnemonic), -we can run the following command with the -`--recover` flag: - -```bash -gnokey add --recover MyKey -``` - -Of course, you can replace `MyKey` with whatever name you want for your key. - -By following the prompts to encrypt the key on disk, and providing a BIP39 -mnemonic, we can successfully add the key to the keystore. - -![gnokey add mnemonic](../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-add-mnemonic.gif) - -## Deleting a private key -To delete a private key from the `gnokey` keystore, we need to know the name or -address of the key to remove. -After we have this information, we can run the following command: - -```bash -gnokey delete MyKey -``` - -After entering the key decryption password, the key will be deleted from the keystore. - -:::caution Recovering a private key -In case you delete or lose access to your private key in the `gnokey` keystore, -you can recover it using the key's mnemonic, or by importing it if it was exported -at a previous point in time. -::: - -## Exporting a private key -Private keys stored in the `gnokey` keystore can be exported to a desired place -on the user's filesystem. - -Keys are exported in their original armor, encrypted or unencrypted. - -To export a key from the keystore, you can run: - -```bash -gnokey export -key MyKey -output-path ~/Work/gno-key.asc -``` - -Follow the prompts presented in the terminal. Namely, you will be asked to -decrypt the key in the keystore, and later to encrypt the armor file on disk. -It is worth noting that you can also export unencrypted key armor, using the `--unsafe` flag. - -![gnokey export](../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-export.gif) - -## Importing a private key -If you have an exported private key file, you can import it into `gnokey` fairly -easily. - -For example, if the key is exported at `~/Work/gno-key.asc`, you can run the -following command: - -```bash -gnokey import -armor-path ~/Work/gno-key.asc -name ImportedKey -``` - -You will be asked to decrypt the encrypted private key armor on disk -(if it is encrypted, if not, use the `--unsafe` flag), and then to provide an -encryption password for storing the key in the keystore. - -After executing the previous command, the `gnokey` keystore will have imported -`ImportedKey`. - -![gnokey import](../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-import.gif) diff --git a/docs/getting-started/playground-start.md b/docs/getting-started/playground-start.md index f62e2748efe..0da950b69c0 100644 --- a/docs/getting-started/playground-start.md +++ b/docs/getting-started/playground-start.md @@ -6,17 +6,17 @@ id: playground-start ## Overview -The Gno Playground is an innovative web-based editor and sandbox that enables developers to +The Gno Playground is an innovative web-based editor and sandbox that enables developers to interactively work with the Gno language. It makes coding, testing, and deploying simple with its diverse set of tools and features. Users can -share code, run tests, and deploy projects to gno.land networks, +share code, run tests, and deploy projects to gno.land networks, making it the perfect tool to get started with Gno development. ## Prerequisites - **A gno.land compatible wallet** - Currently, [Adena](https://www.adena.app/) is the preferred wallet for -Gno.land, with more wallets being introduced in the future. +gno.land, with more wallets being introduced in the future. ## Playground Features @@ -44,25 +44,25 @@ ensuring the shared code remains accessible over an extended period. ### Deploy -The **Deploy** feature allows users to seamlessly deploy their Gno code to the -chain. After connecting a gno.land wallet, users can select their desired +The **Deploy** feature allows users to seamlessly deploy their Gno code to the +chain. After connecting a gno.land wallet, users can select their desired package path and network for deployment. ![default_deploy](../assets/getting-started/playground/default_deploy.png) -After inputting your desired package path, you can select the network you would +After inputting your desired package path, you can select the network you would like to deploy to, such as [Portal Loop](../concepts/portal-loop.md) or local, and click deploy. :::info -The Playground will automatically provide enough test tokens to cover the gas +The Playground will automatically provide enough test tokens to cover the gas cost at the time of deployment, removing the need for using a faucet. ::: ### Format The **Format** feature utilizes the Monaco editor and -[`gofmt`](https://pkg.go.dev/cmd/gofmt) to automatically refine and standardize +[`gofmt`](https://pkg.go.dev/cmd/gofmt) to automatically refine and standardize your Gno code's syntax. ### Run @@ -82,7 +82,7 @@ View the code [here](https://play.gno.land/p/nBq2W8drjMy). ### Test -The **Test** feature will look for `_test.gno` files in your playground and run +The **Test** feature will look for `_test.gno` files in your playground and run the`gno test -v` command on them. Testing your code will open a terminal that will show you the output of the test. Read more about how Gno tests work [here](../concepts/gno-test.md). @@ -95,10 +95,10 @@ It provides a command-line interface for hands-on learning, iterative testing, a ## Learning about gno.land & writing Gno code If you're new here, don't worry—content is regularly produced to breakdown -Gno.land to explain its features. Dive into the essentials of gno.land by +gno.land to explain its features. Dive into the essentials of gno.land by exploring the [Concepts](../concepts/concepts.md) section. To get started writing Gno code, check out the [How-to](../how-to-guides/how-to-guides.md) section, the `examples/` folder on -the [Gno monorepo](https://github.com/gnolang/gno), or one of many community projects and tutorials found in the +the [Gno monorepo](https://github.com/gnolang/gno), or one of many community projects and tutorials found in the [awesome-gno](https://github.com/gnolang/awesome-gno/blob/main/README.md) repo on GitHub. diff --git a/docs/gno-infrastructure/validators/faq.md b/docs/gno-infrastructure/validators/faq.md index c345b49724a..940d3abe7a1 100644 --- a/docs/gno-infrastructure/validators/faq.md +++ b/docs/gno-infrastructure/validators/faq.md @@ -8,7 +8,7 @@ id: validators-faq ### What is a gno.land validator? -Gno.land is based on [Tendermint2](https://docs.gno.land/concepts/tendermint2) that relies on a set of validators +gno.land is based on [Tendermint2](https://docs.gno.land/concepts/tendermint2) that relies on a set of validators selected based on [Proof of Contribution](https://docs.gno.land/concepts/proof-of-contribution) (PoC) to secure the network. Validators are tasked with participating in consensus by committing new blocks and broadcasting votes. Validators are compensated with a portion of transaction fees generated in the network. In gno.land, the voting power of @@ -45,7 +45,7 @@ network. ### What stage is the gno.land project in? -Gno.land is currently in Testnet 3, the single-node testnet stage. The next version, Testnet 4, is scheduled to go live +gno.land is currently in Testnet 3, the single-node testnet stage. The next version, Testnet 4, is scheduled to go live in Q3 2024, which will include a validator set implementation for a multinode environment. ## Becoming a Validator @@ -69,11 +69,11 @@ validators for their work. All validators fairly receive an equal amount of rewa The exact plans for mainnet are still TBD. Based on the latest discussions between contributors, the mainnet will likely have an inital validator set size of 20~50, which will gradually scale with the development and decentralization of the -Gno.land project. +gno.land project. ### How do I make my first contribution? -Gno.land is in active development and external contributions are always welcome! If you’re looking for tasks to begin +gno.land is in active development and external contributions are always welcome! If you’re looking for tasks to begin with, we suggest you visit the [Bounties &](https://github.com/orgs/gnolang/projects/35/views/3) [Worx](https://github.com/orgs/gnolang/projects/35/views/3) board and search for open tasks up for grabs. Start from small challenges and work your way up to the bigger ones. Every @@ -104,41 +104,6 @@ either a full node or a pruned node, it is important to retain enough blocks to ## Technical References -### How do I generate `genesis.json`? - -`genesis.json` is the file that is used to create the initial state of the chain. To generate `genesis.json`, use -the `gnoland genesis generate` command. Refer -to [this section](../../gno-tooling/cli/gnoland.md#gnoland-genesis-generate-flags) for various flags that allow you to -manipulate the file. - -:::warning - -Editing generated genesis.json manually is extremely dangerous. It may corrupt chain initial state which leads chain to -not start - -::: - -### How do I add or remove validators from `genesis.json`? - -Validators inside `genesis.json` will be included in the validator set at genesis. To manipulate the genesis validator -set, use the `gnoland genesis validator` command with the `add` or `remove` subcommands. Refer -to [this section](../../gno-tooling/cli/gnoland.md#gnoland-genesis-validator-flags) for flags that allow you to -configure the name or the voting power of the validator. - -### How do I add the balance information to the `genesis.json`? - -You may premine coins to various addresses. To modify the balances of addresses at genesis, use -the `gnoland genesis balances` command with the `add` or `remove` subcommands. Refer -to [this section](../../gno-tooling/cli/gnoland.md#gnoland-genesis-balances-add-flags) for various flags that allow you -to update the entire balance sheet with a file or modify the balance of a single address. - -:::info - -Not only `ugnot`, but other coins are accepted. However, be aware that coins other than `ugnot` may not work(send, and -etc.) properly. - -::: - ### How do I initialize `gno secrets`? The `gno secrets init` command allows you to initialize the private information required to run the validator, including diff --git a/docs/gno-infrastructure/validators/overview.md b/docs/gno-infrastructure/validators/overview.md index 918bd218f50..e0973ad22d1 100644 --- a/docs/gno-infrastructure/validators/overview.md +++ b/docs/gno-infrastructure/validators/overview.md @@ -6,7 +6,7 @@ id: validators-overview ## Introduction -Gno.land is a blockchain powered by the Gno tech stack, which consists of +gno.land is a blockchain powered by the Gno tech stack, which consists of the [Gno Language](https://docs.gno.land/concepts/gno-language/) (Gno), [Tendermint2](https://docs.gno.land/concepts/tendermint2/) (TM2), and [GnoVM](https://docs.gno.land/concepts/gnovm/). Unlike @@ -17,7 +17,7 @@ selected via governance based on their contribution to the project and technical network is equally distributed across all validators to achieve a high nakamoto coefficient. A portion of all transaction fees paid to the network are evenly shared between all validators to provide a fair incentive structure. -| **Blockchain** | Cosmos | Gno.land | +| **Blockchain** | Cosmos | gno.land | |--------------------------------------|-------------------------|-------------------------------| | **Consensus Protocol** | Comet BFT | Tendermint2 | | **Consensus Mechanism** | Proof of Stake | Proof of Contribution | @@ -78,9 +78,9 @@ be expected from a good, reliable validator. Join the official gno.land community in various channels to receive the latest updates about the project and actively communicate with other validators and contributors. -- [Gno.land Blog](https://gno.land/r/gnoland/blog) -- [Gno.land Discord](https://discord.gg/YFtMjWwUN7) -- [Gno.land Twitter](https://x.com/_gnoland) +- [gno.land Blog](https://gno.land/r/gnoland/blog) +- [gno.land Discord](https://discord.gg/YFtMjWwUN7) +- [gno.land Twitter](https://x.com/_gnoland) :::info diff --git a/docs/gno-infrastructure/validators/setting-up-a-new-chain.md b/docs/gno-infrastructure/validators/setting-up-a-new-chain.md index 0411fa3b02a..5db8a7f1a59 100644 --- a/docs/gno-infrastructure/validators/setting-up-a-new-chain.md +++ b/docs/gno-infrastructure/validators/setting-up-a-new-chain.md @@ -19,7 +19,7 @@ Additionally, you will see the different options you can use to make your Gno in ## Installation -To install the `gnoland` binary, clone the Gno monorepo: +To install the `gnoland` and `gnogenesis` binaries, clone the Gno monorepo: ```bash git clone https://github.com/gnolang/gno.git @@ -30,7 +30,7 @@ Makefile to install the `gnoland` binary: ```bash cd gno.land -make install.gnoland +make install.gnoland && make -C contribs/gnogenesis install ``` To verify that you've installed the binary properly and that you are able to use @@ -93,7 +93,8 @@ Let's break down the most important default settings: :::info Resetting the chain As mentioned, the working directory for the node is located in `data-dir`. To reset the chain, you need -to delete this directory and `genesis.json`, then start the node up again. If you are using the default node configuration, you can run +to delete this directory and `genesis.json`, then start the node up again. If you are using the default node +configuration, you can run `make fclean` from the `gno.land` sub-folder to delete the `gnoland-data` working directory. ::: @@ -201,7 +202,7 @@ executed. Generating an empty `genesis.json` is relatively straightforward: ```shell -gnoland genesis generate +gnogenesis generate ``` The resulting `genesis.json` is empty: @@ -232,7 +233,7 @@ This will generate a `genesis.json` in the calling directory, by default. To che generating the `genesis.json`, you can run the command using the `--help` flag: ```shell -gnoland genesis generate --help +gnogenesis generate --help USAGE generate [flags] @@ -257,7 +258,7 @@ present challenges with users who expect them to be present. The `examples` directory is located in the `$GNOROOT` location, or the local gno repository clone. ```bash -gnoland genesis txs add packages ./examples +gnogenesis txs add packages ./examples ``` ### 4. Add the initial validator set @@ -288,7 +289,7 @@ Updating the `genesis.json` is relatively simple, running the following command validator set: ```shell -gnoland genesis validator add \ +gnogenesis validator add \ --address g14j4dlsh3jzgmhezzp9v8xp7wxs4mvyskuw5ljl \ --pub-key gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zqaqle3fdduqul4slg6zllypq9r8gj4wlfucy6qfnzmjcgqv675kxjz8jvk \ --name Cuttlas diff --git a/docs/gno-tooling/cli/faucet/faucet.md b/docs/gno-tooling/cli/faucet/faucet.md index 4d32f86e9ef..b069a19740a 100644 --- a/docs/gno-tooling/cli/faucet/faucet.md +++ b/docs/gno-tooling/cli/faucet/faucet.md @@ -22,7 +22,7 @@ The Gno faucet works by designating a single address as a faucet address that wi Ensure the faucet account will have enough funds by [premining its balance](../../../gno-infrastructure/premining-balances.md) to a high value. In case you do not have an existing address added to `gnokey`, you can consult -the [Working with Key Pairs](../../../getting-started/local-setup/working-with-key-pairs.md) guide. +the [Working with Key Pairs](../gnokey/working-with-key-pairs.md) guide. ## 2. Start the local chain diff --git a/docs/gno-tooling/cli/gnokey.md b/docs/gno-tooling/cli/gnokey.md deleted file mode 100644 index bf110faec5f..00000000000 --- a/docs/gno-tooling/cli/gnokey.md +++ /dev/null @@ -1,323 +0,0 @@ ---- -id: gno-tooling-gnokey ---- - -# gnokey - -Used for account & key management and general interactions with the Gnoland blockchain. - -## Generate a New Seed Phrase - -Generate a new seed phrase and add it to your keybase with the following command. - -```bash -gnokey generate -``` - -## Add a New Key - -You can add a new private key to the keybase using the following command. - -```bash -gnokey add {KEY_NAME} -``` - -#### **Options** - -| Name | Type | Description | -|-------------|------------|----------------------------------------------------------------------------------------| -| `account` | UInt | Account number for HD derivation. | -| `dryrun` | Boolean | Performs action, but doesn't add key to local keystore. | -| `index` | UInt | Address index number for HD derivation. | -| `ledger` | Boolean | Stores a local reference to a private key on a Ledger device. | -| `multisig` | String \[] | Constructs and stores a multisig public key (implies `--pubkey`). | -| `nobackup` | Boolean | Doesn't print out seed phrase (if others are watching the terminal). | -| `nosort` | Boolean | Keys passed to `--multisig` are taken in the order they're supplied. | -| `pubkey` | String | Parses a public key in bech32 format and save it to disk. | -| `recover` | Boolean | Provides seed phrase to recover existing key instead of creating. | -| `threshold` | Int | K out of N required signatures. For use in conjunction with --multisig (default: `1`). | - -> **Test Seed Phrase:** source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast - -### Using a ledger device - -You can add a ledger device using the following command - -> [!NOTE] -> Before running this command make sure your ledger device is connected, with the cosmos app installed and open in it. - -```bash -gnokey add {LEDGER_KEY_NAME} --ledger -``` - -## List all Known Keys - -List all keys stored in your keybase with the following command. - -```bash -gnokey list -``` - -## Delete a Key - -Delete a key from your keybase with the following command. - -```bash -gnokey delete {KEY_NAME} -``` - -#### **Options** - -| Name | Type | Description | -|---------|---------|------------------------------| -| `yes` | Boolean | Skips confirmation prompt. | -| `force` | Boolean | Removes key unconditionally. | - - -## Export a Private Key (Encrypted & Unencrypted) - -Export a private key's (encrypted or unencrypted) armor using the following command. - -```bash -gnokey export -``` - -#### **Options** - -| Name | Type | Description | -|---------------|--------|---------------------------------------------| -| `key` | String | Name or Bech32 address of the private key | -| `output-path` | String | The desired output path for the armor file | -| `unsafe` | Bool | Export the private key armor as unencrypted | - - -## Import a Private Key (Encrypted & Unencrypted) - -Import a private key's (encrypted or unencrypted) armor with the following command. - -```bash -gnokey import -``` - -#### **Options** - -| Name | Type | Description | -|--------------|--------|---------------------------------------------| -| `armor-path` | String | The path to the encrypted armor file. | -| `name` | String | The name of the private key. | -| `unsafe` | Bool | Import the private key armor as unencrypted | - - -## Make an ABCI Query - -Make an ABCI Query with the following command. - -```bash -gnokey query {QUERY_PATH} -``` - -#### **Query** - -| Query Path | Description | Example | -|---------------------------|--------------------------------------------------------------------|----------------------------------------------------------------------------------------| -| `auth/accounts/{ADDRESS}` | Returns information about an account. | `gnokey query auth/accounts/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5` | -| `bank/balances/{ADDRESS}` | Returns balances of an account. | `gnokey query bank/balances/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5` | -| `vm/qfuncs` | Returns public facing function signatures as JSON. | `gnokey query vm/qfuncs --data "gno.land/r/demo/boards"` | -| `vm/qfile` | Returns the file bytes, or list of files if directory. | `gnokey query vm/qfile --data "gno.land/r/demo/boards"` | -| `vm/qrender` | Calls .Render(path) in readonly mode. | `gnokey query vm/qrender --data "gno.land/r/demo/boards:"` | -| `vm/qeval` | Evaluates any expression in readonly mode and returns the results. | `gnokey query vm/qeval --data "gno.land/r/demo/boards.GetBoardIDFromName("my_board")"` | -| `vm/store` | (not yet supported) Fetches items from the store. | - | -| `vm/package` | (not yet supported) Fetches a package's files. | - | - -#### **Options** - -| Name | Type | Description | -|----------|-----------|------------------------------------------| -| `data` | UInt8 \[] | Queries data bytes. | - - -## Sign and Broadcast a Transaction - -You can sign and broadcast a transaction with the following command. - -```bash -gnokey maketx {SUB_COMMAND} {ADDRESS or KeyName} -``` - -#### **Subcommands** - -| Name | Description | -|----------|------------------------------| -| `addpkg` | Uploads a new package. | -| `call` | Calls a public function. | -| `send` | The amount of coins to send. | - -### `addpkg` - -This subcommand lets you upload a new package. - -```bash -gnokey maketx addpkg \ - -deposit="1ugnot" \ - -gas-fee="1ugnot" \ - -gas-wanted="5000000" \ - -pkgpath={Registered Realm path} \ - -pkgdir={Package folder path} \ - {ADDRESS} \ - > unsigned.tx -``` - -#### **SignBroadcast Options** - -| Name | Type | Description | -|--------------|---------|----------------------------------------------------------------------------------------| -| `gas-wanted` | Int64 | The maximum amount of gas to use for the transaction. | -| `gas-fee` | String | The gas fee to pay for the transaction. | -| `memo` | String | Any descriptive text. | -| `broadcast` | Boolean | Broadcasts the transaction. | -| `chainid` | String | The chainid to sign for (should only be used with `--broadcast`) | -| `simulate` | String | One of `test` (default), `skip` or `only` (should only be used with `--broadcast`)[^1] | - -#### **makeTx AddPackage Options** - -| Name | Type | Description | -|-----------|--------|---------------------------------------| -| `pkgpath` | String | The package path (required). | -| `pkgdir` | String | The path to package files (required). | -| `deposit` | String | The amount of coins to send. | - -### `call` - -This subcommand lets you call any exported function. - -```bash -# Register -gnokey maketx call \ - -gas-fee="1ugnot" \ - -gas-wanted="5000000" \ - -pkgpath="gno.land/r/demo/users" \ - -send="200000000ugnot" \ - -func="Register" \ - -args="" \ - -args={NAME} \ - -args="" \ - {ADDRESS} \ - > unsigned.tx -``` - -:::warning `call` is a state-changing message - -All exported functions, including `Render()`, can be called in two main ways: -`call` and [`query vm/qeval`](#query). - -With `call`, any state change that happened in the function being called will be -applied and persisted in on the blockchain, and the gas used for this call will -be subtracted from the caller balance. - -As opposed to this, an ABCI query, such as `vm/qeval` will not persist state -changes and does not cost gas, only evaluating the expression in read-only mode. - -::: - -#### **SignBroadcast Options** - -| Name | Type | Description | -|--------------|---------|----------------------------------------------------------------------------------------| -| `gas-wanted` | Int64 | The maximum amount of gas to use for the transaction. | -| `gas-fee` | String | The gas fee to pay for the transaction. | -| `memo` | String | Any descriptive text. | -| `broadcast` | Boolean | Broadcasts the transaction. | -| `chainid` | String | The chainid to sign for (should only be used with `--broadcast`) | -| `simulate` | String | One of `test` (default), `skip` or `only` (should only be used with `--broadcast`)[^1] | - -#### **makeTx Call Options** - -| Name | Type | Description | -|-----------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------| -| `send` | String | The amount of coins to send. | -| `pkgpath` | String | The package path (required). | -| `func` | String | The contract to call (required). | -| `args` | String | An argument of the function being called. Can be used multiple times in a single `call` command to accommodate possible multiple function arguments. | - -:::info -Currently, only primitive types are supported as `-args` parameters. This limitation will be addressed in the future. -Alternatively, see how `maketx run` works. -::: - -### `send` - -This subcommand lets you send a native currency to an address. - -```bash -gnokey maketx send \ - -gas-fee="1ugnot" \ - -gas-wanted="5000000" \ - -send={SEND_AMOUNT} \ - -to={TO_ADDRESS} \ - {ADDRESS} \ - > unsigned.tx -``` - -#### **SignBroadcast Options** - -| Name | Type | Description | -|--------------|---------|----------------------------------------------------------------------------------------| -| `gas-wanted` | Int64 | The maximum amount of gas to use for the transaction. | -| `gas-fee` | String | The gas fee to pay for the transaction. | -| `memo` | String | Any descriptive text. | -| `broadcast` | Boolean | Broadcasts the transaction. | -| `chainid` | String | The chainid to sign for (should only be used with `--broadcast`) | -| `simulate` | String | One of `test` (default), `skip` or `only` (should only be used with `--broadcast`)[^1] | - -#### **makeTx Send Options** - -| Name | Type | Description | -|--------|--------|--------------------------| -| `send` | String | Amount of coins to send. | -| `to` | String | The destination address. | - - -## Sign a Document - -Sign a document with the following command. - -```bash -gnokey sign -``` - -#### **Options** - -| Name | Type | Description | -|------------------|---------|------------------------------------------------------------| -| `txpath` | String | The path to file of tx to sign (default: `-`). | -| `chainid` | String | The chainid to sign for (default: `dev`). | -| `number` | UInt | The account number of the account to sign with (required) | -| `sequence` | UInt | The sequence number of the account to sign with (required) | -| `show-signbytes` | Boolean | Shows signature bytes. | - - -## Verify a Document Signature - -Verify a document signature with the following command. - -```bash -gnokey verify -``` - -#### **Options** - -| Name | Type | Description | -|-----------|--------|------------------------------------------| -| `docpath` | String | The path of the document file to verify. | - -## Broadcast a Signed Document - -Broadcast a signed document with the following command. - -```bash -gnokey broadcast {signed transaction file document} -``` - -[^1]: `only` simulates the transaction as a "dry run" (ie. without committing to - the chain), `test` performs simulation and, if successful, commits the - transaction, `skip` skips simulation entirely and commits directly. diff --git a/docs/gno-tooling/cli/gnokey/full-security-tx.md b/docs/gno-tooling/cli/gnokey/full-security-tx.md new file mode 100644 index 00000000000..bccddb30b8a --- /dev/null +++ b/docs/gno-tooling/cli/gnokey/full-security-tx.md @@ -0,0 +1,134 @@ +--- +id: full-security-tx +--- + +# Making an airgapped transaction + +## Prerequisites + +- **`gnokey` installed.** Reference the + [Local Setup](../../../getting-started/local-setup/installation.md#2-installing-the-required-tools) guide for steps + +## Overview + +`gnokey` provides a way to create a transaction, sign it, and later +broadcast it to a chain in the most secure fashion. This approach, while more +complicated than the standard approach shown [in a previous tutorial](./state-changing-calls.md), +grants full control and provides [airgap](https://en.wikipedia.org/wiki/Air_gap_(networking)) +support. + +By separating the signing and the broadcasting steps of submitting a transaction, +users can make sure that the signing happens in a secure, offline environment, +keeping private keys away from possible exposure to attacks coming from the +internet. + +The intended purpose of this functionality is to provide maximum security when +signing and broadcasting a transaction. In practice, this procedure should take +place on two separate machines controlled by the holder of the keys, one with +access to the internet (`Machine A`), and the other one without (`Machine B`), +with the separation of steps as follows: +1. `Machine A`: Fetch account information from the chain +2. `Machine B`: Create an unsigned transaction locally +3. `Machine B`: Sign the transaction +4. `Machine A`: Broadcast the transaction + +## 1. Fetching account information from the chain + +First, we need to fetch data for the account we are using to sign the transaction, +using the [auth/accounts](./querying-a-network.md#authaccounts) query: + +```bash +gnokey query auth/accounts/ -remote "https://rpc.gno.land:443" +``` + +We need to extract the account number and sequence from the output: + +```bash +height: 0 +data: { + "BaseAccount": { + "address": "g1zzqd6phlfx0a809vhmykg5c6m44ap9756s7cjj", + "coins": "10000000ugnot", + "public_key": null, + "account_number": "468", + "sequence": "0" + } +} +``` + +In this case, the account number is `468`, and the sequence (nonce) is `0`. We +will need these values to sign the transaction later. These pieces of information +are crucial during the signing process, as they are included in the signature +of the transaction, preventing replay attacks. + +## 2. Creating an unsigned transaction locally + +To create the transaction you want, you can use the [`call` API](./state-changing-calls.md#call), +without the `-broadcast` flag, while redirecting the output to a local file: + +```bash +gnokey maketx call \ +-pkgpath "gno.land/r/demo/userbook" \ +-func "SignUp" \ +-gas-fee 1000000ugnot \ +-gas-wanted 2000000 \ +mykey > userbook.tx +``` + +This will create a `userbook.tx` file with a null `signature` field. +Now we are ready to sign the transaction. + +## 3. Signing the transaction + +To add a signature to the transaction, we can use the `gnokey sign` subcommand. +To sign, we must set the correct flags for the subcommand: +- `-tx-path` - path to the transaction file to sign, in our case, `userbook.tx` +- `-chainid` - id of the chain to sign for +- `-account-number` - number of the account fetched previously +- `-account-sequence` - sequence of the account fetched previously + +```bash +gnokey sign \ +-tx-path userbook.tx \ +-chainid "portal-loop" \ +-account-number 468 \ +-account-sequence 0 \ +mykey +``` + +After inputting the correct values, `gnokey` will ask for the password to decrypt +the keypair. Once we input the password, we should receive the message that the +signing was completed. If we open the `userbook.tx` file, we will be able to see +that the signature field has been populated. + +We are now ready to broadcast this transaction to the chain. + +## 4. Broadcasting the transaction + +To broadcast the signed transaction to the chain, we can use the `gnokey broadcast` +subcommand, giving it the path to the signed transaction: + +```bash +gnokey broadcast -remote "https://rpc.gno.land:443" userbook.tx +``` + +In this case, we do not need to specify a keypair, as the transaction has already +been signed in a previous step and `gnokey` is only sending it to the RPC endpoint. + +## Verifying a transaction's signature + +To verify a transaction's signature is correct, you can use the `gnokey verify` +subcommand. We can provide the path to the transaction document using the `-docpath` +flag, provide the key we signed the transaction with, and the signature itself. +Make sure the signature is in the `hex` format. + +```bash +gnokey verify -docpath userbook.tx mykey +``` + +## Conclusion + +That's it! 🎉 + +In this tutorial, you've learned to use `gnokey` for creating maximum-security +transactions in an airgapped manner. diff --git a/docs/gno-tooling/cli/gnokey/gnokey.md b/docs/gno-tooling/cli/gnokey/gnokey.md new file mode 100644 index 00000000000..7344f9b539c --- /dev/null +++ b/docs/gno-tooling/cli/gnokey/gnokey.md @@ -0,0 +1,16 @@ +--- +id: gnokey +--- + +# `gnokey` + +## Overview + +In this section, you will learn how to use the `gnokey` binary. `gnokey` is the +gno.land CLI keychain and client, and it allows you to do 4 main things: +- Manage Gno keypairs +- Send state-changing calls (transactions) +- Query a gno.land network +- Sign and broadcast transactions with [airgap protection](https://en.wikipedia.org/wiki/Air_gap_(networking)) + +Check out the rest of this section to learn how to do all of these. diff --git a/docs/gno-tooling/cli/gnokey/querying-a-network.md b/docs/gno-tooling/cli/gnokey/querying-a-network.md new file mode 100644 index 00000000000..1bb1bb8275f --- /dev/null +++ b/docs/gno-tooling/cli/gnokey/querying-a-network.md @@ -0,0 +1,233 @@ +--- +id: querying-a-network +--- + +# Querying a gno.land network + +## Prerequisites + +- **`gnokey` installed.** Reference the + [Local Setup](../../../getting-started/local-setup/installation.md#2-installing-the-required-tools) guide for steps + +## Overview + +gno.land and `gnokey` support ABCI queries. Using ABCI queries, you can query the state of +a gno.land network without spending any gas. All queries need to be pointed towards +a specific remote address from which the state will be retrieved. + +To send ABCI queries, you can use the `gnokey query` subcommand, and provide it +with the appropriate query. The `query` subcommand allows us to send different +types of queries to a gno.land network. + +Below is a list of queries a user can make with `gnokey`: +- `auth/accounts/{ADDRESS}` - returns information about an account +- `bank/balances/{ADDRESS}` - returns balances of an account +- `vm/qfuncs` - returns the exported functions for a given pkgpath +- `vm/qfile` - returns package contents for a given pkgpath +- `vm/qeval` - evaluates an expression in read-only mode on and returns the results +- `vm/qrender` - shorthand for evaluating `vm/qeval Render("")` for a given pkgpath + +Let's see how we can use them. + +## `auth/accounts` + +We can obtain information about a specific address using this subquery. To call it, +we can run the following command: + +```bash +gnokey query auth/accounts/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -remote https://rpc.gno.land:443 +``` + +With this, we are asking the Portal Loop network to deliver information about the +specified address. If everything went correctly, we should get output similar to the following: + +```bash +height: 0 +data: { + "BaseAccount": { + "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "coins": "227984898927ugnot", + "public_key": { + "@type": "/tm.PubKeySecp256k1", + "value": "A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y" + }, + "account_number": "0", + "sequence": "12" + } +} +``` + +The return data will contain the following fields: +- `height` - the height at which the query was executed. This is currently not + supported and is `0` by default. +- `data` - contains the result of the query. + +The `data` field returns a `BaseAccount`, which is the main struct used in [TM2](../../../concepts/tendermint2.md) +to hold account data. It contains the following information: +- `address` - the address of the account +- `coins` - the list of coins the account owns +- `public_key` - the TM2 public key of the account, from which the address is derived +- `account_number` - a unique identifier for the account on the gno.land chain +- `sequence` - a nonce, used for protection against replay attacks + +## `bank/balances` + +With this query, we can fetch [coin](../../../concepts/stdlibs/coin.md) balances +of a specific account. To call it, we can run the following command: + +```bash +gnokey query bank/balances/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -remote https://rpc.gno.land:443 +``` + +If everything went correctly, we should get an output similar to the following: + +```bash +height: 0 +data: "227984898927ugnot" +``` + +The data field will contain the coins the address owns. + +## `vm/qfuncs` + +Using the `vm/qfuncs` query, we can fetch exported functions from a specific package +path. To specify the path we want to query, we can use the `-data` flag: + +```bash +gnokey query vm/qfuncs --data "gno.land/r/demo/wugnot" -remote https://rpc.gno.land:443 +``` + +The output is a string containing all exported functions for the `wugnot` realm: + +```json +height: 0 +data: [ + { + "FuncName": "Deposit", + "Params": null, + "Results": null + }, + { + "FuncName": "Withdraw", + "Params": [ + { + "Name": "amount", + "Type": "uint64", + "Value": "" + } + ], + "Results": null + }, + // other functions +] +``` + +## `vm/qfile` + +With the `vm/qfile` query, we can fetch files and their content found on a +specific package path. To specify the path we want to query, we can use the +`-data` flag: + +```bash +gnokey query vm/qfile -data "gno.land/r/demo/wugnot" -remote https://rpc.gno.land:443 +``` + +If the `-data` field contains only the package path, the output is a list of all +files found within the `wugnot` realm: + +```bash +height: 0 +data: gno.mod +wugnot.gno +z0_filetest.gno +``` + +If the `-data` field also specifies a file name after the path, the source code +of the file will be retrieved: + +```bash +gnokey query vm/qfile -data "gno.land/r/demo/wugnot/wugnot.gno" -remote https://rpc.gno.land:443 +``` + +Output: +```bash +height: 0 +data: package wugnot + +import ( + "std" + "strings" + + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/ufmt" + pusers "gno.land/p/demo/users" + "gno.land/r/demo/users" +) + +var ( + banker *grc20.Banker = grc20.NewBanker("wrapped GNOT", "wugnot", 0) + Token = banker.Token() +) + +const ( + ugnotMinDeposit uint64 = 1000 + wugnotMinDeposit uint64 = 1 +) +... +``` + +## `vm/qeval` + +`vm/qeval` allows us to evaluate a call to an exported function without using gas, +in read-only mode. For example: + +```bash +gnokey query vm/qeval -remote https://rpc.gno.land:443 -data "gno.land/r/demo/wugnot.BalanceOf(\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\")" +``` + +This command will return the `wugnot` balance of the above address without using gas. +Properly escaping quotation marks for string arguments is currently required. + +Currently, `vm/qeval` only supports primitive types in expressions. + +## `vm/qrender` + +`vm/qrender` is an alias for executing `vm/qeval` on the `Render("")` function. +We can use it like this: + +```bash +gnokey query vm/qrender --data "gno.land/r/demo/wugnot:" -remote https://rpc.gno.land:443 +``` + +Running this command will display the current `Render()` output of the WUGNOT +realm, which is also displayed by default on the [realm's page](https://gno.land/r/demo/wugnot): + +```bash +height: 0 +data: # wrapped GNOT ($wugnot) + +* **Decimals**: 0 +* **Total supply**: 5012404 +* **Known accounts**: 2 +``` + +:::info Specifying a path to `Render()` + +To call the `vm/qrender` query with a specific path, use the `:` syntax. +For example, the `wugnot` realm provides a way to display the balance of a specific +address in its `Render()` function. We can fetch the balance of an account by +providing the following custom pattern to the `wugnot` realm: + +```bash +gnokey query vm/qrender --data "gno.land/r/demo/wugnot:balance/g125em6arxsnj49vx35f0n0z34putv5ty3376fg5" -remote https://rpc.gno.land:443 +``` + +To see how this was achieved, check out `wugnot`'s `Render()` function. +::: + +## Conclusion + +That's it! 🎉 + +In this tutorial, you've learned to use `gnokey` to query a gno.land +network. diff --git a/docs/gno-tooling/cli/gnokey/state-changing-calls.md b/docs/gno-tooling/cli/gnokey/state-changing-calls.md new file mode 100644 index 00000000000..79a777cca51 --- /dev/null +++ b/docs/gno-tooling/cli/gnokey/state-changing-calls.md @@ -0,0 +1,466 @@ +--- +id: state-changing-calls +--- + +# Making state-changing calls (transactions) + +## Prerequisites + +- **`gnokey` installed.** Reference the + [Local Setup](../../../getting-started/local-setup/installation.md#2-installing-the-required-tools) guide for steps + +## Overview + +In Gno, there are four types of messages that can change on-chain state: +- `AddPackage` - adds new code to the chain +- `Call` - calls a specific path and function on the chain +- `Send` - sends coins from one address to another +- `Run` - executes a Gno script against on-chain code + +A gno.land transaction contains two main things: +- A base configuration where variables such as `gas-fee`, `gas-wanted`, and others + are defined +- A list of messages to execute on the chain + +Currently, `gnokey` supports single-message transactions, while multiple-message +transactions can be created in Go programs, supported by the +[gnoclient](../../../reference/gnoclient/gnoclient.md) package. + +We will need some testnet coins (GNOTs) for each state-changing call. Visit the [Faucet +Hub](https://faucet.gno.land) to get GNOTs for the Gno testnets that are currently live. + +Let's delve deeper into each of these message types. + +## `AddPackage` + +In case you want to upload new code to the chain, you can use the `AddPackage` +message type. You can send an `AddPackage` transaction with `gnokey` using the +following command: + +```bash +gnokey maketx addpkg +``` + +To understand how to use this subcommand better, let's write a simple "Hello world" +[pure package](../../../concepts/packages.md). First, let's create a folder which will +store our example code. + +```bash +└── example/ +``` + +Then, let's create a `hello_world.gno` file under the `p/` folder: + +```bash +cd example +mkdir p/ && cd p +touch hello_world.gno +``` + +Now, we should have the following folder structure: + +```bash +└── example/ +│ └── p/ +│ └── hello_world.gno +``` + +In the `hello_world.gno` file, add the following code: + +```go +package hello_world + +func Hello() string { + return "Hello, world!" +} +``` + +We are now ready to upload this package to the chain. To do this, we must set the +correct flags for the `addpkg` subcommand. + +The `addpkg` subcommmand uses the following flags and arguments: +- `-pkgpath` - on-chain path where your code will be uploaded to +- `-pkgdir` - local path where your is located +- `-broadcast` - enables broadcasting the transaction to the chain +- `-send` - a deposit amount of GNOT to send along with the transaction +- `-gas-wanted` - the upper limit for units of gas for the execution of the + transaction +- `-gas-fee` - amount of GNOTs to pay per gas unit +- `-chain-id` - id of the chain that we are sending the transaction to +- `-remote` - specifies the remote node RPC listener address + +The `-pkgpath` and `-pkgdir` flags are unique to the `addpkg` subcommand, while +`-broadcast`,`-send`, `-gas-wanted`, `-gas-fee`, `-chain-id`, and `-remote` are +used for setting the base transaction configuration. These flags will be repeated +throughout the tutorial. + +Next, let's configure the `addpkg` subcommand to publish this package to the +[Portal Loop](../../../concepts/portal-loop.md) testnet. Assuming we are in +the `example/p/` folder, the command will look like this: + +```bash +gnokey maketx addpkg \ +-pkgpath "gno.land/p//hello_world" \ +-pkgdir "." \ +-send "" \ +-gas-fee 10000000ugnot \ +-gas-wanted 8000000 \ +-broadcast \ +-chainid portal-loop \ +-remote "https://rpc.gno.land:443" +``` + +Once we have added a desired [namespace](../../../concepts/namespaces.md) to upload the package to, we can specify +a keypair name to use to execute the transaction: + +```bash +gnokey maketx addpkg \ +-pkgpath "gno.land/p/examplenamespace/hello_world" \ +-pkgdir "." \ +-send "" \ +-gas-fee 10000000ugnot \ +-gas-wanted 200000 \ +-broadcast \ +-chainid portal-loop \ +-remote "https://rpc.gno.land:443" +mykey +``` + +If the transaction was successful, you will get output from `gnokey` that is similar to the following: + +``` +OK! +GAS WANTED: 200000 +GAS USED: 117564 +HEIGHT: 3990 +EVENTS: [] +TX HASH: Ni8Oq5dP0leoT/IRkKUKT18iTv8KLL3bH8OFZiV79kM= +``` + +Let's analyze the output, which is standard for any `gnokey` transaction: +- `GAS WANTED: 200000` - the original amount of gas specified for the transaction +- `GAS USED: 117564` - the gas used to execute the transaction +- `HEIGHT: 3990` - the block number at which the transaction was executed at +- `EVENTS: []` - [Gno events](../../../concepts/stdlibs/events.md) emitted by the transaction, in this case, none +- `TX HASH: Ni8Oq5dP0leoT/IRkKUKT18iTv8KLL3bH8OFZiV79kM=` - the hash of the transaction + +Congratulations! You have just uploaded a pure package to the Portal Loop network. +If you wish to deploy to a different network, find the list of all network +configurations in the [Network Configuration](../../../reference/network-config.md) section. + +## `Call` + +The `Call` message type is used to call any exported realm function. +You can send a `Call` transaction with `gnokey` using the following command: + +```bash +gnokey maketx call +``` + +:::info `Call` uses gas + +Using `Call` to call an exported function will use up gas, even if the function +does not modify on-chain state. If you are calling such a function, you can use +the [`query` functionality](./querying-a-network.md) for a read-only call which +does not use gas. + +::: + +For this example, we will call the `wugnot` realm, which wraps GNOTs to a +GRC20-compatible token called `wugnot`. We can find this realm deployed on the +[Portal Loop](../../../concepts/portal-loop.md) testnet, under the `gno.land/r/demo/wugnot` path. + +We will wrap `1000ugnot` into the equivalent in `wugnot`. To do this, we can call +the `Deposit()` function found in the `wugnot` realm. As previously, we will +configure the `maketx call` subcommand: + +```bash +gnokey maketx call \ +-pkgpath "gno.land/r/demo/wugnot" \ +-func "Deposit" \ +-send "1000ugnot" \ +-gas-fee 10000000ugnot \ +-gas-wanted 2000000 \ +-broadcast \ +-chainid portal-loop \ +-remote "https://rpc.gno.land:443" \ +mykey +``` + +In this command, we have specified three main things: +- The path where the realm lives on-chain with the `-pkgpath` flag +- The function that we want to call on the realm with the `-func` flag +- The amount of `ugnot` we want to send to be wrapped, using the `-send` flag + +Apart from this, we have also specified the Portal Loop chain ID, `portal-loop`, +as well as the Portal Loop remote address, `https://rpc.gno.land:443`. + +After running the command, we can expect an output similar to the following: +```bash +OK! +GAS WANTED: 2000000 +GAS USED: 489528 +HEIGHT: 24142 +EVENTS: [{"type":"Transfer","attrs":[{"key":"from","value":""},{"key":"to","value":"g125em6arxsnj49vx35f0n0z34putv5ty3376fg5"},{"key":"value","value":"1000"}],"pkg_path":"gno.land/r/demo/wugnot","func":"Mint"}] +TX HASH: Ni8Oq5dP0leoT/IRkKUKT18iTv8KLL3bH8OFZiV79kM= +``` + +In this case, we can see that the `Deposit()` function emitted an +[event](../../../concepts/stdlibs/events.md) that tells us more about what +happened during the transaction. + +After broadcasting the transaction, we can verify that we have the amount of `wugnot` we expect. We +can call the `BalanceOf()` function in the same realm: + +```bash +gnokey maketx call \ +-pkgpath "gno.land/r/demo/wugnot" \ +-func "BalanceOf" \ +-args "" \ +-gas-fee 10000000ugnot \ +-gas-wanted 2000000 \ +-broadcast \ +-chainid portal-loop \ +-remote "https://rpc.gno.land:443" \ +mykey +``` + +If everything was successful, we should get something similar to the following +output: + +``` +(1000 uint64) + +OK! +GAS WANTED: 2000000 +GAS USED: 396457 +HEIGHT: 64839 +EVENTS: [] +TX HASH: gQP9fJYrZMTK3GgRiio3/V35smzg/jJ62q7t4TLpdV4= +``` + +At the top, you will see the output of the transaction, specifying the value and +type of the return argument. + +In this case, we used `maketx call` to call a read-only function, which simply +checks the `wugnot` balance of a specific address. This is discouraged, as +`maketx call` actually uses gas. To call a read-only function without spending gas, +check out the `vm/qeval` query in the [Querying a network](./querying-a-network.md#vmqeval) section. + +## `Send` + +We can use the `Send` message type to access the TM2 [Banker](../../../concepts/stdlibs/banker.md) +directly and transfer coins from one Gno address to another. + +Coins, such as GNOTs, are always formatted in the following way: + +``` + +100ugnot +``` + +For this example, let's transfer some GNOTs. Just like before, we can configure +our `maketx send` subcommand: +```bash +gnokey maketx send \ +-to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 \ +-send 100ugnot \ +-gas-fee 10000000ugnot \ +-gas-wanted 2000000 \ +-broadcast \ +-chainid portal-loop \ +-remote "https://rpc.gno.land:443" \ +mykey +``` + +Here, we have set the `-to` & `-send` flags to match the recipient, in this case +the publicly-known `test1` address, and `100ugnot` for the coins we want to send, +respectively. + +To check the balance of a specific address, check out the `bank/balances` query +in the [Querying a network](./querying-a-network.md#bankbalances) section. + +## `Run` + +With the `Run` message, you can write a snippet of Gno code and run it against +code on the chain. For this example, we will use the [Userbook realm](https://gno.land/r/demo/userbook), +which simply allows you to register the fact that you have interacted with it. +It contains a simple `SignUp()` function, which we will call with `Run`. + +To understand how to use the `Run` message better, let's write a simple `script.gno` +file. First, create a folder which will store our script. + +```bash +└── example/ +``` + +Then, let's create a `script.gno` file: + +```bash +cd example +touch script.gno +``` + +Now, we should have the following folder structure: + +```bash +└── example/ +│ └── script.gno +``` + +In the `script.gno` file, first define the package to be `main`. Then we can import +the Userbook realm and define a `main()` function with no return values which will +be automatically detected and run. In it, we can call the `SignUp()` function. + +```go +package main + +import "gno.land/r/demo/userbook" + +func main() { + println(userbook.SignUp()) +} +``` + +Now we will be able to provide this to the `maketx run` subcommand: +```bash +gnokey maketx run \ +-gas-fee 1000000ugnot \ +-gas-wanted 20000000 \ +-broadcast \ +-chainid portal-loop \ +-remote "https://rpc.gno.land:443" \ +mykey ./script.gno +``` + +After running this command, the chain will execute the script and apply any state +changes. Additionally, by using `println`, which is only available in the `Run` +& testing context, we will be able to see the return value of the function called. + +### The power of `Run` + +Specifically, the above example could have been replaced with a simple `maketx call` +call. The full potential of run comes out in three specific cases: +1. Calling realm functions multiple times in a loop +2. Calling functions with non-primitive input arguments +3. Calling methods on exported variables + +Let's look at each of these cases in detail. To demonstrate, we'll make a call +to the following example realm: + +```go +package foo + +import "gno.land/p/demo/ufmt" + +var ( + MainFoo *Foo + foos []*Foo +) + +type Foo struct { + bar string + baz int +} + +func init() { + MainFoo = &Foo{bar: "mainBar", baz: 0} +} + +func (f *Foo) String() string { + return ufmt.Sprintf("Foo - (bar: %s) - (baz: %d)\n\n", f.bar, f.baz) +} + +func NewFoo(bar string, baz int) *Foo { + return &Foo{bar: bar, baz: baz} +} + +func AddFoos(multipleFoos []*Foo) { + foos = append(foos, multipleFoos...) +} + +func Render(_ string) string { + var output string + + for _, f := range foos { + output += f.String() + } + + return output +} +``` + +This realm is deployed to [`gno.land/r/docs/examples/run/foo`](https://gno.land/r/docs/examples/run/foo/package.gno) +on the Portal Loop testnet. + +1. Calling realm functions multiple times in a loop: +```go +package main + +import ( + "gno.land/r/docs/examples/run/foo" +) + +func main() { + for i := 0; i < 5; i++ { + println(foo.Render("")) + } +} +``` + +2. Calling functions with non-primitive input arguments: + +Currently, `Call` only supports primitives for arguments. With `Run`, these +limitations are removed; we can execute a function that takes in a struct, array, +or even an array of structs. + +We are unable to call `AddFoos()` with the `Call` message type, while with `Run`, +we can: + +```go +package main + +import ( + "strconv" + + "gno.land/r/docs/examples/run/foo" +) + +func main() { + var multipleFoos []*foo.Foo + + for i := 0; i < 5; i++ { + newFoo := foo.NewFoo( + "bar"+strconv.Itoa(i), + i, + ) + + multipleFoos = append(multipleFoos, newFoo) + } + + foo.AddFoos(multipleFoos) +} + +``` + +3. Calling methods on exported variables: + +```go +package main + +import "gno.land/r/docs/examples/run/foo" + +func main() { + println(foo.MainFoo.String()) +} +``` + +Finally, we can call methods that are on top-level objects in case they exist, +which is not currently possible with the `Call` message. + +## Conclusion + +That's it! 🎉 + +In this tutorial, you've learned to use `gnokey` for sending multiple types of +state-changing calls to a gno.land chain. diff --git a/docs/gno-tooling/cli/gnokey/working-with-key-pairs.md b/docs/gno-tooling/cli/gnokey/working-with-key-pairs.md new file mode 100644 index 00000000000..9bc29da6a18 --- /dev/null +++ b/docs/gno-tooling/cli/gnokey/working-with-key-pairs.md @@ -0,0 +1,220 @@ +--- +id: working-with-key-pairs +--- + +# Working with Key Pairs + +## Overview + +In this tutorial, you will learn how to manage private user keys, which are +required for interacting with the gno.land blockchain. You will understand what +mnemonics are, how they are used, and how you can make interaction seamless with +Gno. + +## Prerequisites + +- **`gnokey` installed.** Reference the + [Local Setup](../../../getting-started/local-setup/installation.md#2-installing-the-required-tools) guide for steps + +## Listing available keys +`gnokey` works by creating a local directory in the filesystem for storing +(encrypted!) user private keys. + +You can find this repository by checking the value of the `--home` flag when +running the following command: + +```bash +gnokey --help +``` + +Example output: + +```bash +USAGE + [flags] [...] + +gno.land keychain & client + +SUBCOMMANDS + add adds key to the keybase + delete deletes a key from the keybase + rotate rotate the password of a key in the keybase to a new password + generate generates a bip39 mnemonic + export exports private key armor + import imports encrypted private key armor + list lists all keys in the keybase + sign signs the given tx document and saves it to disk + verify verifies the document signature + query makes an ABCI query + broadcast broadcasts a signed document + maketx composes a tx document to sign + +FLAGS + -config ... config file (optional) + -home $XDG_CONFIG/gno home directory + -insecure-password-stdin=false WARNING! take password from stdin + -quiet=false suppress output during execution + -remote 127.0.0.1:26657 remote node URL +``` + +In this example, the directory where `gnokey` will store working data +is `/Users/zmilos/Library/Application Support/gno`. + +Keep note of this directory, in case you need to reset the keystore, or migrate +it for some reason. +You can provide a specific `gnokey` working directory using the `--home` flag. + +To list keys currently present in the keystore, we can run: + +```bash +gnokey list +``` + +In case there are no keys present in the keystore, the command will simply +return an empty response. +Otherwise, it will return the list of keys and their accompanying metadata as a +list, for example: + +```bash +0. Manfred (local) - addr: g15uk9d6feap7z078ttcnwc94k60ullrvhmynxjt pub: gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqvn87u43scec4zfgn4la3nt237nehzydzayqxe43fx63lq6rty9c5almet4, path: +1. Milos (local) - addr: g15lppu0tuxets0c0t80tncs4enqzgxt7v4eftcj pub: gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pqw2kkzujprgrfg7vumg85mccsf790n5ep6htpygkuwedwuumf2g7ydm4vqf, path: +``` + +The key response consists of a few pieces of information: + +- The name of the private key +- The derived address (`addr`) +- The public key (`pub`) + +Using these pieces of information, we can interact with gno.land tools and write +blockchain applications. + +## Generating a BIP39 mnemonic + +Using `gnokey`, we can generate a [mnemonic phrase](https://en.bitcoin.it/wiki/Seed_phrase) based on +the [BIP39 standard](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki). + +To generate the mnemonic phrase in the console, you can run: + +```bash +gnokey generate +``` + +![gnokey generate](../../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-generate.gif) + +## Adding a random private key +If we wanted to add a new private key to the keystore, we can run the following +command: + +```bash +gnokey add MyKey +``` + +Of course, you can replace `MyKey` with whatever name you want for your key. + +The `gnokey` tool will prompt you to enter a password to encrypt the key on disk +(don't forget this!). +After you enter the password, the `gnokey` tool will add the key to the keystore, +and return the accompanying [mnemonic phrase](https://en.bitcoin.it/wiki/Seed_phrase), which you should remember +somewhere if you want to recover the key at a future point in time. + +![gnokey add random](../../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-add-random.gif) + +You can check that the key was indeed added to the keystore, by listing available +keys: + +```bash +gnokey list +``` + +![gnokey list](../../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-list.gif) + +## Adding a private key using a mnemonic +To add a private key to the `gnokey` keystore [using an existing mnemonic](#generating-a-bip39-mnemonic), +we can run the following command with the +`--recover` flag: + +```bash +gnokey add --recover MyKey +``` + +Of course, you can replace `MyKey` with whatever name you want for your key. + +By following the prompts to encrypt the key on disk, and providing a BIP39 +mnemonic, we can successfully add the key to the keystore. + +![gnokey add mnemonic](../../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-add-mnemonic.gif) + +## Deleting a private key +To delete a private key from the `gnokey` keystore, we need to know the name or +address of the key to remove. +After we have this information, we can run the following command: + +```bash +gnokey delete MyKey +``` + +After entering the key decryption password, the key will be deleted from the keystore. + +:::caution Recovering a private key +In case you delete or lose access to your private key in the `gnokey` keystore, +you can recover it using the key's mnemonic, or by importing it if it was exported +at a previous point in time. +::: + + +## Rotating the password of a private key to a new password +To rotate the password of a private key from the `gnokey` keystore to a new password, we need to know the name or +address of the key to remove. +After we have this information, we can run the following command: + +```bash +gnokey rotate MyKey +``` + +After entering the current key decryption password and the new password, the password of the key will be updated in the keystore. + +## Exporting a private key +Private keys stored in the `gnokey` keystore can be exported to a desired place +on the user's filesystem. + +Keys are exported in their original armor, encrypted or unencrypted. + +To export a key from the keystore, you can run: + +```bash +gnokey export -key MyKey -output-path ~/Work/gno-key.asc +``` + +Follow the prompts presented in the terminal. Namely, you will be asked to +decrypt the key in the keystore, and later to encrypt the armor file on disk. +It is worth noting that you can also export unencrypted key armor, using the `--unsafe` flag. + +![gnokey export](../../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-export.gif) + +## Importing a private key +If you have an exported private key file, you can import it into `gnokey` fairly +easily. + +For example, if the key is exported at `~/Work/gno-key.asc`, you can run the +following command: + +```bash +gnokey import -armor-path ~/Work/gno-key.asc -name ImportedKey +``` + +You will be asked to decrypt the encrypted private key armor on disk +(if it is encrypted, if not, use the `--unsafe` flag), and then to provide an +encryption password for storing the key in the keystore. + +After executing the previous command, the `gnokey` keystore will have imported +`ImportedKey`. + +![gnokey import](../../../assets/getting-started/local-setup/creating-a-key-pair/gnokey-import.gif) + +## Conclusion + +That's it! 🎉 + +In this tutorial, you've learned to use `gnokey` for managing Gno keypairs. + diff --git a/docs/gno-tooling/cli/gnoland.md b/docs/gno-tooling/cli/gnoland.md index 18175871d90..037a1f19d03 100644 --- a/docs/gno-tooling/cli/gnoland.md +++ b/docs/gno-tooling/cli/gnoland.md @@ -29,164 +29,6 @@ Starts the Gnoland blockchain node, with accompanying setup. | `log-level` | String | The log level for the gnoland node. (default: `debug`) | | `skip-failing-genesis-txs` | Boolean | Doesn’t panic when replaying invalid genesis txs. When starting a production-level chain, it is recommended to set this value to `true` to monitor and analyze failing transactions. (default: `false`) | -### gnoland genesis \ [flags] [\...] - -Gno `genesis.json` manipulation suite for managing genesis parameters. - -#### SUBCOMMANDS - -| Name | Description | -|-------------|---------------------------------------------| -| `generate` | Generates a fresh `genesis.json`. | -| `validator` | Validator set management in `genesis.json`. | -| `verify` | Verifies a `genesis.json`. | -| `balances` | Manages `genesis.json` account balances. | -| `txs` | Manages the initial genesis transactions. | - -### gnoland genesis generate [flags] - -Generates a node's `genesis.json` based on specified parameters. - -#### FLAGS - -| Name | Type | Description | -|------------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `block-max-data-bytes` | Int | The max size of the block data.(default: `2000000`) | -| `block-max-gas` | Int | The max gas limit for the block. (default: `100000000`) | -| `block-max-tx-bytes` | Int | The max size of the block transaction. (default: `1000000`) | -| `block-time-itoa` | Int | The block time itoa (in ms). (default: `100`) | -| `chain-id` | String | The ID of the chain. (default: `dev`) | -| `genesis-time` | Int | The genesis creation time. (default: `utc now timestamp`) | -| `output-path` : | String | The output path for the `genesis.json`. If the genesis-time of the Genesis File is set to a future time, the chain will automatically start at that time if the node is online. (default: `./genesis.json`) | - -### gnoland genesis validator \ [flags] - -Manipulates the `genesis.json` validator set. - -#### SUBCOMANDS - -| Name | Description | -|----------|----------------------------------------------| -| `add` | Adds a new validator to the `genesis.json`. | -| `remove` | Removes a validator from the `genesis.json`. | - -#### FLAGS - -| Name | Type | Description | -|----------------|--------|------------------------------------------------------------| -| `address` | String | The gno bech32 address of the validator. | -| `genesis-path` | String | The path to the `genesis.json`. (default `./genesis.json`) | - -### gnoland genesis validator add [flags] - -Adds a new validator to the `genesis.json`. - -#### FLAGS - -| Name | Type | Description | -|----------------|--------|-----------------------------------------------------------------| -| `address` | String | The gno bech32 address of the validator. | -| `genesis-path` | String | The path to the `genesis.json`. (default: `./genesis.json`) | -| `name` | String | The name of the validator (must be unique). | -| `power` | Uint | The voting power of the validator (must be > 0). (default: `1`) | -| `pub-key` | String | The bech32 string representation of the validator's public key. | - -```bash -gnoland genesis validator add \ --address g1rzuwh5frve732k4futyw45y78rzuty4626zy6h \ --name test1 \ --pub-key gpub1pggj7ard9eg82cjtv4u52epjx56nzwgjyg9zplmcmggxyxyrch0zcyg684yxmerullv3l6hmau58sk4eyxskmny9h7lsnz - -Validator with address g1rzuwh5frve732k4futyw45y78rzuty4626zy6h added to genesis file -``` - -### gnoland genesis validator remove [flags] - -Removes a validator from the `genesis.json`. - -#### FLAGS - -| Name | Type | Description | -|----------------|--------|-------------------------------------------------------------| -| `address` | String | The gno bech32 address of the validator. | -| `genesis-path` | String | The path to the `genesis.json`. (default: `./genesis.json)` | - -```bash -gnoland genesis validator remove \ --address g1rzuwh5frve732k4futyw45y78rzuty4626zy6h - -Validator with address g1rzuwh5frve732k4futyw45y78rzuty4626zy6h removed from genesis file -``` - -### gnoland genesis verify \ [flags] [\…] - -Verifies a `genesis.json`. - -#### FLAGS - -| Name | Type | Description | -|----------------|--------|-----------------------------------------------------------| -| `genesis-path` | String | The path to the `genesis.json`. (default: `genesis.json`) | - -### gnoland genesis balances \ [flags] [\…] - -Manages `genesis.json` account balances. - -#### SUBCOMMANDS - -| Name | Description | -|----------|--------------------------------------------------------| -| `add` | Adds the balance information. | -| `remove` | Removes the balance information of a specific account. | - -### gnoland genesis balances add [flags] - -#### FLAGS - -| Name | Type | Description | -|-----------------|--------|--------------------------------------------------------------------------------------------| -| `balance-sheet` | String | The path to the balance file containing addresses in the format `
=ugnot`. | -| `genesis-path` | String | The path to the `genesis.json` (default: `./genesis.json`) | -| `parse-export` | String | The path to the transaction export containing a list of transactions (JSONL). | -| `single` | String | The direct balance addition in the format `
=ugnot`. | - -```bash -gnoland genesis balances add \ --single g1rzuwh5frve732k4futyw45y78rzuty4626zy6h=100ugnot - -1 pre-mines saved - -g1rzuwh5frve732k4futyw45y78rzuty4626zy6h:{[24 184 235 209 35 102 125 21 90 169 226 200 234 208 158 56 197 197 146 186] [{%!d(string=ugnot) 100}]}ugnot -``` - -### gnoland balances remove [flags] - -#### FLAGS - -| Name | Type | Description | -|----------------|--------|---------------------------------------------------------------------------------------------| -| `address` | String | The address of the account whose balance information should be removed from `genesis.json`. | -| `genesis-path` | String | The path to the `genesis.json`. (default: `./genesis.json`) | - -```bash -gnoland genesis balances remove \ --address=g1rzuwh5frve732k4futyw45y78rzuty4626zy6h - -Pre-mine information for address g1rzuwh5frve732k4futyw45y78rzuty4626zy6h removed -``` - -### gnoland txs \ [flags] [\…] - -Manages genesis transactions through input files. - -#### SUBCOMMANDS - -| Name | Description | -|----------|---------------------------------------------------| -| `add` | Imports transactions into the `genesis.json`. | -| `remove` | Removes the transactions from the `genesis.json`. | -| `export` | Exports the transactions from the `genesis.json`. | - ### gnoland secrets \ [flags] [\…] The gno secrets manipulation suite for managing the validator key, p2p key and diff --git a/docs/how-to-guides/connecting-from-go.md b/docs/how-to-guides/connecting-from-go.md index 6f05a891cd2..1c0478234fc 100644 --- a/docs/how-to-guides/connecting-from-go.md +++ b/docs/how-to-guides/connecting-from-go.md @@ -2,7 +2,7 @@ id: connect-from-go --- -# How to connect a Go app to gno.land +# How to connect a Go app to gno.land This guide will show you how to connect to a gno.land network from your Go application, using the [gnoclient](../reference/gnoclient/gnoclient.md) package. @@ -15,7 +15,7 @@ For this guide, we will build a small Go app that will: ## Prerequisites - A local gno.land keypair generated using -[gnokey](../getting-started/local-setup/working-with-key-pairs.md) +[gnokey](../gno-tooling/cli/gnokey/working-with-key-pairs.md) ## Setup @@ -46,7 +46,7 @@ go get github.com/gnolang/gno/gno.land/pkg/gnoclient ## Main components -The `gnoclient` package exposes a `Client` struct containing a `Signer` and +The `gnoclient` package exposes a `Client` struct containing a `Signer` and `RPCClient` connector. `Client` exposes all available functionality for talking to a gno.land chain. @@ -60,11 +60,11 @@ type Client struct { ### Signer The `Signer` provides functionality to sign transactions with a gno.land keypair. -The keypair can be accessed from a local keybase, or it can be generated +The keypair can be accessed from a local keybase, or it can be generated in-memory from a BIP39 mnemonic. :::info -The keybase directory path is set with the `gnokey --home` flag. +The keybase directory path is set with the `gnokey --home` flag. ::: ### RPCClient @@ -74,7 +74,7 @@ The `RPCCLient` provides connectivity to a gno.land network via HTTP or WebSocke ## Initialize the Signer -For this example, we will initialize the `Signer` from a local keybase: +For this example, we will initialize the `Signer` from a local keybase: ```go package main @@ -92,14 +92,14 @@ func main() { signer := gnoclient.SignerFromKeybase{ Keybase: keybase, Account: "", // Name of your keypair in keybase - Password: "", // Password to decrypt your keypair + Password: "", // Password to decrypt your keypair ChainID: "", // id of gno.land chain } } ``` A few things to note: -- You can view keys in your local keybase by running `gnokey list`. +- You can view keys in your local keybase by running `gnokey list`. - You can get the password from a user input using the IO package. - `Signer` can also be initialized in-memory from a BIP39 mnemonic, using the [`SignerFromBip39`](https://gnolang.github.io/gno/github.com/gnolang/gno@v0.0.0/gno.land/pkg/gnoclient.html#SignerFromBip39) @@ -116,10 +116,10 @@ if err != nil { } ``` -A list of gno.land network endpoints & chain IDs can be found in the +A list of gno.land network endpoints & chain IDs can be found in the [Gno RPC endpoints](../reference/network-config.md) page. -With this, we can initialize the `gnoclient.Client` struct: +With this, we can initialize the `gnoclient.Client` struct: ```go package main @@ -138,7 +138,7 @@ func main() { signer := gnoclient.SignerFromKeybase{ Keybase: keybase, Account: "", // Name of your keypair in keybase - Password: "", // Password to decrypt your keypair + Password: "", // Password to decrypt your keypair ChainID: "", // id of gno.land chain } @@ -147,7 +147,7 @@ func main() { if err != nil { panic(err) } - + // Initialize the gnoclient client := gnoclient.Client{ Signer: signer, @@ -161,7 +161,7 @@ We can now communicate with the gno.land chain. Let's explore some of the functi ## Query account info from a chain -To send transactions to the chain, we need to know the account number (ID) and +To send transactions to the chain, we need to know the account number (ID) and sequence (nonce). We can get this information by querying the chain with the `QueryAccount` function: @@ -219,7 +219,7 @@ txCfg := gnoclient.BaseTxCfg{ ``` For calling an exported (public) function in a Gno realm, we can use the `MsgCall` -message type. We will use the wrapped ugnot realm for this example, wrapping +message type. We will use the wrapped ugnot realm for this example, wrapping `1000000ugnot` (1 $GNOT) for demonstration purposes. ```go @@ -250,11 +250,11 @@ if err != nil { } ``` -Before running your code, make sure your keypair has enough funds to send the -transaction. +Before running your code, make sure your keypair has enough funds to send the +transaction. -If everything went well, you've just sent a state-changing transaction to a -Gno.land chain! +If everything went well, you've just sent a state-changing transaction to a +gno.land chain! ## Reading on-chain state @@ -288,9 +288,7 @@ Congratulations 🎉 You've just built a small demo app in Go that connects to a gno.land chain to query account info, send a transaction, and read on-chain state. -Check out the full example app code [here](https://github.com/leohhhn/connect-gno/blob/master/main.go). +Check out the full example app code [here](https://github.com/leohhhn/connect-gno/blob/master/main.go). To see a real-world example CLI tool use `gnoclient`, check out [gnoblog-cli](https://github.com/gnolang/blog/tree/main/cmd/gnoblog-cli). - - diff --git a/docs/how-to-guides/deploy.md b/docs/how-to-guides/deploy.md index 620a5664f7c..1e27ccd0cad 100644 --- a/docs/how-to-guides/deploy.md +++ b/docs/how-to-guides/deploy.md @@ -129,7 +129,7 @@ given in the `--gas-wanted` flag to cover the deployment cost. Regardless of whether you're deploying a realm or a package, you will be using `gnokey`'s `maketx addpkg` - the usage of `maketx addpkg` in both cases is identical. To read more about the `maketx addpkg` -subcommand, view the `gnokey` [reference](../gno-tooling/cli/gnokey.md#addpkg). +subcommand, view the `gnokey` [reference](../gno-tooling/cli/gnokey/state-changing-calls.md#addpackage). ::: diff --git a/docs/overview.md b/docs/overview.md index 3619e507dba..a687c878dde 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,7 +1,7 @@ --- id: overview slug: / -description: "Gno.land is a Layer 1 blockchain platform that enables the execution of Smart Contracts using an interpreted +description: "gno.land is a Layer 1 blockchain platform that enables the execution of Smart Contracts using an interpreted version of the Go programming language called Gno." --- @@ -9,17 +9,17 @@ version of the Go programming language called Gno." ## What is gno.land? -Gno.land is a Layer 1 blockchain platform that enables the execution of Smart Contracts using an interpreted +gno.land is a Layer 1 blockchain platform that enables the execution of Smart Contracts using an interpreted version of the Go programming language called Gno. ### Key Features and Technology -1. **Interpreted Gno**: Gno.land utilizes the Gno programming language, which is based on Go. It is executed +1. **Interpreted Gno**: gno.land utilizes the Gno programming language, which is based on Go. It is executed through a specialized virtual machine called the GnoVM, purpose-built for blockchain development with built-in determinism and a modified standard library. While Gno shares similarities with Go in terms of syntax, it currently lacks go routine support. However, this feature is planned for future development, ensuring deterministic GnoVM executions. -2. **Consensus Protocol - Tendermint2**: Gno.land achieves consensus between blockchain nodes using the Tendermint2 +2. **Consensus Protocol - Tendermint2**: gno.land achieves consensus between blockchain nodes using the Tendermint2 consensus protocol. This approach ensures secure and reliable network operation. 3. **Inter-Blockchain Communication (IBC)**: In the future, gno.land will be able to communicate and exchange data with other blockchain networks within the Cosmos ecosystem through the Inter-Blockchain Communication (IBC) protocol. @@ -37,19 +37,19 @@ The decision to base gno.land's language on Go was influenced by the following f In comparison to Ethereum, gno.land offers distinct advantages: -1. **Transparent and Auditable Smart Contracts**: Gno.land Smart Contracts are fully transparent and auditable by users +1. **Transparent and Auditable Smart Contracts**: gno.land Smart Contracts are fully transparent and auditable by users because the actual source code is uploaded to the blockchain. In contrast, Ethereum requires contracts to be precompiled into bytecode, leading to less transparency as bytecode is stored on the blockchain, not the human-readable source code. Smart contracts in gno.land can be used as libraries with a simple import statement, making gno.land a defacto source-code repository for the ecosystem. -2. **General-Purpose Language**: Gno.land's Gno is a general-purpose language, similar to Go, extending its +2. **General-Purpose Language**: gno.land's Gno is a general-purpose language, similar to Go, extending its usability beyond the context of blockchain. In contrast, Solidity is designed specifically for Smart Contracts on the Ethereum platform. ## Using the gno.land Documentation -Gno.land's documentation adopts the [Diataxis](https://diataxis.fr/) framework, ensuring structured and predictable content. It includes: +gno.land's documentation adopts the [Diataxis](https://diataxis.fr/) framework, ensuring structured and predictable content. It includes: - A [Getting Started](getting-started/local-setup/local-setup.md) section, covering simple instructions on how to begin your journey into gno.land. - Concise how-to guides for specific technical tasks. - Conceptual explanations, offering context and usage insights. diff --git a/docs/reference/gno-js-client/gno-provider.md b/docs/reference/gno-js-client/gno-provider.md index df808106cc3..c76bfebfe31 100644 --- a/docs/reference/gno-js-client/gno-provider.md +++ b/docs/reference/gno-js-client/gno-provider.md @@ -7,6 +7,38 @@ id: gno-js-provider The `Gno Provider` is an extension on the `tm2-js-client` `Provider`, outlined [here](../tm2-js-client/Provider/provider.md). Both JSON-RPC and WS providers are included with the package. +## Instantiation + +### new GnoWSProvider + +Creates a new instance of the Gno WebSocket Provider, based on [`tm2-js-client` `WSProvider`](../tm2-js-client/Provider/ws-provider.md). + +#### Parameters + +Same as [`tm2-js-client` `WSProvider`](../tm2-js-client/Provider/ws-provider.md). + +#### Usage + +```ts +new GnoWSProvider('ws://staging.gno.land:26657/ws'); +// provider with WS connection is created +``` + +### new GnoJSONRPCProvider + +Creates a new instance of the Gno JSON-RPC Provider, based on [`tm2-js-client` `JSONRPCProvider`](../tm2-js-client/Provider/json-rpc-provider.md). + +#### Parameters + +Same as [`tm2-js-client` `JSONRPCProvider`](../tm2-js-client/Provider/json-rpc-provider.md). + +#### Usage + +```ts +new GnoJSONRPCProvider('http://staging.gno.land:36657'); +// provider is created +``` + ## Realm Methods ### getRenderOutput @@ -116,7 +148,7 @@ Returns **Promise** #### Usage ```ts -await provider.getFileContent('gno.land/r/demo/foo20', 'TotalSupply()') +await provider.getFileContent('gno.land/r/demo/foo20') /* foo20.gno foo20_test.gno diff --git a/docs/reference/gnoclient/gnoclient.md b/docs/reference/gnoclient/gnoclient.md index 0c6c0d87308..672a3772bb7 100644 --- a/docs/reference/gnoclient/gnoclient.md +++ b/docs/reference/gnoclient/gnoclient.md @@ -14,7 +14,7 @@ APIs for common functionality. - Use local keystore to sign & broadcast transactions containing any type of Gno message - Sign & broadcast transactions with batch messages -- Use [ABCI queries](../../gno-tooling/cli/gnokey.md#make-an-abci-query) in +- Use [ABCI queries](../../gno-tooling/cli/gnokey/querying-a-network.md) in your Go code ## Installation @@ -30,5 +30,5 @@ To see the full reference documentation for the `gnoclient` package, we recommen visiting the [`gnoclient godoc page`](https://gnolang.github.io/gno/github.com/gnolang/gno@v0.0.0/gno.land/pkg/gnoclient.html). For a tutorial on how to use the `gnoclient` package, check out -["How to connect a Go app to gno.land"](../../how-to-guides/connecting-from-go.md) +["How to connect a Go app to gno.land"](../../how-to-guides/connecting-from-go.md). diff --git a/docs/reference/go-gno-compatibility.md b/docs/reference/go-gno-compatibility.md index a2f83f2bbc6..9f9d611e4fd 100644 --- a/docs/reference/go-gno-compatibility.md +++ b/docs/reference/go-gno-compatibility.md @@ -184,7 +184,7 @@ Legend: | hash/crc64 | `todo` | | hash/fnv | `todo` | | hash/maphash | `todo` | -| html | `todo` | +| html | `full` | | html/template | `todo` | | image | `tbd` | | image/color | `tbd` | @@ -248,7 +248,7 @@ Legend: | runtime/trace | `gospec` | | slices | `gnics` | | sort | `part`[^6] | -| strconv | `part` | +| strconv | `full`[^10] | | strings | `full` | | sync | `tbd` | | sync/atomic | `tbd` | @@ -292,6 +292,8 @@ Legend: [^8]: `crypto/ed25519` is currently only implemented for `Verify`, which should still cover a majority of use cases. A full implementation is welcome. [^9]: `math/rand` in Gno ports over Go's `math/rand/v2`. +[^10]: `strconv` does not have the methods relating to types `complex64` and + `complex128`. ## Tooling (`gno` binary) @@ -301,9 +303,9 @@ Legend: | go build | gno transpile -gobuild | same intention, limited compatibility | | go clean | gno clean | same intention, limited compatibility | | go doc | gno doc | limited compatibility; see https://github.com/gnolang/gno/issues/522 | -| go env | | | +| go env | gno env | | | go fix | | | -| go fmt | | gofmt (& similar tools, like gofumpt) works on gno code. | +| go fmt | gno fmt | gofmt (& similar tools, like gofumpt) works on gno code. | | go generate | | | | go get | | see `gno mod download`. | | go help | gno $cmd --help | ie. `gno doc --help` | diff --git a/docs/reference/network-config.md b/docs/reference/network-config.md index 6d4fc9ea14a..45a56b772ae 100644 --- a/docs/reference/network-config.md +++ b/docs/reference/network-config.md @@ -4,12 +4,12 @@ id: network-config # Network configurations -| Network | RPC Endpoint | Chain ID | -|-------------|-----------------------------------|---------------| -| Portal Loop | https://rpc.gno.land:443 | `portal-loop` | -| Test4 | https://rpc.test4.gno.land:443 | `test4` | -| Test3 | https://rpc.test3.gno.land:443 | `test3` | -| Staging | https://rpc.staging.gno.land:443 | `staging` | +| Network | RPC Endpoint | Chain ID | +|-------------|----------------------------------|---------------| +| Portal Loop | https://rpc.gno.land:443 | `portal-loop` | +| Test5 | https://rpc.test5.gno.land:443 | `test5` | +| Test4 | https://rpc.test4.gno.land:443 | `test4` | +| Staging | https://rpc.staging.gno.land:443 | `staging` | ### WebSocket endpoints All networks follow the same pattern for websocket connections: diff --git a/docs/reference/stdlibs/std/banker.md b/docs/reference/stdlibs/std/banker.md index 71eb3709ea2..b60b55ee93b 100644 --- a/docs/reference/stdlibs/std/banker.md +++ b/docs/reference/stdlibs/std/banker.md @@ -38,6 +38,10 @@ Returns `Banker` of the specified type. ```go banker := std.GetBanker(std.) ``` + +:::info `Banker` methods expect qualified denomination of the coins. Read more [here](./realm.md#coindenom). +::: + --- ## GetCoins diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index 089de682cfd..6a1da6483fd 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -28,6 +28,18 @@ std.AssertOriginCall() ``` --- +## ChainDomain +```go +func ChainDomain() string +``` +Returns the chain domain. Currently only `gno.land` is supported. + +#### Usage +```go +domain := std.ChainDomain() // gno.land +``` +--- + ## Emit ```go func Emit(typ string, attrs ...string) @@ -49,7 +61,7 @@ Returns the chain ID. #### Usage ```go -chainID := std.GetChainID() // dev | test3 | main ... +chainID := std.GetChainID() // dev | test5 | main ... ``` --- @@ -150,3 +162,19 @@ Derives the Realm address from its `pkgpath` parameter. ```go realmAddr := std.DerivePkgAddr("gno.land/r/demo/tamagotchi") // g1a3tu874agjlkrpzt9x90xv3uzncapcn959yte4 ``` +--- + +## CoinDenom +```go +func CoinDenom(pkgPath, coinName string) string +``` +Composes a qualified denomination string from the realm's `pkgPath` and the provided coin name, e.g. `/gno.land/r/demo/blog:blgcoin`. This method should be used to get fully qualified denominations of coins when interacting with the `Banker` module. It can also be used as a method of the `Realm` object, Read more[here](./realm.md#coindenom). + +#### Parameters +- `pkgPath` **string** - package path of the realm +- `coinName` **string** - The coin name used to build the qualified denomination. Must start with a lowercase letter, followed by 2–15 lowercase letters or digits. + +#### Usage +```go +denom := std.CoinDenom("gno.land/r/demo/blog", "blgcoin") // /gno.land/r/demo/blog:blgcoin +``` diff --git a/docs/reference/stdlibs/std/realm.md b/docs/reference/stdlibs/std/realm.md index 0c99b7134ea..f69cd874c75 100644 --- a/docs/reference/stdlibs/std/realm.md +++ b/docs/reference/stdlibs/std/realm.md @@ -14,6 +14,7 @@ type Realm struct { func (r Realm) Addr() Address {...} func (r Realm) PkgPath() string {...} func (r Realm) IsUser() bool {...} +func (r Realm) CoinDenom(coinName string) string {...} ``` ## Addr @@ -39,3 +40,15 @@ Checks if the realm it was called upon is a user realm. ```go if r.IsUser() {...} ``` +--- +## CoinDenom +Composes a qualified denomination string from the realm's `pkgPath` and the provided coin name, e.g. `/gno.land/r/demo/blog:blgcoin`. This method should be used to get fully qualified denominations of coins when interacting with the `Banker` module. + +#### Parameters +- `coinName` **string** - The coin name used to build the qualified denomination. Must start with a lowercase letter, followed by 2–15 lowercase letters or digits. + +#### Usage +```go +// in "gno.land/r/gnoland/blog" +denom := r.CoinDenom("blgcoin") // /gno.land/r/gnoland/blog:blgcoin +``` diff --git a/docs/reference/stdlibs/std/testing.md b/docs/reference/stdlibs/std/testing.md index e3e87ea7262..8a95ecf7827 100644 --- a/docs/reference/stdlibs/std/testing.md +++ b/docs/reference/stdlibs/std/testing.md @@ -106,7 +106,7 @@ Should be used in combination with [`NewUserRealm`](#newuserrealm) & #### Usage ```go addr := std.Address("g1ecely4gjy0yl6s9kt409ll330q9hk2lj9ls3ec") -std.TestSetRealm(std.NewUserRealm("")) +std.TestSetRealm(std.NewUserRealm(addr)) // or std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/users")) ``` diff --git a/examples/Makefile b/examples/Makefile index 578b4faf15b..cdc73ee6b3a 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -45,7 +45,7 @@ test: .PHONY: lint lint: - go run ../gnovm/cmd/gno lint $(OFFICIAL_PACKAGES) + go run ../gnovm/cmd/gno lint -v $(OFFICIAL_PACKAGES) .PHONY: test.sync test.sync: diff --git a/examples/README.md b/examples/README.md index b112e564d13..758f0f586e5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,21 +1,37 @@ -# Gnolang examples +# Examples -This folder showcases Gnolang realms and library demos. These examples not only aid in engine testing but also provide a glimpse into the potential of Gnolang's capabilities. +This folder showcases example Gno realms (smart contracts) and pure packages (libraries). +These examples provide a glimpse into the potential of gno.land and the capabilities of Gno, +while also serving as a test suite for the GnoVM. -While sharing contracts here can enhance engine testing, it's not mandatory. If considering a separate repository for contracts, be aware that this might restrict the experience due to the continuous efforts around `gno mod` support. A key point to note is that the main repository cannot reference separate code, which might pose developmental challenges. +Pure packages and realms in this folder are pre-deployed to gno.land testnets, +making them readily available for on-chain use. However, **there is no guarantee +that the code is bug-free, so it should be used with caution and an understanding of potential risks.** -## Personal Realms & Shared Content - -**Prioritizing Shared Content:** As we expand our examples and use-cases, it's essential to prioritize shared content that benefits the broader community. These examples serve as a foundation and reference for all users. - -**Personal Realms Inclusion:** We're open to personal realms, but they must exemplify best practices and inspire others. To maintain our repository's organization, we may decline some realms. If so, consider uploading onchain and keeping source code separately. For higher acceptance odds, offer useful or original examples. +## Structure -**Recommended Approach:** -- Use `r/demo` and `p/demo` for generic examples and components that can be imported by others. These are meant to be easily referenced and utilized by the community. -- Personal realms are welcomed if they are easily maintainable with the Continuous Integration (CI) system. If a personal realm becomes cumbersome to maintain or doesn't align with the CI's checks, it might be relocated to a less prominent location or even removed. +This folder mimics the gno.land package path system; the "root" of the system is +the `gno.land` folder. Next, it branches out to `p/` and `r/`, which contain +pure packages and realms, respectively. -## Usage - -Our recommendation is to use the [gno](../gnovm/cmd/gno) utility to develop contracts locally before publishing them on-chain. This approach offers a faster and streamlined workflow, along with additional debugging features. Simply fork or create new contracts and refer to the Makefile. Once everything looks good locally, you can then publish it on a localnet or testnet. +## Personal Realms & Shared Content -For further guidance and insights, please refer to the [`awesome-gno` tutorials](https://github.com/gnolang/awesome-gno#tutorials). +**Prioritizing Shared Content:** As we expand our examples and use-cases, it's +essential to prioritize shared content that benefits the broader community. +These examples serve as a foundation and reference for all users. + +**Personal Realms & Pure Packages:** We welcome personal realms that +exemplify best practices and inspire others. To maintain the organization +of the monorepo, some submissions may be declined. If so, consider uploading +[permissionlessly](../docs/gno-tooling/cli/gnokey/state-changing-calls.md#addpackage) +and storing the source code in a separate repo. For higher +acceptance odds, offer useful and original examples. + +**Recommended Approach:** +- Use `r/demo` and `p/demo` for generic examples and components that can be + imported by others. These are meant to be easily referenced and utilized by the + community. +- Packages under personal namespaces, such as in [r/leon](./gno.land/r/leon), + are welcome if they are easily maintainable with the Continuous Integration (CI) + system. If a personal realm becomes cumbersome to maintain or doesn't align with + the CI's checks, it might be relocated to a less prominent location or even removed. \ No newline at end of file diff --git a/examples/gno.land/p/demo/acl/gno.mod b/examples/gno.land/p/demo/acl/gno.mod index 15d9f078048..04fbf9043c4 100644 --- a/examples/gno.land/p/demo/acl/gno.mod +++ b/examples/gno.land/p/demo/acl/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/acl - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/avl/list/gno.mod b/examples/gno.land/p/demo/avl/list/gno.mod new file mode 100644 index 00000000000..c05923b7708 --- /dev/null +++ b/examples/gno.land/p/demo/avl/list/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/avl/list diff --git a/examples/gno.land/p/demo/avl/list/list.gno b/examples/gno.land/p/demo/avl/list/list.gno new file mode 100644 index 00000000000..594f5fa2a1f --- /dev/null +++ b/examples/gno.land/p/demo/avl/list/list.gno @@ -0,0 +1,314 @@ +// Package list implements a dynamic list data structure backed by an AVL tree. +// It provides O(log n) operations for most list operations while maintaining +// order stability. +// +// The list supports various operations including append, get, set, delete, +// range queries, and iteration. It can store values of any type. +// +// Example usage: +// +// // Create a new list and add elements +// var l list.List +// l.Append(1, 2, 3) +// +// // Get and set elements +// value := l.Get(1) // returns 2 +// l.Set(1, 42) // updates index 1 to 42 +// +// // Delete elements +// l.Delete(0) // removes first element +// +// // Iterate over elements +// l.ForEach(func(index int, value interface{}) bool { +// ufmt.Printf("index %d: %v\n", index, value) +// return false // continue iteration +// }) +// // Output: +// // index 0: 42 +// // index 1: 3 +// +// // Create a list of specific size +// l = list.Make(3, "default") // creates [default, default, default] +// +// // Create a list using a variable declaration +// var l2 list.List +// l2.Append(4, 5, 6) +// println(l2.Len()) // Output: 3 +package list + +import ( + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" +) + +// IList defines the interface for list operations +type IList interface { + Len() int + Append(values ...interface{}) + Get(index int) interface{} + Set(index int, value interface{}) bool + Delete(index int) (interface{}, bool) + Slice(startIndex, endIndex int) []interface{} + ForEach(fn func(index int, value interface{}) bool) + Clone() *List + DeleteRange(startIndex, endIndex int) int +} + +// Verify List implements IList interface +var _ IList = (*List)(nil) + +// List represents an ordered sequence of items backed by an AVL tree +type List struct { + tree avl.Tree + idGen seqid.ID +} + +// Len returns the number of elements in the list. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3) +// println(l.Len()) // Output: 3 +func (l *List) Len() int { + return l.tree.Size() +} + +// Append adds one or more values to the end of the list. +// +// Example: +// +// l := list.New() +// l.Append(1) // adds single value +// l.Append(2, 3, 4) // adds multiple values +// println(l.Len()) // Output: 4 +func (l *List) Append(values ...interface{}) { + for _, v := range values { + l.tree.Set(l.idGen.Next().String(), v) + } +} + +// Get returns the value at the specified index. +// Returns nil if index is out of bounds. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3) +// println(l.Get(1)) // Output: 2 +// println(l.Get(-1)) // Output: nil +// println(l.Get(999)) // Output: nil +func (l *List) Get(index int) interface{} { + if index < 0 || index >= l.tree.Size() { + return nil + } + _, value := l.tree.GetByIndex(index) + return value +} + +// Set updates or appends a value at the specified index. +// Returns true if the operation was successful, false otherwise. +// For empty lists, only index 0 is valid (append case). +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3) +// +// l.Set(1, 42) // updates existing index +// println(l.Get(1)) // Output: 42 +// +// l.Set(3, 4) // appends at end +// println(l.Get(3)) // Output: 4 +// +// l.Set(-1, 5) // invalid index +// println(l.Len()) // Output: 4 (list unchanged) +func (l *List) Set(index int, value interface{}) bool { + size := l.tree.Size() + + // Handle empty list case - only allow index 0 + if size == 0 { + if index == 0 { + l.Append(value) + return true + } + return false + } + + if index < 0 || index > size { + return false + } + + // If setting at the end (append case) + if index == size { + l.Append(value) + return true + } + + // Get the key at the specified index + key, _ := l.tree.GetByIndex(index) + if key == "" { + return false + } + + // Update the value at the existing key + l.tree.Set(key, value) + return true +} + +// Delete removes the element at the specified index. +// Returns the deleted value and true if successful, nil and false otherwise. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3) +// +// val, ok := l.Delete(1) +// println(val, ok) // Output: 2 true +// println(l.Len()) // Output: 2 +// +// val, ok = l.Delete(-1) +// println(val, ok) // Output: nil false +func (l *List) Delete(index int) (interface{}, bool) { + size := l.tree.Size() + // Always return nil, false for empty list + if size == 0 { + return nil, false + } + + if index < 0 || index >= size { + return nil, false + } + + key, value := l.tree.GetByIndex(index) + if key == "" { + return nil, false + } + + l.tree.Remove(key) + return value, true +} + +// Slice returns a slice of values from startIndex (inclusive) to endIndex (exclusive). +// Returns nil if the range is invalid. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3, 4, 5) +// +// println(l.Slice(1, 4)) // Output: [2 3 4] +// println(l.Slice(-1, 2)) // Output: [1 2] +// println(l.Slice(3, 999)) // Output: [4 5] +// println(l.Slice(3, 2)) // Output: nil +func (l *List) Slice(startIndex, endIndex int) []interface{} { + size := l.tree.Size() + + // Normalize bounds + if startIndex < 0 { + startIndex = 0 + } + if endIndex > size { + endIndex = size + } + if startIndex >= endIndex { + return nil + } + + count := endIndex - startIndex + result := make([]interface{}, count) + + i := 0 + l.tree.IterateByOffset(startIndex, count, func(_ string, value interface{}) bool { + result[i] = value + i++ + return false + }) + return result +} + +// ForEach iterates through all elements in the list. +func (l *List) ForEach(fn func(index int, value interface{}) bool) { + if l.tree.Size() == 0 { + return + } + + index := 0 + l.tree.IterateByOffset(0, l.tree.Size(), func(_ string, value interface{}) bool { + result := fn(index, value) + index++ + return result + }) +} + +// Clone creates a shallow copy of the list. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3) +// +// clone := l.Clone() +// clone.Set(0, 42) +// +// println(l.Get(0)) // Output: 1 +// println(clone.Get(0)) // Output: 42 +func (l *List) Clone() *List { + newList := &List{ + tree: avl.Tree{}, + idGen: l.idGen, + } + + size := l.tree.Size() + if size == 0 { + return newList + } + + l.tree.IterateByOffset(0, size, func(_ string, value interface{}) bool { + newList.Append(value) + return false + }) + + return newList +} + +// DeleteRange removes elements from startIndex (inclusive) to endIndex (exclusive). +// Returns the number of elements deleted. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3, 4, 5) +// +// deleted := l.DeleteRange(1, 4) +// println(deleted) // Output: 3 +// println(l.Range(0, l.Len())) // Output: [1 5] +func (l *List) DeleteRange(startIndex, endIndex int) int { + size := l.tree.Size() + + // Normalize bounds + if startIndex < 0 { + startIndex = 0 + } + if endIndex > size { + endIndex = size + } + if startIndex >= endIndex { + return 0 + } + + // Collect keys to delete + keysToDelete := make([]string, 0, endIndex-startIndex) + l.tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, _ interface{}) bool { + keysToDelete = append(keysToDelete, key) + return false + }) + + // Delete collected keys + for _, key := range keysToDelete { + l.tree.Remove(key) + } + + return len(keysToDelete) +} diff --git a/examples/gno.land/p/demo/avl/list/list_test.gno b/examples/gno.land/p/demo/avl/list/list_test.gno new file mode 100644 index 00000000000..0293692f660 --- /dev/null +++ b/examples/gno.land/p/demo/avl/list/list_test.gno @@ -0,0 +1,473 @@ +package list + +import ( + "testing" + + "gno.land/p/demo/ufmt" +) + +func TestList_Basic(t *testing.T) { + var l List + + // Test empty list + if l.Len() != 0 { + t.Errorf("new list should be empty, got len %d", l.Len()) + } + + // Test append and length + l.Append(1, 2, 3) + if l.Len() != 3 { + t.Errorf("expected len 3, got %d", l.Len()) + } + + // Test get + if v := l.Get(0); v != 1 { + t.Errorf("expected 1 at index 0, got %v", v) + } + if v := l.Get(1); v != 2 { + t.Errorf("expected 2 at index 1, got %v", v) + } + if v := l.Get(2); v != 3 { + t.Errorf("expected 3 at index 2, got %v", v) + } + + // Test out of bounds + if v := l.Get(-1); v != nil { + t.Errorf("expected nil for negative index, got %v", v) + } + if v := l.Get(3); v != nil { + t.Errorf("expected nil for out of bounds index, got %v", v) + } +} + +func TestList_Set(t *testing.T) { + var l List + l.Append(1, 2, 3) + + // Test valid set within bounds + if ok := l.Set(1, 42); !ok { + t.Error("Set should return true for valid index") + } + if v := l.Get(1); v != 42 { + t.Errorf("expected 42 after Set, got %v", v) + } + + // Test set at size (append) + if ok := l.Set(3, 4); !ok { + t.Error("Set should return true when appending at size") + } + if v := l.Get(3); v != 4 { + t.Errorf("expected 4 after Set at size, got %v", v) + } + + // Test invalid sets + if ok := l.Set(-1, 10); ok { + t.Error("Set should return false for negative index") + } + if ok := l.Set(5, 10); ok { + t.Error("Set should return false for index > size") + } + + // Verify list state hasn't changed after invalid operations + expected := []interface{}{1, 42, 3, 4} + for i, want := range expected { + if got := l.Get(i); got != want { + t.Errorf("index %d = %v; want %v", i, got, want) + } + } +} + +func TestList_Delete(t *testing.T) { + var l List + l.Append(1, 2, 3) + + // Test valid delete + if v, ok := l.Delete(1); !ok || v != 2 { + t.Errorf("Delete(1) = %v, %v; want 2, true", v, ok) + } + if l.Len() != 2 { + t.Errorf("expected len 2 after delete, got %d", l.Len()) + } + if v := l.Get(1); v != 3 { + t.Errorf("expected 3 at index 1 after delete, got %v", v) + } + + // Test invalid delete + if v, ok := l.Delete(-1); ok || v != nil { + t.Errorf("Delete(-1) = %v, %v; want nil, false", v, ok) + } + if v, ok := l.Delete(2); ok || v != nil { + t.Errorf("Delete(2) = %v, %v; want nil, false", v, ok) + } +} + +func TestList_Slice(t *testing.T) { + var l List + l.Append(1, 2, 3, 4, 5) + + // Test valid ranges + values := l.Slice(1, 4) + expected := []interface{}{2, 3, 4} + if !sliceEqual(values, expected) { + t.Errorf("Slice(1,4) = %v; want %v", values, expected) + } + + // Test edge cases + if values := l.Slice(-1, 2); !sliceEqual(values, []interface{}{1, 2}) { + t.Errorf("Slice(-1,2) = %v; want [1 2]", values) + } + if values := l.Slice(3, 10); !sliceEqual(values, []interface{}{4, 5}) { + t.Errorf("Slice(3,10) = %v; want [4 5]", values) + } + if values := l.Slice(3, 2); values != nil { + t.Errorf("Slice(3,2) = %v; want nil", values) + } +} + +func TestList_ForEach(t *testing.T) { + var l List + l.Append(1, 2, 3) + + sum := 0 + l.ForEach(func(index int, value interface{}) bool { + sum += value.(int) + return false + }) + + if sum != 6 { + t.Errorf("ForEach sum = %d; want 6", sum) + } + + // Test early termination + count := 0 + l.ForEach(func(index int, value interface{}) bool { + count++ + return true // stop after first item + }) + + if count != 1 { + t.Errorf("ForEach early termination count = %d; want 1", count) + } +} + +func TestList_Clone(t *testing.T) { + var l List + l.Append(1, 2, 3) + + clone := l.Clone() + + // Test same length + if clone.Len() != l.Len() { + t.Errorf("clone.Len() = %d; want %d", clone.Len(), l.Len()) + } + + // Test same values + for i := 0; i < l.Len(); i++ { + if clone.Get(i) != l.Get(i) { + t.Errorf("clone.Get(%d) = %v; want %v", i, clone.Get(i), l.Get(i)) + } + } + + // Test independence + l.Set(0, 42) + if clone.Get(0) == l.Get(0) { + t.Error("clone should be independent of original") + } +} + +func TestList_DeleteRange(t *testing.T) { + var l List + l.Append(1, 2, 3, 4, 5) + + // Test valid range delete + deleted := l.DeleteRange(1, 4) + if deleted != 3 { + t.Errorf("DeleteRange(1,4) deleted %d elements; want 3", deleted) + } + if l.Len() != 2 { + t.Errorf("after DeleteRange(1,4) len = %d; want 2", l.Len()) + } + expected := []interface{}{1, 5} + for i, want := range expected { + if got := l.Get(i); got != want { + t.Errorf("after DeleteRange(1,4) index %d = %v; want %v", i, got, want) + } + } + + // Test edge cases + l = List{} + l.Append(1, 2, 3) + + // Delete with negative start + if deleted := l.DeleteRange(-1, 2); deleted != 2 { + t.Errorf("DeleteRange(-1,2) deleted %d elements; want 2", deleted) + } + + // Delete with end > length + l = List{} + l.Append(1, 2, 3) + if deleted := l.DeleteRange(1, 5); deleted != 2 { + t.Errorf("DeleteRange(1,5) deleted %d elements; want 2", deleted) + } + + // Delete invalid range + if deleted := l.DeleteRange(2, 1); deleted != 0 { + t.Errorf("DeleteRange(2,1) deleted %d elements; want 0", deleted) + } + + // Delete empty range + if deleted := l.DeleteRange(1, 1); deleted != 0 { + t.Errorf("DeleteRange(1,1) deleted %d elements; want 0", deleted) + } +} + +func TestList_EmptyOperations(t *testing.T) { + var l List + + // Operations on empty list + if v := l.Get(0); v != nil { + t.Errorf("Get(0) on empty list = %v; want nil", v) + } + + // Set should work at index 0 for empty list (append case) + if ok := l.Set(0, 1); !ok { + t.Error("Set(0,1) on empty list = false; want true") + } + if v := l.Get(0); v != 1 { + t.Errorf("Get(0) after Set = %v; want 1", v) + } + + l = List{} // Reset to empty list + if v, ok := l.Delete(0); ok || v != nil { + t.Errorf("Delete(0) on empty list = %v, %v; want nil, false", v, ok) + } + if values := l.Slice(0, 1); values != nil { + t.Errorf("Range(0,1) on empty list = %v; want nil", values) + } +} + +func TestList_DifferentTypes(t *testing.T) { + var l List + + // Test with different types + l.Append(42, "hello", true, 3.14) + + if v := l.Get(0).(int); v != 42 { + t.Errorf("Get(0) = %v; want 42", v) + } + if v := l.Get(1).(string); v != "hello" { + t.Errorf("Get(1) = %v; want 'hello'", v) + } + if v := l.Get(2).(bool); !v { + t.Errorf("Get(2) = %v; want true", v) + } + if v := l.Get(3).(float64); v != 3.14 { + t.Errorf("Get(3) = %v; want 3.14", v) + } +} + +func TestList_LargeOperations(t *testing.T) { + var l List + + // Test with larger number of elements + n := 1000 + for i := 0; i < n; i++ { + l.Append(i) + } + + if l.Len() != n { + t.Errorf("Len() = %d; want %d", l.Len(), n) + } + + // Test range on large list + values := l.Slice(n-3, n) + expected := []interface{}{n - 3, n - 2, n - 1} + if !sliceEqual(values, expected) { + t.Errorf("Range(%d,%d) = %v; want %v", n-3, n, values, expected) + } + + // Test large range deletion + deleted := l.DeleteRange(100, 900) + if deleted != 800 { + t.Errorf("DeleteRange(100,900) = %d; want 800", deleted) + } + if l.Len() != 200 { + t.Errorf("Len() after large delete = %d; want 200", l.Len()) + } +} + +func TestList_ChainedOperations(t *testing.T) { + var l List + + // Test sequence of operations + l.Append(1, 2, 3) + l.Delete(1) + l.Append(4) + l.Set(1, 5) + + expected := []interface{}{1, 5, 4} + for i, want := range expected { + if got := l.Get(i); got != want { + t.Errorf("index %d = %v; want %v", i, got, want) + } + } +} + +func TestList_RangeEdgeCases(t *testing.T) { + var l List + l.Append(1, 2, 3, 4, 5) + + // Test various edge cases for Range + cases := []struct { + start, end int + want []interface{} + }{ + {-10, 2, []interface{}{1, 2}}, + {3, 10, []interface{}{4, 5}}, + {0, 0, nil}, + {5, 5, nil}, + {4, 3, nil}, + {-1, -1, nil}, + } + + for _, tc := range cases { + got := l.Slice(tc.start, tc.end) + if !sliceEqual(got, tc.want) { + t.Errorf("Slice(%d,%d) = %v; want %v", tc.start, tc.end, got, tc.want) + } + } +} + +func TestList_IndexConsistency(t *testing.T) { + var l List + + // Initial additions + l.Append(1, 2, 3, 4, 5) // [1,2,3,4,5] + + // Delete from middle + l.Delete(2) // [1,2,4,5] + + // Add more elements + l.Append(6, 7) // [1,2,4,5,6,7] + + // Delete range from middle + l.DeleteRange(1, 4) // [1,6,7] + + // Add more elements + l.Append(8, 9, 10) // [1,6,7,8,9,10] + + // Verify sequence is continuous + expected := []interface{}{1, 6, 7, 8, 9, 10} + for i, want := range expected { + if got := l.Get(i); got != want { + t.Errorf("index %d = %v; want %v", i, got, want) + } + } + + // Verify no extra elements exist + if l.Len() != len(expected) { + t.Errorf("length = %d; want %d", l.Len(), len(expected)) + } + + // Verify all indices are accessible + allValues := l.Slice(0, l.Len()) + if !sliceEqual(allValues, expected) { + t.Errorf("Slice(0, Len()) = %v; want %v", allValues, expected) + } + + // Verify no gaps in iteration + var iteratedValues []interface{} + var indices []int + l.ForEach(func(index int, value interface{}) bool { + iteratedValues = append(iteratedValues, value) + indices = append(indices, index) + return false + }) + + // Check values from iteration + if !sliceEqual(iteratedValues, expected) { + t.Errorf("ForEach values = %v; want %v", iteratedValues, expected) + } + + // Check indices are sequential + for i, idx := range indices { + if idx != i { + t.Errorf("ForEach index %d = %d; want %d", i, idx, i) + } + } +} + +func TestList_RecursiveSafety(t *testing.T) { + // Create a new list + l := &List{} + + // Add some initial values + l.Append("id1") + l.Append("id2") + l.Append("id3") + + // Test deep list traversal + found := false + l.ForEach(func(i int, v interface{}) bool { + if str, ok := v.(string); ok { + if str == "id2" { + found = true + return true // stop iteration + } + } + return false // continue iteration + }) + + if !found { + t.Error("Failed to find expected value in list") + } + + short := testing.Short() + + // Test recursive safety by performing multiple operations + for i := 0; i < 1000; i++ { + // Add new value + l.Append(ufmt.Sprintf("id%d", i+4)) + + if !short { + // Search for a value + var lastFound bool + l.ForEach(func(j int, v interface{}) bool { + if str, ok := v.(string); ok { + if str == ufmt.Sprintf("id%d", i+3) { + lastFound = true + return true + } + } + return false + }) + + if !lastFound { + t.Errorf("Failed to find value id%d after insertion", i+3) + } + } + } + + // Verify final length + expectedLen := 1003 // 3 initial + 1000 added + if l.Len() != expectedLen { + t.Errorf("Expected length %d, got %d", expectedLen, l.Len()) + } + + if short { + t.Skip("skipping extended recursive safety test in short mode") + } +} + +// Helper function to compare slices +func sliceEqual(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/examples/gno.land/p/demo/avl/node.gno b/examples/gno.land/p/demo/avl/node.gno index 7308e163768..7d4ddffff02 100644 --- a/examples/gno.land/p/demo/avl/node.gno +++ b/examples/gno.land/p/demo/avl/node.gno @@ -384,7 +384,7 @@ func (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly // TraverseByOffset traverses all nodes, including inner nodes. // A limit of math.MaxInt means no limit. -func (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { +func (node *Node) TraverseByOffset(offset, limit int, ascending bool, leavesOnly bool, cb func(*Node) bool) bool { if node == nil { return false } @@ -401,21 +401,21 @@ func (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnl } // go to the actual recursive function. - return node.traverseByOffset(offset, limit, descending, leavesOnly, cb) + return node.traverseByOffset(offset, limit, ascending, leavesOnly, cb) } // TraverseByOffset traverses the subtree rooted at the node by offset and limit, // in either ascending or descending order, and applies the callback function to each traversed node. // If leavesOnly is true, only leaf nodes are visited. -func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { +func (node *Node) traverseByOffset(offset, limit int, ascending bool, leavesOnly bool, cb func(*Node) bool) bool { // caller guarantees: offset < node.size; limit > 0. if !leavesOnly { if cb(node) { - return true + return true // Stop traversal if callback returns true } } first, second := node.getLeftNode(), node.getRightNode() - if descending { + if !ascending { first, second = second, first } if first.IsLeaf() { @@ -423,10 +423,12 @@ func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnl if offset > 0 { offset-- } else { - cb(first) + if cb(first) { + return true // Stop traversal if callback returns true + } limit-- if limit <= 0 { - return false + return true // Stop traversal when limit is reached } } } else { @@ -437,7 +439,7 @@ func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnl if offset >= first.size { offset -= first.size // 1 } else { - if first.traverseByOffset(offset, limit, descending, leavesOnly, cb) { + if first.traverseByOffset(offset, limit, ascending, leavesOnly, cb) { return true } // number of leaves which could actually be called from inside @@ -460,7 +462,7 @@ func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnl } // => if it is not a leaf, it will still be enough to recursively call this // function with the updated offset and limit - return second.traverseByOffset(offset, limit, descending, leavesOnly, cb) + return second.traverseByOffset(offset, limit, ascending, leavesOnly, cb) } // Only used in testing... diff --git a/examples/gno.land/p/demo/avl/node_test.gno b/examples/gno.land/p/demo/avl/node_test.gno index f24217625ea..3682cbc7c80 100644 --- a/examples/gno.land/p/demo/avl/node_test.gno +++ b/examples/gno.land/p/demo/avl/node_test.gno @@ -17,36 +17,34 @@ Book Browser` tt := []struct { name string - desc bool + asc bool }{ - {"ascending", false}, - {"descending", true}, + {"ascending", true}, + {"descending", false}, } for _, tt := range tt { t.Run(tt.name, func(t *testing.T) { + // use sl to insert the values, and reversed to match the values + // we do this to ensure that the order of TraverseByOffset is independent + // from the insertion order sl := strings.Split(testStrings, "\n") - - // sort a first time in the order opposite to how we'll be traversing - // the tree, to ensure that we are not just iterating through with - // insertion order. sort.Strings(sl) - if !tt.desc { - reverseSlice(sl) + reversed := append([]string{}, sl...) + reverseSlice(reversed) + + if !tt.asc { + sl, reversed = reversed, sl } - r := NewNode(sl[0], nil) - for _, v := range sl[1:] { + r := NewNode(reversed[0], nil) + for _, v := range reversed[1:] { r, _ = r.Set(v, nil) } - // then sort sl in the order we'll be traversing it, so that we can - // compare the result with sl. - reverseSlice(sl) - var result []string for i := 0; i < len(sl); i++ { - r.TraverseByOffset(i, 1, tt.desc, true, func(n *Node) bool { + r.TraverseByOffset(i, 1, tt.asc, true, func(n *Node) bool { result = append(result, n.Key()) return false }) @@ -66,7 +64,7 @@ Browser` exp := sl[i:max] actual := []string{} - r.TraverseByOffset(i, l, tt.desc, true, func(tr *Node) bool { + r.TraverseByOffset(i, l, tt.asc, true, func(tr *Node) bool { actual = append(actual, tr.Key()) return false }) @@ -422,6 +420,30 @@ func TestTraverse(t *testing.T) { t.Errorf("want %v got %v", expected, result) } }) + + t.Run("early termination", func(t *testing.T) { + if len(tt.input) == 0 { + return // Skip for empty tree + } + + var result []string + var count int + tree.Iterate("", "", func(n *Node) bool { + count++ + result = append(result, n.Key()) + return true // Stop after first item + }) + + if count != 1 { + t.Errorf("Expected callback to be called exactly once, got %d calls", count) + } + if len(result) != 1 { + t.Errorf("Expected exactly one result, got %d items", len(result)) + } + if len(result) > 0 && result[0] != tt.expected[0] { + t.Errorf("Expected first item to be %v, got %v", tt.expected[0], result[0]) + } + }) }) } } @@ -435,7 +457,7 @@ func TestRotateWhenHeightDiffers(t *testing.T) { { "right rotation when left subtree is higher", []string{"E", "C", "A", "B", "D"}, - []string{"A", "B", "C", "E", "D"}, + []string{"A", "B", "C", "D", "E"}, }, { "left rotation when right subtree is higher", @@ -445,12 +467,12 @@ func TestRotateWhenHeightDiffers(t *testing.T) { { "left-right rotation", []string{"E", "A", "C", "B", "D"}, - []string{"A", "B", "C", "E", "D"}, + []string{"A", "B", "C", "D", "E"}, }, { "right-left rotation", []string{"A", "E", "C", "B", "D"}, - []string{"A", "B", "C", "E", "D"}, + []string{"A", "B", "C", "D", "E"}, }, } @@ -533,7 +555,7 @@ func slicesEqual(w1, w2 []string) bool { return false } for i := 0; i < len(w1); i++ { - if w1[0] != w2[0] { + if w1[i] != w2[i] { return false } } diff --git a/examples/gno.land/p/demo/avl/pager/gno.mod b/examples/gno.land/p/demo/avl/pager/gno.mod new file mode 100644 index 00000000000..020b809b208 --- /dev/null +++ b/examples/gno.land/p/demo/avl/pager/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/avl/pager diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno new file mode 100644 index 00000000000..f5f909a473d --- /dev/null +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -0,0 +1,223 @@ +package pager + +import ( + "math" + "net/url" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" +) + +// Pager is a struct that holds the AVL tree and pagination parameters. +type Pager struct { + Tree avl.ITree + PageQueryParam string + SizeQueryParam string + DefaultPageSize int + Reversed bool +} + +// Page represents a single page of results. +type Page struct { + Items []Item + PageNumber int + PageSize int + TotalItems int + TotalPages int + HasPrev bool + HasNext bool + Pager *Pager // Reference to the parent Pager +} + +// Item represents a key-value pair in the AVL tree. +type Item struct { + Key string + Value interface{} +} + +// NewPager creates a new Pager with default values. +func NewPager(tree avl.ITree, defaultPageSize int, reversed bool) *Pager { + return &Pager{ + Tree: tree, + PageQueryParam: "page", + SizeQueryParam: "size", + DefaultPageSize: defaultPageSize, + Reversed: reversed, + } +} + +// GetPage retrieves a page of results from the AVL tree. +func (p *Pager) GetPage(pageNumber int) *Page { + return p.GetPageWithSize(pageNumber, p.DefaultPageSize) +} + +func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page { + totalItems := p.Tree.Size() + totalPages := int(math.Ceil(float64(totalItems) / float64(pageSize))) + + page := &Page{ + TotalItems: totalItems, + TotalPages: totalPages, + PageSize: pageSize, + Pager: p, + } + + // pages without content + if pageSize < 1 { + return page + } + + // page number provided is not available + if pageNumber < 1 { + page.HasNext = totalPages > 0 + return page + } + + // page number provided is outside the range of total pages + if pageNumber > totalPages { + page.PageNumber = pageNumber + page.HasPrev = pageNumber > 0 + return page + } + + startIndex := (pageNumber - 1) * pageSize + endIndex := startIndex + pageSize + if endIndex > totalItems { + endIndex = totalItems + } + + items := []Item{} + + if p.Reversed { + p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + items = append(items, Item{Key: key, Value: value}) + return false + }) + } else { + p.Tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + items = append(items, Item{Key: key, Value: value}) + return false + }) + } + + page.Items = items + page.PageNumber = pageNumber + page.HasPrev = pageNumber > 1 + page.HasNext = pageNumber < totalPages + return page +} + +func (p *Pager) MustGetPageByPath(rawURL string) *Page { + page, err := p.GetPageByPath(rawURL) + if err != nil { + panic("invalid path") + } + return page +} + +// GetPageByPath retrieves a page of results based on the query parameters in the URL path. +func (p *Pager) GetPageByPath(rawURL string) (*Page, error) { + pageNumber, pageSize, err := p.ParseQuery(rawURL) + if err != nil { + return nil, err + } + return p.GetPageWithSize(pageNumber, pageSize), nil +} + +// Picker generates the Markdown UI for the page Picker +func (p *Page) Picker() string { + pageNumber := p.PageNumber + pageNumber = max(pageNumber, 1) + + if p.TotalPages <= 1 { + return "" + } + + md := "" + + if p.HasPrev { + // Always show the first page link + md += ufmt.Sprintf("[%d](?%s=%d) | ", 1, p.Pager.PageQueryParam, 1) + + // Before + if p.PageNumber > 4 { + md += "… | " + } + + if p.PageNumber > 3 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber-2, p.Pager.PageQueryParam, p.PageNumber-2) + } + + if p.PageNumber > 2 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber-1, p.Pager.PageQueryParam, p.PageNumber-1) + } + } + + if p.PageNumber > 0 && p.PageNumber <= p.TotalPages { + // Current page + md += ufmt.Sprintf("**%d**", p.PageNumber) + } else { + md += ufmt.Sprintf("_%d_", p.PageNumber) + } + + if p.HasNext { + md += " | " + + if p.PageNumber < p.TotalPages-1 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber+1, p.Pager.PageQueryParam, p.PageNumber+1) + } + + if p.PageNumber < p.TotalPages-2 { + md += ufmt.Sprintf("[%d](?%s=%d) | ", p.PageNumber+2, p.Pager.PageQueryParam, p.PageNumber+2) + } + + if p.PageNumber < p.TotalPages-3 { + md += "… | " + } + + // Always show the last page link + md += ufmt.Sprintf("[%d](?%s=%d)", p.TotalPages, p.Pager.PageQueryParam, p.TotalPages) + } + + return md +} + +// ParseQuery parses the URL to extract the page number and page size. +func (p *Pager) ParseQuery(rawURL string) (int, int, error) { + u, err := url.Parse(rawURL) + if err != nil { + return 1, p.DefaultPageSize, err + } + + query := u.Query() + pageNumber := 1 + pageSize := p.DefaultPageSize + + if p.PageQueryParam != "" { + if pageStr := query.Get(p.PageQueryParam); pageStr != "" { + pageNumber, err = strconv.Atoi(pageStr) + if err != nil || pageNumber < 1 { + pageNumber = 1 + } + } + } + + if p.SizeQueryParam != "" { + if sizeStr := query.Get(p.SizeQueryParam); sizeStr != "" { + pageSize, err = strconv.Atoi(sizeStr) + if err != nil || pageSize < 1 { + pageSize = p.DefaultPageSize + } + } + } + + return pageNumber, pageSize, nil +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/examples/gno.land/p/demo/avl/pager/pager_test.gno b/examples/gno.land/p/demo/avl/pager/pager_test.gno new file mode 100644 index 00000000000..9869924e5b5 --- /dev/null +++ b/examples/gno.land/p/demo/avl/pager/pager_test.gno @@ -0,0 +1,224 @@ +package pager + +import ( + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" +) + +func TestPager_GetPage(t *testing.T) { + // Create a new AVL tree and populate it with some key-value pairs. + tree := avl.NewTree() + tree.Set("a", 1) + tree.Set("b", 2) + tree.Set("c", 3) + tree.Set("d", 4) + tree.Set("e", 5) + + t.Run("normal ordering", func(t *testing.T) { + // Create a new pager. + pager := NewPager(tree, 10, false) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected []Item + }{ + {1, 2, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}}}, + {2, 2, []Item{{Key: "c", Value: 3}, {Key: "d", Value: 4}}}, + {3, 2, []Item{{Key: "e", Value: 5}}}, + {1, 3, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}}}, + {2, 3, []Item{{Key: "d", Value: 4}, {Key: "e", Value: 5}}}, + {1, 5, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}, {Key: "d", Value: 4}, {Key: "e", Value: 5}}}, + {2, 5, []Item{}}, + } + + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + + uassert.Equal(t, len(tt.expected), len(page.Items)) + + for i, item := range page.Items { + uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Value, item.Value) + } + } + }) + + t.Run("reversed ordering", func(t *testing.T) { + // Create a new pager. + pager := NewPager(tree, 10, true) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected []Item + }{ + {1, 2, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}}}, + {2, 2, []Item{{Key: "c", Value: 3}, {Key: "b", Value: 2}}}, + {3, 2, []Item{{Key: "a", Value: 1}}}, + {1, 3, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}, {Key: "c", Value: 3}}}, + {2, 3, []Item{{Key: "b", Value: 2}, {Key: "a", Value: 1}}}, + {1, 5, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}, {Key: "c", Value: 3}, {Key: "b", Value: 2}, {Key: "a", Value: 1}}}, + {2, 5, []Item{}}, + } + + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + + uassert.Equal(t, len(tt.expected), len(page.Items)) + + for i, item := range page.Items { + uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Value, item.Value) + } + } + }) +} + +func TestPager_GetPageByPath(t *testing.T) { + // Create a new AVL tree and populate it with some key-value pairs. + tree := avl.NewTree() + for i := 0; i < 50; i++ { + tree.Set(ufmt.Sprintf("key%d", i), i) + } + + // Create a new pager. + pager := NewPager(tree, 10, false) + + // Define test cases. + tests := []struct { + rawURL string + expectedPage int + expectedSize int + }{ + {"/r/foo:bar/baz?size=10&page=1", 1, 10}, + {"/r/foo:bar/baz?size=10&page=2", 2, 10}, + {"/r/foo:bar/baz?page=3", 3, pager.DefaultPageSize}, + {"/r/foo:bar/baz?size=20", 1, 20}, + {"/r/foo:bar/baz", 1, pager.DefaultPageSize}, + } + + for _, tt := range tests { + page, err := pager.GetPageByPath(tt.rawURL) + urequire.NoError(t, err, ufmt.Sprintf("GetPageByPath(%s) returned error: %v", tt.rawURL, err)) + + uassert.Equal(t, tt.expectedPage, page.PageNumber) + uassert.Equal(t, tt.expectedSize, page.PageSize) + } +} + +func TestPage_Picker(t *testing.T) { + // Create a new AVL tree and populate it with some key-value pairs. + tree := avl.NewTree() + tree.Set("a", 1) + tree.Set("b", 2) + tree.Set("c", 3) + tree.Set("d", 4) + tree.Set("e", 5) + + // Create a new pager. + pager := NewPager(tree, 10, false) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected string + }{ + {1, 2, "**1** | [2](?page=2) | [3](?page=3)"}, + {2, 2, "[1](?page=1) | **2** | [3](?page=3)"}, + {3, 2, "[1](?page=1) | [2](?page=2) | **3**"}, + } + + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + + ui := page.Picker() + uassert.Equal(t, tt.expected, ui) + } +} + +func TestPager_UI_WithManyPages(t *testing.T) { + // Create a new AVL tree and populate it with many key-value pairs. + tree := avl.NewTree() + for i := 0; i < 100; i++ { + tree.Set(ufmt.Sprintf("key%d", i), i) + } + + // Create a new pager. + pager := NewPager(tree, 10, false) + + // Define test cases for a large number of pages. + tests := []struct { + pageNumber int + pageSize int + expected string + }{ + // XXX: -1 + // XXX: 0 + {1, 10, "**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10)"}, + {2, 10, "[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10)"}, + {3, 10, "[1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | … | [10](?page=10)"}, + {4, 10, "[1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6) | … | [10](?page=10)"}, + {5, 10, "[1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6) | [7](?page=7) | … | [10](?page=10)"}, + {6, 10, "[1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6** | [7](?page=7) | [8](?page=8) | … | [10](?page=10)"}, + {7, 10, "[1](?page=1) | … | [5](?page=5) | [6](?page=6) | **7** | [8](?page=8) | [9](?page=9) | [10](?page=10)"}, + {8, 10, "[1](?page=1) | … | [6](?page=6) | [7](?page=7) | **8** | [9](?page=9) | [10](?page=10)"}, + {9, 10, "[1](?page=1) | … | [7](?page=7) | [8](?page=8) | **9** | [10](?page=10)"}, + {10, 10, "[1](?page=1) | … | [8](?page=8) | [9](?page=9) | **10**"}, + // XXX: 11 + } + + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + + ui := page.Picker() + uassert.Equal(t, tt.expected, ui) + } +} + +func TestPager_ParseQuery(t *testing.T) { + // Create a new AVL tree and populate it with some key-value pairs. + tree := avl.NewTree() + tree.Set("a", 1) + tree.Set("b", 2) + tree.Set("c", 3) + tree.Set("d", 4) + tree.Set("e", 5) + + // Create a new pager. + pager := NewPager(tree, 10, false) + + // Define test cases. + tests := []struct { + rawURL string + expectedPage int + expectedSize int + expectedError bool + }{ + {"/r/foo:bar/baz?size=2&page=1", 1, 2, false}, + {"/r/foo:bar/baz?size=3&page=2", 2, 3, false}, + {"/r/foo:bar/baz?size=5&page=3", 3, 5, false}, + {"/r/foo:bar/baz?page=2", 2, pager.DefaultPageSize, false}, + {"/r/foo:bar/baz?size=3", 1, 3, false}, + {"/r/foo:bar/baz", 1, pager.DefaultPageSize, false}, + {"/r/foo:bar/baz?size=0&page=0", 1, pager.DefaultPageSize, false}, + } + + for _, tt := range tests { + page, size, err := pager.ParseQuery(tt.rawURL) + if tt.expectedError { + uassert.Error(t, err, ufmt.Sprintf("ParseQuery(%s) expected error but got none", tt.rawURL)) + } else { + urequire.NoError(t, err, ufmt.Sprintf("ParseQuery(%s) returned error: %v", tt.rawURL, err)) + uassert.Equal(t, tt.expectedPage, page, ufmt.Sprintf("ParseQuery(%s) returned page %d, expected %d", tt.rawURL, page, tt.expectedPage)) + uassert.Equal(t, tt.expectedSize, size, ufmt.Sprintf("ParseQuery(%s) returned size %d, expected %d", tt.rawURL, size, tt.expectedSize)) + } + } +} diff --git a/examples/gno.land/p/demo/avl/pager/z_filetest.gno b/examples/gno.land/p/demo/avl/pager/z_filetest.gno new file mode 100644 index 00000000000..6342888d6b4 --- /dev/null +++ b/examples/gno.land/p/demo/avl/pager/z_filetest.gno @@ -0,0 +1,102 @@ +package main + +import ( + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" +) + +func main() { + // Create a new AVL tree and populate it with some key-value pairs. + var id seqid.ID + tree := avl.NewTree() + for i := 0; i < 42; i++ { + tree.Set(id.Next().String(), i) + } + + // Create a new pager. + pager := pager.NewPager(tree, 7, false) + + for pn := -1; pn < 8; pn++ { + page := pager.GetPage(pn) + + println(ufmt.Sprintf("## Page %d of %d", page.PageNumber, page.TotalPages)) + for idx, item := range page.Items { + println(ufmt.Sprintf("- idx=%d key=%s value=%d", idx, item.Key, item.Value)) + } + println(page.Picker()) + println() + } +} + +// Output: +// ## Page 0 of 6 +// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6) +// +// ## Page 0 of 6 +// _0_ | [1](?page=1) | [2](?page=2) | … | [6](?page=6) +// +// ## Page 1 of 6 +// - idx=0 key=0000001 value=0 +// - idx=1 key=0000002 value=1 +// - idx=2 key=0000003 value=2 +// - idx=3 key=0000004 value=3 +// - idx=4 key=0000005 value=4 +// - idx=5 key=0000006 value=5 +// - idx=6 key=0000007 value=6 +// **1** | [2](?page=2) | [3](?page=3) | … | [6](?page=6) +// +// ## Page 2 of 6 +// - idx=0 key=0000008 value=7 +// - idx=1 key=0000009 value=8 +// - idx=2 key=000000a value=9 +// - idx=3 key=000000b value=10 +// - idx=4 key=000000c value=11 +// - idx=5 key=000000d value=12 +// - idx=6 key=000000e value=13 +// [1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [6](?page=6) +// +// ## Page 3 of 6 +// - idx=0 key=000000f value=14 +// - idx=1 key=000000g value=15 +// - idx=2 key=000000h value=16 +// - idx=3 key=000000j value=17 +// - idx=4 key=000000k value=18 +// - idx=5 key=000000m value=19 +// - idx=6 key=000000n value=20 +// [1](?page=1) | [2](?page=2) | **3** | [4](?page=4) | [5](?page=5) | [6](?page=6) +// +// ## Page 4 of 6 +// - idx=0 key=000000p value=21 +// - idx=1 key=000000q value=22 +// - idx=2 key=000000r value=23 +// - idx=3 key=000000s value=24 +// - idx=4 key=000000t value=25 +// - idx=5 key=000000v value=26 +// - idx=6 key=000000w value=27 +// [1](?page=1) | [2](?page=2) | [3](?page=3) | **4** | [5](?page=5) | [6](?page=6) +// +// ## Page 5 of 6 +// - idx=0 key=000000x value=28 +// - idx=1 key=000000y value=29 +// - idx=2 key=000000z value=30 +// - idx=3 key=0000010 value=31 +// - idx=4 key=0000011 value=32 +// - idx=5 key=0000012 value=33 +// - idx=6 key=0000013 value=34 +// [1](?page=1) | … | [3](?page=3) | [4](?page=4) | **5** | [6](?page=6) +// +// ## Page 6 of 6 +// - idx=0 key=0000014 value=35 +// - idx=1 key=0000015 value=36 +// - idx=2 key=0000016 value=37 +// - idx=3 key=0000017 value=38 +// - idx=4 key=0000018 value=39 +// - idx=5 key=0000019 value=40 +// - idx=6 key=000001a value=41 +// [1](?page=1) | … | [4](?page=4) | [5](?page=5) | **6** +// +// ## Page 7 of 6 +// [1](?page=1) | … | [5](?page=5) | [6](?page=6) | _7_ +// diff --git a/examples/gno.land/p/demo/avl/rolist/gno.mod b/examples/gno.land/p/demo/avl/rolist/gno.mod new file mode 100644 index 00000000000..682513c2cc3 --- /dev/null +++ b/examples/gno.land/p/demo/avl/rolist/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/avl/rolist diff --git a/examples/gno.land/p/demo/avl/rolist/rolist.gno b/examples/gno.land/p/demo/avl/rolist/rolist.gno new file mode 100644 index 00000000000..23a85d9c885 --- /dev/null +++ b/examples/gno.land/p/demo/avl/rolist/rolist.gno @@ -0,0 +1,119 @@ +// Package rolist provides a read-only wrapper for list.List with safe value transformation. +// +// It is useful when you want to expose a read-only view of a list while ensuring that +// the sensitive data cannot be modified. +// +// Example: +// +// // Define a user structure with sensitive data +// type User struct { +// Name string +// Balance int +// Internal string // sensitive field +// } +// +// // Create and populate the original list +// privateList := list.New() +// privateList.Append(&User{ +// Name: "Alice", +// Balance: 100, +// Internal: "sensitive", +// }) +// +// // Create a safe transformation function that copies the struct +// // while excluding sensitive data +// makeEntrySafeFn := func(v interface{}) interface{} { +// u := v.(*User) +// return &User{ +// Name: u.Name, +// Balance: u.Balance, +// Internal: "", // omit sensitive data +// } +// } +// +// // Create a read-only view of the list +// publicList := rolist.Wrap(list, makeEntrySafeFn) +// +// // Safely access the data +// value := publicList.Get(0) +// user := value.(*User) +// // user.Name == "Alice" +// // user.Balance == 100 +// // user.Internal == "" (sensitive data is filtered) +package rolist + +import ( + "gno.land/p/demo/avl/list" +) + +// IReadOnlyList defines the read-only operations available on a list. +type IReadOnlyList interface { + Len() int + Get(index int) interface{} + Slice(startIndex, endIndex int) []interface{} + ForEach(fn func(index int, value interface{}) bool) +} + +// ReadOnlyList wraps a list.List and provides read-only access. +type ReadOnlyList struct { + list *list.List + makeEntrySafeFn func(interface{}) interface{} +} + +// Verify interface implementations +var _ IReadOnlyList = (*ReadOnlyList)(nil) +var _ IReadOnlyList = (interface{ list.IList })(nil) // is subset of list.IList + +// Wrap creates a new ReadOnlyList from an existing list.List and a safety transformation function. +// If makeEntrySafeFn is nil, values will be returned as-is without transformation. +func Wrap(list *list.List, makeEntrySafeFn func(interface{}) interface{}) *ReadOnlyList { + return &ReadOnlyList{ + list: list, + makeEntrySafeFn: makeEntrySafeFn, + } +} + +// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value +func (rol *ReadOnlyList) getSafeValue(value interface{}) interface{} { + if rol.makeEntrySafeFn == nil { + return value + } + return rol.makeEntrySafeFn(value) +} + +// Len returns the number of elements in the list. +func (rol *ReadOnlyList) Len() int { + return rol.list.Len() +} + +// Get returns the value at the specified index, converted to a safe format. +// Returns nil if index is out of bounds. +func (rol *ReadOnlyList) Get(index int) interface{} { + value := rol.list.Get(index) + if value == nil { + return nil + } + return rol.getSafeValue(value) +} + +// Slice returns a slice of values from startIndex (inclusive) to endIndex (exclusive), +// with all values converted to a safe format. +func (rol *ReadOnlyList) Slice(startIndex, endIndex int) []interface{} { + values := rol.list.Slice(startIndex, endIndex) + if values == nil { + return nil + } + + result := make([]interface{}, len(values)) + for i, v := range values { + result[i] = rol.getSafeValue(v) + } + return result +} + +// ForEach iterates through all elements in the list, providing safe versions of the values. +func (rol *ReadOnlyList) ForEach(fn func(index int, value interface{}) bool) { + rol.list.ForEach(func(index int, value interface{}) bool { + return fn(index, rol.getSafeValue(value)) + }) +} diff --git a/examples/gno.land/p/demo/avl/rolist/rolist_test.gno b/examples/gno.land/p/demo/avl/rolist/rolist_test.gno new file mode 100644 index 00000000000..03b0a8cba30 --- /dev/null +++ b/examples/gno.land/p/demo/avl/rolist/rolist_test.gno @@ -0,0 +1,162 @@ +package rolist + +import ( + "testing" + + "gno.land/p/demo/avl/list" +) + +func TestExample(t *testing.T) { + // User represents our internal data structure + type User struct { + ID string + Name string + Balance int + Internal string // sensitive internal data + } + + // Create and populate the original list + l := &list.List{} + l.Append( + &User{ + ID: "1", + Name: "Alice", + Balance: 100, + Internal: "sensitive_data_1", + }, + &User{ + ID: "2", + Name: "Bob", + Balance: 200, + Internal: "sensitive_data_2", + }, + ) + + // Define a makeEntrySafeFn that: + // 1. Creates a defensive copy of the User struct + // 2. Omits sensitive internal data + makeEntrySafeFn := func(v interface{}) interface{} { + originalUser := v.(*User) + return &User{ + ID: originalUser.ID, + Name: originalUser.Name, + Balance: originalUser.Balance, + Internal: "", // Omit sensitive data + } + } + + // Create a read-only view of the list + roList := Wrap(l, makeEntrySafeFn) + + // Test retrieving and verifying a user + t.Run("Get User", func(t *testing.T) { + // Get user from read-only list + value := roList.Get(0) + if value == nil { + t.Fatal("User at index 0 not found") + } + + user := value.(*User) + + // Verify user data is correct + if user.Name != "Alice" || user.Balance != 100 { + t.Errorf("Unexpected user data: got name=%s balance=%d", user.Name, user.Balance) + } + + // Verify sensitive data is not exposed + if user.Internal != "" { + t.Error("Sensitive data should not be exposed") + } + + // Verify it's a different instance than the original + originalUser := l.Get(0).(*User) + if user == originalUser { + t.Error("Read-only list should return a copy, not the original pointer") + } + }) + + // Test slice functionality + t.Run("Slice Users", func(t *testing.T) { + users := roList.Slice(0, 2) + if len(users) != 2 { + t.Fatalf("Expected 2 users, got %d", len(users)) + } + + for _, v := range users { + user := v.(*User) + if user.Internal != "" { + t.Error("Sensitive data exposed in slice") + } + } + }) + + // Test ForEach functionality + t.Run("ForEach Users", func(t *testing.T) { + count := 0 + roList.ForEach(func(index int, value interface{}) bool { + user := value.(*User) + if user.Internal != "" { + t.Error("Sensitive data exposed during iteration") + } + count++ + return false + }) + + if count != 2 { + t.Errorf("Expected 2 users, got %d", count) + } + }) +} + +func TestNilMakeEntrySafeFn(t *testing.T) { + // Create a list with some test data + l := &list.List{} + originalValue := []int{1, 2, 3} + l.Append(originalValue) + + // Create a ReadOnlyList with nil makeEntrySafeFn + roList := Wrap(l, nil) + + // Test that we get back the original value + value := roList.Get(0) + if value == nil { + t.Fatal("Value not found") + } + + // Verify it's the exact same slice (not a copy) + retrievedSlice := value.([]int) + if &retrievedSlice[0] != &originalValue[0] { + t.Error("Expected to get back the original slice reference") + } +} + +func TestReadOnlyList(t *testing.T) { + // Example of a makeEntrySafeFn that appends "_readonly" to demonstrate transformation + makeEntrySafeFn := func(value interface{}) interface{} { + return value.(string) + "_readonly" + } + + l := &list.List{} + l.Append("value1", "value2", "value3") + + roList := Wrap(l, makeEntrySafeFn) + + tests := []struct { + name string + index int + expected interface{} + }{ + {"ExistingIndex0", 0, "value1_readonly"}, + {"ExistingIndex1", 1, "value2_readonly"}, + {"NonExistingIndex", 3, nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value := roList.Get(tt.index) + if value != tt.expected { + t.Errorf("For index %d, expected %v, got %v", tt.index, tt.expected, value) + } + }) + } +} diff --git a/examples/gno.land/p/demo/avl/rotree/gno.mod b/examples/gno.land/p/demo/avl/rotree/gno.mod new file mode 100644 index 00000000000..d2cb439b2eb --- /dev/null +++ b/examples/gno.land/p/demo/avl/rotree/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/avl/rotree diff --git a/examples/gno.land/p/demo/avl/rotree/rotree.gno b/examples/gno.land/p/demo/avl/rotree/rotree.gno new file mode 100644 index 00000000000..17cb4e20ced --- /dev/null +++ b/examples/gno.land/p/demo/avl/rotree/rotree.gno @@ -0,0 +1,177 @@ +// Package rotree provides a read-only wrapper for avl.Tree with safe value transformation. +// +// It is useful when you want to expose a read-only view of a tree while ensuring that +// the sensitive data cannot be modified. +// +// Example: +// +// // Define a user structure with sensitive data +// type User struct { +// Name string +// Balance int +// Internal string // sensitive field +// } +// +// // Create and populate the original tree +// privateTree := avl.NewTree() +// privateTree.Set("alice", &User{ +// Name: "Alice", +// Balance: 100, +// Internal: "sensitive", +// }) +// +// // Create a safe transformation function that copies the struct +// // while excluding sensitive data +// makeEntrySafeFn := func(v interface{}) interface{} { +// u := v.(*User) +// return &User{ +// Name: u.Name, +// Balance: u.Balance, +// Internal: "", // omit sensitive data +// } +// } +// +// // Create a read-only view of the tree +// PublicTree := rotree.Wrap(tree, makeEntrySafeFn) +// +// // Safely access the data +// value, _ := roTree.Get("alice") +// user := value.(*User) +// // user.Name == "Alice" +// // user.Balance == 100 +// // user.Internal == "" (sensitive data is filtered) +package rotree + +import ( + "gno.land/p/demo/avl" +) + +// Wrap creates a new ReadOnlyTree from an existing avl.Tree and a safety transformation function. +// If makeEntrySafeFn is nil, values will be returned as-is without transformation. +// +// makeEntrySafeFn is a function that transforms a tree entry into a safe version that can be exposed to external users. +// This function should be implemented based on the specific safety requirements of your use case: +// +// 1. No-op transformation: For primitive types (int, string, etc.) or already safe objects, +// simply pass nil as the makeEntrySafeFn to return values as-is. +// +// 2. Defensive copying: For mutable types like slices or maps, you should create a deep copy +// to prevent modification of the original data. +// Example: func(v interface{}) interface{} { return append([]int{}, v.([]int)...) } +// +// 3. Read-only wrapper: Return a read-only version of the object that implements +// a limited interface. +// Example: func(v interface{}) interface{} { return NewReadOnlyObject(v) } +// +// 4. DAO transformation: Transform the object into a data access object that +// controls how the underlying data can be accessed. +// Example: func(v interface{}) interface{} { return NewDAO(v) } +// +// The function ensures that the returned object is safe to expose to untrusted code, +// preventing unauthorized modifications to the original data structure. +func Wrap(tree *avl.Tree, makeEntrySafeFn func(interface{}) interface{}) *ReadOnlyTree { + return &ReadOnlyTree{ + tree: tree, + makeEntrySafeFn: makeEntrySafeFn, + } +} + +// ReadOnlyTree wraps an avl.Tree and provides read-only access. +type ReadOnlyTree struct { + tree *avl.Tree + makeEntrySafeFn func(interface{}) interface{} +} + +// IReadOnlyTree defines the read-only operations available on a tree. +type IReadOnlyTree interface { + Size() int + Has(key string) bool + Get(key string) (interface{}, bool) + GetByIndex(index int) (string, interface{}) + Iterate(start, end string, cb avl.IterCbFn) bool + ReverseIterate(start, end string, cb avl.IterCbFn) bool + IterateByOffset(offset int, count int, cb avl.IterCbFn) bool + ReverseIterateByOffset(offset int, count int, cb avl.IterCbFn) bool +} + +// Verify that ReadOnlyTree implements both ITree and IReadOnlyTree +var ( + _ avl.ITree = (*ReadOnlyTree)(nil) + _ IReadOnlyTree = (*ReadOnlyTree)(nil) +) + +// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value +func (roTree *ReadOnlyTree) getSafeValue(value interface{}) interface{} { + if roTree.makeEntrySafeFn == nil { + return value + } + return roTree.makeEntrySafeFn(value) +} + +// Size returns the number of key-value pairs in the tree. +func (roTree *ReadOnlyTree) Size() int { + return roTree.tree.Size() +} + +// Has checks whether a key exists in the tree. +func (roTree *ReadOnlyTree) Has(key string) bool { + return roTree.tree.Has(key) +} + +// Get retrieves the value associated with the given key, converted to a safe format. +func (roTree *ReadOnlyTree) Get(key string) (interface{}, bool) { + value, exists := roTree.tree.Get(key) + if !exists { + return nil, false + } + return roTree.getSafeValue(value), true +} + +// GetByIndex retrieves the key-value pair at the specified index in the tree, with the value converted to a safe format. +func (roTree *ReadOnlyTree) GetByIndex(index int) (string, interface{}) { + key, value := roTree.tree.GetByIndex(index) + return key, roTree.getSafeValue(value) +} + +// Iterate performs an in-order traversal of the tree within the specified key range. +func (roTree *ReadOnlyTree) Iterate(start, end string, cb avl.IterCbFn) bool { + return roTree.tree.Iterate(start, end, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range. +func (roTree *ReadOnlyTree) ReverseIterate(start, end string, cb avl.IterCbFn) bool { + return roTree.tree.ReverseIterate(start, end, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// IterateByOffset performs an in-order traversal of the tree starting from the specified offset. +func (roTree *ReadOnlyTree) IterateByOffset(offset int, count int, cb avl.IterCbFn) bool { + return roTree.tree.IterateByOffset(offset, count, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset. +func (roTree *ReadOnlyTree) ReverseIterateByOffset(offset int, count int, cb avl.IterCbFn) bool { + return roTree.tree.ReverseIterateByOffset(offset, count, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// Set is not supported on ReadOnlyTree and will panic. +func (roTree *ReadOnlyTree) Set(key string, value interface{}) bool { + panic("Set operation not supported on ReadOnlyTree") +} + +// Remove is not supported on ReadOnlyTree and will panic. +func (roTree *ReadOnlyTree) Remove(key string) (value interface{}, removed bool) { + panic("Remove operation not supported on ReadOnlyTree") +} + +// RemoveByIndex is not supported on ReadOnlyTree and will panic. +func (roTree *ReadOnlyTree) RemoveByIndex(index int) (key string, value interface{}) { + panic("RemoveByIndex operation not supported on ReadOnlyTree") +} diff --git a/examples/gno.land/p/demo/avl/rotree/rotree_test.gno b/examples/gno.land/p/demo/avl/rotree/rotree_test.gno new file mode 100644 index 00000000000..fbc14bd688d --- /dev/null +++ b/examples/gno.land/p/demo/avl/rotree/rotree_test.gno @@ -0,0 +1,222 @@ +package rotree + +import ( + "testing" + + "gno.land/p/demo/avl" +) + +func TestExample(t *testing.T) { + // User represents our internal data structure + type User struct { + ID string + Name string + Balance int + Internal string // sensitive internal data + } + + // Create and populate the original tree with user pointers + tree := avl.NewTree() + tree.Set("alice", &User{ + ID: "1", + Name: "Alice", + Balance: 100, + Internal: "sensitive_data_1", + }) + tree.Set("bob", &User{ + ID: "2", + Name: "Bob", + Balance: 200, + Internal: "sensitive_data_2", + }) + + // Define a makeEntrySafeFn that: + // 1. Creates a defensive copy of the User struct + // 2. Omits sensitive internal data + makeEntrySafeFn := func(v interface{}) interface{} { + originalUser := v.(*User) + return &User{ + ID: originalUser.ID, + Name: originalUser.Name, + Balance: originalUser.Balance, + Internal: "", // Omit sensitive data + } + } + + // Create a read-only view of the tree + roTree := Wrap(tree, makeEntrySafeFn) + + // Test retrieving and verifying a user + t.Run("Get User", func(t *testing.T) { + // Get user from read-only tree + value, exists := roTree.Get("alice") + if !exists { + t.Fatal("User 'alice' not found") + } + + user := value.(*User) + + // Verify user data is correct + if user.Name != "Alice" || user.Balance != 100 { + t.Errorf("Unexpected user data: got name=%s balance=%d", user.Name, user.Balance) + } + + // Verify sensitive data is not exposed + if user.Internal != "" { + t.Error("Sensitive data should not be exposed") + } + + // Verify it's a different instance than the original + originalValue, _ := tree.Get("alice") + originalUser := originalValue.(*User) + if user == originalUser { + t.Error("Read-only tree should return a copy, not the original pointer") + } + }) + + // Test iterating over users + t.Run("Iterate Users", func(t *testing.T) { + count := 0 + roTree.Iterate("", "", func(key string, value interface{}) bool { + user := value.(*User) + // Verify each user has empty Internal field + if user.Internal != "" { + t.Error("Sensitive data exposed during iteration") + } + count++ + return false + }) + + if count != 2 { + t.Errorf("Expected 2 users, got %d", count) + } + }) + + // Verify that modifications to the returned user don't affect the original + t.Run("Modification Safety", func(t *testing.T) { + value, _ := roTree.Get("alice") + user := value.(*User) + + // Try to modify the returned user + user.Balance = 999 + user.Internal = "hacked" + + // Verify original is unchanged + originalValue, _ := tree.Get("alice") + originalUser := originalValue.(*User) + if originalUser.Balance != 100 || originalUser.Internal != "sensitive_data_1" { + t.Error("Original user data was modified") + } + }) +} + +func TestReadOnlyTree(t *testing.T) { + // Example of a makeEntrySafeFn that appends "_readonly" to demonstrate transformation + makeEntrySafeFn := func(value interface{}) interface{} { + return value.(string) + "_readonly" + } + + tree := avl.NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + roTree := Wrap(tree, makeEntrySafeFn) + + tests := []struct { + name string + key string + expected interface{} + exists bool + }{ + {"ExistingKey1", "key1", "value1_readonly", true}, + {"ExistingKey2", "key2", "value2_readonly", true}, + {"NonExistingKey", "key4", nil, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value, exists := roTree.Get(tt.key) + if exists != tt.exists || value != tt.expected { + t.Errorf("For key %s, expected %v (exists: %v), got %v (exists: %v)", tt.key, tt.expected, tt.exists, value, exists) + } + }) + } +} + +// Add example tests showing different makeEntrySafeFn implementations +func TestMakeEntrySafeFnVariants(t *testing.T) { + tree := avl.NewTree() + tree.Set("slice", []int{1, 2, 3}) + tree.Set("map", map[string]int{"a": 1}) + + tests := []struct { + name string + makeEntrySafeFn func(interface{}) interface{} + key string + validate func(t *testing.T, value interface{}) + }{ + { + name: "Defensive Copy Slice", + makeEntrySafeFn: func(v interface{}) interface{} { + original := v.([]int) + return append([]int{}, original...) + }, + key: "slice", + validate: func(t *testing.T, value interface{}) { + slice := value.([]int) + // Modify the returned slice + slice[0] = 999 + // Verify original is unchanged + originalValue, _ := tree.Get("slice") + original := originalValue.([]int) + if original[0] != 1 { + t.Error("Original slice was modified") + } + }, + }, + // Add more test cases for different makeEntrySafeFn implementations + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + roTree := Wrap(tree, tt.makeEntrySafeFn) + value, exists := roTree.Get(tt.key) + if !exists { + t.Fatal("Key not found") + } + tt.validate(t, value) + }) + } +} + +func TestNilMakeEntrySafeFn(t *testing.T) { + // Create a tree with some test data + tree := avl.NewTree() + originalValue := []int{1, 2, 3} + tree.Set("test", originalValue) + + // Create a ReadOnlyTree with nil makeEntrySafeFn + roTree := Wrap(tree, nil) + + // Test that we get back the original value + value, exists := roTree.Get("test") + if !exists { + t.Fatal("Key not found") + } + + // Verify it's the exact same slice (not a copy) + retrievedSlice := value.([]int) + if &retrievedSlice[0] != &originalValue[0] { + t.Error("Expected to get back the original slice reference") + } + + // Test through iteration as well + roTree.Iterate("", "", func(key string, value interface{}) bool { + retrievedSlice := value.([]int) + if &retrievedSlice[0] != &originalValue[0] { + t.Error("Expected to get back the original slice reference in iteration") + } + return false + }) +} diff --git a/examples/gno.land/p/demo/avl/tree.gno b/examples/gno.land/p/demo/avl/tree.gno index e7aa55eb7e4..3834246d2cd 100644 --- a/examples/gno.land/p/demo/avl/tree.gno +++ b/examples/gno.land/p/demo/avl/tree.gno @@ -1,5 +1,23 @@ package avl +type ITree interface { + // read operations + + Size() int + Has(key string) bool + Get(key string) (value interface{}, exists bool) + GetByIndex(index int) (key string, value interface{}) + Iterate(start, end string, cb IterCbFn) bool + ReverseIterate(start, end string, cb IterCbFn) bool + IterateByOffset(offset int, count int, cb IterCbFn) bool + ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool + + // write operations + + Set(key string, value interface{}) (updated bool) + Remove(key string) (value interface{}, removed bool) +} + type IterCbFn func(key string, value interface{}) bool //---------------------------------------- @@ -101,3 +119,6 @@ func (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) boo }, ) } + +// Verify that Tree implements TreeInterface +var _ ITree = (*Tree)(nil) diff --git a/examples/gno.land/p/demo/avl/z_0_filetest.gno b/examples/gno.land/p/demo/avl/z_0_filetest.gno index aff79ffabc6..1db1adebd3e 100644 --- a/examples/gno.land/p/demo/avl/z_0_filetest.gno +++ b/examples/gno.land/p/demo/avl/z_0_filetest.gno @@ -215,116 +215,3 @@ func main() { // } // } // } -// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:2]={ -// "Blank": {}, -// "ObjectInfo": { -// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:2", -// "IsEscaped": true, -// "ModTime": "5", -// "RefCount": "2" -// }, -// "Parent": null, -// "Source": { -// "@type": "/gno.RefNode", -// "BlockNode": null, -// "Location": { -// "Column": "0", -// "File": "", -// "Line": "0", -// "PkgPath": "gno.land/r/test" -// } -// }, -// "Values": [ -// { -// "T": { -// "@type": "/gno.PointerType", -// "Elt": { -// "@type": "/gno.RefType", -// "ID": "gno.land/p/demo/avl.Node" -// } -// }, -// "V": { -// "@type": "/gno.PointerValue", -// "Base": { -// "@type": "/gno.RefValue", -// "Hash": "ae86874f9b47fa5e64c30b3e92e9d07f2ec967a4", -// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:6" -// }, -// "Index": "0", -// "TV": null -// } -// }, -// { -// "T": { -// "@type": "/gno.FuncType", -// "Params": [], -// "Results": [] -// }, -// "V": { -// "@type": "/gno.FuncValue", -// "Closure": { -// "@type": "/gno.RefValue", -// "Escaped": true, -// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" -// }, -// "FileName": "main.gno", -// "IsMethod": false, -// "Name": "init.1", -// "NativeName": "", -// "NativePkg": "", -// "PkgPath": "gno.land/r/test", -// "Source": { -// "@type": "/gno.RefNode", -// "BlockNode": null, -// "Location": { -// "Column": "1", -// "File": "main.gno", -// "Line": "10", -// "PkgPath": "gno.land/r/test" -// } -// }, -// "Type": { -// "@type": "/gno.FuncType", -// "Params": [], -// "Results": [] -// } -// } -// }, -// { -// "T": { -// "@type": "/gno.FuncType", -// "Params": [], -// "Results": [] -// }, -// "V": { -// "@type": "/gno.FuncValue", -// "Closure": { -// "@type": "/gno.RefValue", -// "Escaped": true, -// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" -// }, -// "FileName": "main.gno", -// "IsMethod": false, -// "Name": "main", -// "NativeName": "", -// "NativePkg": "", -// "PkgPath": "gno.land/r/test", -// "Source": { -// "@type": "/gno.RefNode", -// "BlockNode": null, -// "Location": { -// "Column": "1", -// "File": "main.gno", -// "Line": "15", -// "PkgPath": "gno.land/r/test" -// } -// }, -// "Type": { -// "@type": "/gno.FuncType", -// "Params": [], -// "Results": [] -// } -// } -// } -// ] -// } diff --git a/examples/gno.land/p/demo/avl/z_1_filetest.gno b/examples/gno.land/p/demo/avl/z_1_filetest.gno index 3b6d40d5ecd..572c49333bc 100644 --- a/examples/gno.land/p/demo/avl/z_1_filetest.gno +++ b/examples/gno.land/p/demo/avl/z_1_filetest.gno @@ -24,6 +24,44 @@ func main() { // Realm: // switchrealm["gno.land/r/test"] +// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:6]={ +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:6", +// "ModTime": "11", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:5", +// "RefCount": "1" +// }, +// "Value": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "1375f6f96a1a3f298347dc8fc0065afa36cb7f0f", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:7" +// } +// } +// } +// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:8]={ +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:8", +// "ModTime": "13", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:5", +// "RefCount": "1" +// }, +// "Value": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "b28057ab7be6383785c0a5503e8a531bdbc21851", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:9" +// } +// } +// } // c[a8ada09dee16d791fd406d629fe29bb0ed084a30:15]={ // "Fields": [ // { @@ -143,7 +181,7 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "2f3adc5d0f2a3fe0331cfa93572a7abdde14c9aa", +// "Hash": "cafae89e4d4aaaefe7fdf0691084508d4274a981", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:8" // }, // "Index": "0", @@ -191,7 +229,7 @@ func main() { // }, // "V": { // "@type": "/gno.RefValue", -// "Hash": "fe20a19f956511f274dc77854e9e5468387260f4", +// "Hash": "b2e446f490656c19a83c43055de29c96e92a1549", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:13" // } // } @@ -235,7 +273,7 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "c89a71bdf045e8bde2059dc9d33839f916e02e5d", +// "Hash": "4e56eeb96eb1d9b27cf603140cd03a1622b6358b", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:6" // }, // "Index": "0", @@ -254,7 +292,7 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "90fa67f8c47db4b9b2a60425dff08d5a3385100f", +// "Hash": "7b61530859954d1d14b2f696c91c5f37d39c21e7", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:12" // }, // "Index": "0", @@ -283,123 +321,10 @@ func main() { // }, // "V": { // "@type": "/gno.RefValue", -// "Hash": "83e42caaf53070dd95b5f859053eb51ed900bbda", +// "Hash": "fedc6d430b38c985dc6a985b2fcaee97e88ba6da", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:11" // } // } // } -// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:2]={ -// "Blank": {}, -// "ObjectInfo": { -// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:2", -// "IsEscaped": true, -// "ModTime": "9", -// "RefCount": "2" -// }, -// "Parent": null, -// "Source": { -// "@type": "/gno.RefNode", -// "BlockNode": null, -// "Location": { -// "Column": "0", -// "File": "", -// "Line": "0", -// "PkgPath": "gno.land/r/test" -// } -// }, -// "Values": [ -// { -// "T": { -// "@type": "/gno.PointerType", -// "Elt": { -// "@type": "/gno.RefType", -// "ID": "gno.land/p/demo/avl.Node" -// } -// }, -// "V": { -// "@type": "/gno.PointerValue", -// "Base": { -// "@type": "/gno.RefValue", -// "Hash": "1faa9fa4ba1935121a6d3f0a623772e9d4499b0a", -// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:10" -// }, -// "Index": "0", -// "TV": null -// } -// }, -// { -// "T": { -// "@type": "/gno.FuncType", -// "Params": [], -// "Results": [] -// }, -// "V": { -// "@type": "/gno.FuncValue", -// "Closure": { -// "@type": "/gno.RefValue", -// "Escaped": true, -// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" -// }, -// "FileName": "main.gno", -// "IsMethod": false, -// "Name": "init.1", -// "NativeName": "", -// "NativePkg": "", -// "PkgPath": "gno.land/r/test", -// "Source": { -// "@type": "/gno.RefNode", -// "BlockNode": null, -// "Location": { -// "Column": "1", -// "File": "main.gno", -// "Line": "10", -// "PkgPath": "gno.land/r/test" -// } -// }, -// "Type": { -// "@type": "/gno.FuncType", -// "Params": [], -// "Results": [] -// } -// } -// }, -// { -// "T": { -// "@type": "/gno.FuncType", -// "Params": [], -// "Results": [] -// }, -// "V": { -// "@type": "/gno.FuncValue", -// "Closure": { -// "@type": "/gno.RefValue", -// "Escaped": true, -// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" -// }, -// "FileName": "main.gno", -// "IsMethod": false, -// "Name": "main", -// "NativeName": "", -// "NativePkg": "", -// "PkgPath": "gno.land/r/test", -// "Source": { -// "@type": "/gno.RefNode", -// "BlockNode": null, -// "Location": { -// "Column": "1", -// "File": "main.gno", -// "Line": "15", -// "PkgPath": "gno.land/r/test" -// } -// }, -// "Type": { -// "@type": "/gno.FuncType", -// "Params": [], -// "Results": [] -// } -// } -// } -// ] -// } // d[a8ada09dee16d791fd406d629fe29bb0ed084a30:4] // d[a8ada09dee16d791fd406d629fe29bb0ed084a30:5] diff --git a/examples/gno.land/p/demo/avl/z_2_filetest.gno b/examples/gno.land/p/demo/avl/z_2_filetest.gno index 43067c31e8f..c45088075d6 100644 --- a/examples/gno.land/p/demo/avl/z_2_filetest.gno +++ b/examples/gno.land/p/demo/avl/z_2_filetest.gno @@ -23,6 +23,44 @@ func main() { // Realm: // switchrealm["gno.land/r/test"] +// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:7]={ +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:7", +// "ModTime": "12", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:6", +// "RefCount": "1" +// }, +// "Value": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "ba7550123807b8da857e38b72f66204b1ec582a2", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:8" +// } +// } +// } +// u[a8ada09dee16d791fd406d629fe29bb0ed084a30:9]={ +// "ObjectInfo": { +// "ID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:9", +// "ModTime": "14", +// "OwnerID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:6", +// "RefCount": "1" +// }, +// "Value": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/p/demo/avl.Node" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "3cb8485664c356fcb5c88dfb96b7455133a6b022", +// "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:10" +// } +// } +// } // c[a8ada09dee16d791fd406d629fe29bb0ed084a30:16]={ // "Fields": [ // { @@ -142,7 +180,7 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "849a50d6c78d65742752e3c89ad8dd556e2e63cb", +// "Hash": "db39c9c0a60e0d5b30dbaf9be6150d3fec16aa4b", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:9" // }, // "Index": "0", @@ -190,7 +228,7 @@ func main() { // }, // "V": { // "@type": "/gno.RefValue", -// "Hash": "a1160b0060ad752dbfe5fe436f7734bb19136150", +// "Hash": "2e9127534f91b385426d76e8e164f50f635cc1de", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:14" // } // } @@ -234,7 +272,7 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "fd95e08763159ac529e26986d652e752e78b6325", +// "Hash": "43e03b0c877b40c34e12bc2b15560e8ecd42ae9d", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:7" // }, // "Index": "0", @@ -253,7 +291,7 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "3ecdcf148fe2f9e97b72a3bedf303b2ba56d4f4b", +// "Hash": "4b123e2424d900a427f9dee88a70ce61f3cdcf5b", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:13" // }, // "Index": "0", @@ -282,7 +320,7 @@ func main() { // }, // "V": { // "@type": "/gno.RefValue", -// "Hash": "63126557dba88f8556f7a0ccbbfc1d218ae7a302", +// "Hash": "76d9227e755efd6674d8fa34e12decb7a9855488", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:12" // } // } @@ -301,7 +339,7 @@ func main() { // "@type": "/gno.PointerValue", // "Base": { // "@type": "/gno.RefValue", -// "Hash": "d31c7e797793e03ffe0bbcb72f963264f8300d22", +// "Hash": "ff46b4dd63457c3fd59801e725f65af524ec829d", // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:11" // }, // "Index": "0", diff --git a/examples/gno.land/p/demo/avlhelpers/avlhelpers.gno b/examples/gno.land/p/demo/avlhelpers/avlhelpers.gno new file mode 100644 index 00000000000..e5fe33cacad --- /dev/null +++ b/examples/gno.land/p/demo/avlhelpers/avlhelpers.gno @@ -0,0 +1,41 @@ +package avlhelpers + +import ( + "gno.land/p/demo/avl" +) + +// Iterate the keys in-order starting from the given prefix. +// It calls the provided callback function for each key-value pair encountered. +// If the callback returns true, the iteration is stopped. +// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes. +func IterateByteStringKeysByPrefix(tree avl.ITree, prefix string, cb avl.IterCbFn) { + end := "" + n := len(prefix) + // To make the end of the search, increment the final character ASCII by one. + for n > 0 { + if ascii := int(prefix[n-1]); ascii < 0xff { + end = prefix[0:n-1] + string(ascii+1) + break + } + + // The last character is 0xff. Try the previous character. + n-- + } + + tree.Iterate(prefix, end, cb) +} + +// Get a list of keys starting from the given prefix. Limit the +// number of results to maxResults. +// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes. +func ListByteStringKeysByPrefix(tree avl.ITree, prefix string, maxResults int) []string { + result := []string{} + IterateByteStringKeysByPrefix(tree, prefix, func(key string, value interface{}) bool { + result = append(result, key) + if len(result) >= maxResults { + return true + } + return false + }) + return result +} diff --git a/examples/gno.land/p/demo/avlhelpers/gno.mod b/examples/gno.land/p/demo/avlhelpers/gno.mod new file mode 100644 index 00000000000..5adffd13a43 --- /dev/null +++ b/examples/gno.land/p/demo/avlhelpers/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/avlhelpers diff --git a/examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno b/examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno new file mode 100644 index 00000000000..5ecda41d1a6 --- /dev/null +++ b/examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno @@ -0,0 +1,91 @@ +// PKGPATH: gno.land/r/test +package test + +import ( + "encoding/hex" + + "gno.land/p/demo/avl" + "gno.land/p/demo/avlhelpers" + "gno.land/p/demo/ufmt" +) + +func main() { + tree := avl.NewTree() + + { + // Empty tree. + matches := avlhelpers.ListByteStringKeysByPrefix(tree, "", 10) + println(ufmt.Sprintf("# matches: %d", len(matches))) + } + + tree.Set("alice", "") + tree.Set("andy", "") + tree.Set("bob", "") + + { + // Match only alice. + matches := avlhelpers.ListByteStringKeysByPrefix(tree, "al", 10) + println(ufmt.Sprintf("# matches: %d", len(matches))) + println("match: " + matches[0]) + } + + { + // Match alice and andy. + matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a", 10) + println(ufmt.Sprintf("# matches: %d", len(matches))) + println("match: " + matches[0]) + println("match: " + matches[1]) + } + + { + // Match alice and andy limited to 1. + matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a", 1) + println(ufmt.Sprintf("# matches: %d", len(matches))) + println("match: " + matches[0]) + } + + tree = avl.NewTree() + tree.Set("a\xff", "") + tree.Set("a\xff\xff", "") + tree.Set("b", "") + tree.Set("\xff\xff\x00", "") + + { + // Match only "a\xff\xff". + matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a\xff\xff", 10) + println(ufmt.Sprintf("# matches: %d", len(matches))) + println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[0])))) + } + + { + // Match "a\xff" and "a\xff\xff". + matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a\xff", 10) + println(ufmt.Sprintf("# matches: %d", len(matches))) + println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[0])))) + println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[1])))) + } + + { + // Edge case: Match only "\xff\xff\x00". + matches := avlhelpers.ListByteStringKeysByPrefix(tree, "\xff\xff", 10) + println(ufmt.Sprintf("# matches: %d", len(matches))) + println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[0])))) + } +} + +// Output: +// # matches: 0 +// # matches: 1 +// match: alice +// # matches: 2 +// match: alice +// match: andy +// # matches: 1 +// match: alice +// # matches: 1 +// match: 61ffff +// # matches: 2 +// match: 61ff +// match: 61ffff +// # matches: 1 +// match: ffff00 diff --git a/examples/gno.land/p/demo/blog/gno.mod b/examples/gno.land/p/demo/blog/gno.mod index 65f58e7a0f6..e4e3def299b 100644 --- a/examples/gno.land/p/demo/blog/gno.mod +++ b/examples/gno.land/p/demo/blog/gno.mod @@ -1,7 +1 @@ module gno.land/p/demo/blog - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/mux v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/btree/btree.gno b/examples/gno.land/p/demo/btree/btree.gno new file mode 100644 index 00000000000..f909ec6bc91 --- /dev/null +++ b/examples/gno.land/p/demo/btree/btree.gno @@ -0,0 +1,1114 @@ +////////// +// +// Copyright 2014 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// Copyright 2024 New Tendermint +// +// This Gno port of the original Go BTree is substantially rewritten/reimplemented +// from the original, primarily for clarity of code, clarity of documentation, +// and for compatibility with Gno. +// +// Authors: +// Original version authors -- https://github.com/google/btree/graphs/contributors +// Kirk Haines +// +////////// + +// Package btree implements in-memory B-Trees of arbitrary degree. +// +// It has a flatter structure than an equivalent red-black or other binary tree, +// which may yield better memory usage and/or performance. +package btree + +import "sort" + +////////// +// +// Types +// +////////// + +// BTreeOption is a function interface for setting options on a btree with `New()`. +type BTreeOption func(*BTree) + +// BTree is an implementation of a B-Tree. +// +// BTree stores Record instances in an ordered structure, allowing easy insertion, +// removal, and iteration. +type BTree struct { + degree int + length int + root *node + cowCtx *copyOnWriteContext +} + +// Any type that implements this interface can be stored in the BTree. This allows considerable +// +// flexiblity in storage within the BTree. +type Record interface { + // Less compares self to `than`, returning true if self is less than `than` + Less(than Record) bool +} + +// records is the storage within a node. It is expressed as a slice of Record, where a Record +// is any struct that implements the Record interface. +type records []Record + +// node is an internal node in a tree. +// +// It must at all times maintain on of the two conditions: +// - len(children) == 0, len(records) unconstrained +// - len(children) == len(records) + 1 +type node struct { + records records + children children + cowCtx *copyOnWriteContext +} + +// children is the list of child nodes below the current node. It is a slice of nodes. +type children []*node + +// FreeNodeList represents a slice of nodes which are available for reuse. The default +// behavior of New() is for each BTree instance to have its own FreeNodeList. However, +// it is possible for multiple instances of BTree to share the same tree. If one uses +// New(WithFreeNodeList()) to create a tree, one may pass an existing FreeNodeList, allowing +// multiple trees to use a single list. In an application with multiple trees, it might +// be more efficient to allocate a single FreeNodeList with a significant initial capacity, +// and then have all of the trees use that same large FreeNodeList. +type FreeNodeList struct { + nodes []*node +} + +// copyOnWriteContext manages node ownership and ensures that cloned trees +// maintain isolation from each other when a node is changed. +// +// Ownership Rules: +// - Each node is associated with a specific copyOnWriteContext. +// - A tree can modify a node directly only if the tree's context matches the node's context. +// - If a tree attempts to modify a node with a different context, it must create a +// new, writable copy of that node (i.e., perform a clone) before making changes. +// +// Write Operation Invariant: +// - During any write operation, the current node being modified must have the same +// context as the tree requesting the write. +// - To maintain this invariant, before descending into a child node, the system checks +// if the child’s context matches the tree's context. +// - If the contexts match, the node can be modified in place. +// - If the contexts do not match, a mutable copy of the child node is created with the +// correct context before proceeding. +// +// Practical Implications: +// - The node currently being modified inherits the requesting tree's context, allowing +// in-place modifications. +// - Child nodes may initially have different contexts. Before any modification, these +// children are copied to ensure they share the correct context, enabling safe and +// isolated updates without affecting other trees that might be referencing the original nodes. +// +// Example Usage: +// When a tree performs a write operation (e.g., inserting or deleting a node), it uses +// its copyOnWriteContext to determine whether it can modify nodes directly or needs to +// create copies. This mechanism ensures that trees can share nodes efficiently while +// maintaining data integrity. +type copyOnWriteContext struct { + nodes *FreeNodeList +} + +// Record implements an interface with a single function, Less. Any type that implements +// RecordIterator allows callers of all of the iteration functions for the BTree +// to evaluate an element of the tree as it is traversed. The function will receive +// a stored element from the tree. The function must return either a true or a false value. +// True indicates that iteration should continue, while false indicates that it should halt. +type RecordIterator func(i Record) bool + +////////// +// +// Functions +// +////////// + +// NewFreeNodeList creates a new free list. +// size is the maximum size of the returned free list. +func NewFreeNodeList(size int) *FreeNodeList { + return &FreeNodeList{nodes: make([]*node, 0, size)} +} + +func (freeList *FreeNodeList) newNode() (nodeInstance *node) { + index := len(freeList.nodes) - 1 + if index < 0 { + return new(node) + } + nodeInstance = freeList.nodes[index] + freeList.nodes[index] = nil + freeList.nodes = freeList.nodes[:index] + + return nodeInstance +} + +// freeNode adds the given node to the list, returning true if it was added +// and false if it was discarded. + +func (freeList *FreeNodeList) freeNode(nodeInstance *node) (nodeWasAdded bool) { + if len(freeList.nodes) < cap(freeList.nodes) { + freeList.nodes = append(freeList.nodes, nodeInstance) + nodeWasAdded = true + } + return +} + +// A default size for the free node list. We might want to run some benchmarks to see if +// there are any pros or cons to this size versus other sizes. This seems to be a reasonable +// compromise to reduce GC pressure by reusing nodes where possible, without stacking up too +// much baggage in a given tree. +const DefaultFreeNodeListSize = 32 + +// WithDegree sets the degree of the B-Tree. +func WithDegree(degree int) BTreeOption { + return func(bt *BTree) { + if degree <= 1 { + panic("Degrees less than 1 do not make any sense for a BTree. Please provide a degree of 1 or greater.") + } + bt.degree = degree + } +} + +// WithFreeNodeList sets a custom free node list for the B-Tree. +func WithFreeNodeList(freeList *FreeNodeList) BTreeOption { + return func(bt *BTree) { + bt.cowCtx = ©OnWriteContext{nodes: freeList} + } +} + +// New creates a new B-Tree with optional configurations. If configuration is not provided, +// it will default to 16 element nodes. Degree may not be less than 1 (which effectively +// makes the tree into a binary tree). +// +// `New(WithDegree(2))`, for example, will create a 2-3-4 tree (each node contains 1-3 records +// and 2-4 children). +// +// `New(WithFreeNodeList(NewFreeNodeList(64)))` will create a tree with a degree of 16, and +// with a free node list with a size of 64. +func New(options ...BTreeOption) *BTree { + btree := &BTree{ + degree: 16, // default degree + cowCtx: ©OnWriteContext{nodes: NewFreeNodeList(DefaultFreeNodeListSize)}, + } + for _, opt := range options { + opt(btree) + } + return btree +} + +// insertAt inserts a value into the given index, pushing all subsequent values +// forward. +func (recordsSlice *records) insertAt(index int, newRecord Record) { + originalLength := len(*recordsSlice) + + // Extend the slice by one element + *recordsSlice = append(*recordsSlice, nil) + + // Move elements from the end to avoid overwriting during the copy + // TODO: Make this work with slice appends, instead. It should be faster? + if index < originalLength { + for position := originalLength; position > index; position-- { + (*recordsSlice)[position] = (*recordsSlice)[position-1] + } + } + + // Insert the new record + (*recordsSlice)[index] = newRecord +} + +// removeAt removes a Record from the records slice at the specified index. +// It shifts subsequent records to fill the gap and returns the removed Record. +func (recordSlicePointer *records) removeAt(index int) Record { + recordSlice := *recordSlicePointer + removedRecord := recordSlice[index] + copy(recordSlice[index:], recordSlice[index+1:]) + recordSlice[len(recordSlice)-1] = nil + *recordSlicePointer = recordSlice[:len(recordSlice)-1] + + return removedRecord +} + +// Pop removes and returns the last Record from the records slice. +// It also clears the reference to the removed Record to aid garbage collection. +func (r *records) pop() Record { + recordSlice := *r + lastIndex := len(recordSlice) - 1 + removedRecord := recordSlice[lastIndex] + recordSlice[lastIndex] = nil + *r = recordSlice[:lastIndex] + return removedRecord +} + +// This slice is intended only as a supply of records for the truncate function +// that follows, and it should not be changed or altered. +var emptyRecords = make(records, 32) + +// truncate reduces the length of the slice to the specified index, +// and clears the elements beyond that index to prevent memory leaks. +// The index must be less than or equal to the current length of the slice. +func (originalSlice *records) truncate(index int) { + // Split the slice into the part to keep and the part to clear. + recordsToKeep := (*originalSlice)[:index] + recordsToClear := (*originalSlice)[index:] + + // Update the original slice to only contain the records to keep. + *originalSlice = recordsToKeep + + // Clear the memory of the part that was truncated. + for len(recordsToClear) > 0 { + // Copy empty values from `emptyRecords` to the recordsToClear slice. + // This effectively "clears" the memory by overwriting elements. + numCleared := copy(recordsToClear, emptyRecords) + recordsToClear = recordsToClear[numCleared:] + } +} + +// Find determines the appropriate index at which a given Record should be inserted +// into the sorted records slice. If the Record already exists in the slice, +// the method returns its index and sets found to true. +// +// Parameters: +// - record: The Record to search for within the records slice. +// +// Returns: +// - insertIndex: The index at which the Record should be inserted. +// - found: A boolean indicating whether the Record already exists in the slice. +func (recordsSlice records) find(record Record) (insertIndex int, found bool) { + totalRecords := len(recordsSlice) + + // Perform a binary search to find the insertion point for the record + insertionPoint := sort.Search(totalRecords, func(currentIndex int) bool { + return record.Less(recordsSlice[currentIndex]) + }) + + if insertionPoint > 0 { + previousRecord := recordsSlice[insertionPoint-1] + + if !previousRecord.Less(record) { + return insertionPoint - 1, true + } + } + + return insertionPoint, false +} + +// insertAt inserts a value into the given index, pushing all subsequent values +// forward. +func (childSlice *children) insertAt(index int, n *node) { + originalLength := len(*childSlice) + + // Extend the slice by one element + *childSlice = append(*childSlice, nil) + + // Move elements from the end to avoid overwriting during the copy + if index < originalLength { + for i := originalLength; i > index; i-- { + (*childSlice)[i] = (*childSlice)[i-1] + } + } + + // Insert the new record + (*childSlice)[index] = n +} + +// removeAt removes a Record from the records slice at the specified index. +// It shifts subsequent records to fill the gap and returns the removed Record. +func (childSlicePointer *children) removeAt(index int) *node { + childSlice := *childSlicePointer + removedChild := childSlice[index] + copy(childSlice[index:], childSlice[index+1:]) + childSlice[len(childSlice)-1] = nil + *childSlicePointer = childSlice[:len(childSlice)-1] + + return removedChild +} + +// Pop removes and returns the last Record from the records slice. +// It also clears the reference to the removed Record to aid garbage collection. +func (childSlicePointer *children) pop() *node { + childSlice := *childSlicePointer + lastIndex := len(childSlice) - 1 + removedChild := childSlice[lastIndex] + childSlice[lastIndex] = nil + *childSlicePointer = childSlice[:lastIndex] + return removedChild +} + +// This slice is intended only as a supply of records for the truncate function +// that follows, and it should not be changed or altered. +var emptyChildren = make(children, 32) + +// truncate reduces the length of the slice to the specified index, +// and clears the elements beyond that index to prevent memory leaks. +// The index must be less than or equal to the current length of the slice. +func (originalSlice *children) truncate(index int) { + // Split the slice into the part to keep and the part to clear. + childrenToKeep := (*originalSlice)[:index] + childrenToClear := (*originalSlice)[index:] + + // Update the original slice to only contain the records to keep. + *originalSlice = childrenToKeep + + // Clear the memory of the part that was truncated. + for len(childrenToClear) > 0 { + // Copy empty values from `emptyChildren` to the recordsToClear slice. + // This effectively "clears" the memory by overwriting elements. + numCleared := copy(childrenToClear, emptyChildren) + + // Slice recordsToClear to exclude the elements that were just cleared. + childrenToClear = childrenToClear[numCleared:] + } +} + +// mutableFor creates a mutable copy of the node if the current node does not +// already belong to the provided copy-on-write context (COW). If the node is +// already associated with the given COW context, it returns the current node. +// +// Parameters: +// - cowCtx: The copy-on-write context that should own the returned node. +// +// Returns: +// - A pointer to the mutable node associated with the given COW context. +// +// If the current node belongs to a different COW context, this function: +// - Allocates a new node using the provided context. +// - Copies the node’s records and children slices into the newly allocated node. +// - Returns the new node which is now owned by the given COW context. +func (n *node) mutableFor(cowCtx *copyOnWriteContext) *node { + // If the current node is already owned by the provided context, return it as-is. + if n.cowCtx == cowCtx { + return n + } + + // Create a new node in the provided context. + newNode := cowCtx.newNode() + + // Copy the records from the current node into the new node. + newNode.records = append(newNode.records[:0], n.records...) + + // Copy the children from the current node into the new node. + newNode.children = append(newNode.children[:0], n.children...) + + return newNode +} + +// mutableChild ensures that the child node at the given index is mutable and +// associated with the same COW context as the parent node. If the child node +// belongs to a different context, a copy of the child is created and stored in the +// parent node. +// +// Parameters: +// - i: The index of the child node to be made mutable. +// +// Returns: +// - A pointer to the mutable child node. +func (n *node) mutableChild(i int) *node { + // Ensure that the child at index `i` is mutable and belongs to the same context as the parent. + mutableChildNode := n.children[i].mutableFor(n.cowCtx) + // Update the child node reference in the current node to the mutable version. + n.children[i] = mutableChildNode + return mutableChildNode +} + +// split splits the given node at the given index. The current node shrinks, +// and this function returns the record that existed at that index and a new node +// containing all records/children after it. +func (n *node) split(i int) (Record, *node) { + record := n.records[i] + next := n.cowCtx.newNode() + next.records = append(next.records, n.records[i+1:]...) + n.records.truncate(i) + if len(n.children) > 0 { + next.children = append(next.children, n.children[i+1:]...) + n.children.truncate(i + 1) + } + return record, next +} + +// maybeSplitChild checks if a child should be split, and if so splits it. +// Returns whether or not a split occurred. +func (n *node) maybeSplitChild(i, maxRecords int) bool { + if len(n.children[i].records) < maxRecords { + return false + } + first := n.mutableChild(i) + record, second := first.split(maxRecords / 2) + n.records.insertAt(i, record) + n.children.insertAt(i+1, second) + return true +} + +// insert adds a record to the subtree rooted at the current node, ensuring that no node in the subtree +// exceeds the maximum number of allowed records (`maxRecords`). If an equivalent record is already present, +// it replaces the existing one and returns it; otherwise, it returns nil. +// +// Parameters: +// - record: The record to be inserted. +// - maxRecords: The maximum number of records allowed per node. +// +// Returns: +// - The record that was replaced if an equivalent record already existed, otherwise nil. +func (n *node) insert(record Record, maxRecords int) Record { + // Find the position where the new record should be inserted and check if an equivalent record already exists. + insertionIndex, recordExists := n.records.find(record) + + if recordExists { + // If an equivalent record is found, replace it and return the old record. + existingRecord := n.records[insertionIndex] + n.records[insertionIndex] = record + return existingRecord + } + + // If the current node is a leaf (has no children), insert the new record at the calculated index. + if len(n.children) == 0 { + n.records.insertAt(insertionIndex, record) + return nil + } + + // Check if the child node at the insertion index needs to be split due to exceeding maxRecords. + if n.maybeSplitChild(insertionIndex, maxRecords) { + // If a split occurred, compare the new record with the record moved up to the current node. + splitRecord := n.records[insertionIndex] + switch { + case record.Less(splitRecord): + // The new record belongs to the first (left) split node; no change to insertion index. + case splitRecord.Less(record): + // The new record belongs to the second (right) split node; move the insertion index to the next position. + insertionIndex++ + default: + // If the record is equivalent to the split record, replace it and return the old record. + existingRecord := n.records[insertionIndex] + n.records[insertionIndex] = record + return existingRecord + } + } + + // Recursively insert the record into the appropriate child node, now guaranteed to have space. + return n.mutableChild(insertionIndex).insert(record, maxRecords) +} + +// get finds the given key in the subtree and returns it. +func (n *node) get(key Record) Record { + i, found := n.records.find(key) + if found { + return n.records[i] + } else if len(n.children) > 0 { + return n.children[i].get(key) + } + return nil +} + +// min returns the first record in the subtree. +func min(n *node) Record { + if n == nil { + return nil + } + for len(n.children) > 0 { + n = n.children[0] + } + if len(n.records) == 0 { + return nil + } + return n.records[0] +} + +// max returns the last record in the subtree. +func max(n *node) Record { + if n == nil { + return nil + } + for len(n.children) > 0 { + n = n.children[len(n.children)-1] + } + if len(n.records) == 0 { + return nil + } + return n.records[len(n.records)-1] +} + +// toRemove details what record to remove in a node.remove call. +type toRemove int + +const ( + removeRecord toRemove = iota // removes the given record + removeMin // removes smallest record in the subtree + removeMax // removes largest record in the subtree +) + +// remove removes a record from the subtree rooted at the current node. +// +// Parameters: +// - record: The record to be removed (can be nil when the removal type indicates min or max). +// - minRecords: The minimum number of records a node should have after removal. +// - typ: The type of removal operation to perform (removeMin, removeMax, or removeRecord). +// +// Returns: +// - The record that was removed, or nil if no such record was found. +func (n *node) remove(record Record, minRecords int, removalType toRemove) Record { + var targetIndex int + var recordFound bool + + // Determine the index of the record to remove based on the removal type. + switch removalType { + case removeMax: + // If this node is a leaf, remove and return the last record. + if len(n.children) == 0 { + return n.records.pop() + } + targetIndex = len(n.records) // The last record index for removing max. + + case removeMin: + // If this node is a leaf, remove and return the first record. + if len(n.children) == 0 { + return n.records.removeAt(0) + } + targetIndex = 0 // The first record index for removing min. + + case removeRecord: + // Locate the index of the record to be removed. + targetIndex, recordFound = n.records.find(record) + if len(n.children) == 0 { + if recordFound { + return n.records.removeAt(targetIndex) + } + return nil // The record was not found in the leaf node. + } + + default: + panic("invalid removal type") + } + + // If the current node has children, handle the removal recursively. + if len(n.children[targetIndex].records) <= minRecords { + // If the target child node has too few records, grow it before proceeding with removal. + return n.growChildAndRemove(targetIndex, record, minRecords, removalType) + } + + // Get a mutable reference to the child node at the target index. + targetChild := n.mutableChild(targetIndex) + + // If the record to be removed was found in the current node: + if recordFound { + // Replace the current record with its predecessor from the child node, and return the removed record. + replacedRecord := n.records[targetIndex] + n.records[targetIndex] = targetChild.remove(nil, minRecords, removeMax) + return replacedRecord + } + + // Recursively remove the record from the child node. + return targetChild.remove(record, minRecords, removalType) +} + +// growChildAndRemove grows child 'i' to make sure it's possible to remove an +// record from it while keeping it at minRecords, then calls remove to actually +// remove it. +// +// Most documentation says we have to do two sets of special casing: +// 1. record is in this node +// 2. record is in child +// +// In both cases, we need to handle the two subcases: +// +// A) node has enough values that it can spare one +// B) node doesn't have enough values +// +// For the latter, we have to check: +// +// a) left sibling has node to spare +// b) right sibling has node to spare +// c) we must merge +// +// To simplify our code here, we handle cases #1 and #2 the same: +// If a node doesn't have enough records, we make sure it does (using a,b,c). +// We then simply redo our remove call, and the second time (regardless of +// whether we're in case 1 or 2), we'll have enough records and can guarantee +// that we hit case A. +func (n *node) growChildAndRemove(i int, record Record, minRecords int, typ toRemove) Record { + if i > 0 && len(n.children[i-1].records) > minRecords { + // Steal from left child + child := n.mutableChild(i) + stealFrom := n.mutableChild(i - 1) + stolenRecord := stealFrom.records.pop() + child.records.insertAt(0, n.records[i-1]) + n.records[i-1] = stolenRecord + if len(stealFrom.children) > 0 { + child.children.insertAt(0, stealFrom.children.pop()) + } + } else if i < len(n.records) && len(n.children[i+1].records) > minRecords { + // steal from right child + child := n.mutableChild(i) + stealFrom := n.mutableChild(i + 1) + stolenRecord := stealFrom.records.removeAt(0) + child.records = append(child.records, n.records[i]) + n.records[i] = stolenRecord + if len(stealFrom.children) > 0 { + child.children = append(child.children, stealFrom.children.removeAt(0)) + } + } else { + if i >= len(n.records) { + i-- + } + child := n.mutableChild(i) + // merge with right child + mergeRecord := n.records.removeAt(i) + mergeChild := n.children.removeAt(i + 1).mutableFor(n.cowCtx) + child.records = append(child.records, mergeRecord) + child.records = append(child.records, mergeChild.records...) + child.children = append(child.children, mergeChild.children...) + n.cowCtx.freeNode(mergeChild) + } + return n.remove(record, minRecords, typ) +} + +type direction int + +const ( + descend = direction(-1) + ascend = direction(+1) +) + +// iterate provides a simple method for iterating over elements in the tree. +// +// When ascending, the 'start' should be less than 'stop' and when descending, +// the 'start' should be greater than 'stop'. Setting 'includeStart' to true +// will force the iterator to include the first record when it equals 'start', +// thus creating a "greaterOrEqual" or "lessThanEqual" rather than just a +// "greaterThan" or "lessThan" queries. +func (n *node) iterate(dir direction, start, stop Record, includeStart bool, hit bool, iter RecordIterator) (bool, bool) { + var ok, found bool + var index int + switch dir { + case ascend: + if start != nil { + index, _ = n.records.find(start) + } + for i := index; i < len(n.records); i++ { + if len(n.children) > 0 { + if hit, ok = n.children[i].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + if !includeStart && !hit && start != nil && !start.Less(n.records[i]) { + hit = true + continue + } + hit = true + if stop != nil && !n.records[i].Less(stop) { + return hit, false + } + if !iter(n.records[i]) { + return hit, false + } + } + if len(n.children) > 0 { + if hit, ok = n.children[len(n.children)-1].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + case descend: + if start != nil { + index, found = n.records.find(start) + if !found { + index = index - 1 + } + } else { + index = len(n.records) - 1 + } + for i := index; i >= 0; i-- { + if start != nil && !n.records[i].Less(start) { + if !includeStart || hit || start.Less(n.records[i]) { + continue + } + } + if len(n.children) > 0 { + if hit, ok = n.children[i+1].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + if stop != nil && !stop.Less(n.records[i]) { + return hit, false // continue + } + hit = true + if !iter(n.records[i]) { + return hit, false + } + } + if len(n.children) > 0 { + if hit, ok = n.children[0].iterate(dir, start, stop, includeStart, hit, iter); !ok { + return hit, false + } + } + } + return hit, true +} + +func (tree *BTree) Iterate(dir direction, start, stop Record, includeStart bool, hit bool, iter RecordIterator) (bool, bool) { + return tree.root.iterate(dir, start, stop, includeStart, hit, iter) +} + +// Clone creates a new BTree instance that shares the current tree's structure using a copy-on-write (COW) approach. +// +// How Cloning Works: +// - The cloned tree (`clonedTree`) shares the current tree’s nodes in a read-only state. This means that no additional memory +// is allocated for shared nodes, and read operations on the cloned tree are as fast as on the original tree. +// - When either the original tree (`t`) or the cloned tree (`clonedTree`) needs to perform a write operation (such as an insert, delete, etc.), +// a new copy of the affected nodes is created on-demand. This ensures that modifications to one tree do not affect the other. +// +// Performance Implications: +// - **Clone Creation:** The creation of a clone is inexpensive since it only involves copying references to the original tree's nodes +// and creating new copy-on-write contexts. +// - **Read Operations:** Reading from either the original tree or the cloned tree has no additional performance overhead compared to the original tree. +// - **Write Operations:** The first write operation on either tree may experience a slight slow-down due to the allocation of new nodes, +// but subsequent write operations will perform at the same speed as if the tree were not cloned. +// +// Returns: +// - A new BTree instance (`clonedTree`) that shares the original tree's structure. +func (t *BTree) Clone() *BTree { + // Create two independent copy-on-write contexts, one for the original tree (`t`) and one for the cloned tree. + originalContext := *t.cowCtx + clonedContext := *t.cowCtx + + // Create a shallow copy of the current tree, which will be the new cloned tree. + clonedTree := *t + + // Assign the new contexts to their respective trees. + t.cowCtx = &originalContext + clonedTree.cowCtx = &clonedContext + + return &clonedTree +} + +// maxRecords returns the max number of records to allow per node. +func (t *BTree) maxRecords() int { + return t.degree*2 - 1 +} + +// minRecords returns the min number of records to allow per node (ignored for the +// root node). +func (t *BTree) minRecords() int { + return t.degree - 1 +} + +func (c *copyOnWriteContext) newNode() (n *node) { + n = c.nodes.newNode() + n.cowCtx = c + return +} + +type freeType int + +const ( + ftFreelistFull freeType = iota // node was freed (available for GC, not stored in nodes) + ftStored // node was stored in the nodes for later use + ftNotOwned // node was ignored by COW, since it's owned by another one +) + +// freeNode frees a node within a given COW context, if it's owned by that +// context. It returns what happened to the node (see freeType const +// documentation). +func (c *copyOnWriteContext) freeNode(n *node) freeType { + if n.cowCtx == c { + // clear to allow GC + n.records.truncate(0) + n.children.truncate(0) + n.cowCtx = nil + if c.nodes.freeNode(n) { + return ftStored + } else { + return ftFreelistFull + } + } else { + return ftNotOwned + } +} + +// Insert adds the given record to the B-tree. If a record already exists in the tree with the same value, +// it is replaced, and the old record is returned. Otherwise, it returns nil. +// +// Notes: +// - The function panics if a nil record is provided as input. +// - If the root node is empty, a new root node is created and the record is inserted. +// +// Parameters: +// - record: The record to be inserted into the B-tree. +// +// Returns: +// - The replaced record if an equivalent record already exists, or nil if no replacement occurred. +func (t *BTree) Insert(record Record) Record { + if record == nil { + panic("nil record cannot be added to BTree") + } + + // If the tree is empty (no root), create a new root node and insert the record. + if t.root == nil { + t.root = t.cowCtx.newNode() + t.root.records = append(t.root.records, record) + t.length++ + return nil + } + + // Ensure that the root node is mutable (associated with the current tree's copy-on-write context). + t.root = t.root.mutableFor(t.cowCtx) + + // If the root node is full (contains the maximum number of records), split the root. + if len(t.root.records) >= t.maxRecords() { + // Split the root node, promoting the middle record and creating a new child node. + middleRecord, newChildNode := t.root.split(t.maxRecords() / 2) + + // Create a new root node to hold the promoted middle record. + oldRoot := t.root + t.root = t.cowCtx.newNode() + t.root.records = append(t.root.records, middleRecord) + t.root.children = append(t.root.children, oldRoot, newChildNode) + } + + // Insert the new record into the subtree rooted at the current root node. + replacedRecord := t.root.insert(record, t.maxRecords()) + + // If no record was replaced, increase the tree's length. + if replacedRecord == nil { + t.length++ + } + + return replacedRecord +} + +// Delete removes an record equal to the passed in record from the tree, returning +// it. If no such record exists, returns nil. +func (t *BTree) Delete(record Record) Record { + return t.deleteRecord(record, removeRecord) +} + +// DeleteMin removes the smallest record in the tree and returns it. +// If no such record exists, returns nil. +func (t *BTree) DeleteMin() Record { + return t.deleteRecord(nil, removeMin) +} + +// Shift is identical to DeleteMin. If the tree is thought of as an ordered list, then Shift() +// removes the element at the start of the list, the smallest element, and returns it. +func (t *BTree) Shift() Record { + return t.deleteRecord(nil, removeMin) +} + +// DeleteMax removes the largest record in the tree and returns it. +// If no such record exists, returns nil. +func (t *BTree) DeleteMax() Record { + return t.deleteRecord(nil, removeMax) +} + +// Pop is identical to DeleteMax. If the tree is thought of as an ordered list, then Shift() +// removes the element at the end of the list, the largest element, and returns it. +func (t *BTree) Pop() Record { + return t.deleteRecord(nil, removeMax) +} + +// deleteRecord removes a record from the B-tree based on the specified removal type (removeMin, removeMax, or removeRecord). +// It returns the removed record if it was found, or nil if no matching record was found. +// +// Parameters: +// - record: The record to be removed (can be nil if the removal type indicates min or max). +// - removalType: The type of removal operation to perform (removeMin, removeMax, or removeRecord). +// +// Returns: +// - The removed record if it existed in the tree, or nil if it was not found. +func (t *BTree) deleteRecord(record Record, removalType toRemove) Record { + // If the tree is empty or the root has no records, return nil. + if t.root == nil || len(t.root.records) == 0 { + return nil + } + + // Ensure the root node is mutable (associated with the tree's copy-on-write context). + t.root = t.root.mutableFor(t.cowCtx) + + // Attempt to remove the specified record from the root node. + removedRecord := t.root.remove(record, t.minRecords(), removalType) + + // Check if the root node has become empty but still has children. + // In this case, the tree height should be reduced, making the first child the new root. + if len(t.root.records) == 0 && len(t.root.children) > 0 { + oldRoot := t.root + t.root = t.root.children[0] + // Free the old root node, as it is no longer needed. + t.cowCtx.freeNode(oldRoot) + } + + // If a record was successfully removed, decrease the tree's length. + if removedRecord != nil { + t.length-- + } + + return removedRecord +} + +// AscendRange calls the iterator for every value in the tree within the range +// [greaterOrEqual, lessThan), until iterator returns false. +func (t *BTree) AscendRange(greaterOrEqual, lessThan Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, greaterOrEqual, lessThan, true, false, iterator) +} + +// AscendLessThan calls the iterator for every value in the tree within the range +// [first, pivot), until iterator returns false. +func (t *BTree) AscendLessThan(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, nil, pivot, false, false, iterator) +} + +// AscendGreaterOrEqual calls the iterator for every value in the tree within +// the range [pivot, last], until iterator returns false. +func (t *BTree) AscendGreaterOrEqual(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, pivot, nil, true, false, iterator) +} + +// Ascend calls the iterator for every value in the tree within the range +// [first, last], until iterator returns false. +func (t *BTree) Ascend(iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(ascend, nil, nil, false, false, iterator) +} + +// DescendRange calls the iterator for every value in the tree within the range +// [lessOrEqual, greaterThan), until iterator returns false. +func (t *BTree) DescendRange(lessOrEqual, greaterThan Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, lessOrEqual, greaterThan, true, false, iterator) +} + +// DescendLessOrEqual calls the iterator for every value in the tree within the range +// [pivot, first], until iterator returns false. +func (t *BTree) DescendLessOrEqual(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, pivot, nil, true, false, iterator) +} + +// DescendGreaterThan calls the iterator for every value in the tree within +// the range [last, pivot), until iterator returns false. +func (t *BTree) DescendGreaterThan(pivot Record, iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, nil, pivot, false, false, iterator) +} + +// Descend calls the iterator for every value in the tree within the range +// [last, first], until iterator returns false. +func (t *BTree) Descend(iterator RecordIterator) { + if t.root == nil { + return + } + t.root.iterate(descend, nil, nil, false, false, iterator) +} + +// Get looks for the key record in the tree, returning it. It returns nil if +// unable to find that record. +func (t *BTree) Get(key Record) Record { + if t.root == nil { + return nil + } + return t.root.get(key) +} + +// Min returns the smallest record in the tree, or nil if the tree is empty. +func (t *BTree) Min() Record { + return min(t.root) +} + +// Max returns the largest record in the tree, or nil if the tree is empty. +func (t *BTree) Max() Record { + return max(t.root) +} + +// Has returns true if the given key is in the tree. +func (t *BTree) Has(key Record) bool { + return t.Get(key) != nil +} + +// Len returns the number of records currently in the tree. +func (t *BTree) Len() int { + return t.length +} + +// Clear removes all elements from the B-tree. +// +// Parameters: +// - addNodesToFreelist: +// - If true, the tree's nodes are added to the freelist during the clearing process, +// up to the freelist's capacity. +// - If false, the root node is simply dereferenced, allowing Go's garbage collector +// to reclaim the memory. +// +// Benefits: +// - **Performance:** +// - Significantly faster than deleting each element individually, as it avoids the overhead +// of searching and updating the tree structure for each deletion. +// - More efficient than creating a new tree, since it reuses existing nodes by adding them +// to the freelist instead of discarding them to the garbage collector. +// +// Time Complexity: +// - **O(1):** +// - When `addNodesToFreelist` is false. +// - When `addNodesToFreelist` is true but the freelist is already full. +// - **O(freelist size):** +// - When adding nodes to the freelist up to its capacity. +// - **O(tree size):** +// - When iterating through all nodes to add to the freelist, but none can be added due to +// ownership by another tree. + +func (tree *BTree) Clear(addNodesToFreelist bool) { + if tree.root != nil && addNodesToFreelist { + tree.root.reset(tree.cowCtx) + } + tree.root = nil + tree.length = 0 +} + +// reset adds all nodes in the current subtree to the freelist. +// +// The function operates recursively: +// - It first attempts to reset all child nodes. +// - If the freelist becomes full at any point, the process stops immediately. +// +// Parameters: +// - copyOnWriteCtx: The copy-on-write context managing the freelist. +// +// Returns: +// - true: Indicates that the parent node should continue attempting to reset its nodes. +// - false: Indicates that the freelist is full and no further nodes should be added. +// +// Usage: +// This method is called during the `Clear` operation of the B-tree to efficiently reuse +// nodes by adding them to the freelist, thereby avoiding unnecessary allocations and reducing +// garbage collection overhead. +func (currentNode *node) reset(copyOnWriteCtx *copyOnWriteContext) bool { + // Iterate through each child node and attempt to reset it. + for _, childNode := range currentNode.children { + // If any child reset operation signals that the freelist is full, stop the process. + if !childNode.reset(copyOnWriteCtx) { + return false + } + } + + // Attempt to add the current node to the freelist. + // If the freelist is full after this operation, indicate to the parent to stop. + freelistStatus := copyOnWriteCtx.freeNode(currentNode) + return freelistStatus != ftFreelistFull +} diff --git a/examples/gno.land/p/demo/btree/btree_test.gno b/examples/gno.land/p/demo/btree/btree_test.gno new file mode 100644 index 00000000000..a0f7c1c55ca --- /dev/null +++ b/examples/gno.land/p/demo/btree/btree_test.gno @@ -0,0 +1,676 @@ +package btree + +import ( + "fmt" + "sort" + "testing" +) + +// Content represents a key-value pair where the Key can be either an int or string +// and the Value can be any type. +type Content struct { + Key interface{} + Value interface{} +} + +// Less compares two Content records by their Keys. +// The Key must be either an int or a string. +func (c Content) Less(than Record) bool { + other, ok := than.(Content) + if !ok { + panic("cannot compare: incompatible types") + } + + switch key := c.Key.(type) { + case int: + switch otherKey := other.Key.(type) { + case int: + return key < otherKey + case string: + return true // ints are always less than strings + default: + panic("unsupported key type: must be int or string") + } + case string: + switch otherKey := other.Key.(type) { + case int: + return false // strings are always greater than ints + case string: + return key < otherKey + default: + panic("unsupported key type: must be int or string") + } + default: + panic("unsupported key type: must be int or string") + } +} + +type ContentSlice []Content + +func (s ContentSlice) Len() int { + return len(s) +} + +func (s ContentSlice) Less(i, j int) bool { + return s[i].Less(s[j]) +} + +func (s ContentSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ContentSlice) Copy() ContentSlice { + newSlice := make(ContentSlice, len(s)) + copy(newSlice, s) + return newSlice +} + +// Ensure Content implements the Record interface. +var _ Record = Content{} + +// **************************************************************************** +// Test helpers +// **************************************************************************** + +func genericSeeding(tree *BTree, size int) *BTree { + for i := 0; i < size; i++ { + tree.Insert(Content{Key: i, Value: fmt.Sprintf("Value_%d", i)}) + } + return tree +} + +func intSlicesCompare(left, right []int) int { + if len(left) != len(right) { + if len(left) > len(right) { + return 1 + } else { + return -1 + } + } + + for position, leftInt := range left { + if leftInt != right[position] { + if leftInt > right[position] { + return 1 + } else { + return -1 + } + } + } + + return 0 +} + +// **************************************************************************** +// Tests +// **************************************************************************** + +func TestLen(t *testing.T) { + length := genericSeeding(New(WithDegree(10)), 7).Len() + if length != 7 { + t.Errorf("Length is incorrect. Expected 7, but got %d.", length) + } + + length = genericSeeding(New(WithDegree(5)), 111).Len() + if length != 111 { + t.Errorf("Length is incorrect. Expected 111, but got %d.", length) + } + + length = genericSeeding(New(WithDegree(30)), 123).Len() + if length != 123 { + t.Errorf("Length is incorrect. Expected 123, but got %d.", length) + } + +} + +func TestHas(t *testing.T) { + tree := genericSeeding(New(WithDegree(10)), 40) + + if tree.Has(Content{Key: 7}) != true { + t.Errorf("Has(7) reported false, but it should be true.") + } + if tree.Has(Content{Key: 39}) != true { + t.Errorf("Has(40) reported false, but it should be true.") + } + if tree.Has(Content{Key: 1111}) == true { + t.Errorf("Has(1111) reported true, but it should be false.") + } +} + +func TestMin(t *testing.T) { + min := Content(genericSeeding(New(WithDegree(10)), 53).Min()) + + if min.Key != 0 { + t.Errorf("Minimum should have been 0, but it was reported as %d.", min) + } +} + +func TestMax(t *testing.T) { + max := Content(genericSeeding(New(WithDegree(10)), 53).Min()) + + if max.Key != 0 { + t.Errorf("Minimum should have been 0, but it was reported as %d.", max) + } +} + +func TestGet(t *testing.T) { + tree := genericSeeding(New(WithDegree(10)), 40) + + if Content(tree.Get(Content{Key: 7})).Value != "Value_7" { + t.Errorf("Get(7) should have returned 'Value_7', but it returned %v.", tree.Get(Content{Key: 7})) + } + if Content(tree.Get(Content{Key: 39})).Value != "Value_39" { + t.Errorf("Get(40) should have returnd 'Value_39', but it returned %v.", tree.Get(Content{Key: 39})) + } + if tree.Get(Content{Key: 1111}) != nil { + t.Errorf("Get(1111) returned %v, but it should be nil.", Content(tree.Get(Content{Key: 1111}))) + } +} + +func TestDescend(t *testing.T) { + tree := genericSeeding(New(WithDegree(10)), 5) + + expected := []int{4, 3, 2, 1, 0} + found := []int{} + + tree.Descend(func(_record Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("Descend returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDescendGreaterThan(t *testing.T) { + tree := genericSeeding(New(WithDegree(10)), 10) + + expected := []int{9, 8, 7, 6, 5} + found := []int{} + + tree.DescendGreaterThan(Content{Key: 4}, func(_record Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendGreaterThan returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDescendLessOrEqual(t *testing.T) { + tree := genericSeeding(New(WithDegree(10)), 10) + + expected := []int{4, 3, 2, 1, 0} + found := []int{} + + tree.DescendLessOrEqual(Content{Key: 4}, func(_record Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendLessOrEqual returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDescendRange(t *testing.T) { + tree := genericSeeding(New(WithDegree(10)), 10) + + expected := []int{6, 5, 4, 3, 2} + found := []int{} + + tree.DescendRange(Content{Key: 6}, Content{Key: 1}, func(_record Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendRange returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscend(t *testing.T) { + tree := genericSeeding(New(WithDegree(10)), 5) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + tree.Ascend(func(_record Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("Ascend returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscendGreaterOrEqual(t *testing.T) { + tree := genericSeeding(New(WithDegree(10)), 10) + + expected := []int{5, 6, 7, 8, 9} + found := []int{} + + tree.AscendGreaterOrEqual(Content{Key: 5}, func(_record Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("AscendGreaterOrEqual returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscendLessThan(t *testing.T) { + tree := genericSeeding(New(WithDegree(10)), 10) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + tree.AscendLessThan(Content{Key: 5}, func(_record Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendLessOrEqual returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestAscendRange(t *testing.T) { + tree := genericSeeding(New(WithDegree(10)), 10) + + expected := []int{2, 3, 4, 5, 6} + found := []int{} + + tree.AscendRange(Content{Key: 2}, Content{Key: 7}, func(_record Record) bool { + record := Content(_record) + found = append(found, int(record.Key)) + return true + }) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("DescendRange returned the wrong sequence. Expected %v, but got %v.", expected, found) + } +} + +func TestDeleteMin(t *testing.T) { + tree := genericSeeding(New(WithDegree(3)), 100) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + found = append(found, int(Content(tree.DeleteMin()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestShift(t *testing.T) { + tree := genericSeeding(New(WithDegree(3)), 100) + + expected := []int{0, 1, 2, 3, 4} + found := []int{} + + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + found = append(found, int(Content(tree.Shift()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of Shift returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestDeleteMax(t *testing.T) { + tree := genericSeeding(New(WithDegree(3)), 100) + + expected := []int{99, 98, 97, 96, 95} + found := []int{} + + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + found = append(found, int(Content(tree.DeleteMax()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestPop(t *testing.T) { + tree := genericSeeding(New(WithDegree(3)), 100) + + expected := []int{99, 98, 97, 96, 95} + found := []int{} + + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + found = append(found, int(Content(tree.Pop()).Key)) + + if intSlicesCompare(expected, found) != 0 { + t.Errorf("5 rounds of DeleteMin returned the wrong elements. Expected %v, but got %v.", expected, found) + } +} + +func TestInsertGet(t *testing.T) { + tree := New(WithDegree(4)) + + expected := []Content{} + + for count := 0; count < 20; count++ { + value := fmt.Sprintf("Value_%d", count) + tree.Insert(Content{Key: count, Value: value}) + expected = append(expected, Content{Key: count, Value: value}) + } + + for count := 0; count < 20; count++ { + if tree.Get(Content{Key: count}) != expected[count] { + t.Errorf("Insert/Get doesn't appear to be working. Expected to retrieve %v with key %d, but got %v.", expected[count], count, tree.Get(Content{Key: count})) + } + } +} + +func TestClone(t *testing.T) { +} + +// ***** The following tests are functional or stress testing type tests. + +func TestBTree(t *testing.T) { + // Create a B-Tree of degree 3 + tree := New(WithDegree(3)) + + //insertData := []Content{} + var insertData ContentSlice + + // Insert integer keys + intKeys := []int{10, 20, 5, 6, 12, 30, 7, 17} + for _, key := range intKeys { + content := Content{Key: key, Value: fmt.Sprintf("Value_%d", key)} + insertData = append(insertData, content) + result := tree.Insert(content) + if result != nil { + t.Errorf("**** Already in the tree? %v", result) + } + } + + // Insert string keys + stringKeys := []string{"apple", "banana", "cherry", "date", "fig", "grape"} + for _, key := range stringKeys { + content := Content{Key: key, Value: fmt.Sprintf("Fruit_%s", key)} + insertData = append(insertData, content) + tree.Insert(content) + } + + if tree.Len() != 14 { + t.Errorf("Tree length wrong. Expected 14 but got %d", tree.Len()) + } + + // Search for existing and non-existing keys + searchTests := []struct { + test Content + expected bool + }{ + {Content{Key: 10, Value: "Value_10"}, true}, + {Content{Key: 15, Value: ""}, false}, + {Content{Key: "banana", Value: "Fruit_banana"}, true}, + {Content{Key: "kiwi", Value: ""}, false}, + } + + t.Logf("Search Tests:\n") + for _, test := range searchTests { + val := tree.Get(test.test) + + if test.expected { + if val != nil && Content(val).Value == test.test.Value { + t.Logf("Found expected key:value %v:%v", test.test.Key, test.test.Value) + } else { + if val == nil { + t.Logf("Didn't find %v, but expected", test.test.Key) + } else { + t.Errorf("Expected key %v:%v, but found %v:%v.", test.test.Key, test.test.Value, Content(val).Key, Content(val).Value) + } + } + } else { + if val != nil { + t.Errorf("Did not expect key %v, but found key:value %v:%v", test.test.Key, Content(val).Key, Content(val).Value) + } else { + t.Logf("Didn't find %v, but wasn't expected", test.test.Key) + } + } + } + + // Iterate in order + t.Logf("\nIn-order Iteration:\n") + pos := 0 + + if tree.Len() != 14 { + t.Errorf("Tree length wrong. Expected 14 but got %d", tree.Len()) + } + + sortedInsertData := insertData.Copy() + sort.Sort(sortedInsertData) + + t.Logf("Insert Data Length: %d", len(insertData)) + t.Logf("Sorted Data Length: %d", len(sortedInsertData)) + t.Logf("Tree Length: %d", tree.Len()) + + tree.Ascend(func(_record Record) bool { + record := Content(_record) + t.Logf("Key:Value == %v:%v", record.Key, record.Value) + if record.Key != sortedInsertData[pos].Key { + t.Errorf("Out of order! Expected %v, but got %v", sortedInsertData[pos].Key, record.Key) + } + pos++ + return true + }) + // // Reverse Iterate + t.Logf("\nReverse-order Iteration:\n") + pos = len(sortedInsertData) - 1 + + tree.Descend(func(_record Record) bool { + record := Content(_record) + t.Logf("Key:Value == %v:%v", record.Key, record.Value) + if record.Key != sortedInsertData[pos].Key { + t.Errorf("Out of order! Expected %v, but got %v", sortedInsertData[pos].Key, record.Key) + } + pos-- + return true + }) + + deleteTests := []Content{ + Content{Key: 10, Value: "Value_10"}, + Content{Key: 15, Value: ""}, + Content{Key: "banana", Value: "Fruit_banana"}, + Content{Key: "kiwi", Value: ""}, + } + for _, test := range deleteTests { + fmt.Printf("\nDeleting %+v\n", test) + tree.Delete(test) + } + + if tree.Len() != 12 { + t.Errorf("Tree length wrong. Expected 12 but got %d", tree.Len()) + } + + for _, test := range deleteTests { + val := tree.Get(test) + if val != nil { + t.Errorf("Did not expect key %v, but found key:value %v:%v", test.Key, Content(val).Key, Content(val).Value) + } else { + t.Logf("Didn't find %v, but wasn't expected", test.Key) + } + } +} + +func TestStress(t *testing.T) { + // Loop through creating B-Trees with a range of degrees from 3 to 12, stepping by 3. + // Insert 1000 records into each tree, then search for each record. + // Delete half of the records, skipping every other one, then search for each record. + + for degree := 3; degree <= 12; degree += 3 { + t.Logf("Testing B-Tree of degree %d\n", degree) + tree := New(WithDegree(degree)) + + // Insert 1000 records + t.Logf("Inserting 1000 records\n") + for i := 0; i < 1000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Insert(content) + } + + // Search for all records + for i := 0; i < 1000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + val := tree.Get(content) + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + + // Delete half of the records + for i := 0; i < 1000; i += 2 { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + } + + // Search for all records + for i := 0; i < 1000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + val := tree.Get(content) + if i%2 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + } + } + + // Now create a very large tree, with 100000 records + // Then delete roughly one third of them, using a very basic random number generation scheme + // (implement it right here) to determine which records to delete. + // Print a few lines using Logf to let the user know what's happening. + + t.Logf("Testing B-Tree of degree 10 with 100000 records\n") + tree := New(WithDegree(10)) + + // Insert 100000 records + t.Logf("Inserting 100000 records\n") + for i := 0; i < 100000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Insert(content) + } + + // Implement a very basic random number generator + seed := 0 + random := func() int { + seed = (seed*1103515245 + 12345) & 0x7fffffff + return seed + } + + // Delete one third of the records + t.Logf("Deleting one third of the records\n") + for i := 0; i < 35000; i++ { + content := Content{Key: random() % 100000, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + } +} + +// Write a test that populates a large B-Tree with 10000 records. +// It should then `Clone` the tree, make some changes to both the original and the clone, +// And then clone the clone, and make some changes to all three trees, and then check that the changes are isolated +// to the tree they were made in. + +func TestBTreeCloneIsolation(t *testing.T) { + t.Logf("Creating B-Tree of degree 10 with 10000 records\n") + tree := genericSeeding(New(WithDegree(10)), 10000) + + // Clone the tree + t.Logf("Cloning the tree\n") + clone := tree.Clone() + + // Make some changes to the original and the clone + t.Logf("Making changes to the original and the clone\n") + for i := 0; i < 10000; i += 2 { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + content = Content{Key: i + 1, Value: fmt.Sprintf("Value_%d", i+1)} + clone.Delete(content) + } + + // Clone the clone + t.Logf("Cloning the clone\n") + clone2 := clone.Clone() + + // Make some changes to all three trees + t.Logf("Making changes to all three trees\n") + for i := 0; i < 10000; i += 3 { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + tree.Delete(content) + content = Content{Key: i, Value: fmt.Sprintf("Value_%d", i+1)} + clone.Delete(content) + content = Content{Key: i + 2, Value: fmt.Sprintf("Value_%d", i+2)} + clone2.Delete(content) + } + + // Check that the changes are isolated to the tree they were made in + t.Logf("Checking that the changes are isolated to the tree they were made in\n") + for i := 0; i < 10000; i++ { + content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} + val := tree.Get(content) + + if i%3 == 0 || i%2 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + + val = clone.Get(content) + if i%2 != 0 || i%3 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + + val = clone2.Get(content) + if i%2 != 0 || (i-2)%3 == 0 { + if val != nil { + t.Errorf("Didn't expect key %v, but found key:value %v:%v", content.Key, Content(val).Key, Content(val).Value) + } + } else { + if val == nil { + t.Errorf("Expected key %v, but didn't find it", content.Key) + } + } + } +} diff --git a/examples/gno.land/p/demo/btree/gno.mod b/examples/gno.land/p/demo/btree/gno.mod new file mode 100644 index 00000000000..aed2fe6b730 --- /dev/null +++ b/examples/gno.land/p/demo/btree/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/btree diff --git a/examples/gno.land/p/demo/combinederr/combinederr.gno b/examples/gno.land/p/demo/combinederr/combinederr.gno new file mode 100644 index 00000000000..f446c7846bd --- /dev/null +++ b/examples/gno.land/p/demo/combinederr/combinederr.gno @@ -0,0 +1,40 @@ +package combinederr + +import "strings" + +// CombinedError is a combined execution error +type CombinedError struct { + errors []error +} + +// Error returns the combined execution error +func (e *CombinedError) Error() string { + if len(e.errors) == 0 { + return "" + } + + var sb strings.Builder + + for _, err := range e.errors { + sb.WriteString(err.Error() + "; ") + } + + // Remove the last semicolon and space + result := sb.String() + + return result[:len(result)-2] +} + +// Add adds a new error to the execution error +func (e *CombinedError) Add(err error) { + if err == nil { + return + } + + e.errors = append(e.errors, err) +} + +// Size returns a +func (e *CombinedError) Size() int { + return len(e.errors) +} diff --git a/examples/gno.land/p/demo/combinederr/gno.mod b/examples/gno.land/p/demo/combinederr/gno.mod new file mode 100644 index 00000000000..4c99e0ba7ef --- /dev/null +++ b/examples/gno.land/p/demo/combinederr/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/combinederr diff --git a/examples/gno.land/p/demo/dao/dao.gno b/examples/gno.land/p/demo/dao/dao.gno new file mode 100644 index 00000000000..e3a2ba72c5b --- /dev/null +++ b/examples/gno.land/p/demo/dao/dao.gno @@ -0,0 +1,34 @@ +package dao + +const ( + ProposalAddedEvent = "ProposalAdded" // emitted when a new proposal has been added + ProposalAcceptedEvent = "ProposalAccepted" // emitted when a proposal has been accepted + ProposalNotAcceptedEvent = "ProposalNotAccepted" // emitted when a proposal has not been accepted + ProposalExecutedEvent = "ProposalExecuted" // emitted when a proposal has been executed + + ProposalEventIDKey = "proposal-id" + ProposalEventAuthorKey = "proposal-author" + ProposalEventExecutionKey = "exec-status" +) + +// ProposalRequest is a single govdao proposal request +// that contains the necessary information to +// log and generate a valid proposal +type ProposalRequest struct { + Title string // the title associated with the proposal + Description string // the description associated with the proposal + Executor Executor // the proposal executor +} + +// DAO defines the DAO abstraction +type DAO interface { + // PropStore is the DAO proposal storage + PropStore + + // Propose adds a new proposal to the executor-based GOVDAO. + // Returns the generated proposal ID + Propose(request ProposalRequest) (uint64, error) + + // ExecuteProposal executes the proposal with the given ID + ExecuteProposal(id uint64) error +} diff --git a/examples/gno.land/p/demo/dao/doc.gno b/examples/gno.land/p/demo/dao/doc.gno new file mode 100644 index 00000000000..3fb28204013 --- /dev/null +++ b/examples/gno.land/p/demo/dao/doc.gno @@ -0,0 +1,5 @@ +// Package dao houses common DAO building blocks (framework), which can be used or adopted by any +// specific DAO implementation. By design, the DAO should house the proposals it receives, but not the actual +// DAO members or proposal votes. These abstractions should be implemented by a separate entity, to keep the DAO +// agnostic of implementation details such as these (member / vote management). +package dao diff --git a/examples/gno.land/p/demo/dao/events.gno b/examples/gno.land/p/demo/dao/events.gno new file mode 100644 index 00000000000..97bc794e6f3 --- /dev/null +++ b/examples/gno.land/p/demo/dao/events.gno @@ -0,0 +1,56 @@ +package dao + +import ( + "std" + + "gno.land/p/demo/ufmt" +) + +// EmitProposalAdded emits an event signaling that +// a given proposal was added +func EmitProposalAdded(id uint64, proposer std.Address) { + std.Emit( + ProposalAddedEvent, + ProposalEventIDKey, ufmt.Sprintf("%d", id), + ProposalEventAuthorKey, proposer.String(), + ) +} + +// EmitProposalAccepted emits an event signaling that +// a given proposal was accepted +func EmitProposalAccepted(id uint64) { + std.Emit( + ProposalAcceptedEvent, + ProposalEventIDKey, ufmt.Sprintf("%d", id), + ) +} + +// EmitProposalNotAccepted emits an event signaling that +// a given proposal was not accepted +func EmitProposalNotAccepted(id uint64) { + std.Emit( + ProposalNotAcceptedEvent, + ProposalEventIDKey, ufmt.Sprintf("%d", id), + ) +} + +// EmitProposalExecuted emits an event signaling that +// a given proposal was executed, with the given status +func EmitProposalExecuted(id uint64, status ProposalStatus) { + std.Emit( + ProposalExecutedEvent, + ProposalEventIDKey, ufmt.Sprintf("%d", id), + ProposalEventExecutionKey, status.String(), + ) +} + +// EmitVoteAdded emits an event signaling that +// a vote was cast for a given proposal +func EmitVoteAdded(id uint64, voter std.Address, option VoteOption) { + std.Emit( + VoteAddedEvent, + VoteAddedIDKey, ufmt.Sprintf("%d", id), + VoteAddedAuthorKey, voter.String(), + VoteAddedOptionKey, option.String(), + ) +} diff --git a/examples/gno.land/p/demo/dao/executor.gno b/examples/gno.land/p/demo/dao/executor.gno new file mode 100644 index 00000000000..9291c2c53c5 --- /dev/null +++ b/examples/gno.land/p/demo/dao/executor.gno @@ -0,0 +1,9 @@ +package dao + +// Executor represents a minimal closure-oriented proposal design. +// It is intended to be used by a govdao governance proposal (v1, v2, etc) +type Executor interface { + // Execute executes the given proposal, and returns any error encountered + // during the execution + Execute() error +} diff --git a/examples/gno.land/p/demo/dao/gno.mod b/examples/gno.land/p/demo/dao/gno.mod new file mode 100644 index 00000000000..fbb23299116 --- /dev/null +++ b/examples/gno.land/p/demo/dao/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/dao diff --git a/examples/gno.land/p/demo/dao/proposals.gno b/examples/gno.land/p/demo/dao/proposals.gno new file mode 100644 index 00000000000..66abcb248c5 --- /dev/null +++ b/examples/gno.land/p/demo/dao/proposals.gno @@ -0,0 +1,65 @@ +package dao + +import "std" + +// ProposalStatus is the currently active proposal status, +// changed based on DAO functionality. +// Status transitions: +// +// ACTIVE -> ACCEPTED -> EXECUTION(SUCCEEDED/FAILED) +// +// ACTIVE -> NOT ACCEPTED +type ProposalStatus string + +var ( + Active ProposalStatus = "active" // proposal is still active + Accepted ProposalStatus = "accepted" // proposal gathered quorum + NotAccepted ProposalStatus = "not accepted" // proposal failed to gather quorum + ExecutionSuccessful ProposalStatus = "execution successful" // proposal is executed successfully + ExecutionFailed ProposalStatus = "execution failed" // proposal has failed during execution +) + +func (s ProposalStatus) String() string { + return string(s) +} + +// PropStore defines the proposal storage abstraction +type PropStore interface { + // Proposals returns the given paginated proposals + Proposals(offset, count uint64) []Proposal + + // ProposalByID returns the proposal associated with + // the given ID, if any + ProposalByID(id uint64) (Proposal, error) + + // Size returns the number of proposals in + // the proposal store + Size() int +} + +// Proposal is the single proposal abstraction +type Proposal interface { + // Author returns the author of the proposal + Author() std.Address + + // Title returns the title of the proposal + Title() string + + // Description returns the description of the proposal + Description() string + + // Status returns the status of the proposal + Status() ProposalStatus + + // Executor returns the proposal executor + Executor() Executor + + // Stats returns the voting stats of the proposal + Stats() Stats + + // IsExpired returns a flag indicating if the proposal expired + IsExpired() bool + + // Render renders the proposal in a readable format + Render() string +} diff --git a/examples/gno.land/p/demo/dao/vote.gno b/examples/gno.land/p/demo/dao/vote.gno new file mode 100644 index 00000000000..94369f41e1b --- /dev/null +++ b/examples/gno.land/p/demo/dao/vote.gno @@ -0,0 +1,69 @@ +package dao + +// NOTE: +// This voting pods will be removed in a future version of the +// p/demo/dao package. A DAO shouldn't have to comply with or define how the voting mechanism works internally; +// it should be viewed as an entity that makes decisions +// +// The extent of "votes being enforced" in this implementation is just in the context +// of types a DAO can use (import), and in the context of "Stats", where +// there is a notion of "Yay", "Nay" and "Abstain" votes. +const ( + VoteAddedEvent = "VoteAdded" // emitted when a vote was cast for a proposal + + VoteAddedIDKey = "proposal-id" + VoteAddedAuthorKey = "author" + VoteAddedOptionKey = "option" +) + +// VoteOption is the limited voting option for a DAO proposal +type VoteOption string + +const ( + YesVote VoteOption = "YES" // Proposal should be accepted + NoVote VoteOption = "NO" // Proposal should be rejected + AbstainVote VoteOption = "ABSTAIN" // Side is not chosen +) + +func (v VoteOption) String() string { + return string(v) +} + +// Stats encompasses the proposal voting stats +type Stats struct { + YayVotes uint64 + NayVotes uint64 + AbstainVotes uint64 + + TotalVotingPower uint64 +} + +// YayPercent returns the percentage (0-100) of the yay votes +// in relation to the total voting power +func (v Stats) YayPercent() uint64 { + return v.YayVotes * 100 / v.TotalVotingPower +} + +// NayPercent returns the percentage (0-100) of the nay votes +// in relation to the total voting power +func (v Stats) NayPercent() uint64 { + return v.NayVotes * 100 / v.TotalVotingPower +} + +// AbstainPercent returns the percentage (0-100) of the abstain votes +// in relation to the total voting power +func (v Stats) AbstainPercent() uint64 { + return v.AbstainVotes * 100 / v.TotalVotingPower +} + +// MissingVotes returns the summed voting power that has not +// participated in proposal voting yet +func (v Stats) MissingVotes() uint64 { + return v.TotalVotingPower - (v.YayVotes + v.NayVotes + v.AbstainVotes) +} + +// MissingVotesPercent returns the percentage (0-100) of the missing votes +// in relation to the total voting power +func (v Stats) MissingVotesPercent() uint64 { + return v.MissingVotes() * 100 / v.TotalVotingPower +} diff --git a/examples/gno.land/p/demo/dom/gno.mod b/examples/gno.land/p/demo/dom/gno.mod index 83ca827cf66..bd8bba14d06 100644 --- a/examples/gno.land/p/demo/dom/gno.mod +++ b/examples/gno.land/p/demo/dom/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/dom - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/p/demo/entropy/entropy.gno b/examples/gno.land/p/demo/entropy/entropy.gno index 5e35b8c7227..9e8f656c21b 100644 --- a/examples/gno.land/p/demo/entropy/entropy.gno +++ b/examples/gno.land/p/demo/entropy/entropy.gno @@ -87,3 +87,11 @@ func (i *Instance) Value() uint32 { i.addEntropy() return i.value } + +func (i *Instance) Value64() uint64 { + i.addEntropy() + high := i.value + i.addEntropy() + + return (uint64(high) << 32) | uint64(i.value) +} diff --git a/examples/gno.land/p/demo/entropy/entropy_test.gno b/examples/gno.land/p/demo/entropy/entropy_test.gno index 0deb3ab9aa2..895bfd1e394 100644 --- a/examples/gno.land/p/demo/entropy/entropy_test.gno +++ b/examples/gno.land/p/demo/entropy/entropy_test.gno @@ -33,6 +33,26 @@ func TestInstanceValue(t *testing.T) { } } +func TestInstanceValue64(t *testing.T) { + baseEntropy := New() + baseResult := computeValue64(t, baseEntropy) + + sameHeightEntropy := New() + sameHeightResult := computeValue64(t, sameHeightEntropy) + + if baseResult != sameHeightResult { + t.Errorf("should have the same result: new=%s, base=%s", sameHeightResult, baseResult) + } + + std.TestSkipHeights(1) + differentHeightEntropy := New() + differentHeightResult := computeValue64(t, differentHeightEntropy) + + if baseResult == differentHeightResult { + t.Errorf("should have different result: new=%s, base=%s", differentHeightResult, baseResult) + } +} + func computeValue(t *testing.T, r *Instance) string { t.Helper() @@ -44,3 +64,15 @@ func computeValue(t *testing.T, r *Instance) string { return out } + +func computeValue64(t *testing.T, r *Instance) string { + t.Helper() + + out := "" + for i := 0; i < 10; i++ { + val := int(r.Value64()) + out += strconv.Itoa(val) + " " + } + + return out +} diff --git a/examples/gno.land/p/demo/entropy/z_filetest.gno b/examples/gno.land/p/demo/entropy/z_filetest.gno index 85ed1b10a3d..ddee29b22fd 100644 --- a/examples/gno.land/p/demo/entropy/z_filetest.gno +++ b/examples/gno.land/p/demo/entropy/z_filetest.gno @@ -15,6 +15,7 @@ func main() { println(r.Value()) println(r.Value()) println(r.Value()) + println(r.Value64()) // should be the same println("---") @@ -24,6 +25,7 @@ func main() { println(r.Value()) println(r.Value()) println(r.Value()) + println(r.Value64()) std.TestSkipHeights(1) println("---") @@ -33,6 +35,7 @@ func main() { println(r.Value()) println(r.Value()) println(r.Value()) + println(r.Value64()) } // Output: @@ -42,15 +45,18 @@ func main() { // 1950222777 // 3348280598 // 438354259 +// 6353385488959065197 // --- // 4129293727 // 2141104956 // 1950222777 // 3348280598 // 438354259 +// 6353385488959065197 // --- // 49506731 // 1539580078 // 2695928529 // 1895482388 // 3462727799 +// 16745038698684748445 diff --git a/examples/gno.land/p/demo/fqname/fqname.gno b/examples/gno.land/p/demo/fqname/fqname.gno new file mode 100644 index 00000000000..07d9e4b4621 --- /dev/null +++ b/examples/gno.land/p/demo/fqname/fqname.gno @@ -0,0 +1,77 @@ +// Package fqname provides utilities for handling fully qualified identifiers in +// Gno. A fully qualified identifier typically includes a package path followed +// by a dot (.) and then the name of a variable, function, type, or other +// package-level declaration. +package fqname + +import ( + "strings" +) + +// Parse splits a fully qualified identifier into its package path and name +// components. It handles cases with and without slashes in the package path. +// +// pkgpath, name := fqname.Parse("gno.land/p/demo/avl.Tree") +// ufmt.Sprintf("Package: %s, Name: %s\n", id.Package, id.Name) +// // Output: Package: gno.land/p/demo/avl, Name: Tree +func Parse(fqname string) (pkgpath, name string) { + // Find the index of the last slash. + lastSlashIndex := strings.LastIndex(fqname, "/") + if lastSlashIndex == -1 { + // No slash found, handle it as a simple package name with dot notation. + dotIndex := strings.LastIndex(fqname, ".") + if dotIndex == -1 { + return fqname, "" + } + return fqname[:dotIndex], fqname[dotIndex+1:] + } + + // Get the part after the last slash. + afterSlash := fqname[lastSlashIndex+1:] + + // Check for a dot in the substring after the last slash. + dotIndex := strings.Index(afterSlash, ".") + if dotIndex == -1 { + // No dot found after the last slash + return fqname, "" + } + + // Split at the dot to separate the base and the suffix. + base := fqname[:lastSlashIndex+1+dotIndex] + suffix := afterSlash[dotIndex+1:] + + return base, suffix +} + +// Construct a qualified identifier. +// +// fqName := fqname.Construct("gno.land/r/demo/foo20", "Token") +// fmt.Println("Fully Qualified Name:", fqName) +// // Output: gno.land/r/demo/foo20.Token +func Construct(pkgpath, name string) string { + // TODO: ensure pkgpath is valid - and as such last part does not contain a dot. + if name == "" { + return pkgpath + } + return pkgpath + "." + name +} + +// RenderLink creates a formatted link for a fully qualified identifier. +// If the package path starts with "gno.land", it converts it to a markdown link. +// If the domain is different or missing, it returns the input as is. +func RenderLink(pkgPath, slug string) string { + if strings.HasPrefix(pkgPath, "gno.land") { + pkgLink := strings.TrimPrefix(pkgPath, "gno.land") + if slug != "" { + return "[" + pkgPath + "](" + pkgLink + ")." + slug + } + + return "[" + pkgPath + "](" + pkgLink + ")" + } + + if slug != "" { + return pkgPath + "." + slug + } + + return pkgPath +} diff --git a/examples/gno.land/p/demo/fqname/fqname_test.gno b/examples/gno.land/p/demo/fqname/fqname_test.gno new file mode 100644 index 00000000000..5f0f83968a3 --- /dev/null +++ b/examples/gno.land/p/demo/fqname/fqname_test.gno @@ -0,0 +1,74 @@ +package fqname + +import ( + "testing" + + "gno.land/p/demo/uassert" +) + +func TestParse(t *testing.T) { + tests := []struct { + input string + expectedPkgPath string + expectedName string + }{ + {"gno.land/p/demo/avl.Tree", "gno.land/p/demo/avl", "Tree"}, + {"gno.land/p/demo/avl", "gno.land/p/demo/avl", ""}, + {"gno.land/p/demo/avl.Tree.Node", "gno.land/p/demo/avl", "Tree.Node"}, + {"gno.land/p/demo/avl/nested.Package.Func", "gno.land/p/demo/avl/nested", "Package.Func"}, + {"path/filepath.Split", "path/filepath", "Split"}, + {"path.Split", "path", "Split"}, + {"path/filepath", "path/filepath", ""}, + {"path", "path", ""}, + {"", "", ""}, + } + + for _, tt := range tests { + pkgpath, name := Parse(tt.input) + uassert.Equal(t, tt.expectedPkgPath, pkgpath, "Package path did not match") + uassert.Equal(t, tt.expectedName, name, "Name did not match") + } +} + +func TestConstruct(t *testing.T) { + tests := []struct { + pkgpath string + name string + expected string + }{ + {"gno.land/r/demo/foo20", "Token", "gno.land/r/demo/foo20.Token"}, + {"gno.land/r/demo/foo20", "", "gno.land/r/demo/foo20"}, + {"path", "", "path"}, + {"path", "Split", "path.Split"}, + {"path/filepath", "", "path/filepath"}, + {"path/filepath", "Split", "path/filepath.Split"}, + {"", "JustName", ".JustName"}, + {"", "", ""}, + } + + for _, tt := range tests { + result := Construct(tt.pkgpath, tt.name) + uassert.Equal(t, tt.expected, result, "Constructed FQName did not match expected") + } +} + +func TestRenderLink(t *testing.T) { + tests := []struct { + pkgPath string + slug string + expected string + }{ + {"gno.land/p/demo/avl", "Tree", "[gno.land/p/demo/avl](/p/demo/avl).Tree"}, + {"gno.land/p/demo/avl", "", "[gno.land/p/demo/avl](/p/demo/avl)"}, + {"github.com/a/b", "C", "github.com/a/b.C"}, + {"example.com/pkg", "Func", "example.com/pkg.Func"}, + {"gno.land/r/demo/foo20", "Token", "[gno.land/r/demo/foo20](/r/demo/foo20).Token"}, + {"gno.land/r/demo/foo20", "", "[gno.land/r/demo/foo20](/r/demo/foo20)"}, + {"", "", ""}, + } + + for _, tt := range tests { + result := RenderLink(tt.pkgPath, tt.slug) + uassert.Equal(t, tt.expected, result, "Rendered link did not match expected") + } +} diff --git a/examples/gno.land/p/demo/fqname/gno.mod b/examples/gno.land/p/demo/fqname/gno.mod new file mode 100644 index 00000000000..afee55e0b7b --- /dev/null +++ b/examples/gno.land/p/demo/fqname/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/fqname diff --git a/examples/gno.land/p/demo/gnorkle/agent/gno.mod b/examples/gno.land/p/demo/gnorkle/agent/gno.mod index 093ca9cf38e..e784354c35e 100644 --- a/examples/gno.land/p/demo/gnorkle/agent/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/agent/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/gnorkle/agent - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod b/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod index c651c62cb1b..05363a3cd06 100644 --- a/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/feeds/static/gno.mod @@ -1,13 +1 @@ module gno.land/p/demo/gnorkle/feeds/static - -require ( - gno.land/p/demo/gnorkle/feed v0.0.0-latest - gno.land/p/demo/gnorkle/gnorkle v0.0.0-latest - gno.land/p/demo/gnorkle/ingester v0.0.0-latest - gno.land/p/demo/gnorkle/ingesters/single v0.0.0-latest - gno.land/p/demo/gnorkle/message v0.0.0-latest - gno.land/p/demo/gnorkle/storage/simple v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod b/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod index 88fb202863f..ce2c2c3706d 100644 --- a/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/gnorkle/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/gnorkle/gnorkle - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/gnorkle/agent v0.0.0-latest - gno.land/p/demo/gnorkle/feed v0.0.0-latest - gno.land/p/demo/gnorkle/ingester v0.0.0-latest - gno.land/p/demo/gnorkle/message v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/gnorkle/instance.gno b/examples/gno.land/p/demo/gnorkle/gnorkle/instance.gno index 22746d569a8..eea4782909e 100644 --- a/examples/gno.land/p/demo/gnorkle/gnorkle/instance.gno +++ b/examples/gno.land/p/demo/gnorkle/gnorkle/instance.gno @@ -227,7 +227,7 @@ func (i *Instance) GetFeedDefinitions(forAddress string) (string, error) { first = false buf.Write(taskBytes) - return true + return false }) if err != nil { diff --git a/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod b/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod index 71120966a0c..8cf5a9a30d8 100644 --- a/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/ingesters/single/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/gnorkle/ingesters/single - -require ( - gno.land/p/demo/gnorkle/gnorkle v0.0.0-latest - gno.land/p/demo/gnorkle/ingester v0.0.0-latest - gno.land/p/demo/gnorkle/storage/simple v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/gnorkle/message/gno.mod b/examples/gno.land/p/demo/gnorkle/message/gno.mod index 4baad40ef86..5544d0eb873 100644 --- a/examples/gno.land/p/demo/gnorkle/message/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/message/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/gnorkle/message - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod b/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod index cd673a8771c..b842e2b514c 100644 --- a/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod +++ b/examples/gno.land/p/demo/gnorkle/storage/simple/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/gnorkle/storage/simple - -require ( - gno.land/p/demo/gnorkle/feed v0.0.0-latest - gno.land/p/demo/gnorkle/storage v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc1155/gno.mod b/examples/gno.land/p/demo/grc/grc1155/gno.mod index d6db0700146..1c3ec6360eb 100644 --- a/examples/gno.land/p/demo/grc/grc1155/gno.mod +++ b/examples/gno.land/p/demo/grc/grc1155/gno.mod @@ -1,7 +1 @@ module gno.land/p/demo/grc/grc1155 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc20/banker.gno b/examples/gno.land/p/demo/grc/grc20/banker.gno deleted file mode 100644 index f643d3e2635..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/banker.gno +++ /dev/null @@ -1,217 +0,0 @@ -package grc20 - -import ( - "std" - "strconv" - - "gno.land/p/demo/avl" - "gno.land/p/demo/ufmt" -) - -// Banker implements a token banker with admin privileges. -// -// The Banker is intended to be used in two main ways: -// 1. as a temporary object used to make the initial minting, then deleted. -// 2. preserved in an unexported variable to support conditional administrative -// tasks protected by the contract. -type Banker struct { - name string - symbol string - decimals uint - totalSupply uint64 - balances avl.Tree // std.Address(owner) -> uint64 - allowances avl.Tree // string(owner+":"+spender) -> uint64 - token *token // to share the same pointer -} - -func NewBanker(name, symbol string, decimals uint) *Banker { - if name == "" { - panic("name should not be empty") - } - if symbol == "" { - panic("symbol should not be empty") - } - // XXX additional checks (length, characters, limits, etc) - - b := Banker{ - name: name, - symbol: symbol, - decimals: decimals, - } - t := &token{banker: &b} - b.token = t - return &b -} - -func (b Banker) Token() Token { return b.token } // Token returns a grc20 safe-object implementation. -func (b Banker) GetName() string { return b.name } -func (b Banker) GetSymbol() string { return b.symbol } -func (b Banker) GetDecimals() uint { return b.decimals } -func (b Banker) TotalSupply() uint64 { return b.totalSupply } -func (b Banker) KnownAccounts() int { return b.balances.Size() } - -func (b *Banker) Mint(address std.Address, amount uint64) error { - if !address.IsValid() { - return ErrInvalidAddress - } - - // TODO: check for overflow - - b.totalSupply += amount - currentBalance := b.BalanceOf(address) - newBalance := currentBalance + amount - - b.balances.Set(string(address), newBalance) - - std.Emit( - TransferEvent, - "from", "", - "to", string(address), - "value", strconv.Itoa(int(amount)), - ) - - return nil -} - -func (b *Banker) Burn(address std.Address, amount uint64) error { - if !address.IsValid() { - return ErrInvalidAddress - } - // TODO: check for overflow - - currentBalance := b.BalanceOf(address) - if currentBalance < amount { - return ErrInsufficientBalance - } - - b.totalSupply -= amount - newBalance := currentBalance - amount - - b.balances.Set(string(address), newBalance) - - std.Emit( - TransferEvent, - "from", string(address), - "to", "", - "value", strconv.Itoa(int(amount)), - ) - - return nil -} - -func (b Banker) BalanceOf(address std.Address) uint64 { - balance, found := b.balances.Get(address.String()) - if !found { - return 0 - } - return balance.(uint64) -} - -func (b *Banker) SpendAllowance(owner, spender std.Address, amount uint64) error { - if !owner.IsValid() { - return ErrInvalidAddress - } - if !spender.IsValid() { - return ErrInvalidAddress - } - - currentAllowance := b.Allowance(owner, spender) - if currentAllowance < amount { - return ErrInsufficientAllowance - } - - key := allowanceKey(owner, spender) - newAllowance := currentAllowance - amount - - if newAllowance == 0 { - b.allowances.Remove(key) - } else { - b.allowances.Set(key, newAllowance) - } - - return nil -} - -func (b *Banker) Transfer(from, to std.Address, amount uint64) error { - if !from.IsValid() { - return ErrInvalidAddress - } - if !to.IsValid() { - return ErrInvalidAddress - } - if from == to { - return ErrCannotTransferToSelf - } - - toBalance := b.BalanceOf(to) - fromBalance := b.BalanceOf(from) - - // debug. - // println("from", from, "to", to, "amount", amount, "fromBalance", fromBalance, "toBalance", toBalance) - - if fromBalance < amount { - return ErrInsufficientBalance - } - - newToBalance := toBalance + amount - newFromBalance := fromBalance - amount - - b.balances.Set(string(to), newToBalance) - b.balances.Set(string(from), newFromBalance) - - std.Emit( - TransferEvent, - "from", from.String(), - "to", to.String(), - "value", strconv.Itoa(int(amount)), - ) - return nil -} - -func (b *Banker) TransferFrom(spender, from, to std.Address, amount uint64) error { - if err := b.SpendAllowance(from, spender, amount); err != nil { - return err - } - return b.Transfer(from, to, amount) -} - -func (b *Banker) Allowance(owner, spender std.Address) uint64 { - allowance, found := b.allowances.Get(allowanceKey(owner, spender)) - if !found { - return 0 - } - return allowance.(uint64) -} - -func (b *Banker) Approve(owner, spender std.Address, amount uint64) error { - if !owner.IsValid() { - return ErrInvalidAddress - } - if !spender.IsValid() { - return ErrInvalidAddress - } - - b.allowances.Set(allowanceKey(owner, spender), amount) - - std.Emit( - ApprovalEvent, - "owner", string(owner), - "spender", string(spender), - "value", strconv.Itoa(int(amount)), - ) - - return nil -} - -func (b *Banker) RenderHome() string { - str := "" - str += ufmt.Sprintf("# %s ($%s)\n\n", b.name, b.symbol) - str += ufmt.Sprintf("* **Decimals**: %d\n", b.decimals) - str += ufmt.Sprintf("* **Total supply**: %d\n", b.totalSupply) - str += ufmt.Sprintf("* **Known accounts**: %d\n", b.KnownAccounts()) - return str -} - -func allowanceKey(owner, spender std.Address) string { - return owner.String() + ":" + spender.String() -} diff --git a/examples/gno.land/p/demo/grc/grc20/banker_test.gno b/examples/gno.land/p/demo/grc/grc20/banker_test.gno deleted file mode 100644 index 00a1e75df1f..00000000000 --- a/examples/gno.land/p/demo/grc/grc20/banker_test.gno +++ /dev/null @@ -1,51 +0,0 @@ -package grc20 - -import ( - "testing" - - "gno.land/p/demo/testutils" - "gno.land/p/demo/ufmt" - "gno.land/p/demo/urequire" -) - -func TestBankerImpl(t *testing.T) { - dummy := NewBanker("Dummy", "DUMMY", 4) - urequire.False(t, dummy == nil, "dummy should not be nil") -} - -func TestAllowance(t *testing.T) { - var ( - owner = testutils.TestAddress("owner") - spender = testutils.TestAddress("spender") - dest = testutils.TestAddress("dest") - ) - - b := NewBanker("Dummy", "DUMMY", 6) - urequire.NoError(t, b.Mint(owner, 100000000)) - urequire.NoError(t, b.Approve(owner, spender, 5000000)) - urequire.Error(t, b.TransferFrom(spender, owner, dest, 10000000), ErrInsufficientAllowance.Error(), "should not be able to transfer more than approved") - - tests := []struct { - spend uint64 - exp uint64 - }{ - {3, 4999997}, - {999997, 4000000}, - {4000000, 0}, - } - - for _, tt := range tests { - b0 := b.BalanceOf(dest) - urequire.NoError(t, b.TransferFrom(spender, owner, dest, tt.spend)) - a := b.Allowance(owner, spender) - urequire.Equal(t, a, tt.exp, ufmt.Sprintf("allowance exp: %d, got %d", tt.exp, a)) - b := b.BalanceOf(dest) - expB := b0 + tt.spend - urequire.Equal(t, b, expB, ufmt.Sprintf("balance exp: %d, got %d", expB, b)) - } - - urequire.Error(t, b.TransferFrom(spender, owner, dest, 1), "no allowance") - key := allowanceKey(owner, spender) - urequire.False(t, b.allowances.Has(key), "allowance should be removed") - urequire.Equal(t, b.Allowance(owner, spender), uint64(0), "allowance should be 0") -} diff --git a/examples/gno.land/p/demo/grc/grc20/examples_test.gno b/examples/gno.land/p/demo/grc/grc20/examples_test.gno new file mode 100644 index 00000000000..6a2bfa11d8c --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/examples_test.gno @@ -0,0 +1,18 @@ +package grc20 + +// XXX: write Examples + +func ExampleInit() {} +func ExampleExposeBankForMaketxRunOrImports() {} +func ExampleCustomTellerImpl() {} +func ExampleAllowance() {} +func ExampleRealmBanker() {} +func ExamplePrevRealmBanker() {} +func ExampleAccountBanker() {} +func ExampleTransfer() {} +func ExampleApprove() {} +func ExampleTransferFrom() {} +func ExampleMint() {} +func ExampleBurn() {} + +// ... diff --git a/examples/gno.land/p/demo/grc/grc20/gno.mod b/examples/gno.land/p/demo/grc/grc20/gno.mod index e872d80ec12..37377b32e73 100644 --- a/examples/gno.land/p/demo/grc/grc20/gno.mod +++ b/examples/gno.land/p/demo/grc/grc20/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/grc/grc20 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/grc/exts v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc20/mock.gno b/examples/gno.land/p/demo/grc/grc20/mock.gno new file mode 100644 index 00000000000..4952470d665 --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/mock.gno @@ -0,0 +1,3 @@ +package grc20 + +// XXX: func Mock(t *Token) diff --git a/examples/gno.land/p/demo/grc/grc20/tellers.gno b/examples/gno.land/p/demo/grc/grc20/tellers.gno new file mode 100644 index 00000000000..ee5d2d7fcca --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/tellers.gno @@ -0,0 +1,139 @@ +package grc20 + +import ( + "std" +) + +// CallerTeller returns a GRC20 compatible teller that checks the PrevRealm +// caller for each call. It's usually safe to expose it publicly to let users +// manipulate their tokens directly, or for realms to use their allowance. +func (tok *Token) CallerTeller() Teller { + if tok == nil { + panic("Token cannot be nil") + } + + return &fnTeller{ + accountFn: func() std.Address { + caller := std.PrevRealm().Addr() + return caller + }, + Token: tok, + } +} + +// ReadonlyTeller is a GRC20 compatible teller that panics for any write operation. +func (tok *Token) ReadonlyTeller() Teller { + if tok == nil { + panic("Token cannot be nil") + } + + return &fnTeller{ + accountFn: nil, + Token: tok, + } +} + +// RealmTeller returns a GRC20 compatible teller that will store the +// caller realm permanently. Calling anything through this teller will +// result in allowance or balance changes for the realm that initialized the teller. +// The initializer of this teller should usually never share the resulting Teller from +// this method except maybe for advanced delegation flows such as a DAO treasury +// management. +func (tok *Token) RealmTeller() Teller { + if tok == nil { + panic("Token cannot be nil") + } + + caller := std.PrevRealm().Addr() + + return &fnTeller{ + accountFn: func() std.Address { + return caller + }, + Token: tok, + } +} + +// RealmSubTeller is like RealmTeller but uses the provided slug to derive a +// subaccount. +func (tok *Token) RealmSubTeller(slug string) Teller { + if tok == nil { + panic("Token cannot be nil") + } + + caller := std.PrevRealm().Addr() + account := accountSlugAddr(caller, slug) + + return &fnTeller{ + accountFn: func() std.Address { + return account + }, + Token: tok, + } +} + +// ImpersonateTeller returns a GRC20 compatible teller that impersonates as a +// specified address. This allows operations to be performed as if they were +// executed by the given address, enabling the caller to manipulate tokens on +// behalf of that address. +// +// It is particularly useful in scenarios where a contract needs to perform +// actions on behalf of a user or another account, without exposing the +// underlying logic or requiring direct access to the user's account. The +// returned teller will use the provided address for all operations, effectively +// masking the original caller. +// +// This method should be used with caution, as it allows for potentially +// sensitive operations to be performed under the guise of another address. +func (ledger *PrivateLedger) ImpersonateTeller(addr std.Address) Teller { + if ledger == nil { + panic("Ledger cannot be nil") + } + + return &fnTeller{ + accountFn: func() std.Address { + return addr + }, + Token: ledger.token, + } +} + +// generic tellers methods. +// + +func (ft *fnTeller) Transfer(to std.Address, amount uint64) error { + if ft.accountFn == nil { + return ErrReadonly + } + caller := ft.accountFn() + return ft.Token.ledger.Transfer(caller, to, amount) +} + +func (ft *fnTeller) Approve(spender std.Address, amount uint64) error { + if ft.accountFn == nil { + return ErrReadonly + } + caller := ft.accountFn() + return ft.Token.ledger.Approve(caller, spender, amount) +} + +func (ft *fnTeller) TransferFrom(owner, to std.Address, amount uint64) error { + if ft.accountFn == nil { + return ErrReadonly + } + spender := ft.accountFn() + return ft.Token.ledger.TransferFrom(owner, spender, to, amount) +} + +// helpers +// + +// accountSlugAddr returns the address derived from the specified address and slug. +func accountSlugAddr(addr std.Address, slug string) std.Address { + // XXX: use a new `std.XXX` call for this. + if slug == "" { + return addr + } + key := addr.String() + "/" + slug + return std.DerivePkgAddr(key) // temporarily using this helper +} diff --git a/examples/gno.land/p/demo/grc/grc20/tellers_test.gno b/examples/gno.land/p/demo/grc/grc20/tellers_test.gno new file mode 100644 index 00000000000..2a724964edc --- /dev/null +++ b/examples/gno.land/p/demo/grc/grc20/tellers_test.gno @@ -0,0 +1,130 @@ +package grc20 + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" +) + +func TestCallerTellerImpl(t *testing.T) { + tok, _ := NewToken("Dummy", "DUMMY", 4) + teller := tok.CallerTeller() + urequire.False(t, tok == nil) + var _ Teller = teller +} + +func TestTeller(t *testing.T) { + var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + carl = testutils.TestAddress("carl") + ) + + token, ledger := NewToken("Dummy", "DUMMY", 6) + + checkBalances := func(aliceEB, bobEB, carlEB uint64) { + t.Helper() + exp := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceEB, bobEB, carlEB) + aliceGB := token.BalanceOf(alice) + bobGB := token.BalanceOf(bob) + carlGB := token.BalanceOf(carl) + got := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceGB, bobGB, carlGB) + uassert.Equal(t, got, exp, "invalid balances") + } + checkAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint64) { + t.Helper() + exp := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abEB, acEB, baEB, bcEB, caEB, cbEB) + abGB := token.Allowance(alice, bob) + acGB := token.Allowance(alice, carl) + baGB := token.Allowance(bob, alice) + bcGB := token.Allowance(bob, carl) + caGB := token.Allowance(carl, alice) + cbGB := token.Allowance(carl, bob) + got := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abGB, acGB, baGB, bcGB, caGB, cbGB) + uassert.Equal(t, got, exp, "invalid allowances") + } + + checkBalances(0, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.Mint(alice, 1000)) + urequire.NoError(t, ledger.Mint(alice, 100)) + checkBalances(1100, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.Approve(alice, bob, 99999999)) + checkBalances(1100, 0, 0) + checkAllowances(99999999, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.Approve(alice, bob, 400)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.Error(t, ledger.TransferFrom(alice, bob, carl, 100000000)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.TransferFrom(alice, bob, carl, 100)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.Error(t, ledger.SpendAllowance(alice, bob, 2000000)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.NoError(t, ledger.SpendAllowance(alice, bob, 100)) + checkBalances(1000, 0, 100) + checkAllowances(200, 0, 0, 0, 0, 0) +} + +func TestCallerTeller(t *testing.T) { + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + carl := testutils.TestAddress("carl") + + token, ledger := NewToken("Dummy", "DUMMY", 6) + teller := token.CallerTeller() + + checkBalances := func(aliceEB, bobEB, carlEB uint64) { + t.Helper() + exp := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceEB, bobEB, carlEB) + aliceGB := token.BalanceOf(alice) + bobGB := token.BalanceOf(bob) + carlGB := token.BalanceOf(carl) + got := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceGB, bobGB, carlGB) + uassert.Equal(t, got, exp, "invalid balances") + } + checkAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint64) { + t.Helper() + exp := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abEB, acEB, baEB, bcEB, caEB, cbEB) + abGB := token.Allowance(alice, bob) + acGB := token.Allowance(alice, carl) + baGB := token.Allowance(bob, alice) + bcGB := token.Allowance(bob, carl) + caGB := token.Allowance(carl, alice) + cbGB := token.Allowance(carl, bob) + got := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abGB, acGB, baGB, bcGB, caGB, cbGB) + uassert.Equal(t, got, exp, "invalid allowances") + } + + urequire.NoError(t, ledger.Mint(alice, 1000)) + checkBalances(1000, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + std.TestSetOrigCaller(alice) + urequire.NoError(t, teller.Approve(bob, 600)) + checkBalances(1000, 0, 0) + checkAllowances(600, 0, 0, 0, 0, 0) + + std.TestSetOrigCaller(bob) + urequire.Error(t, teller.TransferFrom(alice, carl, 700)) + checkBalances(1000, 0, 0) + checkAllowances(600, 0, 0, 0, 0, 0) + urequire.NoError(t, teller.TransferFrom(alice, carl, 400)) + checkBalances(600, 0, 400) + checkAllowances(200, 0, 0, 0, 0, 0) +} diff --git a/examples/gno.land/p/demo/grc/grc20/token.gno b/examples/gno.land/p/demo/grc/grc20/token.gno index e13599e90bb..3ab3abc63a3 100644 --- a/examples/gno.land/p/demo/grc/grc20/token.gno +++ b/examples/gno.land/p/demo/grc/grc20/token.gno @@ -1,88 +1,248 @@ package grc20 import ( + "math/overflow" "std" + "strconv" - "gno.land/p/demo/grc/exts" + "gno.land/p/demo/ufmt" ) -// token implements the Token interface. -// -// It is generated with Banker.Token(). -// It can safely be explosed publicly. -type token struct { - banker *Banker +// NewToken creates a new Token. +// It returns a pointer to the Token and a pointer to the Ledger. +// Expected usage: Token, admin := NewToken("Dummy", "DUMMY", 4) +func NewToken(name, symbol string, decimals uint) (*Token, *PrivateLedger) { + if name == "" { + panic("name should not be empty") + } + if symbol == "" { + panic("symbol should not be empty") + } + // XXX additional checks (length, characters, limits, etc) + + ledger := &PrivateLedger{} + token := &Token{ + name: name, + symbol: symbol, + decimals: decimals, + ledger: ledger, + } + ledger.token = token + return token, ledger } -// var _ Token = (*token)(nil) -func (t *token) GetName() string { return t.banker.name } -func (t *token) GetSymbol() string { return t.banker.symbol } -func (t *token) GetDecimals() uint { return t.banker.decimals } -func (t *token) TotalSupply() uint64 { return t.banker.totalSupply } +// GetName returns the name of the token. +func (tok Token) GetName() string { return tok.name } + +// GetSymbol returns the symbol of the token. +func (tok Token) GetSymbol() string { return tok.symbol } -func (t *token) BalanceOf(owner std.Address) uint64 { - return t.banker.BalanceOf(owner) +// GetDecimals returns the number of decimals used to get the token's precision. +func (tok Token) GetDecimals() uint { return tok.decimals } + +// TotalSupply returns the total supply of the token. +func (tok Token) TotalSupply() uint64 { return tok.ledger.totalSupply } + +// KnownAccounts returns the number of known accounts in the bank. +func (tok Token) KnownAccounts() int { return tok.ledger.balances.Size() } + +// BalanceOf returns the balance of the specified address. +func (tok Token) BalanceOf(address std.Address) uint64 { + return tok.ledger.balanceOf(address) } -func (t *token) Transfer(to std.Address, amount uint64) error { - caller := std.PrevRealm().Addr() - return t.banker.Transfer(caller, to, amount) +// Allowance returns the allowance of the specified owner and spender. +func (tok Token) Allowance(owner, spender std.Address) uint64 { + return tok.ledger.allowance(owner, spender) } -func (t *token) Allowance(owner, spender std.Address) uint64 { - return t.banker.Allowance(owner, spender) +func (tok *Token) RenderHome() string { + str := "" + str += ufmt.Sprintf("# %s ($%s)\n\n", tok.name, tok.symbol) + str += ufmt.Sprintf("* **Decimals**: %d\n", tok.decimals) + str += ufmt.Sprintf("* **Total supply**: %d\n", tok.ledger.totalSupply) + str += ufmt.Sprintf("* **Known accounts**: %d\n", tok.KnownAccounts()) + return str } -func (t *token) Approve(spender std.Address, amount uint64) error { - caller := std.PrevRealm().Addr() - return t.banker.Approve(caller, spender, amount) +// Getter returns a TokenGetter function that returns this token. This allows +// storing indirect pointers to a token in a remote realm. +func (tok *Token) Getter() TokenGetter { + return func() *Token { + return tok + } } -func (t *token) TransferFrom(from, to std.Address, amount uint64) error { - spender := std.PrevRealm().Addr() - if err := t.banker.SpendAllowance(from, spender, amount); err != nil { +// SpendAllowance decreases the allowance of the specified owner and spender. +func (led *PrivateLedger) SpendAllowance(owner, spender std.Address, amount uint64) error { + if !owner.IsValid() { + return ErrInvalidAddress + } + if !spender.IsValid() { + return ErrInvalidAddress + } + + currentAllowance := led.allowance(owner, spender) + if currentAllowance < amount { + return ErrInsufficientAllowance + } + + key := allowanceKey(owner, spender) + newAllowance := currentAllowance - amount + + if newAllowance == 0 { + led.allowances.Remove(key) + } else { + led.allowances.Set(key, newAllowance) + } + + return nil +} + +// Transfer transfers tokens from the specified from address to the specified to address. +func (led *PrivateLedger) Transfer(from, to std.Address, amount uint64) error { + if !from.IsValid() { + return ErrInvalidAddress + } + if !to.IsValid() { + return ErrInvalidAddress + } + if from == to { + return ErrCannotTransferToSelf + } + + var ( + toBalance = led.balanceOf(to) + fromBalance = led.balanceOf(from) + ) + + if fromBalance < amount { + return ErrInsufficientBalance + } + + var ( + newToBalance = toBalance + amount + newFromBalance = fromBalance - amount + ) + + led.balances.Set(string(to), newToBalance) + led.balances.Set(string(from), newFromBalance) + + std.Emit( + TransferEvent, + "from", from.String(), + "to", to.String(), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// TransferFrom transfers tokens from the specified owner to the specified to address. +// It first checks if the owner has sufficient balance and then decreases the allowance. +func (led *PrivateLedger) TransferFrom(owner, spender, to std.Address, amount uint64) error { + if led.balanceOf(owner) < amount { + return ErrInsufficientBalance + } + if err := led.SpendAllowance(owner, spender, amount); err != nil { return err } - return t.banker.Transfer(from, to, amount) + // XXX: since we don't "panic", we should take care of rollbacking spendAllowance if transfer fails. + return led.Transfer(owner, to, amount) +} + +// Approve sets the allowance of the specified owner and spender. +func (led *PrivateLedger) Approve(owner, spender std.Address, amount uint64) error { + if !owner.IsValid() || !spender.IsValid() { + return ErrInvalidAddress + } + + led.allowances.Set(allowanceKey(owner, spender), amount) + + std.Emit( + ApprovalEvent, + "owner", string(owner), + "spender", string(spender), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// Mint increases the total supply of the token and adds the specified amount to the specified address. +func (led *PrivateLedger) Mint(address std.Address, amount uint64) error { + if !address.IsValid() { + return ErrInvalidAddress + } + + // XXX: math/overflow is not supporting uint64. + // This checks prevents overflow but makes the totalSupply limited to a uint63. + sum, ok := overflow.Add64(int64(led.totalSupply), int64(amount)) + if !ok { + return ErrOverflow + } + + led.totalSupply = uint64(sum) + currentBalance := led.balanceOf(address) + newBalance := currentBalance + amount + + led.balances.Set(string(address), newBalance) + + std.Emit( + TransferEvent, + "from", "", + "to", string(address), + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// Burn decreases the total supply of the token and subtracts the specified amount from the specified address. +func (led *PrivateLedger) Burn(address std.Address, amount uint64) error { + if !address.IsValid() { + return ErrInvalidAddress + } + + currentBalance := led.balanceOf(address) + if currentBalance < amount { + return ErrInsufficientBalance + } + + led.totalSupply -= amount + newBalance := currentBalance - amount + + led.balances.Set(string(address), newBalance) + + std.Emit( + TransferEvent, + "from", string(address), + "to", "", + "value", strconv.Itoa(int(amount)), + ) + + return nil +} + +// balanceOf returns the balance of the specified address. +func (led PrivateLedger) balanceOf(address std.Address) uint64 { + balance, found := led.balances.Get(address.String()) + if !found { + return 0 + } + return balance.(uint64) +} + +// allowance returns the allowance of the specified owner and spender. +func (led PrivateLedger) allowance(owner, spender std.Address) uint64 { + allowance, found := led.allowances.Get(allowanceKey(owner, spender)) + if !found { + return 0 + } + return allowance.(uint64) } -type Token2 interface { - exts.TokenMetadata - - // Returns the amount of tokens in existence. - TotalSupply() uint64 - - // Returns the amount of tokens owned by `account`. - BalanceOf(account std.Address) uint64 - - // Moves `amount` tokens from the caller's account to `to`. - // - // Returns an error if the operation failed. - Transfer(to std.Address, amount uint64) error - - // Returns the remaining number of tokens that `spender` will be - // allowed to spend on behalf of `owner` through {transferFrom}. This is - // zero by default. - // - // This value changes when {approve} or {transferFrom} are called. - Allowance(owner, spender std.Address) uint64 - - // Sets `amount` as the allowance of `spender` over the caller's tokens. - // - // Returns an error if the operation failed. - // - // IMPORTANT: Beware that changing an allowance with this method brings the risk - // that someone may use both the old and the new allowance by unfortunate - // transaction ordering. One possible solution to mitigate this race - // condition is to first reduce the spender's allowance to 0 and set the - // desired value afterwards: - // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - Approve(spender std.Address, amount uint64) error - - // Moves `amount` tokens from `from` to `to` using the - // allowance mechanism. `amount` is then deducted from the caller's - // allowance. - // - // Returns an error if the operation failed. - TransferFrom(from, to std.Address, amount uint64) error +// allowanceKey returns the key for the allowance of the specified owner and spender. +func allowanceKey(owner, spender std.Address) string { + return owner.String() + ":" + spender.String() } diff --git a/examples/gno.land/p/demo/grc/grc20/token_test.gno b/examples/gno.land/p/demo/grc/grc20/token_test.gno index 713ad734ed8..c68513554f0 100644 --- a/examples/gno.land/p/demo/grc/grc20/token_test.gno +++ b/examples/gno.land/p/demo/grc/grc20/token_test.gno @@ -1,72 +1,89 @@ package grc20 import ( - "std" "testing" "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" "gno.land/p/demo/ufmt" "gno.land/p/demo/urequire" ) -func TestUserTokenImpl(t *testing.T) { - bank := NewBanker("Dummy", "DUMMY", 4) - tok := bank.Token() - _ = tok +func TestTestImpl(t *testing.T) { + bank, _ := NewToken("Dummy", "DUMMY", 4) + urequire.False(t, bank == nil, "dummy should not be nil") } -func TestUserApprove(t *testing.T) { - owner := testutils.TestAddress("owner") - spender := testutils.TestAddress("spender") - dest := testutils.TestAddress("dest") - - bank := NewBanker("Dummy", "DUMMY", 6) - tok := bank.Token() - - // Set owner as the original caller - std.TestSetOrigCaller(owner) - // Mint 100000000 tokens for owner - urequire.NoError(t, bank.Mint(owner, 100000000)) - - // Approve spender to spend 5000000 tokens - urequire.NoError(t, tok.Approve(spender, 5000000)) - - // Set spender as the original caller - std.TestSetOrigCaller(spender) - // Try to transfer 10000000 tokens from owner to dest, should fail because it exceeds allowance - urequire.Error(t, - tok.TransferFrom(owner, dest, 10000000), - ErrInsufficientAllowance.Error(), - "should not be able to transfer more than approved", +func TestToken(t *testing.T) { + var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + carl = testutils.TestAddress("carl") ) - // Define a set of test data with spend amount and expected remaining allowance - tests := []struct { - spend uint64 // Spend amount - exp uint64 // Remaining allowance - }{ - {3, 4999997}, - {999997, 4000000}, - {4000000, 0}, - } + bank, adm := NewToken("Dummy", "DUMMY", 6) - // perform transfer operation,and check if allowance and balance are correct - for _, tt := range tests { - b0 := tok.BalanceOf(dest) - // Perform transfer from owner to dest - urequire.NoError(t, tok.TransferFrom(owner, dest, tt.spend)) - a := tok.Allowance(owner, spender) - // Check if allowance equals expected value - urequire.True(t, a == tt.exp, ufmt.Sprintf("allowance exp: %d,got %d", tt.exp, a)) - - // Get dest current balance - b := tok.BalanceOf(dest) - // Calculate expected balance ,should be initial balance plus transfer amount - expB := b0 + tt.spend - // Check if balance equals expected value - urequire.True(t, b == expB, ufmt.Sprintf("balance exp: %d,got %d", expB, b)) + checkBalances := func(aliceEB, bobEB, carlEB uint64) { + t.Helper() + exp := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceEB, bobEB, carlEB) + aliceGB := bank.BalanceOf(alice) + bobGB := bank.BalanceOf(bob) + carlGB := bank.BalanceOf(carl) + got := ufmt.Sprintf("alice=%d bob=%d carl=%d", aliceGB, bobGB, carlGB) + uassert.Equal(t, got, exp, "invalid balances") + } + checkAllowances := func(abEB, acEB, baEB, bcEB, caEB, cbEB uint64) { + t.Helper() + exp := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abEB, acEB, baEB, bcEB, caEB, cbEB) + abGB := bank.Allowance(alice, bob) + acGB := bank.Allowance(alice, carl) + baGB := bank.Allowance(bob, alice) + bcGB := bank.Allowance(bob, carl) + caGB := bank.Allowance(carl, alice) + cbGB := bank.Allowance(carl, bob) + got := ufmt.Sprintf("ab=%d ac=%d ba=%d bc=%d ca=%d cb=%s", abGB, acGB, baGB, bcGB, caGB, cbGB) + uassert.Equal(t, got, exp, "invalid allowances") } - // Try to transfer one token from owner to dest ,should fail because no allowance left - urequire.Error(t, tok.TransferFrom(owner, dest, 1), ErrInsufficientAllowance.Error(), "no allowance") + checkBalances(0, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.Mint(alice, 1000)) + urequire.NoError(t, adm.Mint(alice, 100)) + checkBalances(1100, 0, 0) + checkAllowances(0, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.Approve(alice, bob, 99999999)) + checkBalances(1100, 0, 0) + checkAllowances(99999999, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.Approve(alice, bob, 400)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.Error(t, adm.TransferFrom(alice, bob, carl, 100000000)) + checkBalances(1100, 0, 0) + checkAllowances(400, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.TransferFrom(alice, bob, carl, 100)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.Error(t, adm.SpendAllowance(alice, bob, 2000000)) + checkBalances(1000, 0, 100) + checkAllowances(300, 0, 0, 0, 0, 0) + + urequire.NoError(t, adm.SpendAllowance(alice, bob, 100)) + checkBalances(1000, 0, 100) + checkAllowances(200, 0, 0, 0, 0, 0) +} + +func TestOverflow(t *testing.T) { + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + tok, adm := NewToken("Dummy", "DUMMY", 6) + + urequire.NoError(t, adm.Mint(alice, 2<<62)) + urequire.Equal(t, tok.BalanceOf(alice), uint64(2<<62)) + urequire.Error(t, adm.Mint(bob, 2<<62)) } diff --git a/examples/gno.land/p/demo/grc/grc20/types.gno b/examples/gno.land/p/demo/grc/grc20/types.gno index fe3aef349d9..816bbe8a1d9 100644 --- a/examples/gno.land/p/demo/grc/grc20/types.gno +++ b/examples/gno.land/p/demo/grc/grc20/types.gno @@ -4,17 +4,17 @@ import ( "errors" "std" + "gno.land/p/demo/avl" "gno.land/p/demo/grc/exts" ) -var ( - ErrInsufficientBalance = errors.New("insufficient balance") - ErrInsufficientAllowance = errors.New("insufficient allowance") - ErrInvalidAddress = errors.New("invalid address") - ErrCannotTransferToSelf = errors.New("cannot send transfer to self") -) - -type Token interface { +// Teller interface defines the methods that a GRC20 token must implement. It +// extends the TokenMetadata interface to include methods for managing token +// transfers, allowances, and querying balances. +// +// The Teller interface is designed to ensure that any token adhering to this +// standard provides a consistent API for interacting with fungible tokens. +type Teller interface { exts.TokenMetadata // Returns the amount of tokens in existence. @@ -39,11 +39,11 @@ type Token interface { // // Returns an error if the operation failed. // - // IMPORTANT: Beware that changing an allowance with this method brings the risk - // that someone may use both the old and the new allowance by unfortunate - // transaction ordering. One possible solution to mitigate this race - // condition is to first reduce the spender's allowance to 0 and set the - // desired value afterwards: + // IMPORTANT: Beware that changing an allowance with this method brings + // the risk that someone may use both the old and the new allowance by + // unfortunate transaction ordering. One possible solution to mitigate + // this race condition is to first reduce the spender's allowance to 0 + // and set the desired value afterwards: // https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 Approve(spender std.Address, amount uint64) error @@ -55,7 +55,69 @@ type Token interface { TransferFrom(from, to std.Address, amount uint64) error } +// Token represents a fungible token with a name, symbol, and a certain number +// of decimal places. It maintains a ledger for tracking balances and allowances +// of addresses. +// +// The Token struct provides methods for retrieving token metadata, such as the +// name, symbol, and decimals, as well as methods for interacting with the +// ledger, including checking balances and allowances. +type Token struct { + // Name of the token (e.g., "Dummy Token"). + name string + // Symbol of the token (e.g., "DUMMY"). + symbol string + // Number of decimal places used for the token's precision. + decimals uint + // Pointer to the PrivateLedger that manages balances and allowances. + ledger *PrivateLedger +} + +// TokenGetter is a function type that returns a Token pointer. This type allows +// bypassing a limitation where we cannot directly pass Token pointers between +// realms. Instead, we pass this function which can then be called to get the +// Token pointer. For more details on this limitation and workaround, see: +// https://github.com/gnolang/gno/pull/3135 +type TokenGetter func() *Token + +// PrivateLedger is a struct that holds the balances and allowances for the +// token. It provides administrative functions for minting, burning, +// transferring tokens, and managing allowances. +// +// The PrivateLedger is not safe to expose publicly, as it contains sensitive +// information regarding token balances and allowances, and allows direct, +// unrestricted access to all administrative functions. +type PrivateLedger struct { + // Total supply of the token managed by this ledger. + totalSupply uint64 + // std.Address -> uint64 + balances avl.Tree + // owner.(std.Address)+":"+spender.(std.Address)) -> uint64 + allowances avl.Tree + // Pointer to the associated Token struct + token *Token +} + +var ( + ErrInsufficientBalance = errors.New("insufficient balance") + ErrInsufficientAllowance = errors.New("insufficient allowance") + ErrInvalidAddress = errors.New("invalid address") + ErrCannotTransferToSelf = errors.New("cannot send transfer to self") + ErrReadonly = errors.New("banker is readonly") + ErrRestrictedTokenOwner = errors.New("restricted to bank owner") + ErrOverflow = errors.New("Mint overflow") +) + const ( + MintEvent = "Mint" + BurnEvent = "Burn" TransferEvent = "Transfer" ApprovalEvent = "Approval" ) + +type fnTeller struct { + accountFn func() std.Address + *Token +} + +var _ Teller = (*fnTeller)(nil) diff --git a/examples/gno.land/p/demo/grc/grc721/basic_nft.gno b/examples/gno.land/p/demo/grc/grc721/basic_nft.gno index bec7338db42..0505aaa1c26 100644 --- a/examples/gno.land/p/demo/grc/grc721/basic_nft.gno +++ b/examples/gno.land/p/demo/grc/grc721/basic_nft.gno @@ -2,6 +2,7 @@ package grc721 import ( "std" + "strconv" "gno.land/p/demo/avl" "gno.land/p/demo/ufmt" @@ -120,8 +121,12 @@ func (s *basicNFT) Approve(to std.Address, tid TokenID) error { } s.tokenApprovals.Set(string(tid), to.String()) - event := ApprovalEvent{owner, to, tid} - emit(&event) + std.Emit( + ApprovalEvent, + "owner", string(owner), + "to", string(to), + "tokenId", string(tid), + ) return nil } @@ -219,8 +224,11 @@ func (s *basicNFT) Burn(tid TokenID) error { s.balances.Set(owner.String(), balance) s.owners.Remove(string(tid)) - event := TransferEvent{owner, zeroAddress, tid} - emit(&event) + std.Emit( + BurnEvent, + "from", string(owner), + "tokenId", string(tid), + ) s.afterTokenTransfer(owner, zeroAddress, tid, 1) @@ -238,8 +246,12 @@ func (s *basicNFT) setApprovalForAll(owner, operator std.Address, approved bool) key := owner.String() + ":" + operator.String() s.operatorApprovals.Set(key, approved) - event := ApprovalForAllEvent{owner, operator, approved} - emit(&event) + std.Emit( + ApprovalForAllEvent, + "owner", string(owner), + "to", string(operator), + "approved", strconv.FormatBool(approved), + ) return nil } @@ -291,8 +303,12 @@ func (s *basicNFT) transfer(from, to std.Address, tid TokenID) error { s.balances.Set(to.String(), toBalance) s.owners.Set(string(tid), to) - event := TransferEvent{from, to, tid} - emit(&event) + std.Emit( + TransferEvent, + "from", string(from), + "to", string(to), + "tokenId", string(tid), + ) s.afterTokenTransfer(from, to, tid, 1) @@ -324,8 +340,11 @@ func (s *basicNFT) mint(to std.Address, tid TokenID) error { s.balances.Set(to.String(), toBalance) s.owners.Set(string(tid), to) - event := TransferEvent{zeroAddress, to, tid} - emit(&event) + std.Emit( + MintEvent, + "to", string(to), + "tokenId", string(tid), + ) s.afterTokenTransfer(zeroAddress, to, tid, 1) diff --git a/examples/gno.land/p/demo/grc/grc721/gno.mod b/examples/gno.land/p/demo/grc/grc721/gno.mod index 9e1d6f56ffc..f27caee5282 100644 --- a/examples/gno.land/p/demo/grc/grc721/gno.mod +++ b/examples/gno.land/p/demo/grc/grc721/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/grc/grc721 - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno b/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno index 360f73ed106..05fad41be18 100644 --- a/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno +++ b/examples/gno.land/p/demo/grc/grc721/grc721_metadata.gno @@ -85,9 +85,12 @@ func (s *metadataNFT) mint(to std.Address, tid TokenID) error { // Set owner of the token ID to the recipient address s.basicNFT.owners.Set(string(tid), to) - // Emit transfer event - event := TransferEvent{zeroAddress, to, tid} - emit(&event) + std.Emit( + TransferEvent, + "from", string(zeroAddress), + "to", string(to), + "tokenId", string(tid), + ) s.basicNFT.afterTokenTransfer(zeroAddress, to, tid, 1) diff --git a/examples/gno.land/p/demo/grc/grc721/igrc721.gno b/examples/gno.land/p/demo/grc/grc721/igrc721.gno index 387547a7e26..6c26c953d51 100644 --- a/examples/gno.land/p/demo/grc/grc721/igrc721.gno +++ b/examples/gno.land/p/demo/grc/grc721/igrc721.gno @@ -19,20 +19,10 @@ type ( TokenURI string ) -type TransferEvent struct { - From std.Address - To std.Address - TokenID TokenID -} - -type ApprovalEvent struct { - Owner std.Address - Approved std.Address - TokenID TokenID -} - -type ApprovalForAllEvent struct { - Owner std.Address - Operator std.Address - Approved bool -} +const ( + MintEvent = "Mint" + BurnEvent = "Burn" + TransferEvent = "Transfer" + ApprovalEvent = "Approval" + ApprovalForAllEvent = "ApprovalForAll" +) diff --git a/examples/gno.land/p/demo/grc/grc777/gno.mod b/examples/gno.land/p/demo/grc/grc777/gno.mod index 9fbf2f2b7cd..da5c762b2ec 100644 --- a/examples/gno.land/p/demo/grc/grc777/gno.mod +++ b/examples/gno.land/p/demo/grc/grc777/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/grc/grc777 - -require gno.land/p/demo/grc/exts v0.0.0-latest diff --git a/examples/gno.land/p/demo/groups/gno.mod b/examples/gno.land/p/demo/groups/gno.mod index f0749e3f411..d33df3866fa 100644 --- a/examples/gno.land/p/demo/groups/gno.mod +++ b/examples/gno.land/p/demo/groups/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/groups - -require ( - gno.land/p/demo/rat v0.0.0-latest - gno.land/r/demo/boards v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/groups/groups.gno b/examples/gno.land/p/demo/groups/groups.gno deleted file mode 100644 index fcf77dd2a74..00000000000 --- a/examples/gno.land/p/demo/groups/groups.gno +++ /dev/null @@ -1,8 +0,0 @@ -package groups - -import "gno.land/r/demo/boards" - -// TODO implement something and test. -type Group struct { - Board *boards.Board -} diff --git a/examples/gno.land/p/demo/int256/LICENSE b/examples/gno.land/p/demo/int256/LICENSE deleted file mode 100644 index fc7e78a4875..00000000000 --- a/examples/gno.land/p/demo/int256/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Trịnh Đức Bảo Linh(Kevin) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/examples/gno.land/p/demo/int256/README.md b/examples/gno.land/p/demo/int256/README.md deleted file mode 100644 index be467471199..00000000000 --- a/examples/gno.land/p/demo/int256/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Fixed size signed 256-bit math library - -1. This is a library specialized at replacing the big.Int library for math based on signed 256-bit types. -2. It uses [uint256](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo/uint256) as the underlying type. - -ported from [mempooler/int256](https://github.com/mempooler/int256) diff --git a/examples/gno.land/p/demo/int256/absolute.gno b/examples/gno.land/p/demo/int256/absolute.gno deleted file mode 100644 index 825dd60c62a..00000000000 --- a/examples/gno.land/p/demo/int256/absolute.gno +++ /dev/null @@ -1,18 +0,0 @@ -package int256 - -import "gno.land/p/demo/uint256" - -// Abs returns |z| -func (z *Int) Abs() *uint256.Uint { - return z.abs.Clone() -} - -// AbsGt returns true if |z| > x, where x is a uint256 -func (z *Int) AbsGt(x *uint256.Uint) bool { - return z.abs.Gt(x) -} - -// AbsLt returns true if |z| < x, where x is a uint256 -func (z *Int) AbsLt(x *uint256.Uint) bool { - return z.abs.Lt(x) -} diff --git a/examples/gno.land/p/demo/int256/absolute_test.gno b/examples/gno.land/p/demo/int256/absolute_test.gno deleted file mode 100644 index 55f6e41d0c8..00000000000 --- a/examples/gno.land/p/demo/int256/absolute_test.gno +++ /dev/null @@ -1,105 +0,0 @@ -package int256 - -import ( - "testing" - - "gno.land/p/demo/uint256" -) - -func TestAbs(t *testing.T) { - tests := []struct { - x, want string - }{ - {"0", "0"}, - {"1", "1"}, - {"-1", "1"}, - {"-2", "2"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - got := x.Abs() - - if got.ToString() != tc.want { - t.Errorf("Abs(%s) = %v, want %v", tc.x, got.ToString(), tc.want) - } - } -} - -func TestAbsGt(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "0", "false"}, - {"1", "0", "true"}, - {"-1", "0", "true"}, - {"-1", "1", "false"}, - {"-2", "1", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "false"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.AbsGt(y) - - if got != (tc.want == "true") { - t.Errorf("AbsGt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestAbsLt(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "0", "false"}, - {"1", "0", "false"}, - {"-1", "0", "false"}, - {"-1", "1", "false"}, - {"-2", "1", "false"}, - {"-5", "10", "true"}, - {"31330", "31337", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "false"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "false"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "false"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.AbsLt(y) - - if got != (tc.want == "true") { - t.Errorf("AbsLt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} diff --git a/examples/gno.land/p/demo/int256/arithmetic.gno b/examples/gno.land/p/demo/int256/arithmetic.gno index ce05426f585..572dd15e7e6 100644 --- a/examples/gno.land/p/demo/int256/arithmetic.gno +++ b/examples/gno.land/p/demo/int256/arithmetic.gno @@ -1,196 +1,350 @@ package int256 -import "gno.land/p/demo/uint256" +import ( + "gno.land/p/demo/uint256" +) -func (z *Int) Add(x, y *Int) *Int { - z.initiateAbs() - - neg := x.neg +const divisionByZeroError = "division by zero" - if x.neg == y.neg { - // x + y == x + y - // (-x) + (-y) == -(x + y) - z.abs = z.abs.Add(x.abs, y.abs) - } else { - // x + (-y) == x - y == -(y - x) - // (-x) + y == y - x == -(x - y) - if x.abs.Cmp(y.abs) >= 0 { - z.abs = z.abs.Sub(x.abs, y.abs) - } else { - neg = !neg - z.abs = z.abs.Sub(y.abs, x.abs) - } - } - z.neg = neg // 0 has no sign +// Add adds two int256 values and saves the result in z. +func (z *Int) Add(x, y *Int) *Int { + z.value.Add(&x.value, &y.value) return z } -// AddUint256 set z to the sum x + y, where y is a uint256, and returns z +// AddUint256 adds int256 and uint256 values and saves the result in z. func (z *Int) AddUint256(x *Int, y *uint256.Uint) *Int { - if x.neg { - if x.abs.Gt(y) { - z.abs.Sub(x.abs, y) - z.neg = true - } else { - z.abs.Sub(y, x.abs) - z.neg = false - } - } else { - z.abs.Add(x.abs, y) - z.neg = false - } + z.value.Add(&x.value, y) return z } -// Sets z to the sum x + y, where z and x are uint256s and y is an int256. -func AddDelta(z, x *uint256.Uint, y *Int) { - if y.neg { - z.Sub(x, y.abs) - } else { - z.Add(x, y.abs) - } -} - -// Sets z to the sum x + y, where z and x are uint256s and y is an int256. -func AddDeltaOverflow(z, x *uint256.Uint, y *Int) bool { - var overflow bool - if y.neg { - _, overflow = z.SubOverflow(x, y.abs) - } else { - _, overflow = z.AddOverflow(x, y.abs) - } - return overflow -} - -// Sub sets z to the difference x-y and returns z. +// Sub subtracts two int256 values and saves the result in z. func (z *Int) Sub(x, y *Int) *Int { - z.initiateAbs() - - neg := x.neg - if x.neg != y.neg { - // x - (-y) == x + y - // (-x) - y == -(x + y) - z.abs = z.abs.Add(x.abs, y.abs) - } else { - // x - y == x - y == -(y - x) - // (-x) - (-y) == y - x == -(x - y) - if x.abs.Cmp(y.abs) >= 0 { - z.abs = z.abs.Sub(x.abs, y.abs) - } else { - neg = !neg - z.abs = z.abs.Sub(y.abs, x.abs) - } - } - z.neg = neg // 0 has no sign + z.value.Sub(&x.value, &y.value) return z } -// SubUint256 set z to the difference x - y, where y is a uint256, and returns z +// SubUint256 subtracts uint256 and int256 values and saves the result in z. func (z *Int) SubUint256(x *Int, y *uint256.Uint) *Int { - if x.neg { - z.abs.Add(x.abs, y) - z.neg = true - } else { - if x.abs.Lt(y) { - z.abs.Sub(y, x.abs) - z.neg = true - } else { - z.abs.Sub(x.abs, y) - z.neg = false - } - } + z.value.Sub(&x.value, y) return z } -// Mul sets z to the product x*y and returns z. +// Mul multiplies two int256 values and saves the result in z. +// +// It considers the signs of the operands to determine the sign of the result. func (z *Int) Mul(x, y *Int) *Int { - z.initiateAbs() + xAbs, xSign := x.Abs(), x.Sign() + yAbs, ySign := y.Abs(), y.Sign() + + z.value.Mul(xAbs, yAbs) + + if xSign != ySign { + z.value.Neg(&z.value) + } - z.abs = z.abs.Mul(x.abs, y.abs) - z.neg = x.neg != y.neg // 0 has no sign return z } -// MulUint256 sets z to the product x*y, where y is a uint256, and returns z -func (z *Int) MulUint256(x *Int, y *uint256.Uint) *Int { - z.abs.Mul(x.abs, y) - if z.abs.IsZero() { - z.neg = false - } else { - z.neg = x.neg +// Abs returns the absolute value of z. +func (z *Int) Abs() *uint256.Uint { + if z.Sign() >= 0 { + return &z.value } - return z + + var absValue uint256.Uint + absValue.Sub(uint0, &z.value).Neg(&z.value) + + return &absValue } -// Div sets z to the quotient x/y for y != 0 and returns z. +// Div performs integer division z = x / y and returns z. +// If y == 0, it panics with a "division by zero" error. +// +// This function handles signed division using two's complement representation: +// 1. Determine the sign of the quotient based on the signs of x and y. +// 2. Perform unsigned division on the absolute values. +// 3. Adjust the result's sign if necessary. +// +// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity): +// +// Let x = -6 (11111010 in two's complement) and y = 3 (00000011) +// +// Step 2: Determine signs +// +// x: negative (MSB is 1) +// y: positive (MSB is 0) +// +// Step 3: Calculate absolute values +// +// |x| = 6: 11111010 -> 00000110 +// NOT: 00000101 +// +1: 00000110 +// +// |y| = 3: 00000011 (already positive) +// +// Step 4: Unsigned division +// +// 6 / 3 = 2: 00000010 +// +// Step 5: Adjust sign (x and y have different signs) +// +// -2: 00000010 -> 11111110 +// NOT: 11111101 +// +1: 11111110 +// +// Note: This implementation rounds towards zero, as is standard in Go. func (z *Int) Div(x, y *Int) *Int { - z.initiateAbs() - - z.abs.Div(x.abs, y.abs) - if x.neg == y.neg { - z.neg = false - } else { - z.neg = true + // Step 1: Check for division by zero + if y.IsZero() { + panic(divisionByZeroError) } - return z -} -// DivUint256 sets z to the quotient x/y, where y is a uint256, and returns z -// If y == 0, z is set to 0 -func (z *Int) DivUint256(x *Int, y *uint256.Uint) *Int { - z.abs.Div(x.abs, y) - if z.abs.IsZero() { - z.neg = false - } else { - z.neg = x.neg + // Step 2, 3: Calculate the absolute values of x and y + xAbs, xSign := x.Abs(), x.Sign() + yAbs, ySign := y.Abs(), y.Sign() + + // Step 4: Perform unsigned division on the absolute values + z.value.Div(xAbs, yAbs) + + // Step 5: Adjust the sign of the result + // if x and y have different signs, the result must be negative + if xSign != ySign { + z.value.Neg(&z.value) } + return z } -// Quo sets z to the quotient x/y for y != 0 and returns z. -// If y == 0, a division-by-zero run-time panic occurs. -// OBS: differs from mempooler int256, we need to panic manually if y == 0 -// Quo implements truncated division (like Go); see QuoRem for more details. +// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity): +// +// Let x = -7 (11111001 in two's complement) and y = 3 (00000011) +// +// Step 2: Determine signs +// +// x: negative (MSB is 1) +// y: positive (MSB is 0) +// +// Step 3: Calculate absolute values +// +// |x| = 7: 11111001 -> 00000111 +// NOT: 00000110 +// +1: 00000111 +// +// |y| = 3: 00000011 (already positive) +// +// Step 4: Unsigned division +// +// 7 / 3 = 2: 00000010 +// +// Step 5: Adjust sign (x and y have different signs) +// +// -2: 00000010 -> 11111110 +// NOT: 11111101 +// +1: 11111110 +// +// Final result: -2 (11111110 in two's complement) +// +// Note: This implementation rounds towards zero, as is standard in Go. func (z *Int) Quo(x, y *Int) *Int { + // Step 1: Check for division by zero if y.IsZero() { - panic("division by zero") + panic(divisionByZeroError) } - z.initiateAbs() + // Step 2, 3: Calculate the absolute values of x and y + xAbs, xSign := x.Abs(), x.Sign() + yAbs, ySign := y.Abs(), y.Sign() + + // perform unsigned division on the absolute values + z.value.Div(xAbs, yAbs) + + // Step 5: Adjust the sign of the result + // if x and y have different signs, the result must be negative + if xSign != ySign { + z.value.Neg(&z.value) + } - z.abs = z.abs.Div(x.abs, y.abs) - z.neg = !(z.abs.IsZero()) && x.neg != y.neg // 0 has no sign return z } // Rem sets z to the remainder x%y for y != 0 and returns z. -// If y == 0, a division-by-zero run-time panic occurs. -// OBS: differs from mempooler int256, we need to panic manually if y == 0 -// Rem implements truncated modulus (like Go); see QuoRem for more details. +// +// The function performs the following steps: +// 1. Check for division by zero +// 2. Determine the signs of x and y +// 3. Calculate the absolute values of x and y +// 4. Perform unsigned division and get the remainder +// 5. Adjust the sign of the remainder +// +// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity): +// +// Let x = -7 (11111001 in two's complement) and y = 3 (00000011) +// +// Step 2: Determine signs +// +// x: negative (MSB is 1) +// y: positive (MSB is 0) +// +// Step 3: Calculate absolute values +// +// |x| = 7: 11111001 -> 00000111 +// NOT: 00000110 +// +1: 00000111 +// +// |y| = 3: 00000011 (already positive) +// +// Step 4: Unsigned division +// +// 7 / 3 = 2 remainder 1 +// q = 2: 00000010 (not used in result) +// r = 1: 00000001 +// +// Step 5: Adjust sign of remainder (x is negative) +// +// -1: 00000001 -> 11111111 +// NOT: 11111110 +// +1: 11111111 +// +// Final result: -1 (11111111 in two's complement) +// +// Note: The sign of the remainder is always the same as the sign of the dividend (x). func (z *Int) Rem(x, y *Int) *Int { + // Step 1: Check for division by zero if y.IsZero() { - panic("division by zero") + panic(divisionByZeroError) } - z.initiateAbs() + // Step 2, 3 + xAbs, xSign := x.Abs(), x.Sign() + yAbs := y.Abs() + + // Step 4: Perform unsigned division and get the remainder + var q, r uint256.Uint + q.DivMod(xAbs, yAbs, &r) + + // Step 5: Adjust the sign of the remainder + if xSign < 0 { + r.Neg(&r) + } - z.abs.Mod(x.abs, y.abs) - z.neg = z.abs.Sign() > 0 && x.neg // 0 has no sign + z.value.Set(&r) return z } // Mod sets z to the modulus x%y for y != 0 and returns z. -// If y == 0, z is set to 0 (OBS: differs from the big.Int) +// The result (z) has the same sign as the divisor y. func (z *Int) Mod(x, y *Int) *Int { - if x.neg { - z.abs.Div(x.abs, y.abs) - z.abs.Add(z.abs, one) - z.abs.Mul(z.abs, y.abs) - z.abs.Sub(z.abs, x.abs) - z.abs.Mod(z.abs, y.abs) - } else { - z.abs.Mod(x.abs, y.abs) + return z.ModE(x, y) +} + +// DivE performs Euclidean division of x by y, setting z to the quotient and returning z. +// If y == 0, it panics with a "division by zero" error. +// +// Euclidean division satisfies the following properties: +// 1. The remainder is always non-negative: 0 <= x mod y < |y| +// 2. It follows the identity: x = y * (x div y) + (x mod y) +func (z *Int) DivE(x, y *Int) *Int { + if y.IsZero() { + panic(divisionByZeroError) } - z.neg = false + + // Compute the truncated division quotient + z.Quo(x, y) + + // Compute the remainder + r := new(Int).Rem(x, y) + + // If the remainder is negative, adjust the quotient + if r.Sign() < 0 { + if y.Sign() > 0 { + z.Sub(z, NewInt(1)) + } else { + z.Add(z, NewInt(1)) + } + } + return z } + +// ModE computes the Euclidean modulus of x by y, setting z to the result and returning z. +// If y == 0, it panics with a "division by zero" error. +// +// The Euclidean modulus is always non-negative and satisfies: +// +// 0 <= x mod y < |y| +// +// Example visualization for 8-bit integers (scaled down from 256-bit for simplicity): +// +// Case 1: Let x = -7 (11111001 in two's complement) and y = 3 (00000011) +// +// Step 1: Compute remainder (using Rem) +// +// Result of Rem: -1 (11111111 in two's complement) +// +// Step 2: Adjust sign (result is negative, y is positive) +// +// -1 + 3 = 2 +// 11111111 + 00000011 = 00000010 +// +// Final result: 2 (00000010) +// +// Case 2: Let x = -7 (11111001 in two's complement) and y = -3 (11111101 in two's complement) +// +// Step 1: Compute remainder (using Rem) +// +// Result of Rem: -1 (11111111 in two's complement) +// +// Step 2: Adjust sign (result is negative, y is negative) +// +// No adjustment needed +// +// Final result: -1 (11111111 in two's complement) +// +// Note: This implementation ensures that the result always has the same sign as y, +// which is different from the Rem operation. +func (z *Int) ModE(x, y *Int) *Int { + if y.IsZero() { + panic(divisionByZeroError) + } + + // Perform T-division to get the remainder + z.Rem(x, y) + + // Adjust the remainder if necessary + if z.Sign() >= 0 { + return z + } + if y.Sign() > 0 { + return z.Add(z, y) + } + + return z.Sub(z, y) +} + +// Sets z to the sum x + y, where z and x are uint256s and y is an int256. +// +// If the y is positive, it adds y.value to x. otherwise, it subtracts y.Abs() from x. +func AddDelta(z, x *uint256.Uint, y *Int) { + if y.Sign() >= 0 { + z.Add(x, &y.value) + } else { + z.Sub(x, y.Abs()) + } +} + +// Sets z to the sum x + y, where z and x are uint256s and y is an int256. +// +// This function returns true if the addition overflows, false otherwise. +func AddDeltaOverflow(z, x *uint256.Uint, y *Int) bool { + var overflow bool + if y.Sign() >= 0 { + _, overflow = z.AddOverflow(x, &y.value) + } else { + var absY uint256.Uint + absY.Sub(uint0, &y.value) // absY = -y.value + _, overflow = z.SubOverflow(x, &absY) + } + + return overflow +} diff --git a/examples/gno.land/p/demo/int256/arithmetic_test.gno b/examples/gno.land/p/demo/int256/arithmetic_test.gno index c4aeb18e3c5..0b55552aca4 100644 --- a/examples/gno.land/p/demo/int256/arithmetic_test.gno +++ b/examples/gno.land/p/demo/int256/arithmetic_test.gno @@ -6,6 +6,36 @@ import ( "gno.land/p/demo/uint256" ) +const ( + // 2^255 - 1 + MAX_INT256 = "57896044618658097711785492504343953926634992332820282019728792003956564819967" + // -(2^255 - 1) + MINUS_MAX_INT256 = "-57896044618658097711785492504343953926634992332820282019728792003956564819967" + + // 2^255 - 1 + MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935" + MAX_UINT256_MINUS_1 = "115792089237316195423570985008687907853269984665640564039457584007913129639934" + + MINUS_MAX_UINT256 = "-115792089237316195423570985008687907853269984665640564039457584007913129639935" + MINUS_MAX_UINT256_PLUS_1 = "-115792089237316195423570985008687907853269984665640564039457584007913129639934" + + TWO_POW_128 = "340282366920938463463374607431768211456" + MINUS_TWO_POW_128 = "-340282366920938463463374607431768211456" + MINUS_TWO_POW_128_MINUS_1 = "-340282366920938463463374607431768211457" + TWO_POW_128_MINUS_1 = "340282366920938463463374607431768211455" + + TWO_POW_129_MINUS_1 = "680564733841876926926749214863536422911" + + TWO_POW_254 = "28948022309329048855892746252171976963317496166410141009864396001978282409984" + MINUS_TWO_POW_254 = "-28948022309329048855892746252171976963317496166410141009864396001978282409984" + HALF_MAX_INT256 = "28948022309329048855892746252171976963317496166410141009864396001978282409983" + MINUS_HALF_MAX_INT256 = "-28948022309329048855892746252171976963317496166410141009864396001978282409983" + + TWO_POW_255 = "57896044618658097711785492504343953926634992332820282019728792003956564819968" + MIN_INT256 = "-57896044618658097711785492504343953926634992332820282019728792003956564819968" + MIN_INT256_MINUS_1 = "-57896044618658097711785492504343953926634992332820282019728792003956564819969" +) + func TestAdd(t *testing.T) { tests := []struct { x, y, want string @@ -15,14 +45,18 @@ func TestAdd(t *testing.T) { {"1", "1", "2"}, {"1", "2", "3"}, // NEGATIVE - {"-1", "1", "-0"}, // TODO: remove negative sign for 0 ?? + {"-1", "1", "0"}, {"1", "-1", "0"}, + {"3", "-3", "0"}, {"-1", "-1", "-2"}, {"-1", "-2", "-3"}, {"-1", "3", "2"}, {"3", "-1", "2"}, // OVERFLOW - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "0"}, + {MAX_UINT256, "1", "0"}, + {MAX_INT256, "1", MIN_INT256}, + {MIN_INT256, "-1", MAX_INT256}, + {MAX_INT256, MAX_INT256, "-2"}, } for _, tc := range tests { @@ -48,7 +82,7 @@ func TestAdd(t *testing.T) { got.Add(x, y) if got.Neq(want) { - t.Errorf("Add(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Add(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } @@ -63,10 +97,10 @@ func TestAddUint256(t *testing.T) { {"1", "2", "3"}, {"-1", "1", "0"}, {"-1", "3", "2"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "1"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639934", "-1"}, + {MINUS_MAX_UINT256_PLUS_1, MAX_UINT256, "1"}, + {MINUS_MAX_UINT256, MAX_UINT256_MINUS_1, "-1"}, // OVERFLOW - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "0"}, + {MINUS_MAX_UINT256, MAX_UINT256, "0"}, } for _, tc := range tests { @@ -92,7 +126,7 @@ func TestAddUint256(t *testing.T) { got.AddUint256(x, y) if got.Neq(want) { - t.Errorf("AddUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("AddUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } @@ -108,7 +142,7 @@ func TestAddDelta(t *testing.T) { {"1", "2", "3", "5"}, {"5", "10", "-3", "7"}, // underflow - {"1", "2", "-3", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + {"1", "2", "-3", MAX_UINT256}, } for _, tc := range tests { @@ -139,7 +173,7 @@ func TestAddDelta(t *testing.T) { AddDelta(z, x, y) if z.Neq(want) { - t.Errorf("AddDelta(%s, %s, %s) = %v, want %v", tc.z, tc.x, tc.y, z.ToString(), want.ToString()) + t.Errorf("AddDelta(%s, %s, %s) = %v, want %v", tc.z, tc.x, tc.y, z.String(), want.String()) } } } @@ -188,10 +222,12 @@ func TestSub(t *testing.T) { {"1", "1", "0"}, {"-1", "1", "-2"}, {"1", "-1", "2"}, - {"-1", "-1", "-0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", "-0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "-115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - {x: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", y: "1", want: "-0"}, + {"-1", "-1", "0"}, + {MINUS_MAX_UINT256, MINUS_MAX_UINT256, "0"}, + {MINUS_MAX_UINT256, "0", MINUS_MAX_UINT256}, + {MAX_INT256, MIN_INT256, "-1"}, + {MIN_INT256, MIN_INT256, "0"}, + {MAX_INT256, MAX_INT256, "0"}, } for _, tc := range tests { @@ -217,7 +253,7 @@ func TestSub(t *testing.T) { got.Sub(x, y) if got.Neq(want) { - t.Errorf("Sub(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Sub(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } @@ -233,9 +269,9 @@ func TestSubUint256(t *testing.T) { {"-1", "1", "-2"}, {"-1", "3", "-4"}, // underflow - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "-0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "2", "-1"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "3", "-2"}, + {MINUS_MAX_UINT256, "1", "0"}, + {MINUS_MAX_UINT256, "2", "-1"}, + {MINUS_MAX_UINT256, "3", "-2"}, } for _, tc := range tests { @@ -261,7 +297,7 @@ func TestSubUint256(t *testing.T) { got.SubUint256(x, y) if got.Neq(want) { - t.Errorf("SubUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("SubUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } @@ -275,6 +311,12 @@ func TestMul(t *testing.T) { {"5", "-3", "-15"}, {"0", "3", "0"}, {"3", "0", "0"}, + {"-5", "-3", "15"}, + {MAX_UINT256, "1", MAX_UINT256}, + {MAX_INT256, "2", "-2"}, + {TWO_POW_254, "2", MIN_INT256}, + {MINUS_TWO_POW_254, "2", MIN_INT256}, + {MAX_INT256, "1", MAX_INT256}, } for _, tc := range tests { @@ -300,68 +342,66 @@ func TestMul(t *testing.T) { got.Mul(x, y) if got.Neq(want) { - t.Errorf("Mul(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Mul(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } -func TestMulUint256(t *testing.T) { +func TestDiv(t *testing.T) { tests := []struct { - x, y, want string + x, y, expected string }{ - {"0", "1", "0"}, - {"1", "0", "0"}, {"1", "1", "1"}, - {"1", "2", "2"}, + {"0", "1", "0"}, {"-1", "1", "-1"}, - {"-1", "3", "-3"}, - {"3", "4", "12"}, - {"-3", "4", "-12"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "-115792089237316195423570985008687907853269984665640564039457584007913129639932"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "115792089237316195423570985008687907853269984665640564039457584007913129639932"}, + {"1", "-1", "-1"}, + {"-1", "-1", "1"}, + {"-6", "3", "-2"}, + {"10", "-2", "-5"}, + {"-10", "3", "-3"}, + {"7", "3", "2"}, + {"-7", "3", "-2"}, + // the maximum value of a positive number in int256 is less than the maximum value of a uint256 + {MAX_INT256, "2", HALF_MAX_INT256}, + {MINUS_MAX_INT256, "2", MINUS_HALF_MAX_INT256}, + {MAX_INT256, "-1", MINUS_MAX_INT256}, } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.MulUint256(x, y) - - if got.Neq(want) { - t.Errorf("MulUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } + for _, tt := range tests { + t.Run(tt.x+"/"+tt.y, func(t *testing.T) { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + result := Zero().Div(x, y) + if result.String() != tt.expected { + t.Errorf("Div(%s, %s) = %s, want %s", tt.x, tt.y, result.String(), tt.expected) + } + }) } + + t.Run("Division by zero", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("Div(1, 0) did not panic") + } + }() + x := MustFromDecimal("1") + y := MustFromDecimal("0") + Zero().Div(x, y) + }) } -func TestDiv(t *testing.T) { +func TestQuo(t *testing.T) { tests := []struct { x, y, want string }{ {"0", "1", "0"}, - {"0", "-1", "-0"}, - {"10", "0", "0"}, + {"0", "-1", "0"}, {"10", "1", "10"}, {"10", "-1", "-10"}, - {"-10", "0", "-0"}, {"-10", "1", "-10"}, {"-10", "-1", "10"}, {"10", "-3", "-3"}, + {"-10", "3", "-3"}, {"10", "3", "3"}, } @@ -385,29 +425,28 @@ func TestDiv(t *testing.T) { } got := New() - got.Div(x, y) + got.Quo(x, y) if got.Neq(want) { - t.Errorf("Div(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Quo(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } -func TestDivUint256(t *testing.T) { +func TestRem(t *testing.T) { tests := []struct { x, y, want string }{ {"0", "1", "0"}, - {"1", "0", "0"}, - {"1", "1", "1"}, - {"1", "2", "0"}, - {"-1", "1", "-1"}, - {"-1", "3", "0"}, - {"4", "3", "1"}, - {"25", "5", "5"}, - {"25", "4", "6"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "-57896044618658097711785492504343953926634992332820282019728792003956564819967"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, + {"0", "-1", "0"}, + {"10", "1", "0"}, + {"10", "-1", "0"}, + {"-10", "1", "0"}, + {"-10", "-1", "0"}, + {"10", "3", "1"}, + {"10", "-3", "1"}, + {"-10", "3", "-1"}, + {"-10", "-3", "-1"}, } for _, tc := range tests { @@ -417,7 +456,7 @@ func TestDivUint256(t *testing.T) { continue } - y, err := uint256.FromDecimal(tc.y) + y, err := FromDecimal(tc.y) if err != nil { t.Error(err) continue @@ -430,26 +469,28 @@ func TestDivUint256(t *testing.T) { } got := New() - got.DivUint256(x, y) + got.Rem(x, y) if got.Neq(want) { - t.Errorf("DivUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Rem(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } -func TestQuo(t *testing.T) { +func TestMod(t *testing.T) { tests := []struct { x, y, want string }{ {"0", "1", "0"}, {"0", "-1", "0"}, - {"10", "1", "10"}, - {"10", "-1", "-10"}, - {"-10", "1", "-10"}, - {"-10", "-1", "10"}, - {"10", "-3", "-3"}, - {"10", "3", "3"}, + {"10", "1", "0"}, + {"10", "-1", "0"}, + {"-10", "1", "0"}, + {"-10", "-1", "0"}, + {"10", "3", "1"}, + {"10", "-3", "1"}, + {"-10", "3", "2"}, + {"-10", "-3", "2"}, } for _, tc := range tests { @@ -472,31 +513,51 @@ func TestQuo(t *testing.T) { } got := New() - got.Quo(x, y) + got.Mod(x, y) if got.Neq(want) { - t.Errorf("Quo(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Mod(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } -func TestRem(t *testing.T) { +func TestModeOverflow(t *testing.T) { tests := []struct { x, y, want string }{ - {"0", "1", "0"}, - {"0", "-1", "0"}, - {"10", "1", "0"}, - {"10", "-1", "0"}, - {"-10", "1", "0"}, - {"-10", "-1", "0"}, - {"10", "3", "1"}, - {"10", "-3", "1"}, - {"-10", "3", "-1"}, - {"-10", "-3", "-1"}, + {MIN_INT256, "2", "0"}, // MIN_INT256 % 2 = 0 + {MAX_INT256, "2", "1"}, // MAX_INT256 % 2 = 1 + {MIN_INT256, "-1", "0"}, // MIN_INT256 % -1 = 0 + {MAX_INT256, "-1", "0"}, // MAX_INT256 % -1 = 0 + } + + for _, tt := range tests { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + want := MustFromDecimal(tt.want) + got := New().Mod(x, y) + if got.Neq(want) { + t.Errorf("Mod(%s, %s) = %v, want %v", tt.x, tt.y, got.String(), want.String()) + } + } +} + +func TestModPanic(t *testing.T) { + tests := []struct { + x, y string + }{ + {"10", "0"}, + {"10", "-0"}, + {"-10", "0"}, + {"-10", "-0"}, } for _, tc := range tests { + defer func() { + if r := recover(); r == nil { + t.Errorf("Mod(%s, %s) did not panic", tc.x, tc.y) + } + }() x, err := FromDecimal(tc.x) if err != nil { t.Error(err) @@ -509,37 +570,92 @@ func TestRem(t *testing.T) { continue } - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue + result := New().Mod(x, y) + t.Errorf("Mod(%s, %s) = %v, want %v", tc.x, tc.y, result.String(), "0") + } +} + +func TestDivE(t *testing.T) { + testCases := []struct { + x, y int64 + want int64 + }{ + {8, 3, 2}, + {8, -3, -2}, + {-8, 3, -3}, + {-8, -3, 3}, + {1, 2, 0}, + {1, -2, 0}, + {-1, 2, -1}, + {-1, -2, 1}, + {0, 1, 0}, + {0, -1, 0}, + } + + for _, tc := range testCases { + x := NewInt(tc.x) + y := NewInt(tc.y) + want := NewInt(tc.want) + got := new(Int).DivE(x, y) + if got.Cmp(want) != 0 { + t.Errorf("DivE(%v, %v) = %v, want %v", tc.x, tc.y, got, want) } + } +} - got := New() - got.Rem(x, y) +func TestDivEByZero(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("DivE did not panic on division by zero") + } + }() - if got.Neq(want) { - t.Errorf("Rem(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + x := NewInt(1) + y := NewInt(0) + new(Int).DivE(x, y) +} + +func TestModEByZero(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("ModE did not panic on division by zero") } + }() + + x := NewInt(1) + y := NewInt(0) + new(Int).ModE(x, y) +} + +func TestLargeNumbers(t *testing.T) { + x, _ := new(Int).SetString("123456789012345678901234567890") + y, _ := new(Int).SetString("987654321098765432109876543210") + + // Expected results (calculated separately) + expectedQ, _ := new(Int).SetString("0") + expectedR, _ := new(Int).SetString("123456789012345678901234567890") + + gotQ := new(Int).DivE(x, y) + gotR := new(Int).ModE(x, y) + + if gotQ.Cmp(expectedQ) != 0 { + t.Errorf("DivE with large numbers: got %v, want %v", gotQ, expectedQ) + } + + if gotR.Cmp(expectedR) != 0 { + t.Errorf("ModE with large numbers: got %v, want %v", gotR, expectedR) } } -func TestMod(t *testing.T) { +func TestAbs(t *testing.T) { tests := []struct { - x, y, want string + x, want string }{ - {"0", "1", "0"}, - {"0", "-1", "0"}, - {"10", "0", "0"}, - {"10", "1", "0"}, - {"10", "-1", "0"}, - {"-10", "0", "0"}, - {"-10", "1", "0"}, - {"-10", "-1", "0"}, - {"10", "3", "1"}, - {"10", "-3", "1"}, - {"-10", "3", "2"}, - {"-10", "-3", "2"}, + {"0", "0"}, + {"1", "1"}, + {"-1", "1"}, + {"-2", "2"}, + {"-100000000000", "100000000000"}, } for _, tc := range tests { @@ -549,23 +665,10 @@ func TestMod(t *testing.T) { continue } - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } + got := x.Abs() - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.Mod(x, y) - - if got.Neq(want) { - t.Errorf("Mod(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + if got.String() != tc.want { + t.Errorf("Abs(%s) = %v, want %v", tc.x, got.String(), tc.want) } } } diff --git a/examples/gno.land/p/demo/int256/bitwise.gno b/examples/gno.land/p/demo/int256/bitwise.gno index c0d0f65f78f..1a1fe2e9720 100644 --- a/examples/gno.land/p/demo/int256/bitwise.gno +++ b/examples/gno.land/p/demo/int256/bitwise.gno @@ -1,94 +1,54 @@ package int256 -import ( - "gno.land/p/demo/uint256" -) - -// Or sets z = x | y and returns z. -func (z *Int) Or(x, y *Int) *Int { - if x.neg == y.neg { - if x.neg { - // (-x) | (-y) == ^(x-1) | ^(y-1) == ^((x-1) & (y-1)) == -(((x-1) & (y-1)) + 1) - x1 := new(uint256.Uint).Sub(x.abs, one) - y1 := new(uint256.Uint).Sub(y.abs, one) - z.abs = z.abs.Add(z.abs.And(x1, y1), one) - z.neg = true // z cannot be zero if x and y are negative - return z - } - - // x | y == x | y - z.abs = z.abs.Or(x.abs, y.abs) - z.neg = false - return z - } - - // x.neg != y.neg - if x.neg { - x, y = y, x // | is symmetric - } - - // x | (-y) == x | ^(y-1) == ^((y-1) &^ x) == -(^((y-1) &^ x) + 1) - y1 := new(uint256.Uint).Sub(y.abs, one) - z.abs = z.abs.Add(z.abs.AndNot(y1, x.abs), one) - z.neg = true // z cannot be zero if one of x or y is negative - +// Not sets z to the bitwise NOT of x and returns z. +// +// The bitwise NOT operation flips each bit of the operand. +func (z *Int) Not(x *Int) *Int { + z.value.Not(&x.value) return z } -// And sets z = x & y and returns z. +// And sets z to the bitwise AND of x and y and returns z. +// +// The bitwise AND operation results in a value that has a bit set +// only if both corresponding bits of the operands are set. func (z *Int) And(x, y *Int) *Int { - if x.neg == y.neg { - if x.neg { - // (-x) & (-y) == ^(x-1) & ^(y-1) == ^((x-1) | (y-1)) == -(((x-1) | (y-1)) + 1) - x1 := new(uint256.Uint).Sub(x.abs, one) - y1 := new(uint256.Uint).Sub(y.abs, one) - z.abs = z.abs.Add(z.abs.Or(x1, y1), one) - z.neg = true // z cannot be zero if x and y are negative - return z - } - - // x & y == x & y - z.abs = z.abs.And(x.abs, y.abs) - z.neg = false - return z - } + z.value.And(&x.value, &y.value) + return z +} - // x.neg != y.neg - // REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1192-1202;drc=d57303e65f00b84b528ee682747dbe1fd3316d30 - if x.neg { - x, y = y, x // & is symmetric - } +// Or sets z to the bitwise OR of x and y and returns z. +// +// The bitwise OR operation results in a value that has a bit set +// if at least one of the corresponding bits of the operands is set. +func (z *Int) Or(x, y *Int) *Int { + z.value.Or(&x.value, &y.value) + return z +} - // x & (-y) == x & ^(y-1) == x &^ (y-1) - y1 := new(uint256.Uint).Sub(y.abs, uint256.One()) - z.abs = z.abs.AndNot(x.abs, y1) - z.neg = false +// Xor sets z to the bitwise XOR of x and y and returns z. +// +// The bitwise XOR operation results in a value that has a bit set +// only if the corresponding bits of the operands are different. +func (z *Int) Xor(x, y *Int) *Int { + z.value.Xor(&x.value, &y.value) return z } -// Rsh sets z = x >> n and returns z. -// OBS: Different from original implementation it was using math.Big +// Rsh sets z to the result of right-shifting x by n bits and returns z. +// +// Right shift operation moves all bits in the operand to the right by the specified number of positions. +// Bits shifted out on the right are discarded, and zeros are shifted in on the left. func (z *Int) Rsh(x *Int, n uint) *Int { - if !x.neg { - z.abs.Rsh(x.abs, n) - z.neg = x.neg - return z - } - - // REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1118-1126;drc=d57303e65f00b84b528ee682747dbe1fd3316d30 - t := NewInt(0).Sub(FromUint256(x.abs), NewInt(1)) - t = t.Rsh(t, n) - - _tmp := t.Add(t, NewInt(1)) - z.abs = _tmp.Abs() - z.neg = true - + z.value.Rsh(&x.value, n) return z } -// Lsh sets z = x << n and returns z. +// Lsh sets z to the result of left-shifting x by n bits and returns z. +// +// Left shift operation moves all bits in the operand to the left by the specified number of positions. +// Bits shifted out on the left are discarded, and zeros are shifted in on the right. func (z *Int) Lsh(x *Int, n uint) *Int { - z.abs.Lsh(x.abs, n) - z.neg = x.neg + z.value.Lsh(&x.value, n) return z } diff --git a/examples/gno.land/p/demo/int256/bitwise_test.gno b/examples/gno.land/p/demo/int256/bitwise_test.gno index 8dc16cd17ac..fc7b9bb578f 100644 --- a/examples/gno.land/p/demo/int256/bitwise_test.gno +++ b/examples/gno.land/p/demo/int256/bitwise_test.gno @@ -2,198 +2,157 @@ package int256 import ( "testing" - - "gno.land/p/demo/uint256" ) -func TestOr(t *testing.T) { +func TestBitwise_And(t *testing.T) { tests := []struct { - name string - x, y, want Int + x, y, want string }{ - { - name: "all zeroes", - x: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "all ones", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - }, - { - name: "mixed", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - }, - { - name: "one operand all ones", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - }, + {"5", "1", "1"}, // 0101 & 0001 = 0001 + {"-1", "1", "1"}, // 1111 & 0001 = 0001 + {"-5", "3", "3"}, // 1111...1011 & 0000...0011 = 0000...0011 + {MAX_UINT256, MAX_UINT256, MAX_UINT256}, + {TWO_POW_128, TWO_POW_128_MINUS_1, "0"}, // 2^128 & (2^128 - 1) = 0 + {TWO_POW_128, MAX_UINT256, TWO_POW_128}, // 2^128 & MAX_INT256 + {MAX_UINT256, TWO_POW_128, TWO_POW_128}, // MAX_INT256 & 2^128 } for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - got := New() - got.Or(&tc.x, &tc.y) - - if got.Neq(&tc.want) { - t.Errorf("Or(%v, %v) = %v, want %v", tc.x, tc.y, got, tc.want) - } - }) + x, _ := FromDecimal(tc.x) + y, _ := FromDecimal(tc.y) + want, _ := FromDecimal(tc.want) + + got := new(Int).And(x, y) + + if got.Neq(want) { + t.Errorf("And(%s, %s) = %s, want %s", x.String(), y.String(), got.String(), want.String()) + } } } -func TestAnd(t *testing.T) { +func TestBitwise_Or(t *testing.T) { tests := []struct { - name string - x, y, want Int + x, y, want string }{ - { - name: "all zeroes", - x: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "all ones", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - }, - { - name: "mixed", - x: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "mixed 2", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "mixed 3", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "one operand zero", - x: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0, 0, 0, 0}}, neg: false}, - }, - { - name: "one operand all ones", - x: Int{abs: &uint256.Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, neg: false}, - y: Int{abs: &uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false}, - want: Int{abs: &uint256.Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, neg: false}, - }, + {"5", "1", "5"}, // 0101 | 0001 = 0101 + {"-1", "1", "-1"}, // 1111 | 0001 = 1111 + {"-5", "3", "-5"}, // 1111...1011 | 0000...0011 = 1111...1011 + {TWO_POW_128, TWO_POW_128_MINUS_1, TWO_POW_129_MINUS_1}, + {TWO_POW_128, MAX_UINT256, MAX_UINT256}, + {"0", TWO_POW_128, TWO_POW_128}, // 0 | 2^128 = 2^128 + {MAX_UINT256, TWO_POW_128, MAX_UINT256}, // MAX_INT256 | 2^128 = MAX_INT256 } for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - got := New() - got.And(&tc.x, &tc.y) - - if got.Neq(&tc.want) { - t.Errorf("And(%v, %v) = %v, want %v", tc.x, tc.y, got, tc.want) - } - }) + x, _ := FromDecimal(tc.x) + y, _ := FromDecimal(tc.y) + want, _ := FromDecimal(tc.want) + + got := new(Int).Or(x, y) + + if got.Neq(want) { + t.Errorf( + "Or(%s, %s) = %s, want %s", + x.String(), y.String(), got.String(), want.String(), + ) + } } } -func TestRsh(t *testing.T) { +func TestBitwise_Not(t *testing.T) { tests := []struct { - x string - n uint - want string + x, want string }{ - {"1024", 0, "1024"}, - {"1024", 1, "512"}, - {"1024", 2, "256"}, - {"1024", 10, "1"}, - {"1024", 11, "0"}, - {"18446744073709551615", 0, "18446744073709551615"}, - {"18446744073709551615", 1, "9223372036854775807"}, - {"18446744073709551615", 62, "3"}, - {"18446744073709551615", 63, "1"}, - {"18446744073709551615", 64, "0"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 0, "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 1, "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 128, "340282366920938463463374607431768211455"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 255, "1"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 256, "0"}, - {"-1024", 0, "-1024"}, - {"-1024", 1, "-512"}, - {"-1024", 2, "-256"}, - {"-1024", 10, "-1"}, - {"-1024", 10, "-1"}, - {"-9223372036854775808", 0, "-9223372036854775808"}, - {"-9223372036854775808", 1, "-4611686018427387904"}, - {"-9223372036854775808", 62, "-2"}, - {"-9223372036854775808", 63, "-1"}, - {"-9223372036854775808", 64, "-1"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 0, "-57896044618658097711785492504343953926634992332820282019728792003956564819968"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 1, "-28948022309329048855892746252171976963317496166410141009864396001978282409984"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 253, "-4"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 254, "-2"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 255, "-1"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 256, "-1"}, + {"5", "-6"}, // 0101 -> 1111...1010 + {"-1", "0"}, // 1111...1111 -> 0000...0000 + {TWO_POW_128, MINUS_TWO_POW_128_MINUS_1}, // NOT 2^128 + {TWO_POW_255, MIN_INT256_MINUS_1}, // NOT 2^255 } for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue + x, _ := FromDecimal(tc.x) + want, _ := FromDecimal(tc.want) + + got := new(Int).Not(x) + + if got.Neq(want) { + t.Errorf("Not(%s) = %s, want %s", x.String(), got.String(), want.String()) } + } +} + +func TestBitwise_Xor(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"5", "1", "4"}, // 0101 ^ 0001 = 0100 + {"-1", "1", "-2"}, // 1111...1111 ^ 0000...0001 = 1111...1110 + {"-5", "3", "-8"}, // 1111...1011 ^ 0000...0011 = 1111...1000 + {TWO_POW_128, TWO_POW_128, "0"}, // 2^128 ^ 2^128 = 0 + {MAX_UINT256, TWO_POW_128, MINUS_TWO_POW_128_MINUS_1}, // MAX_INT256 ^ 2^128 + {TWO_POW_255, MAX_UINT256, MIN_INT256_MINUS_1}, // 2^255 ^ MAX_INT256 + } - got := New() - got.Rsh(x, tc.n) + for _, tt := range tests { + x, _ := FromDecimal(tt.x) + y, _ := FromDecimal(tt.y) + want, _ := FromDecimal(tt.want) - if got.ToString() != tc.want { - t.Errorf("Rsh(%s, %d) = %v, want %v", tc.x, tc.n, got.ToString(), tc.want) + got := new(Int).Xor(x, y) + + if got.Neq(want) { + t.Errorf("Xor(%s, %s) = %s, want %s", x.String(), y.String(), got.String(), want.String()) } } } -func TestLsh(t *testing.T) { +func TestBitwise_Rsh(t *testing.T) { tests := []struct { x string n uint want string }{ - {"1", 0, "1"}, - {"1", 1, "2"}, - {"1", 2, "4"}, - {"2", 0, "2"}, - {"2", 1, "4"}, - {"2", 2, "8"}, - {"-2", 0, "-2"}, - {"-4", 0, "-4"}, - {"-8", 0, "-8"}, + {"5", 1, "2"}, // 0101 >> 1 = 0010 + {"42", 3, "5"}, // 00101010 >> 3 = 00000101 + {TWO_POW_128, 128, "1"}, + {MAX_UINT256, 255, "1"}, + {TWO_POW_255, 254, "2"}, + {MINUS_TWO_POW_128, 128, TWO_POW_128_MINUS_1}, } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue + for _, tt := range tests { + x, _ := FromDecimal(tt.x) + want, _ := FromDecimal(tt.want) + + got := new(Int).Rsh(x, tt.n) + + if got.Neq(want) { + t.Errorf("Rsh(%s, %d) = %s, want %s", x.String(), tt.n, got.String(), want.String()) } + } +} + +func TestBitwise_Lsh(t *testing.T) { + tests := []struct { + x string + n uint + want string + }{ + {"5", 2, "20"}, // 0101 << 2 = 10100 + {"42", 5, "1344"}, // 00101010 << 5 = 10101000000 + {"1", 128, TWO_POW_128}, // 1 << 128 = 2^128 + {"2", 254, TWO_POW_255}, + {"1", 255, MIN_INT256}, // 1 << 255 = MIN_INT256 (overflow) + } + + for _, tt := range tests { + x, _ := FromDecimal(tt.x) + want, _ := FromDecimal(tt.want) - got := New() - got.Lsh(x, tc.n) + got := new(Int).Lsh(x, tt.n) - if got.ToString() != tc.want { - t.Errorf("Lsh(%s, %d) = %v, want %v", tc.x, tc.n, got.ToString(), tc.want) + if got.Neq(want) { + t.Errorf("Lsh(%s, %d) = %s, want %s", x.String(), tt.n, got.String(), want.String()) } } } diff --git a/examples/gno.land/p/demo/int256/cmp.gno b/examples/gno.land/p/demo/int256/cmp.gno index 426dfd76485..c91a25568e9 100644 --- a/examples/gno.land/p/demo/int256/cmp.gno +++ b/examples/gno.land/p/demo/int256/cmp.gno @@ -1,86 +1,59 @@ package int256 -// Eq returns true if z == x func (z *Int) Eq(x *Int) bool { - return (z.neg == x.neg) && z.abs.Eq(x.abs) + return z.value.Eq(&x.value) } -// Neq returns true if z != x func (z *Int) Neq(x *Int) bool { return !z.Eq(x) } -// Cmp compares x and y and returns: +// Cmp compares z and x and returns: // -// -1 if x < y -// 0 if x == y -// +1 if x > y -func (z *Int) Cmp(x *Int) (r int) { - // x cmp y == x cmp y - // x cmp (-y) == x - // (-x) cmp y == y - // (-x) cmp (-y) == -(x cmp y) - switch { - case z == x: - // nothing to do - case z.neg == x.neg: - r = z.abs.Cmp(x.abs) - if z.neg { - r = -r - } - case z.neg: - r = -1 - default: - r = 1 +// - 1 if z > x +// - 0 if z == x +// - -1 if z < x +func (z *Int) Cmp(x *Int) int { + zSign, xSign := z.Sign(), x.Sign() + + if zSign == xSign { + return z.value.Cmp(&x.value) } - return + + if zSign == 0 { + return -xSign + } + + return zSign } // IsZero returns true if z == 0 func (z *Int) IsZero() bool { - return z.abs.IsZero() + return z.value.IsZero() } // IsNeg returns true if z < 0 func (z *Int) IsNeg() bool { - return z.neg + return z.Sign() < 0 } -// Lt returns true if z < x func (z *Int) Lt(x *Int) bool { - if z.neg { - if x.neg { - return z.abs.Gt(x.abs) - } else { - return true - } - } else { - if x.neg { - return false - } else { - return z.abs.Lt(x.abs) - } - } + return z.Cmp(x) < 0 } -// Gt returns true if z > x func (z *Int) Gt(x *Int) bool { - if z.neg { - if x.neg { - return z.abs.Lt(x.abs) - } else { - return false - } - } else { - if x.neg { - return true - } else { - return z.abs.Gt(x.abs) - } - } + return z.Cmp(x) > 0 +} + +func (z *Int) Le(x *Int) bool { + return z.Cmp(x) <= 0 +} + +func (z *Int) Ge(x *Int) bool { + return z.Cmp(x) >= 0 } // Clone creates a new Int identical to z func (z *Int) Clone() *Int { - return &Int{z.abs.Clone(), z.neg} + return New().FromUint256(&z.value) } diff --git a/examples/gno.land/p/demo/int256/cmp_test.gno b/examples/gno.land/p/demo/int256/cmp_test.gno index 81b9231babe..c1c6559de3c 100644 --- a/examples/gno.land/p/demo/int256/cmp_test.gno +++ b/examples/gno.land/p/demo/int256/cmp_test.gno @@ -85,7 +85,7 @@ func TestCmp(t *testing.T) { {"-1", "0", -1}, {"0", "-1", 1}, {"1", "1", 0}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", 1}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", -1}, } for _, tc := range tests { @@ -140,7 +140,7 @@ func TestIsNeg(t *testing.T) { want bool }{ {"0", false}, - {"-0", true}, // TODO: should this be false? + {"-0", false}, {"1", false}, {"-1", true}, {"10", false}, @@ -173,7 +173,6 @@ func TestLt(t *testing.T) { {"0", "-1", false}, {"1", "1", false}, {"-1", "-1", false}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", false}, } for _, tc := range tests { @@ -208,7 +207,6 @@ func TestGt(t *testing.T) { {"0", "-1", true}, {"1", "1", false}, {"-1", "-1", false}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, } for _, tc := range tests { @@ -232,21 +230,19 @@ func TestGt(t *testing.T) { } func TestClone(t *testing.T) { - tests := []struct { - x string - }{ - {"0"}, - {"-0"}, - {"1"}, - {"-1"}, - {"10"}, - {"-10"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + tests := []string{ + "0", + "-0", + "1", + "-1", + "10", + "-10", + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "-115792089237316195423570985008687907853269984665640564039457584007913129639935", } - for _, tc := range tests { - x, err := FromDecimal(tc.x) + for _, xStr := range tests { + x, err := FromDecimal(xStr) if err != nil { t.Error(err) continue @@ -254,8 +250,8 @@ func TestClone(t *testing.T) { y := x.Clone() - if x.Cmp(y) != 0 { - t.Errorf("Clone(%s) = %v, want %v", tc.x, y, x) + if x.Neq(y) { + t.Errorf("cloned value is not equal to original value") } } } diff --git a/examples/gno.land/p/demo/int256/conversion.gno b/examples/gno.land/p/demo/int256/conversion.gno index ee6e7560f15..c8829ea754b 100644 --- a/examples/gno.land/p/demo/int256/conversion.gno +++ b/examples/gno.land/p/demo/int256/conversion.gno @@ -1,86 +1,107 @@ package int256 -import "gno.land/p/demo/uint256" +import ( + "math" -// SetInt64 sets z to x and returns z. -func (z *Int) SetInt64(x int64) *Int { - z.initiateAbs() + "gno.land/p/demo/uint256" +) - neg := false - if x < 0 { - neg = true - x = -x - } - if z.abs == nil { - panic("abs is nil") +// SetInt64 sets the Int to the value of the provided int64. +// +// This method allows for easy conversion from standard Go integer types +// to Int, correctly handling both positive and negative values. +func (z *Int) SetInt64(v int64) *Int { + if v >= 0 { + z.value.SetUint64(uint64(v)) + } else { + z.value.SetUint64(uint64(-v)).Neg(&z.value) } - z.abs = z.abs.SetUint64(uint64(x)) - z.neg = neg return z } -// SetUint64 sets z to x and returns z. -func (z *Int) SetUint64(x uint64) *Int { - z.initiateAbs() - - if z.abs == nil { - panic("abs is nil") - } - z.abs = z.abs.SetUint64(x) - z.neg = false +// SetUint64 sets the Int to the value of the provided uint64. +func (z *Int) SetUint64(v uint64) *Int { + z.value.SetUint64(v) return z } // Uint64 returns the lower 64-bits of z func (z *Int) Uint64() uint64 { - return z.abs.Uint64() + if z.Sign() < 0 { + panic("cannot convert negative int256 to uint64") + } + if z.value.Gt(uint256.NewUint(0).SetUint64(math.MaxUint64)) { + panic("overflow: int256 does not fit in uint64 type") + } + return z.value.Uint64() } // Int64 returns the lower 64-bits of z func (z *Int) Int64() int64 { - _abs := z.abs.Clone() - - if z.neg { - return -int64(_abs.Uint64()) + if z.Sign() >= 0 { + if z.value.BitLen() > 64 { + panic("overflow: int256 does not fit in int64 type") + } + return int64(z.value.Uint64()) + } + var temp uint256.Uint + temp.Sub(uint256.NewUint(0), &z.value) // temp = -z.value + if temp.BitLen() > 64 { + panic("overflow: int256 does not fit in int64 type") } - return int64(_abs.Uint64()) + return -int64(temp.Uint64()) } // Neg sets z to -x and returns z.) func (z *Int) Neg(x *Int) *Int { - z.abs.Set(x.abs) - if z.abs.IsZero() { - z.neg = false + if x.IsZero() { + z.value.Clear() } else { - z.neg = !x.neg + z.value.Neg(&x.value) } return z } // Set sets z to x and returns z. func (z *Int) Set(x *Int) *Int { - z.abs.Set(x.abs) - z.neg = x.neg + z.value.Set(&x.value) return z } // SetFromUint256 converts a uint256.Uint to Int and sets the value to z. func (z *Int) SetUint256(x *uint256.Uint) *Int { - z.abs.Set(x) - z.neg = false + z.value.Set(x) return z } -// OBS, differs from original mempooler int256 -// ToString returns the decimal representation of z. -func (z *Int) ToString() string { - if z == nil { - panic("int256: nil pointer to ToString()") +// ToString returns a string representation of z in base 10. +// The string is prefixed with a minus sign if z is negative. +func (z *Int) String() string { + if z.value.IsZero() { + return "0" + } + sign := z.Sign() + var temp uint256.Uint + if sign >= 0 { + temp.Set(&z.value) + } else { + // temp = -z.value + temp.Sub(uint256.NewUint(0), &z.value) + } + s := temp.Dec() + if sign < 0 { + return "-" + s } + return s +} - t := z.abs.Dec() - if z.neg { - return "-" + t +// NilToZero returns the Int if it's not nil, or a new zero-valued Int otherwise. +// +// This method is useful for safely handling potentially nil Int pointers, +// ensuring that operations always have a valid Int to work with. +func (z *Int) NilToZero() *Int { + if z == nil { + return Zero() } - return t + return z } diff --git a/examples/gno.land/p/demo/int256/conversion_test.gno b/examples/gno.land/p/demo/int256/conversion_test.gno index da54c226669..44e59fe79de 100644 --- a/examples/gno.land/p/demo/int256/conversion_test.gno +++ b/examples/gno.land/p/demo/int256/conversion_test.gno @@ -8,43 +8,20 @@ import ( func TestSetInt64(t *testing.T) { tests := []struct { - x int64 - want string - }{ - {0, "0"}, - {1, "1"}, - {-1, "-1"}, - {9223372036854775807, "9223372036854775807"}, - {-9223372036854775808, "-9223372036854775808"}, - } - - for _, tc := range tests { - var z Int - z.SetInt64(tc.x) - - got := z.ToString() - if got != tc.want { - t.Errorf("SetInt64(%d) = %s, want %s", tc.x, got, tc.want) - } - } -} - -func TestSetUint64(t *testing.T) { - tests := []struct { - x uint64 - want string + v int64 + expect int }{ - {0, "0"}, - {1, "1"}, + {0, 0}, + {1, 1}, + {-1, -1}, + {9223372036854775807, 1}, // overflow (max int64) + {-9223372036854775808, -1}, // underflow (min int64) } - for _, tc := range tests { - var z Int - z.SetUint64(tc.x) - - got := z.ToString() - if got != tc.want { - t.Errorf("SetUint64(%d) = %s, want %s", tc.x, got, tc.want) + for _, tt := range tests { + z := New().SetInt64(tt.v) + if z.Sign() != tt.expect { + t.Errorf("SetInt64(%d) = %d, want %d", tt.v, z.Sign(), tt.expect) } } } @@ -59,24 +36,39 @@ func TestUint64(t *testing.T) { {"9223372036854775807", 9223372036854775807}, {"9223372036854775808", 9223372036854775808}, {"18446744073709551615", 18446744073709551615}, - {"18446744073709551616", 0}, - {"18446744073709551617", 1}, - {"-1", 1}, - {"-18446744073709551615", 18446744073709551615}, - {"-18446744073709551616", 0}, - {"-18446744073709551617", 1}, } - for _, tc := range tests { - z := MustFromDecimal(tc.x) + for _, tt := range tests { + z := MustFromDecimal(tt.x) got := z.Uint64() - if got != tc.want { - t.Errorf("Uint64(%s) = %d, want %d", tc.x, got, tc.want) + if got != tt.want { + t.Errorf("Uint64(%s) = %d, want %d", tt.x, got, tt.want) } } } +func TestUint64_Panic(t *testing.T) { + tests := []struct { + x string + }{ + {"-1"}, + {"18446744073709551616"}, + {"18446744073709551617"}, + } + + for _, tt := range tests { + defer func() { + if r := recover(); r == nil { + t.Errorf("Uint64(%s) did not panic", tt.x) + } + }() + + z := MustFromDecimal(tt.x) + z.Uint64() + } +} + func TestInt64(t *testing.T) { tests := []struct { x string @@ -85,22 +77,40 @@ func TestInt64(t *testing.T) { {"0", 0}, {"1", 1}, {"9223372036854775807", 9223372036854775807}, - {"18446744073709551616", 0}, - {"18446744073709551617", 1}, {"-1", -1}, {"-9223372036854775808", -9223372036854775808}, } - for _, tc := range tests { - z := MustFromDecimal(tc.x) + for _, tt := range tests { + z := MustFromDecimal(tt.x) got := z.Int64() - if got != tc.want { - t.Errorf("Uint64(%s) = %d, want %d", tc.x, got, tc.want) + if got != tt.want { + t.Errorf("Uint64(%s) = %d, want %d", tt.x, got, tt.want) } } } +func TestInt64_Panic(t *testing.T) { + tests := []struct { + x string + }{ + {"18446744073709551616"}, + {"18446744073709551617"}, + } + + for _, tt := range tests { + defer func() { + if r := recover(); r == nil { + t.Errorf("Int64(%s) did not panic", tt.x) + } + }() + + z := MustFromDecimal(tt.x) + z.Int64() + } +} + func TestNeg(t *testing.T) { tests := []struct { x string @@ -113,13 +123,13 @@ func TestNeg(t *testing.T) { {"-18446744073709551615", "18446744073709551615"}, } - for _, tc := range tests { - z := MustFromDecimal(tc.x) + for _, tt := range tests { + z := MustFromDecimal(tt.x) z.Neg(z) - got := z.ToString() - if got != tc.want { - t.Errorf("Neg(%s) = %s, want %s", tc.x, got, tc.want) + got := z.String() + if got != tt.want { + t.Errorf("Neg(%s) = %s, want %s", tt.x, got, tt.want) } } } @@ -136,13 +146,13 @@ func TestSet(t *testing.T) { {"-18446744073709551615", "-18446744073709551615"}, } - for _, tc := range tests { - z := MustFromDecimal(tc.x) + for _, tt := range tests { + z := MustFromDecimal(tt.x) z.Set(z) - got := z.ToString() - if got != tc.want { - t.Errorf("Set(%s) = %s, want %s", tc.x, got, tc.want) + got := z.String() + if got != tt.want { + t.Errorf("Set(%s) = %s, want %s", tt.x, got, tt.want) } } } @@ -158,14 +168,54 @@ func TestSetUint256(t *testing.T) { {"18446744073709551615", "18446744073709551615"}, } - for _, tc := range tests { + for _, tt := range tests { got := New() - z := uint256.MustFromDecimal(tc.x) + z := uint256.MustFromDecimal(tt.x) got.SetUint256(z) - if got.ToString() != tc.want { - t.Errorf("SetUint256(%s) = %s, want %s", tc.x, got.ToString(), tc.want) + if got.String() != tt.want { + t.Errorf("SetUint256(%s) = %s, want %s", tt.x, got.String(), tt.want) } } } + +func TestString(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"0", "0"}, + {"1", "1"}, + {"-1", "-1"}, + {"123456789", "123456789"}, + {"-123456789", "-123456789"}, + {"18446744073709551615", "18446744073709551615"}, // max uint64 + {"-18446744073709551615", "-18446744073709551615"}, + {TWO_POW_128_MINUS_1, TWO_POW_128_MINUS_1}, + {MINUS_TWO_POW_128, MINUS_TWO_POW_128}, + {MIN_INT256, MIN_INT256}, + {MAX_INT256, MAX_INT256}, + } + + for _, tt := range tests { + x, err := FromDecimal(tt.input) + if err != nil { + t.Errorf("Failed to parse input (%s): %v", tt.input, err) + continue + } + + output := x.String() + + if output != tt.expected { + t.Errorf("String(%s) = %s, want %s", tt.input, output, tt.expected) + } + } +} + +func TestNilToZero(t *testing.T) { + z := New().NilToZero() + if z.Sign() != 0 { + t.Errorf("NilToZero() = %d, want %d", z.Sign(), 0) + } +} diff --git a/examples/gno.land/p/demo/int256/doc.gno b/examples/gno.land/p/demo/int256/doc.gno new file mode 100644 index 00000000000..ec7d2d3bf9a --- /dev/null +++ b/examples/gno.land/p/demo/int256/doc.gno @@ -0,0 +1,73 @@ +// The int256 package provides a 256-bit signed interger type for gno, +// supporting arithmetic operations and bitwise manipulation. +// +// It designed for applications that require high-precision arithmetic +// beyond the standard 64-bit range. +// +// ## Features +// +// - 256-bit Signed Integers: Support for large integer ranging from -2^255 to 2^255-1. +// - Two's Complement Representation: Efficient storage and computation using two's complement. +// - Arithmetic Operations: Add, Sub, Mul, Div, Mod, Inc, Dec, etc. +// - Bitwise Operations: And, Or, Xor, Not, etc. +// - Comparison Operations: Cmp, Eq, Lt, Gt, etc. +// - Conversion Functions: Int to Uint, Uint to Int, etc. +// - String Parsing and Formatting: Convert to and from decimal string representation. +// +// ## Notes +// +// - Some methods may panic when encountering invalid inputs or overflows. +// - The `int256.Int` type can interact with `uint256.Uint` from the `p/demo/uint256` package. +// - Unlike `math/big.Int`, the `int256.Int` type has fixed size (256-bit) and does not support +// arbitrary precision arithmetic. +// +// # Division and modulus operations +// +// This package provides three different division and modulus operations: +// +// - Div and Rem: Truncated division (T-division) +// - Quo and Mod: Floored division (F-division) +// - DivE and ModE: Euclidean division (E-division) +// +// Truncated division (Div, Rem) is the most common implementation in modern processors +// and programming languages. It rounds quotients towards zero and the remainder +// always has the same sign as the dividend. +// +// Floored division (Quo, Mod) always rounds quotients towards negative infinity. +// This ensures that the modulus is always non-negative for a positive divisor, +// which can be useful in certain algorithms. +// +// Euclidean division (DivE, ModE) ensures that the remainder is always non-negative, +// regardless of the signs of the dividend and divisor. This has several mathematical +// advantages: +// +// 1. It satisfies the unique division with remainder theorem. +// 2. It preserves division and modulus properties for negative divisors. +// 3. It allows for optimizations in divisions by powers of two. +// +// [+] Currently, ModE and Mod are shared the same implementation. +// +// ## Performance considerations: +// +// - For most operations, the performance difference between these division types is negligible. +// - Euclidean division may require an extra comparison and potentially an addition, +// which could impact performance in extremely performance-critical scenarios. +// - For divisions by powers of two, Euclidean division can be optimized to use +// bitwise operations, potentially offering better performance. +// +// ## Usage guidelines: +// +// - Use Div and Rem for general-purpose division that matches most common expectations. +// - Use Quo and Mod when you need a non-negative remainder for positive divisors, +// or when implementing algorithms that assume floored division. +// - Use DivE and ModE when you need the mathematical properties of Euclidean division, +// or when working with algorithms that specifically require it. +// +// Note: When working with negative numbers, be aware of the differences in behavior +// between these division types, especially at the boundaries of integer ranges. +// +// ## References +// +// Daan Leijen, “Division and Modulus for Computer Scientists”: +// https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/divmodnote-letter.pdf +package int256 diff --git a/examples/gno.land/p/demo/int256/gno.mod b/examples/gno.land/p/demo/int256/gno.mod index ef906c83c93..33fb0bc4e72 100644 --- a/examples/gno.land/p/demo/int256/gno.mod +++ b/examples/gno.land/p/demo/int256/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/int256 - -require gno.land/p/demo/uint256 v0.0.0-latest diff --git a/examples/gno.land/p/demo/int256/int256.gno b/examples/gno.land/p/demo/int256/int256.gno index caccd17d531..dd3064ae946 100644 --- a/examples/gno.land/p/demo/int256/int256.gno +++ b/examples/gno.land/p/demo/int256/int256.gno @@ -1,64 +1,87 @@ -// This package provides a 256-bit signed integer type, Int, and associated functions. package int256 import ( + "errors" + "gno.land/p/demo/uint256" ) -var one = uint256.NewUint(1) +var ( + int1 = NewInt(1) + uint0 = uint256.NewUint(0) + uint1 = uint256.NewUint(1) +) type Int struct { - abs *uint256.Uint - neg bool + value uint256.Uint } -// Zero returns a new Int set to 0. -func Zero() *Int { - return NewInt(0) +// New creates and returns a new Int initialized to zero. +func New() *Int { + return &Int{} } -// One returns a new Int set to 1. -func One() *Int { - return NewInt(1) +// NewInt allocates and returns a new Int set to the value of the provided int64. +func NewInt(x int64) *Int { + return New().SetInt64(x) } -// Sign returns: +// Zero returns a new Int initialized to 0. // -// -1 if x < 0 -// 0 if x == 0 -// +1 if x > 0 -func (z *Int) Sign() int { - z.initiateAbs() +// This function is useful for creating a starting point for calculations or +// when an explicit zero value is needed. +func Zero() *Int { return &Int{} } - if z.abs.IsZero() { - return 0 - } - if z.neg { - return -1 - } - return 1 -} - -// New returns a new Int set to 0. -func New() *Int { +// One returns a new Int initialized to one. +// +// This function is convenient for operations that require a unit value, +// such as incrementing or serving as an identity element in multiplication. +func One() *Int { return &Int{ - abs: new(uint256.Uint), + value: *uint256.NewUint(1), } } -// NewInt allocates and returns a new Int set to x. -func NewInt(x int64) *Int { - return New().SetInt64(x) +// Sign determines the sign of the Int. +// +// It returns -1 for negative numbers, 0 for zero, and +1 for positive numbers. +func (z *Int) Sign() int { + if z == nil || z.IsZero() { + return 0 + } + // Right shift the value by 255 bits to check the sign bit. + // In two's complement representation, the most significant bit (MSB) is the sign bit. + // If the MSB is 0, the number is positive; if it is 1, the number is negative. + // + // Example: + // Original value: 1 0 1 0 ... 0 1 (256 bits) + // After Rsh 255: 0 0 0 0 ... 0 1 (1 bit) + // + // This approach is highly efficient as it avoids the need for comparisons + // or arithmetic operations on the full 256-bit number. Instead it reduces + // the problem to checking a single bit. + // + // Additionally, this method will work correctly for all values, + // including the minimum possible negative number (which in two's complement + // doesn't have a positive counterpart in the same bit range). + var temp uint256.Uint + if temp.Rsh(&z.value, 255).IsZero() { + return 1 + } + return -1 } -// FromDecimal returns a new Int from a decimal string. -// Returns a new Int and an error if the string is not a valid decimal. +// FromDecimal creates a new Int from a decimal string representation. +// It handles both positive and negative values. +// +// This function is useful for parsing user input or reading numeric data +// from text-based formats. func FromDecimal(s string) (*Int, error) { - return new(Int).SetString(s) + return New().SetString(s) } -// MustFromDecimal returns a new Int from a decimal string. -// Panics if the string is not a valid decimal. +// MustFromDecimal is similar to FromDecimal but panics if the input string +// is not a valid decimal representation. func MustFromDecimal(s string) *Int { z, err := FromDecimal(s) if err != nil { @@ -67,60 +90,40 @@ func MustFromDecimal(s string) *Int { return z } -// SetString sets s to the value of z and returns z and a boolean indicating success. +// SetString sets the Int to the value represented by the input string. +// This method supports decimal string representations of integers and handles +// both positive and negative values. func (z *Int) SetString(s string) (*Int, error) { - neg := false - // Remove max one leading + - if len(s) > 0 && s[0] == '+' { - neg = false - s = s[1:] + if len(s) == 0 { + return nil, errors.New("cannot set int256 from empty string") } - if len(s) > 0 && s[0] == '-' { - neg = true + // Check for negative sign + neg := s[0] == '-' + if neg || s[0] == '+' { s = s[1:] } - var ( - abs *uint256.Uint - err error - ) - abs, err = uint256.FromDecimal(s) + + // Convert string to uint256 + temp, err := uint256.FromDecimal(s) if err != nil { return nil, err } - return &Int{ - abs, - neg, - }, nil -} - -// FromUint256 is a convenience-constructor from uint256.Uint. -// Returns a new Int and whether overflow occurred. -// OBS: If u is `nil`, this method returns `nil, false` -func FromUint256(x *uint256.Uint) *Int { - if x == nil { - return nil + // If negative, negate the uint256 value + if neg { + temp.Neg(temp) } - z := Zero() - z.SetUint256(x) - return z + z.value.Set(temp) + return z, nil } -// OBS, differs from original mempooler int256 -// NilToZero sets z to 0 and return it if it's nil, otherwise it returns z -func (z *Int) NilToZero() *Int { - if z == nil { - return NewInt(0) - } +// FromUint256 sets the Int to the value of the provided Uint256. +// +// This method allows for conversion from unsigned 256-bit integers +// to signed integers. +func (z *Int) FromUint256(v *uint256.Uint) *Int { + z.value.Set(v) return z } - -// initiateAbs sets default value for `z` or `z.abs` value if is nil -// OBS: differs from mempooler int256. It checks not only `z.abs` but also `z` -func (z *Int) initiateAbs() { - if z == nil || z.abs == nil { - z.abs = new(uint256.Uint) - } -} diff --git a/examples/gno.land/p/demo/int256/int256_test.gno b/examples/gno.land/p/demo/int256/int256_test.gno index 7c8181d1bec..9fbe22bf072 100644 --- a/examples/gno.land/p/demo/int256/int256_test.gno +++ b/examples/gno.land/p/demo/int256/int256_test.gno @@ -1,7 +1,153 @@ -// ported from github.com/mempooler/int256 package int256 -import "testing" +import ( + "testing" + + "gno.land/p/demo/uint256" +) + +func TestInitializers(t *testing.T) { + tests := []struct { + name string + fn func() *Int + wantSign int + wantStr string + }{ + {"Zero", Zero, 0, "0"}, + {"New", New, 0, "0"}, + {"One", One, 1, "1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + z := tt.fn() + if z.Sign() != tt.wantSign { + t.Errorf("%s() = %d, want %d", tt.name, z.Sign(), tt.wantSign) + } + if z.String() != tt.wantStr { + t.Errorf("%s() = %s, want %s", tt.name, z.String(), tt.wantStr) + } + }) + } +} + +func TestNewInt(t *testing.T) { + tests := []struct { + input int64 + expected int + }{ + {0, 0}, + {1, 1}, + {-1, -1}, + {9223372036854775807, 1}, // max int64 + {-9223372036854775808, -1}, // min int64 + } + + for _, tt := range tests { + z := NewInt(tt.input) + if z.Sign() != tt.expected { + t.Errorf("NewInt(%d) = %d, want %d", tt.input, z.Sign(), tt.expected) + } + } +} + +func TestFromDecimal(t *testing.T) { + tests := []struct { + input string + expected int + isError bool + }{ + {"0", 0, false}, + {"1", 1, false}, + {"-1", -1, false}, + {"123456789", 1, false}, + {"-123456789", -1, false}, + {"invalid", 0, true}, + } + + for _, tt := range tests { + z, err := FromDecimal(tt.input) + if tt.isError { + if err == nil { + t.Errorf("FromDecimal(%s) expected error, but got nil", tt.input) + } + } else { + if err != nil { + t.Errorf("FromDecimal(%s) unexpected error: %v", tt.input, err) + } else if z.Sign() != tt.expected { + t.Errorf("FromDecimal(%s) sign is incorrect. Expected: %d, Actual: %d", tt.input, tt.expected, z.Sign()) + } + } + } +} + +func TestMustFromDecimal(t *testing.T) { + tests := []struct { + input string + expected int + shouldPanic bool + }{ + {"0", 0, false}, + {"1", 1, false}, + {"-1", -1, false}, + {"123", 1, false}, + {"invalid", 0, true}, + } + + for _, tt := range tests { + if tt.shouldPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("MustFromDecimal(%q) expected panic, but got nil", tt.input) + } + }() + } + + z := MustFromDecimal(tt.input) + if !tt.shouldPanic && z.Sign() != tt.expected { + t.Errorf("MustFromDecimal(%q) sign is incorrect. Expected: %d, Actual: %d", tt.input, tt.expected, z.Sign()) + } + } +} + +func TestSetUint64(t *testing.T) { + tests := []uint64{ + 0, + 1, + 18446744073709551615, // max uint64 + } + + for _, tt := range tests { + z := New().SetUint64(tt) + if z.Sign() < 0 { + t.Errorf("SetUint64(%d) result is negative", tt) + } + if tt == 0 && z.Sign() != 0 { + t.Errorf("SetUint64(0) result is not zero") + } + if tt > 0 && z.Sign() != 1 { + t.Errorf("SetUint64(%d) result is not positive", tt) + } + } +} + +func TestFromUint256(t *testing.T) { + tests := []struct { + input *uint256.Uint + expected int + }{ + {uint256.NewUint(0), 0}, + {uint256.NewUint(1), 1}, + {uint256.NewUint(18446744073709551615), 1}, + } + + for _, tt := range tests { + z := New().FromUint256(tt.input) + if z.Sign() != tt.expected { + t.Errorf("FromUint256(%v) = %d, want %d", tt.input, z.Sign(), tt.expected) + } + } +} func TestSign(t *testing.T) { tests := []struct { @@ -9,15 +155,59 @@ func TestSign(t *testing.T) { want int }{ {"0", 0}, + {"-0", 0}, + {"+0", 0}, {"1", 1}, {"-1", -1}, + {"9223372036854775807", 1}, + {"-9223372036854775808", -1}, } - for _, tc := range tests { - z := MustFromDecimal(tc.x) + for _, tt := range tests { + z := MustFromDecimal(tt.x) got := z.Sign() - if got != tc.want { - t.Errorf("Sign(%s) = %d, want %d", tc.x, got, tc.want) + if got != tt.want { + t.Errorf("Sign(%s) = %d, want %d", tt.x, got, tt.want) + } + } +} + +func BenchmarkSign(b *testing.B) { + z := New() + for i := 0; i < b.N; i++ { + z.SetUint64(uint64(i)) + z.Sign() + } +} + +func TestSetAndToString(t *testing.T) { + tests := []struct { + input string + expected int + isError bool + }{ + {"0", 0, false}, + {"1", 1, false}, + {"-1", -1, false}, + {"123456789", 1, false}, + {"-123456789", -1, false}, + {"invalid", 0, true}, + } + + for _, tt := range tests { + z, err := New().SetString(tt.input) + if tt.isError { + if err == nil { + t.Errorf("SetString(%s) expected error, but got nil", tt.input) + } + } else { + if err != nil { + t.Errorf("SetString(%s) unexpected error: %v", tt.input, err) + } else if z.Sign() != tt.expected { + t.Errorf("SetString(%s) sign is incorrect. Expected: %d, Actual: %d", tt.input, tt.expected, z.Sign()) + } else if z.String() != tt.input { + t.Errorf("SetString(%s) string representation is incorrect. Expected: %s, Actual: %s", tt.input, tt.input, z.String()) + } } } } diff --git a/examples/gno.land/p/demo/json/README.md b/examples/gno.land/p/demo/json/README.md index 86bc9928194..d983333d246 100644 --- a/examples/gno.land/p/demo/json/README.md +++ b/examples/gno.land/p/demo/json/README.md @@ -75,7 +75,6 @@ The converted `Node` type allows you to modify the JSON data or search and extra package main import ( - "fmt" "gno.land/p/demo/json" "gno.land/p/demo/ufmt" ) @@ -100,7 +99,6 @@ Encoding (or Marshaling) is the functionality that converts JSON data represente package main import ( - "fmt" "gno.land/p/demo/json" "gno.land/p/demo/ufmt" ) @@ -133,7 +131,6 @@ Here is an example of finding data with a specific key. For more examples, pleas package main import ( - "fmt" "gno.land/p/demo/json" "gno.land/p/demo/ufmt" ) diff --git a/examples/gno.land/p/demo/json/buffer.gno b/examples/gno.land/p/demo/json/buffer.gno index 23fb53fb0ea..a217ee653f9 100644 --- a/examples/gno.land/p/demo/json/buffer.gno +++ b/examples/gno.land/p/demo/json/buffer.gno @@ -3,7 +3,6 @@ package json import ( "errors" "io" - "strings" "gno.land/p/demo/ufmt" ) @@ -112,28 +111,6 @@ func (b *buffer) skip(bs byte) error { return io.EOF } -// skipAny moves the index until it encounters one of the given set of bytes. -func (b *buffer) skipAny(endTokens map[byte]bool) error { - for b.index < b.length { - if _, exists := endTokens[b.data[b.index]]; exists { - return nil - } - - b.index++ - } - - // build error message - var tokens []string - for token := range endTokens { - tokens = append(tokens, string(token)) - } - - return ufmt.Errorf( - "EOF reached before encountering one of the expected tokens: %s", - strings.Join(tokens, ", "), - ) -} - // skipAndReturnIndex moves the buffer index forward by one and returns the new index. func (b *buffer) skipAndReturnIndex() (int, error) { err := b.step() @@ -165,7 +142,7 @@ func (b *buffer) skipUntil(endTokens map[byte]bool) (int, error) { // significantTokens is a map where the keys are the significant characters in a JSON path. // The values in the map are all true, which allows us to use the map as a set for quick lookups. -var significantTokens = map[byte]bool{ +var significantTokens = [256]bool{ dot: true, // access properties of an object dollarSign: true, // root object atSign: true, // current object @@ -174,7 +151,7 @@ var significantTokens = map[byte]bool{ } // filterTokens stores the filter expression tokens. -var filterTokens = map[byte]bool{ +var filterTokens = [256]bool{ aesterisk: true, // wildcard andSign: true, orSign: true, @@ -186,7 +163,7 @@ func (b *buffer) skipToNextSignificantToken() { for b.index < b.length { current := b.data[b.index] - if _, ok := significantTokens[current]; ok { + if significantTokens[current] { break } @@ -205,7 +182,7 @@ func (b *buffer) backslash() bool { count := 0 for i := b.index - 1; ; i-- { - if i >= b.length || b.data[i] != backSlash { + if b.data[i] != backSlash { break } @@ -220,7 +197,7 @@ func (b *buffer) backslash() bool { } // numIndex holds a map of valid numeric characters -var numIndex = map[byte]bool{ +var numIndex = [256]bool{ '0': true, '1': true, '2': true, @@ -255,11 +232,11 @@ func (b *buffer) pathToken() error { } if err := b.skip(c); err != nil { - return errors.New("unmatched quote in path") + return errUnmatchedQuotePath } if b.index >= b.length { - return errors.New("unmatched quote in path") + return errUnmatchedQuotePath } case c == bracketOpen || c == parenOpen: @@ -269,7 +246,7 @@ func (b *buffer) pathToken() error { case c == bracketClose || c == parenClose: inToken = true if len(stack) == 0 || (c == bracketClose && stack[len(stack)-1] != bracketOpen) || (c == parenClose && stack[len(stack)-1] != parenOpen) { - return errors.New("mismatched bracket or parenthesis") + return errUnmatchedParenthesis } stack = stack[:len(stack)-1] @@ -284,7 +261,7 @@ func (b *buffer) pathToken() error { inToken = true inNumber = true } else if !inToken { - return errors.New("unexpected operator at start of token") + return errInvalidToken } default: @@ -300,7 +277,7 @@ func (b *buffer) pathToken() error { end: if len(stack) != 0 { - return errors.New("unclosed bracket or parenthesis at end of path") + return errUnmatchedParenthesis } if first == b.index { @@ -315,15 +292,15 @@ end: } func pathStateContainsValidPathToken(c byte) bool { - if _, ok := significantTokens[c]; ok { + if significantTokens[c] { return true } - if _, ok := filterTokens[c]; ok { + if filterTokens[c] { return true } - if _, ok := numIndex[c]; ok { + if numIndex[c] { return true } @@ -342,7 +319,7 @@ func (b *buffer) numeric(token bool) error { for ; b.index < b.length; b.index++ { b.class = b.getClasses(doubleQuote) if b.class == __ { - return errors.New("invalid token found while parsing path") + return errInvalidToken } b.state = StateTransitionTable[b.last][b.class] @@ -351,7 +328,7 @@ func (b *buffer) numeric(token bool) error { break } - return errors.New("invalid token found while parsing path") + return errInvalidToken } if b.state < __ { @@ -366,7 +343,7 @@ func (b *buffer) numeric(token bool) error { } if b.last != ZE && b.last != IN && b.last != FR && b.last != E3 { - return errors.New("invalid token found while parsing path") + return errInvalidToken } return nil @@ -407,12 +384,12 @@ func (b *buffer) string(search byte, token bool) error { b.class = b.getClasses(search) if b.class == __ { - return errors.New("invalid token found while parsing path") + return errInvalidToken } b.state = StateTransitionTable[b.last][b.class] if b.state == __ { - return errors.New("invalid token found while parsing path") + return errInvalidToken } if b.state < __ { @@ -431,11 +408,11 @@ func (b *buffer) word(bs []byte) error { max := len(bs) index := 0 - for ; b.index < b.length; b.index++ { + for ; b.index < b.length && index < max; b.index++ { c = b.data[b.index] if c != bs[index] { - return errors.New("invalid token found while parsing path") + return errInvalidToken } index++ @@ -445,7 +422,7 @@ func (b *buffer) word(bs []byte) error { } if index != max { - return errors.New("invalid token found while parsing path") + return errInvalidToken } return nil diff --git a/examples/gno.land/p/demo/json/buffer_test.gno b/examples/gno.land/p/demo/json/buffer_test.gno index b8dce390a61..f4102040be5 100644 --- a/examples/gno.land/p/demo/json/buffer_test.gno +++ b/examples/gno.land/p/demo/json/buffer_test.gno @@ -1,6 +1,8 @@ package json -import "testing" +import ( + "testing" +) func TestBufferCurrent(t *testing.T) { tests := []struct { @@ -242,37 +244,6 @@ func TestBufferSkip(t *testing.T) { } } -func TestBufferSkipAny(t *testing.T) { - tests := []struct { - name string - buffer *buffer - s map[byte]bool - wantErr bool - }{ - { - name: "Skip any valid byte", - buffer: &buffer{data: []byte("test"), length: 4, index: 0}, - s: map[byte]bool{'e': true, 'o': true}, - wantErr: false, - }, - { - name: "Skip any to EOF", - buffer: &buffer{data: []byte("test"), length: 4, index: 0}, - s: map[byte]bool{'x': true, 'y': true}, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.buffer.skipAny(tt.s) - if (err != nil) != tt.wantErr { - t.Errorf("buffer.skipAny() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - func TestSkipToNextSignificantToken(t *testing.T) { tests := []struct { name string diff --git a/examples/gno.land/p/demo/json/builder.gno b/examples/gno.land/p/demo/json/builder.gno new file mode 100644 index 00000000000..4693d5ec550 --- /dev/null +++ b/examples/gno.land/p/demo/json/builder.gno @@ -0,0 +1,89 @@ +package json + +type NodeBuilder struct { + node *Node +} + +func Builder() *NodeBuilder { + return &NodeBuilder{node: ObjectNode("", nil)} +} + +func (b *NodeBuilder) WriteString(key, value string) *NodeBuilder { + b.node.AppendObject(key, StringNode("", value)) + return b +} + +func (b *NodeBuilder) WriteNumber(key string, value float64) *NodeBuilder { + b.node.AppendObject(key, NumberNode("", value)) + return b +} + +func (b *NodeBuilder) WriteBool(key string, value bool) *NodeBuilder { + b.node.AppendObject(key, BoolNode("", value)) + return b +} + +func (b *NodeBuilder) WriteNull(key string) *NodeBuilder { + b.node.AppendObject(key, NullNode("")) + return b +} + +func (b *NodeBuilder) WriteObject(key string, fn func(*NodeBuilder)) *NodeBuilder { + nestedBuilder := &NodeBuilder{node: ObjectNode("", nil)} + fn(nestedBuilder) + b.node.AppendObject(key, nestedBuilder.node) + return b +} + +func (b *NodeBuilder) WriteArray(key string, fn func(*ArrayBuilder)) *NodeBuilder { + arrayBuilder := &ArrayBuilder{nodes: []*Node{}} + fn(arrayBuilder) + b.node.AppendObject(key, ArrayNode("", arrayBuilder.nodes)) + return b +} + +func (b *NodeBuilder) Node() *Node { + return b.node +} + +type ArrayBuilder struct { + nodes []*Node +} + +func (ab *ArrayBuilder) WriteString(value string) *ArrayBuilder { + ab.nodes = append(ab.nodes, StringNode("", value)) + return ab +} + +func (ab *ArrayBuilder) WriteNumber(value float64) *ArrayBuilder { + ab.nodes = append(ab.nodes, NumberNode("", value)) + return ab +} + +func (ab *ArrayBuilder) WriteInt(value int) *ArrayBuilder { + return ab.WriteNumber(float64(value)) +} + +func (ab *ArrayBuilder) WriteBool(value bool) *ArrayBuilder { + ab.nodes = append(ab.nodes, BoolNode("", value)) + return ab +} + +func (ab *ArrayBuilder) WriteNull() *ArrayBuilder { + ab.nodes = append(ab.nodes, NullNode("")) + return ab +} + +func (ab *ArrayBuilder) WriteObject(fn func(*NodeBuilder)) *ArrayBuilder { + nestedBuilder := &NodeBuilder{node: ObjectNode("", nil)} + fn(nestedBuilder) + ab.nodes = append(ab.nodes, nestedBuilder.node) + return ab +} + +func (ab *ArrayBuilder) WriteArray(fn func(*ArrayBuilder)) *ArrayBuilder { + nestedArrayBuilder := &ArrayBuilder{nodes: []*Node{}} + fn(nestedArrayBuilder) + ab.nodes = append(ab.nodes, ArrayNode("", nestedArrayBuilder.nodes)) + return ab +} diff --git a/examples/gno.land/p/demo/json/builder_test.gno b/examples/gno.land/p/demo/json/builder_test.gno new file mode 100644 index 00000000000..4c882d0d6c8 --- /dev/null +++ b/examples/gno.land/p/demo/json/builder_test.gno @@ -0,0 +1,103 @@ +package json + +import ( + "testing" +) + +func TestNodeBuilder(t *testing.T) { + tests := []struct { + name string + build func() *Node + expected string + }{ + { + name: "plain object", + build: func() *Node { + return Builder(). + WriteString("name", "Alice"). + WriteNumber("age", 30). + WriteBool("is_student", false). + Node() + }, + expected: `{"name":"Alice","age":30,"is_student":false}`, + }, + { + name: "nested object", + build: func() *Node { + return Builder(). + WriteString("name", "Alice"). + WriteObject("address", func(b *NodeBuilder) { + b.WriteString("city", "New York"). + WriteNumber("zipcode", 10001) + }). + Node() + }, + expected: `{"name":"Alice","address":{"city":"New York","zipcode":10001}}`, + }, + { + name: "null node", + build: func() *Node { + return Builder().WriteNull("foo").Node() + }, + expected: `{"foo":null}`, + }, + { + name: "array node", + build: func() *Node { + return Builder(). + WriteArray("items", func(ab *ArrayBuilder) { + ab.WriteString("item1"). + WriteString("item2"). + WriteString("item3") + }). + Node() + }, + expected: `{"items":["item1","item2","item3"]}`, + }, + { + name: "array with objects", + build: func() *Node { + return Builder(). + WriteArray("users", func(ab *ArrayBuilder) { + ab.WriteObject(func(b *NodeBuilder) { + b.WriteString("name", "Bob"). + WriteNumber("age", 25) + }). + WriteObject(func(b *NodeBuilder) { + b.WriteString("name", "Carol"). + WriteNumber("age", 27) + }) + }). + Node() + }, + expected: `{"users":[{"name":"Bob","age":25},{"name":"Carol","age":27}]}`, + }, + { + name: "array with various types", + build: func() *Node { + return Builder(). + WriteArray("values", func(ab *ArrayBuilder) { + ab.WriteString("item1"). + WriteNumber(123). + WriteBool(true). + WriteNull() + }). + Node() + }, + expected: `{"values":["item1",123,true,null]}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node := tt.build() + value, err := Marshal(node) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if string(value) != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, string(value)) + } + }) + } +} diff --git a/examples/gno.land/p/demo/json/decode_test.gno b/examples/gno.land/p/demo/json/decode_test.gno index 8aad07169f2..dc92f1f84cd 100644 --- a/examples/gno.land/p/demo/json/decode_test.gno +++ b/examples/gno.land/p/demo/json/decode_test.gno @@ -8,8 +8,8 @@ import ( type testNode struct { name string input []byte - _type ValueType value []byte + _type ValueType } func simpleValid(test *testNode, t *testing.T) { diff --git a/examples/gno.land/p/demo/json/eisel_lemire/gno.mod b/examples/gno.land/p/demo/json/eisel_lemire/gno.mod deleted file mode 100644 index d6670de82e2..00000000000 --- a/examples/gno.land/p/demo/json/eisel_lemire/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/p/demo/json/eisel_lemire diff --git a/examples/gno.land/p/demo/json/encode.gno b/examples/gno.land/p/demo/json/encode.gno index be90d7aa73d..55828650e22 100644 --- a/examples/gno.land/p/demo/json/encode.gno +++ b/examples/gno.land/p/demo/json/encode.gno @@ -3,10 +3,8 @@ package json import ( "bytes" "errors" - "math" "strconv" - "gno.land/p/demo/json/ryu" "gno.land/p/demo/ufmt" ) @@ -44,17 +42,8 @@ func Marshal(node *Node) ([]byte, error) { return nil, err } - // ufmt does not support %g. by doing so, we need to check if the number is an integer - // after then, apply the correct format for each float and integer numbers. - if math.Mod(nVal, 1.0) == 0 { - // must convert float to integer. otherwise it will be overflowed. - num := ufmt.Sprintf("%d", int(nVal)) - buf.WriteString(num) - } else { - // use ryu algorithm to convert float to string - num := ryu.FormatFloat64(nVal) - buf.WriteString(num) - } + num := strconv.FormatFloat(nVal, 'f', -1, 64) + buf.WriteString(num) case String: sVal, err = node.GetString() diff --git a/examples/gno.land/p/demo/json/encode_test.gno b/examples/gno.land/p/demo/json/encode_test.gno index e8e53993b5c..831a9e0e0a2 100644 --- a/examples/gno.land/p/demo/json/encode_test.gno +++ b/examples/gno.land/p/demo/json/encode_test.gno @@ -37,10 +37,9 @@ func TestMarshal_Primitive(t *testing.T) { name: "42", node: NumberNode("", 42), }, - // TODO: fix output for not to use scientific notation { - name: "1.005e+02", - node: NumberNode("", 100.5), + name: "3.14", + node: NumberNode("", 3.14), }, { name: `[1,2,3]`, diff --git a/examples/gno.land/p/demo/json/errors.gno b/examples/gno.land/p/demo/json/errors.gno new file mode 100644 index 00000000000..e0836dccdff --- /dev/null +++ b/examples/gno.land/p/demo/json/errors.gno @@ -0,0 +1,34 @@ +package json + +import "errors" + +var ( + errNilNode = errors.New("node is nil") + errNotArrayNode = errors.New("node is not array") + errNotBoolNode = errors.New("node is not boolean") + errNotNullNode = errors.New("node is not null") + errNotNumberNode = errors.New("node is not number") + errNotObjectNode = errors.New("node is not object") + errNotStringNode = errors.New("node is not string") + errInvalidToken = errors.New("invalid token") + errIndexNotFound = errors.New("index not found") + errInvalidAppend = errors.New("can't append value to non-appendable node") + errInvalidAppendCycle = errors.New("appending value to itself or its children or parents will cause a cycle") + errInvalidEscapeSequence = errors.New("invalid escape sequence") + errInvalidStringValue = errors.New("invalid string value") + errEmptyBooleanNode = errors.New("boolean node is empty") + errEmptyStringNode = errors.New("string node is empty") + errKeyRequired = errors.New("key is required for object") + errUnmatchedParenthesis = errors.New("mismatched bracket or parenthesis") + errUnmatchedQuotePath = errors.New("unmatched quote in path") +) + +var ( + errInvalidStringInput = errors.New("invalid string input") + errMalformedBooleanValue = errors.New("malformed boolean value") + errEmptyByteSlice = errors.New("empty byte slice") + errInvalidExponentValue = errors.New("invalid exponent value") + errNonDigitCharacters = errors.New("non-digit characters found") + errNumericRangeExceeded = errors.New("numeric value exceeds the range limit") + errMultipleDecimalPoints = errors.New("multiple decimal points found") +) diff --git a/examples/gno.land/p/demo/json/escape.gno b/examples/gno.land/p/demo/json/escape.gno index 5a834068127..ee3e4a79855 100644 --- a/examples/gno.land/p/demo/json/escape.gno +++ b/examples/gno.land/p/demo/json/escape.gno @@ -1,8 +1,6 @@ package json import ( - "bytes" - "errors" "unicode/utf8" ) @@ -13,6 +11,9 @@ const ( surrogateEnd = 0xDFFF basicMultilingualPlaneOffset = 0xFFFF badHex = -1 + + singleUnicodeEscapeLen = 6 + surrogatePairLen = 12 ) var hexLookupTable = [256]int{ @@ -42,48 +43,32 @@ func h2i(c byte) int { // // it returns the processed slice and any error encountered during the Unescape operation. func Unescape(input, output []byte) ([]byte, error) { - // find the index of the first backslash in the input slice. - firstBackslash := bytes.IndexByte(input, backSlash) - if firstBackslash == -1 { - return input, nil - } - - // ensure the output slice has enough capacity to hold the result. + // ensure the output slice has enough capacity to hold the input slice. inputLen := len(input) if cap(output) < inputLen { output = make([]byte, inputLen) } - output = output[:inputLen] - copy(output, input[:firstBackslash]) - - input = input[firstBackslash:] - buf := output[firstBackslash:] - - for len(input) > 0 { - inLen, bufLen, err := processEscapedUTF8(input, buf) - if err != nil { - return nil, err - } - - input = input[inLen:] // the number of bytes consumed in the input - buf = buf[bufLen:] // the number of bytes written to buf + inPos, outPos := 0, 0 - // find the next backslash in the remaining input - nextBackslash := bytes.IndexByte(input, backSlash) - if nextBackslash == -1 { - copy(buf, input) - buf = buf[len(input):] - break + for inPos < len(input) { + c := input[inPos] + if c != backSlash { + output[outPos] = c + inPos++ + outPos++ + } else { + // process escape sequence + inLen, outLen, err := processEscapedUTF8(input[inPos:], output[outPos:]) + if err != nil { + return nil, err + } + inPos += inLen + outPos += outLen } - - copy(buf, input[:nextBackslash]) - - input = input[nextBackslash:] - buf = buf[nextBackslash:] } - return output[:len(output)-len(buf)], nil + return output[:outPos], nil } // isSurrogatePair returns true if the rune is a surrogate pair. @@ -94,6 +79,16 @@ func isSurrogatePair(r rune) bool { return highSurrogateOffset <= r && r <= surrogateEnd } +// isHighSurrogate checks if the rune is a high surrogate (U+D800 to U+DBFF). +func isHighSurrogate(r rune) bool { + return r >= highSurrogateOffset && r <= 0xDBFF +} + +// isLowSurrogate checks if the rune is a low surrogate (U+DC00 to U+DFFF). +func isLowSurrogate(r rune) bool { + return r >= lowSurrogateOffset && r <= surrogateEnd +} + // combineSurrogates reconstruct the original unicode code points in the // supplemental plane by combinin the high and low surrogate. // @@ -122,28 +117,41 @@ func decodeSingleUnicodeEscape(b []byte) (rune, bool) { } // decodeUnicodeEscape decodes a Unicode escape sequence from a byte slice. +// It handles both single Unicode escape sequences and surrogate pairs. func decodeUnicodeEscape(b []byte) (rune, int) { + // decode the first Unicode escape sequence. r, ok := decodeSingleUnicodeEscape(b) if !ok { return utf8.RuneError, -1 } - // determine valid unicode escapes within the BMP + // if the rune is within the BMP and not a surrogate, return it if r <= basicMultilingualPlaneOffset && !isSurrogatePair(r) { return r, 6 } - // Decode the following escape sequence to verify a UTF-16 susergate pair. - r2, ok := decodeSingleUnicodeEscape(b[6:]) - if !ok { + if !isHighSurrogate(r) { + // invalid surrogate pair. return utf8.RuneError, -1 } - if r2 < lowSurrogateOffset { + // if the rune is a high surrogate, need to decode the next escape sequence. + + // ensure there are enough bytes for the next escape sequence. + if len(b) < surrogatePairLen { return utf8.RuneError, -1 } - - return combineSurrogates(r, r2), 12 + // decode the second Unicode escape sequence. + r2, ok := decodeSingleUnicodeEscape(b[singleUnicodeEscapeLen:]) + if !ok { + return utf8.RuneError, -1 + } + // check if the second rune is a low surrogate. + if isLowSurrogate(r2) { + combined := combineSurrogates(r, r2) + return combined, surrogatePairLen + } + return utf8.RuneError, -1 } var escapeByteSet = [256]byte{ @@ -165,7 +173,6 @@ func Unquote(s []byte, border byte) (string, bool) { } // unquoteBytes takes a byte slice and unquotes it by removing -// TODO: consider to move this function to the strconv package. func unquoteBytes(s []byte, border byte) ([]byte, bool) { if len(s) < 2 || s[0] != border || s[len(s)-1] != border { return nil, false @@ -259,21 +266,12 @@ func unquoteBytes(s []byte, border byte) ([]byte, bool) { return b[:w], true } -// processEscapedUTF8 processes the escape sequence in the given byte slice and -// and converts them to UTF-8 characters. The function returns the length of the processed input and output. -// -// The input 'in' must contain the escape sequence to be processed, -// and 'out' provides a space to store the converted characters. -// -// The function returns (input length, output length) if the escape sequence is correct. -// Unicode escape sequences (e.g. \uXXXX) are decoded to UTF-8, other default escape sequences are -// converted to their corresponding special characters (e.g. \n -> newline). -// -// If the escape sequence is invalid, or if 'in' does not completely enclose the escape sequence, -// function returns (-1, -1) to indicate an error. +// processEscapedUTF8 converts escape sequences to UTF-8 characters. +// It decodes Unicode escape sequences (\uXXXX) to UTF-8 and +// converts standard escape sequences (e.g., \n) to their corresponding special characters. func processEscapedUTF8(in, out []byte) (int, int, error) { if len(in) < 2 || in[0] != backSlash { - return -1, -1, errors.New("invalid escape sequence") + return -1, -1, errInvalidEscapeSequence } escapeSeqLen := 2 @@ -282,7 +280,7 @@ func processEscapedUTF8(in, out []byte) (int, int, error) { if escapeChar != 'u' { val := escapeByteSet[escapeChar] if val == 0 { - return -1, -1, errors.New("invalid escape sequence") + return -1, -1, errInvalidEscapeSequence } out[0] = val @@ -291,7 +289,7 @@ func processEscapedUTF8(in, out []byte) (int, int, error) { r, size := decodeUnicodeEscape(in) if size == -1 { - return -1, -1, errors.New("invalid escape sequence") + return -1, -1, errInvalidEscapeSequence } outLen := utf8.EncodeRune(out, r) diff --git a/examples/gno.land/p/demo/json/escape_test.gno b/examples/gno.land/p/demo/json/escape_test.gno index 40c118d93ce..0e2e696e83c 100644 --- a/examples/gno.land/p/demo/json/escape_test.gno +++ b/examples/gno.land/p/demo/json/escape_test.gno @@ -103,24 +103,25 @@ func TestDecodeSingleUnicodeEscape(t *testing.T) { } func TestDecodeUnicodeEscape(t *testing.T) { - testCases := []struct { - input string + tests := []struct { + input []byte expected rune size int }{ - {"\\u0041", 'A', 6}, - {"\\u03B1", 'α', 6}, - {"\\u1F600", 0x1F60, 6}, - {"\\uD830\\uDE03", 0x1C203, 12}, - {"\\uD800\\uDC00", 0x00010000, 12}, - - {"\\u004", utf8.RuneError, -1}, - {"\\uXYZW", utf8.RuneError, -1}, - {"\\uD83D\\u0041", utf8.RuneError, -1}, + {[]byte(`\u0041`), 'A', 6}, + {[]byte(`\uD83D\uDE00`), 0x1F600, 12}, // 😀 + {[]byte(`\uD834\uDD1E`), 0x1D11E, 12}, // 𝄞 + {[]byte(`\uFFFF`), '\uFFFF', 6}, + {[]byte(`\uXYZW`), utf8.RuneError, -1}, + {[]byte(`\uD800`), utf8.RuneError, -1}, // single high surrogate + {[]byte(`\uDC00`), utf8.RuneError, -1}, // single low surrogate + {[]byte(`\uD800\uDC00`), 0x10000, 12}, // First code point above U+FFFF + {[]byte(`\uDBFF\uDFFF`), 0x10FFFF, 12}, // Maximum code point + {[]byte(`\uD83D\u0041`), utf8.RuneError, -1}, // invalid surrogate pair } - for _, tc := range testCases { - r, size := decodeUnicodeEscape([]byte(tc.input)) + for _, tc := range tests { + r, size := decodeUnicodeEscape(tc.input) if r != tc.expected || size != tc.size { t.Errorf("decodeUnicodeEscape(%q) = (%U, %d); want (%U, %d)", tc.input, r, size, tc.expected, tc.size) } @@ -128,7 +129,7 @@ func TestDecodeUnicodeEscape(t *testing.T) { } func TestUnescapeToUTF8(t *testing.T) { - testCases := []struct { + tests := []struct { input []byte expectedIn int expectedOut int @@ -150,7 +151,7 @@ func TestUnescapeToUTF8(t *testing.T) { {[]byte(`\uD83D\u0041`), -1, -1, true}, // invalid unicode escape sequence } - for _, tc := range testCases { + for _, tc := range tests { input := make([]byte, len(tc.input)) copy(input, tc.input) output := make([]byte, utf8.UTFMax) @@ -166,23 +167,32 @@ func TestUnescapeToUTF8(t *testing.T) { } func TestUnescape(t *testing.T) { - testCases := []struct { + tests := []struct { name string input []byte expected []byte + isError bool }{ - {"NoEscape", []byte("hello world"), []byte("hello world")}, - {"SingleEscape", []byte("hello\\nworld"), []byte("hello\nworld")}, - {"MultipleEscapes", []byte("line1\\nline2\\r\\nline3"), []byte("line1\nline2\r\nline3")}, - {"UnicodeEscape", []byte("snowman:\\u2603"), []byte("snowman:\u2603")}, - {"Complex", []byte("tc\\n\\u2603\\r\\nend"), []byte("tc\n\u2603\r\nend")}, + {"NoEscape", []byte("hello world"), []byte("hello world"), false}, + {"SingleEscape", []byte("hello\\nworld"), []byte("hello\nworld"), false}, + {"MultipleEscapes", []byte("line1\\nline2\\r\\nline3"), []byte("line1\nline2\r\nline3"), false}, + {"UnicodeEscape", []byte("snowman:\\u2603"), []byte("snowman:\u2603"), false}, + {"SurrogatePair", []byte("emoji:\\uD83D\\uDE00"), []byte("emoji:😀"), false}, + {"InvalidEscape", []byte("hello\\xworld"), nil, true}, + {"IncompleteUnicode", []byte("incomplete:\\u123"), nil, true}, + {"InvalidSurrogatePair", []byte("invalid:\\uD83D\\u0041"), nil, true}, } - for _, tc := range testCases { + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - output, _ := Unescape(tc.input, make([]byte, len(tc.input)+10)) - if !bytes.Equal(output, tc.expected) { - t.Errorf("unescape(%q) = %q; want %q", tc.input, output, tc.expected) + output := make([]byte, len(tc.input)*2) // Allocate extra space for possible expansion + result, err := Unescape(tc.input, output) + if (err != nil) != tc.isError { + t.Errorf("Unescape(%q) error = %v; want error = %v", tc.input, err, tc.isError) + } + + if !tc.isError && !bytes.Equal(result, tc.expected) { + t.Errorf("Unescape(%q) = %q; want %q", tc.input, result, tc.expected) } }) } @@ -206,6 +216,7 @@ func TestUnquoteBytes(t *testing.T) { {[]byte("\"\\u0041\""), '"', []byte("A"), true}, {[]byte(`"Hello, 世界"`), '"', []byte("Hello, 世界"), true}, {[]byte(`"Hello, \x80"`), '"', nil, false}, + {[]byte(`"invalid surrogate: \uD83D\u0041"`), '"', nil, false}, } for _, tc := range tests { diff --git a/examples/gno.land/p/demo/json/gno.mod b/examples/gno.land/p/demo/json/gno.mod index 8a380644acc..831fa56c0f9 100644 --- a/examples/gno.land/p/demo/json/gno.mod +++ b/examples/gno.land/p/demo/json/gno.mod @@ -1,7 +1 @@ module gno.land/p/demo/json - -require ( - gno.land/p/demo/json/eisel_lemire v0.0.0-latest - gno.land/p/demo/json/ryu v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/json/indent.gno b/examples/gno.land/p/demo/json/indent.gno index cdcfd4524ee..cdf9d5e976f 100644 --- a/examples/gno.land/p/demo/json/indent.gno +++ b/examples/gno.land/p/demo/json/indent.gno @@ -9,21 +9,7 @@ import ( // A factor no higher than 2 ensures that wasted space never exceeds 50%. const indentGrowthFactor = 2 -// IndentJSON takes a JSON byte slice and a string for indentation, -// then formats the JSON according to the specified indent string. -// This function applies indentation rules as follows: -// -// 1. For top-level arrays and objects, no additional indentation is applied. -// -// 2. For nested structures like arrays within arrays or objects, indentation increases. -// -// 3. Indentation is applied after opening brackets ('[' or '{') and before closing brackets (']' or '}'). -// -// 4. Commas and colons are handled appropriately to maintain valid JSON format. -// -// 5. Nested arrays within objects or arrays receive new lines and indentation based on their depth level. -// -// The function returns the formatted JSON as a byte slice and an error if any issues occurred during formatting. +// IndentJSON formats the JSON data with the specified indentation. func Indent(data []byte, indent string) ([]byte, error) { var ( out bytes.Buffer diff --git a/examples/gno.land/p/demo/json/node.gno b/examples/gno.land/p/demo/json/node.gno index 1e71a101e62..c917150bc3d 100644 --- a/examples/gno.land/p/demo/json/node.gno +++ b/examples/gno.land/p/demo/json/node.gno @@ -44,7 +44,7 @@ func NewNode(prev *Node, b *buffer, typ ValueType, key **string) (*Node, error) prev.next[strconv.Itoa(size)] = curr } else if prev.IsObject() { if key == nil { - return nil, errors.New("key is required for object") + return nil, errKeyRequired } prev.next[**key] = curr @@ -88,7 +88,7 @@ func (n *Node) HasKey(key string) bool { // GetKey returns the value of the given key from the current object node. func (n *Node) GetKey(key string) (*Node, error) { if n == nil { - return nil, errors.New("node is nil") + return nil, errNilNode } if n.Type() != Object { @@ -174,7 +174,7 @@ func (n *Node) Value() (value interface{}, err error) { return nil, nil case Number: - value, err = ParseFloatLiteral(n.source()) + value, err = strconv.ParseFloat(string(n.source()), 64) if err != nil { return nil, err } @@ -185,14 +185,14 @@ func (n *Node) Value() (value interface{}, err error) { var ok bool value, ok = Unquote(n.source(), doubleQuote) if !ok { - return "", errors.New("invalid string value") + return "", errInvalidStringValue } n.value = value case Boolean: if len(n.source()) == 0 { - return nil, errors.New("empty boolean value") + return nil, errEmptyBooleanNode } b := n.source()[0] @@ -319,11 +319,11 @@ func (n *Node) MustIndex(expectIdx int) *Node { // if the index is negative, it returns the index is from the end of the array. func (n *Node) GetIndex(idx int) (*Node, error) { if n == nil { - return nil, errors.New("node is nil") + return nil, errNilNode } if !n.IsArray() { - return nil, errors.New("node is not array") + return nil, errNotArrayNode } if idx > n.Size() { @@ -336,7 +336,7 @@ func (n *Node) GetIndex(idx int) (*Node, error) { child, ok := n.next[strconv.Itoa(idx)] if !ok { - return nil, errors.New("index not found") + return nil, errIndexNotFound } return child, nil @@ -556,11 +556,11 @@ func (n *Node) root() *Node { // } func (n *Node) GetNull() (interface{}, error) { if n == nil { - return nil, errors.New("node is nil") + return nil, errNilNode } if !n.IsNull() { - return nil, errors.New("node is not null") + return nil, errNotNullNode } return nil, nil @@ -590,11 +590,11 @@ func (n *Node) MustNull() interface{} { // println(val) // 10.5 func (n *Node) GetNumeric() (float64, error) { if n == nil { - return 0, errors.New("node is nil") + return 0, errNilNode } if n.nodeType != Number { - return 0, errors.New("node is not number") + return 0, errNotNumberNode } val, err := n.Value() @@ -604,7 +604,7 @@ func (n *Node) GetNumeric() (float64, error) { v, ok := val.(float64) if !ok { - return 0, errors.New("node is not number") + return 0, errNotNumberNode } return v, nil @@ -639,11 +639,11 @@ func (n *Node) MustNumeric() float64 { // println(str) // "foo" func (n *Node) GetString() (string, error) { if n == nil { - return "", errors.New("string node is empty") + return "", errEmptyStringNode } if !n.IsString() { - return "", errors.New("node type is not string") + return "", errNotStringNode } val, err := n.Value() @@ -653,7 +653,7 @@ func (n *Node) GetString() (string, error) { v, ok := val.(string) if !ok { - return "", errors.New("node is not string") + return "", errNotStringNode } return v, nil @@ -683,11 +683,11 @@ func (n *Node) MustString() string { // println(val) // true func (n *Node) GetBool() (bool, error) { if n == nil { - return false, errors.New("node is nil") + return false, errNilNode } if n.nodeType != Boolean { - return false, errors.New("node is not boolean") + return false, errNotBoolNode } val, err := n.Value() @@ -697,7 +697,7 @@ func (n *Node) GetBool() (bool, error) { v, ok := val.(bool) if !ok { - return false, errors.New("node is not boolean") + return false, errNotBoolNode } return v, nil @@ -732,11 +732,11 @@ func (n *Node) MustBool() bool { // result: "foo", 1 func (n *Node) GetArray() ([]*Node, error) { if n == nil { - return nil, errors.New("node is nil") + return nil, errNilNode } if n.nodeType != Array { - return nil, errors.New("node is not array") + return nil, errNotArrayNode } val, err := n.Value() @@ -746,7 +746,7 @@ func (n *Node) GetArray() ([]*Node, error) { v, ok := val.([]*Node) if !ok { - return nil, errors.New("node is not array") + return nil, errNotArrayNode } return v, nil @@ -788,7 +788,7 @@ func (n *Node) MustArray() []*Node { // result: ["bar", "baz", 1, "foo"] func (n *Node) AppendArray(value ...*Node) error { if !n.IsArray() { - return errors.New("can't append value to non-array node") + return errInvalidAppend } for _, val := range value { @@ -836,11 +836,11 @@ func (n *Node) ArrayEach(callback func(i int, target *Node)) { // result: map[string]*Node{"key": StringNode("key", "value")} func (n *Node) GetObject() (map[string]*Node, error) { if n == nil { - return nil, errors.New("node is nil") + return nil, errNilNode } if !n.IsObject() { - return nil, errors.New("node is not object") + return nil, errNotObjectNode } val, err := n.Value() @@ -850,7 +850,7 @@ func (n *Node) GetObject() (map[string]*Node, error) { v, ok := val.(map[string]*Node) if !ok { - return nil, errors.New("node is not object") + return nil, errNotObjectNode } return v, nil @@ -873,7 +873,7 @@ func (n *Node) MustObject() map[string]*Node { // If the current node is not object type, it returns an error. func (n *Node) AppendObject(key string, value *Node) error { if !n.IsObject() { - return errors.New("can't append value to non-object node") + return errInvalidAppend } if err := n.append(&key, value); err != nil { @@ -1003,7 +1003,7 @@ func (n *Node) dropIndex(idx int) { // append is a helper function to append the given value to the current container type node. func (n *Node) append(key *string, val *Node) error { if n.isSameOrParentNode(val) { - return errors.New("can't append same or parent node") + return errInvalidAppendCycle } if val.prev != nil { diff --git a/examples/gno.land/p/demo/json/parser.gno b/examples/gno.land/p/demo/json/parser.gno index 9a2c3a8c817..bae06cb3789 100644 --- a/examples/gno.land/p/demo/json/parser.gno +++ b/examples/gno.land/p/demo/json/parser.gno @@ -2,27 +2,22 @@ package json import ( "bytes" - "errors" - "strconv" - - el "gno.land/p/demo/json/eisel_lemire" ) const ( - absMinInt64 = 1 << 63 - maxInt64 = absMinInt64 - 1 - maxUint64 = 1<<64 - 1 + unescapeStackBufSize = 64 + absMinInt64 = 1 << 63 + maxInt64 = absMinInt64 - 1 + maxUint64 = 1<<64 - 1 ) -const unescapeStackBufSize = 64 - // PaseStringLiteral parses a string from the given byte slice. func ParseStringLiteral(data []byte) (string, error) { var buf [unescapeStackBufSize]byte bf, err := Unescape(data, buf[:]) if err != nil { - return "", errors.New("invalid string input found while parsing string value") + return "", errInvalidStringInput } return string(bf), nil @@ -36,150 +31,6 @@ func ParseBoolLiteral(data []byte) (bool, error) { case bytes.Equal(data, falseLiteral): return false, nil default: - return false, errors.New("JSON Error: malformed boolean value found while parsing boolean value") - } -} - -// PaseFloatLiteral parses a float64 from the given byte slice. -// -// It utilizes double-precision (64-bit) floating-point format as defined -// by the IEEE 754 standard, providing a decimal precision of approximately 15 digits. -func ParseFloatLiteral(bytes []byte) (float64, error) { - if len(bytes) == 0 { - return -1, errors.New("JSON Error: empty byte slice found while parsing float value") - } - - neg, bytes := trimNegativeSign(bytes) - - var exponentPart []byte - for i, c := range bytes { - if lower(c) == 'e' { - exponentPart = bytes[i+1:] - bytes = bytes[:i] - break - } - } - - man, exp10, err := extractMantissaAndExp10(bytes) - if err != nil { - return -1, err - } - - if len(exponentPart) > 0 { - exp, err := strconv.Atoi(string(exponentPart)) - if err != nil { - return -1, errors.New("JSON Error: invalid exponent value found while parsing float value") - } - exp10 += exp - } - - // for fast float64 conversion - f, success := el.EiselLemire64(man, exp10, neg) - if !success { - return 0, nil - } - - return f, nil -} - -func ParseIntLiteral(bytes []byte) (int64, error) { - if len(bytes) == 0 { - return 0, errors.New("JSON Error: empty byte slice found while parsing integer value") - } - - neg, bytes := trimNegativeSign(bytes) - - var n uint64 = 0 - for _, c := range bytes { - if notDigit(c) { - return 0, errors.New("JSON Error: non-digit characters found while parsing integer value") - } - - if n > maxUint64/10 { - return 0, errors.New("JSON Error: numeric value exceeds the range limit") - } - - n *= 10 - - n1 := n + uint64(c-'0') - if n1 < n { - return 0, errors.New("JSON Error: numeric value exceeds the range limit") - } - - n = n1 - } - - if n > maxInt64 { - if neg && n == absMinInt64 { - return -absMinInt64, nil - } - - return 0, errors.New("JSON Error: numeric value exceeds the range limit") + return false, errMalformedBooleanValue } - - if neg { - return -int64(n), nil - } - - return int64(n), nil -} - -// extractMantissaAndExp10 parses a byte slice representing a decimal number and extracts the mantissa and the exponent of its base-10 representation. -// It iterates through the bytes, constructing the mantissa by treating each byte as a digit. -// If a decimal point is encountered, the function keeps track of the position of the decimal point to calculate the exponent. -// The function ensures that: -// - The number contains at most one decimal point. -// - All characters in the byte slice are digits or a single decimal point. -// - The resulting mantissa does not overflow a uint64. -func extractMantissaAndExp10(bytes []byte) (uint64, int, error) { - var ( - man uint64 - exp10 int - decimalFound bool - ) - - for _, c := range bytes { - if c == dot { - if decimalFound { - return 0, 0, errors.New("JSON Error: multiple decimal points found while parsing float value") - } - decimalFound = true - continue - } - - if notDigit(c) { - return 0, 0, errors.New("JSON Error: non-digit characters found while parsing integer value") - } - - digit := uint64(c - '0') - - if man > (maxUint64-digit)/10 { - return 0, 0, errors.New("JSON Error: numeric value exceeds the range limit") - } - - man = man*10 + digit - - if decimalFound { - exp10-- - } - } - - return man, exp10, nil -} - -func trimNegativeSign(bytes []byte) (bool, []byte) { - if bytes[0] == minus { - return true, bytes[1:] - } - - return false, bytes -} - -func notDigit(c byte) bool { - return (c & 0xF0) != 0x30 -} - -// lower converts a byte to lower case if it is an uppercase letter. -func lower(c byte) byte { - return c | 0x20 } diff --git a/examples/gno.land/p/demo/json/parser_test.gno b/examples/gno.land/p/demo/json/parser_test.gno index 078aa048a61..a05e313f67b 100644 --- a/examples/gno.land/p/demo/json/parser_test.gno +++ b/examples/gno.land/p/demo/json/parser_test.gno @@ -64,125 +64,3 @@ func TestParseBoolLiteral(t *testing.T) { } } } - -func TestParseFloatLiteral(t *testing.T) { - tests := []struct { - input string - expected float64 - }{ - {"123", 123}, - {"-123", -123}, - {"123.456", 123.456}, - {"-123.456", -123.456}, - {"12345678.1234567890", 12345678.1234567890}, - {"-12345678.09123456789", -12345678.09123456789}, - {"0.123", 0.123}, - {"-0.123", -0.123}, - {"", -1}, - {"abc", -1}, - {"123.45.6", -1}, - {"999999999999999999999", -1}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - got, _ := ParseFloatLiteral([]byte(tt.input)) - if got != tt.expected { - t.Errorf("ParseFloatLiteral(%s): got %v, want %v", tt.input, got, tt.expected) - } - }) - } -} - -func TestParseFloatWithScientificNotation(t *testing.T) { - tests := []struct { - input string - expected float64 - }{ - {"1e6", 1000000}, - {"1E6", 1000000}, - {"1.23e10", 1.23e10}, - {"1.23E10", 1.23e10}, - {"-1.23e10", -1.23e10}, - {"-1.23E10", -1.23e10}, - {"2.45e-8", 2.45e-8}, - {"2.45E-8", 2.45e-8}, - {"-2.45e-8", -2.45e-8}, - {"-2.45E-8", -2.45e-8}, - {"5e0", 5}, - {"-5e0", -5}, - {"5E+0", 5}, - {"5e+1", 50}, - {"1e-1", 0.1}, - {"1E-1", 0.1}, - {"-1e-1", -0.1}, - {"-1E-1", -0.1}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - got, err := ParseFloatLiteral([]byte(tt.input)) - if got != tt.expected { - t.Errorf("ParseFloatLiteral(%s): got %v, want %v", tt.input, got, tt.expected) - } - - if err != nil { - t.Errorf("ParseFloatLiteral(%s): got error %v", tt.input, err) - } - }) - } -} - -func TestParseFloat_May_Interoperability_Problem(t *testing.T) { - tests := []struct { - input string - shouldErr bool - }{ - {"3.141592653589793238462643383279", true}, - {"1E400", false}, // TODO: should error - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - _, err := ParseFloatLiteral([]byte(tt.input)) - if tt.shouldErr && err == nil { - t.Errorf("ParseFloatLiteral(%s): expected error, but not error", tt.input) - } - }) - } -} - -func TestParseIntLiteral(t *testing.T) { - tests := []struct { - input string - expected int64 - }{ - {"0", 0}, - {"1", 1}, - {"-1", -1}, - {"12345", 12345}, - {"-12345", -12345}, - {"9223372036854775807", 9223372036854775807}, - {"-9223372036854775808", -9223372036854775808}, - {"-92233720368547758081", 0}, - {"18446744073709551616", 0}, - {"9223372036854775808", 0}, - {"-9223372036854775809", 0}, - {"", 0}, - {"abc", 0}, - {"12345x", 0}, - {"123e5", 0}, - {"9223372036854775807x", 0}, - {"27670116110564327410", 0}, - {"-27670116110564327410", 0}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - got, _ := ParseIntLiteral([]byte(tt.input)) - if got != tt.expected { - t.Errorf("ParseIntLiteral(%s): got %v, want %v", tt.input, got, tt.expected) - } - }) - } -} diff --git a/examples/gno.land/p/demo/json/ryu/License b/examples/gno.land/p/demo/json/ryu/License deleted file mode 100644 index 55beeadce54..00000000000 --- a/examples/gno.land/p/demo/json/ryu/License +++ /dev/null @@ -1,21 +0,0 @@ -# Apache License - -Copyright 2018 Ulf Adams -Modifications copyright 2019 Caleb Spare - -The contents of this file may be used under the terms of the Apache License, -Version 2.0. - - (See accompanying file LICENSE or copy at - ) - -Unless required by applicable law or agreed to in writing, this software -is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -KIND, either express or implied. - -The code in this file is part of a Go translation of the C code originally written by -Ulf Adams, which can be found at . The original source -code is licensed under the Apache License 2.0. This code is a derivative work thereof, -adapted and modified to meet the specifications of the Gno language project. - -Please note that the modifications are also under the Apache License 2.0 unless otherwise specified. diff --git a/examples/gno.land/p/demo/json/ryu/floatconv.gno b/examples/gno.land/p/demo/json/ryu/floatconv.gno deleted file mode 100644 index 617141d2734..00000000000 --- a/examples/gno.land/p/demo/json/ryu/floatconv.gno +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2018 Ulf Adams -// Modifications copyright 2019 Caleb Spare -// -// The contents of this file may be used under the terms of the Apache License, -// Version 2.0. -// -// (See accompanying file LICENSE or copy at -// http://www.apache.org/licenses/LICENSE-2.0) -// -// Unless required by applicable law or agreed to in writing, this software -// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. -// -// The code in this file is part of a Go translation of the C code originally written by -// Ulf Adams, which can be found at https://github.com/ulfjack/ryu. The original source -// code is licensed under the Apache License 2.0. This code is a derivative work thereof, -// adapted and modified to meet the specifications of the Gno language project. -// -// original Go implementation can be found at https://github.com/cespare/ryu. -// -// Please note that the modifications are also under the Apache License 2.0 unless -// otherwise specified. - -// Package ryu implements the Ryu algorithm for quickly converting floating -// point numbers into strings. -package ryu - -import ( - "math" -) - -const ( - mantBits32 = 23 - expBits32 = 8 - bias32 = 127 - - mantBits64 = 52 - expBits64 = 11 - bias64 = 1023 -) - -// FormatFloat64 converts a 64-bit floating point number f to a string. -// It behaves like strconv.FormatFloat(f, 'e', -1, 64). -func FormatFloat64(f float64) string { - b := make([]byte, 0, 24) - b = AppendFloat64(b, f) - return string(b) -} - -// AppendFloat64 appends the string form of the 64-bit floating point number f, -// as generated by FormatFloat64, to b and returns the extended buffer. -func AppendFloat64(b []byte, f float64) []byte { - // Step 1: Decode the floating-point number. - // Unify normalized and subnormal cases. - u := math.Float64bits(f) - neg := u>>(mantBits64+expBits64) != 0 - mant := u & (uint64(1)<> mantBits64) & (uint64(1)<= 0, "e >= 0") - assert(e <= 1650, "e <= 1650") - return (uint32(e) * 78913) >> 18 -} - -// log10Pow5 returns floor(log_10(5^e)). -func log10Pow5(e int32) uint32 { - // The first value this approximation fails for is 5^2621 - // which is just greater than 10^1832. - assert(e >= 0, "e >= 0") - assert(e <= 2620, "e <= 2620") - return (uint32(e) * 732923) >> 20 -} - -// pow5Bits returns ceil(log_2(5^e)), or else 1 if e==0. -func pow5Bits(e int32) int32 { - // This approximation works up to the point that the multiplication - // overflows at e = 3529. If the multiplication were done in 64 bits, - // it would fail at 5^4004 which is just greater than 2^9297. - assert(e >= 0, "e >= 0") - assert(e <= 3528, "e <= 3528") - return int32((uint32(e)*1217359)>>19 + 1) -} diff --git a/examples/gno.land/p/demo/json/ryu/floatconv_test.gno b/examples/gno.land/p/demo/json/ryu/floatconv_test.gno deleted file mode 100644 index 7f01d4034f7..00000000000 --- a/examples/gno.land/p/demo/json/ryu/floatconv_test.gno +++ /dev/null @@ -1,33 +0,0 @@ -package ryu - -import ( - "math" - "testing" -) - -func TestFormatFloat64(t *testing.T) { - tests := []struct { - name string - value float64 - expected string - }{ - {"positive infinity", math.Inf(1), "+Inf"}, - {"negative infinity", math.Inf(-1), "-Inf"}, - {"NaN", math.NaN(), "NaN"}, - {"zero", 0.0, "0e+00"}, - {"negative zero", -0.0, "0e+00"}, - {"positive number", 3.14159, "3.14159e+00"}, - {"negative number", -2.71828, "-2.71828e+00"}, - {"very small number", 1.23e-20, "1.23e-20"}, - {"very large number", 1.23e+20, "1.23e+20"}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - result := FormatFloat64(test.value) - if result != test.expected { - t.Errorf("FormatFloat64(%v) = %q, expected %q", test.value, result, test.expected) - } - }) - } -} diff --git a/examples/gno.land/p/demo/json/ryu/gno.mod b/examples/gno.land/p/demo/json/ryu/gno.mod deleted file mode 100644 index 86a1988b052..00000000000 --- a/examples/gno.land/p/demo/json/ryu/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/p/demo/json/ryu diff --git a/examples/gno.land/p/demo/json/ryu/ryu64.gno b/examples/gno.land/p/demo/json/ryu/ryu64.gno deleted file mode 100644 index 249e3d0f526..00000000000 --- a/examples/gno.land/p/demo/json/ryu/ryu64.gno +++ /dev/null @@ -1,344 +0,0 @@ -package ryu - -import ( - "math/bits" -) - -type uint128 struct { - lo uint64 - hi uint64 -} - -// dec64 is a floating decimal type representing m * 10^e. -type dec64 struct { - m uint64 - e int32 -} - -func (d dec64) append(b []byte, neg bool) []byte { - // Step 5: Print the decimal representation. - if neg { - b = append(b, '-') - } - - out := d.m - outLen := decimalLen64(out) - bufLen := outLen - if bufLen > 1 { - bufLen++ // extra space for '.' - } - - // Print the decimal digits. - n := len(b) - if cap(b)-len(b) >= bufLen { - // Avoid function call in the common case. - b = b[:len(b)+bufLen] - } else { - b = append(b, make([]byte, bufLen)...) - } - - // Avoid expensive 64-bit divisions. - // We have at most 17 digits, and uint32 can store 9 digits. - // If the output doesn't fit into a uint32, cut off 8 digits - // so the rest will fit into a uint32. - var i int - if out>>32 > 0 { - var out32 uint32 - out, out32 = out/1e8, uint32(out%1e8) - for ; i < 8; i++ { - b[n+outLen-i] = '0' + byte(out32%10) - out32 /= 10 - } - } - out32 := uint32(out) - for ; i < outLen-1; i++ { - b[n+outLen-i] = '0' + byte(out32%10) - out32 /= 10 - } - b[n] = '0' + byte(out32%10) - - // Print the '.' if needed. - if outLen > 1 { - b[n+1] = '.' - } - - // Print the exponent. - b = append(b, 'e') - exp := d.e + int32(outLen) - 1 - if exp < 0 { - b = append(b, '-') - exp = -exp - } else { - // Unconditionally print a + here to match strconv's formatting. - b = append(b, '+') - } - // Always print at least two digits to match strconv's formatting. - d2 := exp % 10 - exp /= 10 - d1 := exp % 10 - d0 := exp / 10 - if d0 > 0 { - b = append(b, '0'+byte(d0)) - } - b = append(b, '0'+byte(d1), '0'+byte(d2)) - - return b -} - -func float64ToDecimalExactInt(mant, exp uint64) (d dec64, ok bool) { - e := exp - bias64 - if e > mantBits64 { - return d, false - } - shift := mantBits64 - e - mant |= 1 << mantBits64 // implicit 1 - d.m = mant >> shift - if d.m<= 0 { - // This expression is slightly faster than max(0, log10Pow2(e2) - 1). - q := log10Pow2(e2) - boolToUint32(e2 > 3) - e10 = int32(q) - k := pow5InvNumBits64 + pow5Bits(int32(q)) - 1 - i := -e2 + int32(q) + k - mul := pow5InvSplit64[q] - vr = mulShift64(4*m2, mul, i) - vp = mulShift64(4*m2+2, mul, i) - vm = mulShift64(4*m2-1-mmShift, mul, i) - if q <= 21 { - // This should use q <= 22, but I think 21 is also safe. - // Smaller values may still be safe, but it's more - // difficult to reason about them. Only one of mp, mv, - // and mm can be a multiple of 5, if any. - if mv%5 == 0 { - vrIsTrailingZeros = multipleOfPowerOfFive64(mv, q) - } else if acceptBounds { - // Same as min(e2 + (^mm & 1), pow5Factor64(mm)) >= q - // <=> e2 + (^mm & 1) >= q && pow5Factor64(mm) >= q - // <=> true && pow5Factor64(mm) >= q, since e2 >= q. - vmIsTrailingZeros = multipleOfPowerOfFive64(mv-1-mmShift, q) - } else if multipleOfPowerOfFive64(mv+2, q) { - vp-- - } - } - } else { - // This expression is slightly faster than max(0, log10Pow5(-e2) - 1). - q := log10Pow5(-e2) - boolToUint32(-e2 > 1) - e10 = int32(q) + e2 - i := -e2 - int32(q) - k := pow5Bits(i) - pow5NumBits64 - j := int32(q) - k - mul := pow5Split64[i] - vr = mulShift64(4*m2, mul, j) - vp = mulShift64(4*m2+2, mul, j) - vm = mulShift64(4*m2-1-mmShift, mul, j) - if q <= 1 { - // {vr,vp,vm} is trailing zeros if {mv,mp,mm} has at least q trailing 0 bits. - // mv = 4 * m2, so it always has at least two trailing 0 bits. - vrIsTrailingZeros = true - if acceptBounds { - // mm = mv - 1 - mmShift, so it has 1 trailing 0 bit iff mmShift == 1. - vmIsTrailingZeros = mmShift == 1 - } else { - // mp = mv + 2, so it always has at least one trailing 0 bit. - vp-- - } - } else if q < 63 { // TODO(ulfjack/cespare): Use a tighter bound here. - // We need to compute min(ntz(mv), pow5Factor64(mv) - e2) >= q - 1 - // <=> ntz(mv) >= q - 1 && pow5Factor64(mv) - e2 >= q - 1 - // <=> ntz(mv) >= q - 1 (e2 is negative and -e2 >= q) - // <=> (mv & ((1 << (q - 1)) - 1)) == 0 - // We also need to make sure that the left shift does not overflow. - vrIsTrailingZeros = multipleOfPowerOfTwo64(mv, q-1) - } - } - - // Step 4: Find the shortest decimal representation - // in the interval of valid representations. - var removed int32 - var lastRemovedDigit uint8 - var out uint64 - // On average, we remove ~2 digits. - if vmIsTrailingZeros || vrIsTrailingZeros { - // General case, which happens rarely (~0.7%). - for { - vpDiv10 := vp / 10 - vmDiv10 := vm / 10 - if vpDiv10 <= vmDiv10 { - break - } - vmMod10 := vm % 10 - vrDiv10 := vr / 10 - vrMod10 := vr % 10 - vmIsTrailingZeros = vmIsTrailingZeros && vmMod10 == 0 - vrIsTrailingZeros = vrIsTrailingZeros && lastRemovedDigit == 0 - lastRemovedDigit = uint8(vrMod10) - vr = vrDiv10 - vp = vpDiv10 - vm = vmDiv10 - removed++ - } - if vmIsTrailingZeros { - for { - vmDiv10 := vm / 10 - vmMod10 := vm % 10 - if vmMod10 != 0 { - break - } - vpDiv10 := vp / 10 - vrDiv10 := vr / 10 - vrMod10 := vr % 10 - vrIsTrailingZeros = vrIsTrailingZeros && lastRemovedDigit == 0 - lastRemovedDigit = uint8(vrMod10) - vr = vrDiv10 - vp = vpDiv10 - vm = vmDiv10 - removed++ - } - } - if vrIsTrailingZeros && lastRemovedDigit == 5 && vr%2 == 0 { - // Round even if the exact number is .....50..0. - lastRemovedDigit = 4 - } - out = vr - // We need to take vr + 1 if vr is outside bounds - // or we need to round up. - if (vr == vm && (!acceptBounds || !vmIsTrailingZeros)) || lastRemovedDigit >= 5 { - out++ - } - } else { - // Specialized for the common case (~99.3%). - // Percentages below are relative to this. - roundUp := false - for vp/100 > vm/100 { - // Optimization: remove two digits at a time (~86.2%). - roundUp = vr%100 >= 50 - vr /= 100 - vp /= 100 - vm /= 100 - removed += 2 - } - // Loop iterations below (approximately), without optimization above: - // 0: 0.03%, 1: 13.8%, 2: 70.6%, 3: 14.0%, 4: 1.40%, 5: 0.14%, 6+: 0.02% - // Loop iterations below (approximately), with optimization above: - // 0: 70.6%, 1: 27.8%, 2: 1.40%, 3: 0.14%, 4+: 0.02% - for vp/10 > vm/10 { - roundUp = vr%10 >= 5 - vr /= 10 - vp /= 10 - vm /= 10 - removed++ - } - // We need to take vr + 1 if vr is outside bounds - // or we need to round up. - out = vr + boolToUint64(vr == vm || roundUp) - } - - return dec64{m: out, e: e10 + removed} -} - -var powersOf10 = [...]uint64{ - 1e0, - 1e1, - 1e2, - 1e3, - 1e4, - 1e5, - 1e6, - 1e7, - 1e8, - 1e9, - 1e10, - 1e11, - 1e12, - 1e13, - 1e14, - 1e15, - 1e16, - 1e17, - // We only need to find the length of at most 17 digit numbers. -} - -func decimalLen64(u uint64) int { - // http://graphics.stanford.edu/~seander/bithacks.html#IntegerLog10 - log2 := 64 - bits.LeadingZeros64(u) - 1 - t := (log2 + 1) * 1233 >> 12 - return t - boolToInt(u < powersOf10[t]) + 1 -} - -func mulShift64(m uint64, mul uint128, shift int32) uint64 { - hihi, hilo := bits.Mul64(m, mul.hi) - lohi, _ := bits.Mul64(m, mul.lo) - sum := uint128{hi: hihi, lo: lohi + hilo} - if sum.lo < lohi { - sum.hi++ // overflow - } - return shiftRight128(sum, shift-64) -} - -func shiftRight128(v uint128, shift int32) uint64 { - // The shift value is always modulo 64. - // In the current implementation of the 64-bit version - // of Ryu, the shift value is always < 64. - // (It is in the range [2, 59].) - // Check this here in case a future change requires larger shift - // values. In this case this function needs to be adjusted. - assert(shift < 64, "shift < 64") - return (v.hi << uint64(64-shift)) | (v.lo >> uint(shift)) -} - -func pow5Factor64(v uint64) uint32 { - for n := uint32(0); ; n++ { - q, r := v/5, v%5 - if r != 0 { - return n - } - v = q - } -} - -func multipleOfPowerOfFive64(v uint64, p uint32) bool { - return pow5Factor64(v) >= p -} - -func multipleOfPowerOfTwo64(v uint64, p uint32) bool { - return uint32(bits.TrailingZeros64(v)) >= p -} diff --git a/examples/gno.land/p/demo/json/ryu/table.gno b/examples/gno.land/p/demo/json/ryu/table.gno deleted file mode 100644 index fe33ad90a57..00000000000 --- a/examples/gno.land/p/demo/json/ryu/table.gno +++ /dev/null @@ -1,678 +0,0 @@ -// Code generated by running "go generate". DO NOT EDIT. - -// Copyright 2018 Ulf Adams -// Modifications copyright 2019 Caleb Spare -// -// The contents of this file may be used under the terms of the Apache License, -// Version 2.0. -// -// (See accompanying file LICENSE or copy at -// http://www.apache.org/licenses/LICENSE-2.0) -// -// Unless required by applicable law or agreed to in writing, this software -// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. -// -// The code in this file is part of a Go translation of the C code written by -// Ulf Adams which may be found at https://github.com/ulfjack/ryu. That source -// code is licensed under Apache 2.0 and this code is derivative work thereof. - -package ryu - -const pow5NumBits32 = 61 - -var pow5Split32 = [...]uint64{ - 1152921504606846976, 1441151880758558720, 1801439850948198400, 2251799813685248000, - 1407374883553280000, 1759218604441600000, 2199023255552000000, 1374389534720000000, - 1717986918400000000, 2147483648000000000, 1342177280000000000, 1677721600000000000, - 2097152000000000000, 1310720000000000000, 1638400000000000000, 2048000000000000000, - 1280000000000000000, 1600000000000000000, 2000000000000000000, 1250000000000000000, - 1562500000000000000, 1953125000000000000, 1220703125000000000, 1525878906250000000, - 1907348632812500000, 1192092895507812500, 1490116119384765625, 1862645149230957031, - 1164153218269348144, 1455191522836685180, 1818989403545856475, 2273736754432320594, - 1421085471520200371, 1776356839400250464, 2220446049250313080, 1387778780781445675, - 1734723475976807094, 2168404344971008868, 1355252715606880542, 1694065894508600678, - 2117582368135750847, 1323488980084844279, 1654361225106055349, 2067951531382569187, - 1292469707114105741, 1615587133892632177, 2019483917365790221, -} - -const pow5InvNumBits32 = 59 - -var pow5InvSplit32 = [...]uint64{ - 576460752303423489, 461168601842738791, 368934881474191033, 295147905179352826, - 472236648286964522, 377789318629571618, 302231454903657294, 483570327845851670, - 386856262276681336, 309485009821345069, 495176015714152110, 396140812571321688, - 316912650057057351, 507060240091291761, 405648192073033409, 324518553658426727, - 519229685853482763, 415383748682786211, 332306998946228969, 531691198313966350, - 425352958651173080, 340282366920938464, 544451787073501542, 435561429658801234, - 348449143727040987, 557518629963265579, 446014903970612463, 356811923176489971, - 570899077082383953, 456719261665907162, 365375409332725730, -} - -const pow5NumBits64 = 121 - -var pow5Split64 = [...]uint128{ - {0, 72057594037927936}, - {0, 90071992547409920}, - {0, 112589990684262400}, - {0, 140737488355328000}, - {0, 87960930222080000}, - {0, 109951162777600000}, - {0, 137438953472000000}, - {0, 85899345920000000}, - {0, 107374182400000000}, - {0, 134217728000000000}, - {0, 83886080000000000}, - {0, 104857600000000000}, - {0, 131072000000000000}, - {0, 81920000000000000}, - {0, 102400000000000000}, - {0, 128000000000000000}, - {0, 80000000000000000}, - {0, 100000000000000000}, - {0, 125000000000000000}, - {0, 78125000000000000}, - {0, 97656250000000000}, - {0, 122070312500000000}, - {0, 76293945312500000}, - {0, 95367431640625000}, - {0, 119209289550781250}, - {4611686018427387904, 74505805969238281}, - {10376293541461622784, 93132257461547851}, - {8358680908399640576, 116415321826934814}, - {612489549322387456, 72759576141834259}, - {14600669991935148032, 90949470177292823}, - {13639151471491547136, 113686837721616029}, - {3213881284082270208, 142108547152020037}, - {4314518811765112832, 88817841970012523}, - {781462496279003136, 111022302462515654}, - {10200200157203529728, 138777878078144567}, - {13292654125893287936, 86736173798840354}, - {7392445620511834112, 108420217248550443}, - {4628871007212404736, 135525271560688054}, - {16728102434789916672, 84703294725430033}, - {7075069988205232128, 105879118406787542}, - {18067209522111315968, 132348898008484427}, - {8986162942105878528, 82718061255302767}, - {6621017659204960256, 103397576569128459}, - {3664586055578812416, 129246970711410574}, - {16125424340018921472, 80779356694631608}, - {1710036351314100224, 100974195868289511}, - {15972603494424788992, 126217744835361888}, - {9982877184015493120, 78886090522101180}, - {12478596480019366400, 98607613152626475}, - {10986559581596820096, 123259516440783094}, - {2254913720070624656, 77037197775489434}, - {12042014186943056628, 96296497219361792}, - {15052517733678820785, 120370621524202240}, - {9407823583549262990, 75231638452626400}, - {11759779479436578738, 94039548065783000}, - {14699724349295723422, 117549435082228750}, - {4575641699882439235, 73468396926392969}, - {10331238143280436948, 91835496157991211}, - {8302361660673158281, 114794370197489014}, - {1154580038986672043, 143492962746861268}, - {9944984561221445835, 89683101716788292}, - {12431230701526807293, 112103877145985365}, - {1703980321626345405, 140129846432481707}, - {17205888765512323542, 87581154020301066}, - {12283988920035628619, 109476442525376333}, - {1519928094762372062, 136845553156720417}, - {12479170105294952299, 85528470722950260}, - {15598962631618690374, 106910588403687825}, - {5663645234241199255, 133638235504609782}, - {17374836326682913246, 83523897190381113}, - {7883487353071477846, 104404871487976392}, - {9854359191339347308, 130506089359970490}, - {10770660513014479971, 81566305849981556}, - {13463325641268099964, 101957882312476945}, - {2994098996302961243, 127447352890596182}, - {15706369927971514489, 79654595556622613}, - {5797904354682229399, 99568244445778267}, - {2635694424925398845, 124460305557222834}, - {6258995034005762182, 77787690973264271}, - {3212057774079814824, 97234613716580339}, - {17850130272881932242, 121543267145725423}, - {18073860448192289507, 75964541966078389}, - {8757267504958198172, 94955677457597987}, - {6334898362770359811, 118694596821997484}, - {13182683513586250689, 74184123013748427}, - {11866668373555425458, 92730153767185534}, - {5609963430089506015, 115912692208981918}, - {17341285199088104971, 72445432630613698}, - {12453234462005355406, 90556790788267123}, - {10954857059079306353, 113195988485333904}, - {13693571323849132942, 141494985606667380}, - {17781854114260483896, 88434366004167112}, - {3780573569116053255, 110542957505208891}, - {114030942967678664, 138178696881511114}, - {4682955357782187069, 86361685550944446}, - {15077066234082509644, 107952106938680557}, - {5011274737320973344, 134940133673350697}, - {14661261756894078100, 84337583545844185}, - {4491519140835433913, 105421979432305232}, - {5614398926044292391, 131777474290381540}, - {12732371365632458552, 82360921431488462}, - {6692092170185797382, 102951151789360578}, - {17588487249587022536, 128688939736700722}, - {15604490549419276989, 80430587335437951}, - {14893927168346708332, 100538234169297439}, - {14005722942005997511, 125672792711621799}, - {15671105866394830300, 78545495444763624}, - {1142138259283986260, 98181869305954531}, - {15262730879387146537, 122727336632443163}, - {7233363790403272633, 76704585395276977}, - {13653390756431478696, 95880731744096221}, - {3231680390257184658, 119850914680120277}, - {4325643253124434363, 74906821675075173}, - {10018740084832930858, 93633527093843966}, - {3300053069186387764, 117041908867304958}, - {15897591223523656064, 73151193042065598}, - {10648616992549794273, 91438991302581998}, - {4087399203832467033, 114298739128227498}, - {14332621041645359599, 142873423910284372}, - {18181260187883125557, 89295889943927732}, - {4279831161144355331, 111619862429909666}, - {14573160988285219972, 139524828037387082}, - {13719911636105650386, 87203017523366926}, - {7926517508277287175, 109003771904208658}, - {684774848491833161, 136254714880260823}, - {7345513307948477581, 85159196800163014}, - {18405263671790372785, 106448996000203767}, - {18394893571310578077, 133061245000254709}, - {13802651491282805250, 83163278125159193}, - {3418256308821342851, 103954097656448992}, - {4272820386026678563, 129942622070561240}, - {2670512741266674102, 81214138794100775}, - {17173198981865506339, 101517673492625968}, - {3019754653622331308, 126897091865782461}, - {4193189667727651020, 79310682416114038}, - {14464859121514339583, 99138353020142547}, - {13469387883465536574, 123922941275178184}, - {8418367427165960359, 77451838296986365}, - {15134645302384838353, 96814797871232956}, - {471562554271496325, 121018497339041196}, - {9518098633274461011, 75636560836900747}, - {7285937273165688360, 94545701046125934}, - {18330793628311886258, 118182126307657417}, - {4539216990053847055, 73863828942285886}, - {14897393274422084627, 92329786177857357}, - {4786683537745442072, 115412232722321697}, - {14520892257159371055, 72132645451451060}, - {18151115321449213818, 90165806814313825}, - {8853836096529353561, 112707258517892282}, - {1843923083806916143, 140884073147365353}, - {12681666973447792349, 88052545717103345}, - {2017025661527576725, 110065682146379182}, - {11744654113764246714, 137582102682973977}, - {422879793461572340, 85988814176858736}, - {528599741826965425, 107486017721073420}, - {660749677283706782, 134357522151341775}, - {7330497575943398595, 83973451344588609}, - {13774807988356636147, 104966814180735761}, - {3383451930163631472, 131208517725919702}, - {15949715511634433382, 82005323578699813}, - {6102086334260878016, 102506654473374767}, - {3015921899398709616, 128133318091718459}, - {18025852251620051174, 80083323807324036}, - {4085571240815512351, 100104154759155046}, - {14330336087874166247, 125130193448943807}, - {15873989082562435760, 78206370905589879}, - {15230800334775656796, 97757963631987349}, - {5203442363187407284, 122197454539984187}, - {946308467778435600, 76373409087490117}, - {5794571603150432404, 95466761359362646}, - {16466586540792816313, 119333451699203307}, - {7985773578781816244, 74583407312002067}, - {5370530955049882401, 93229259140002584}, - {6713163693812353001, 116536573925003230}, - {18030785363914884337, 72835358703127018}, - {13315109668038829614, 91044198378908773}, - {2808829029766373305, 113805247973635967}, - {17346094342490130344, 142256559967044958}, - {6229622945628943561, 88910349979403099}, - {3175342663608791547, 111137937474253874}, - {13192550366365765242, 138922421842817342}, - {3633657960551215372, 86826513651760839}, - {18377130505971182927, 108533142064701048}, - {4524669058754427043, 135666427580876311}, - {9745447189362598758, 84791517238047694}, - {2958436949848472639, 105989396547559618}, - {12921418224165366607, 132486745684449522}, - {12687572408530742033, 82804216052780951}, - {11247779492236039638, 103505270065976189}, - {224666310012885835, 129381587582470237}, - {2446259452971747599, 80863492239043898}, - {12281196353069460307, 101079365298804872}, - {15351495441336825384, 126349206623506090}, - {14206370669262903769, 78968254139691306}, - {8534591299723853903, 98710317674614133}, - {15279925143082205283, 123387897093267666}, - {14161639232853766206, 77117435683292291}, - {13090363022639819853, 96396794604115364}, - {16362953778299774816, 120495993255144205}, - {12532689120651053212, 75309995784465128}, - {15665861400813816515, 94137494730581410}, - {10358954714162494836, 117671868413226763}, - {4168503687137865320, 73544917758266727}, - {598943590494943747, 91931147197833409}, - {5360365506546067587, 114913933997291761}, - {11312142901609972388, 143642417496614701}, - {9375932322719926695, 89776510935384188}, - {11719915403399908368, 112220638669230235}, - {10038208235822497557, 140275798336537794}, - {10885566165816448877, 87672373960336121}, - {18218643725697949000, 109590467450420151}, - {18161618638695048346, 136988084313025189}, - {13656854658398099168, 85617552695640743}, - {12459382304570236056, 107021940869550929}, - {1739169825430631358, 133777426086938662}, - {14922039196176308311, 83610891304336663}, - {14040862976792997485, 104513614130420829}, - {3716020665709083144, 130642017663026037}, - {4628355925281870917, 81651261039391273}, - {10397130925029726550, 102064076299239091}, - {8384727637859770284, 127580095374048864}, - {5240454773662356427, 79737559608780540}, - {6550568467077945534, 99671949510975675}, - {3576524565420044014, 124589936888719594}, - {6847013871814915412, 77868710555449746}, - {17782139376623420074, 97335888194312182}, - {13004302183924499284, 121669860242890228}, - {17351060901807587860, 76043662651806392}, - {3242082053549933210, 95054578314757991}, - {17887660622219580224, 118818222893447488}, - {11179787888887237640, 74261389308404680}, - {13974734861109047050, 92826736635505850}, - {8245046539531533005, 116033420794382313}, - {16682369133275677888, 72520887996488945}, - {7017903361312433648, 90651109995611182}, - {17995751238495317868, 113313887494513977}, - {8659630992836983623, 141642359368142472}, - {5412269370523114764, 88526474605089045}, - {11377022731581281359, 110658093256361306}, - {4997906377621825891, 138322616570451633}, - {14652906532082110942, 86451635356532270}, - {9092761128247862869, 108064544195665338}, - {2142579373455052779, 135080680244581673}, - {12868327154477877747, 84425425152863545}, - {2250350887815183471, 105531781441079432}, - {2812938609768979339, 131914726801349290}, - {6369772649532999991, 82446704250843306}, - {17185587848771025797, 103058380313554132}, - {3035240737254230630, 128822975391942666}, - {6508711479211282048, 80514359619964166}, - {17359261385868878368, 100642949524955207}, - {17087390713908710056, 125803686906194009}, - {3762090168551861929, 78627304316371256}, - {4702612710689827411, 98284130395464070}, - {15101637925217060072, 122855162994330087}, - {16356052730901744401, 76784476871456304}, - {1998321839917628885, 95980596089320381}, - {7109588318324424010, 119975745111650476}, - {13666864735807540814, 74984840694781547}, - {12471894901332038114, 93731050868476934}, - {6366496589810271835, 117163813585596168}, - {3979060368631419896, 73227383490997605}, - {9585511479216662775, 91534229363747006}, - {2758517312166052660, 114417786704683758}, - {12671518677062341634, 143022233380854697}, - {1002170145522881665, 89388895863034186}, - {10476084718758377889, 111736119828792732}, - {13095105898447972362, 139670149785990915}, - {5878598177316288774, 87293843616244322}, - {16571619758500136775, 109117304520305402}, - {11491152661270395161, 136396630650381753}, - {264441385652915120, 85247894156488596}, - {330551732066143900, 106559867695610745}, - {5024875683510067779, 133199834619513431}, - {10058076329834874218, 83249896637195894}, - {3349223375438816964, 104062370796494868}, - {4186529219298521205, 130077963495618585}, - {14145795808130045513, 81298727184761615}, - {13070558741735168987, 101623408980952019}, - {11726512408741573330, 127029261226190024}, - {7329070255463483331, 79393288266368765}, - {13773023837756742068, 99241610332960956}, - {17216279797195927585, 124052012916201195}, - {8454331864033760789, 77532508072625747}, - {5956228811614813082, 96915635090782184}, - {7445286014518516353, 121144543863477730}, - {9264989777501460624, 75715339914673581}, - {16192923240304213684, 94644174893341976}, - {1794409976670715490, 118305218616677471}, - {8039035263060279037, 73940761635423419}, - {5437108060397960892, 92425952044279274}, - {16019757112352226923, 115532440055349092}, - {788976158365366019, 72207775034593183}, - {14821278253238871236, 90259718793241478}, - {9303225779693813237, 112824648491551848}, - {11629032224617266546, 141030810614439810}, - {11879831158813179495, 88144256634024881}, - {1014730893234310657, 110180320792531102}, - {10491785653397664129, 137725400990663877}, - {8863209042587234033, 86078375619164923}, - {6467325284806654637, 107597969523956154}, - {17307528642863094104, 134497461904945192}, - {10817205401789433815, 84060913690590745}, - {18133192770664180173, 105076142113238431}, - {18054804944902837312, 131345177641548039}, - {18201782118205355176, 82090736025967524}, - {4305483574047142354, 102613420032459406}, - {14605226504413703751, 128266775040574257}, - {2210737537617482988, 80166734400358911}, - {16598479977304017447, 100208418000448638}, - {11524727934775246001, 125260522500560798}, - {2591268940807140847, 78287826562850499}, - {17074144231291089770, 97859783203563123}, - {16730994270686474309, 122324729004453904}, - {10456871419179046443, 76452955627783690}, - {3847717237119032246, 95566194534729613}, - {9421332564826178211, 119457743168412016}, - {5888332853016361382, 74661089480257510}, - {16583788103125227536, 93326361850321887}, - {16118049110479146516, 116657952312902359}, - {16991309721690548428, 72911220195563974}, - {12015765115258409727, 91139025244454968}, - {15019706394073012159, 113923781555568710}, - {9551260955736489391, 142404726944460888}, - {5969538097335305869, 89002954340288055}, - {2850236603241744433, 111253692925360069}, -} - -const pow5InvNumBits64 = 122 - -var pow5InvSplit64 = [...]uint128{ - {1, 288230376151711744}, - {3689348814741910324, 230584300921369395}, - {2951479051793528259, 184467440737095516}, - {17118578500402463900, 147573952589676412}, - {12632330341676300947, 236118324143482260}, - {10105864273341040758, 188894659314785808}, - {15463389048156653253, 151115727451828646}, - {17362724847566824558, 241785163922925834}, - {17579528692795369969, 193428131138340667}, - {6684925324752475329, 154742504910672534}, - {18074578149087781173, 247588007857076054}, - {18149011334012135262, 198070406285660843}, - {3451162622983977240, 158456325028528675}, - {5521860196774363583, 253530120045645880}, - {4417488157419490867, 202824096036516704}, - {7223339340677503017, 162259276829213363}, - {7867994130342094503, 259614842926741381}, - {2605046489531765280, 207691874341393105}, - {2084037191625412224, 166153499473114484}, - {10713157136084480204, 265845599156983174}, - {12259874523609494487, 212676479325586539}, - {13497248433629505913, 170141183460469231}, - {14216899864323388813, 272225893536750770}, - {11373519891458711051, 217780714829400616}, - {5409467098425058518, 174224571863520493}, - {4965798542738183305, 278759314981632789}, - {7661987648932456967, 223007451985306231}, - {2440241304404055250, 178405961588244985}, - {3904386087046488400, 285449538541191976}, - {17880904128604832013, 228359630832953580}, - {14304723302883865611, 182687704666362864}, - {15133127457049002812, 146150163733090291}, - {16834306301794583852, 233840261972944466}, - {9778096226693756759, 187072209578355573}, - {15201174610838826053, 149657767662684458}, - {2185786488890659746, 239452428260295134}, - {5437978005854438120, 191561942608236107}, - {15418428848909281466, 153249554086588885}, - {6222742084545298729, 245199286538542217}, - {16046240111861969953, 196159429230833773}, - {1768945645263844993, 156927543384667019}, - {10209010661905972635, 251084069415467230}, - {8167208529524778108, 200867255532373784}, - {10223115638361732810, 160693804425899027}, - {1599589762411131202, 257110087081438444}, - {4969020624670815285, 205688069665150755}, - {3975216499736652228, 164550455732120604}, - {13739044029062464211, 263280729171392966}, - {7301886408508061046, 210624583337114373}, - {13220206756290269483, 168499666669691498}, - {17462981995322520850, 269599466671506397}, - {6591687966774196033, 215679573337205118}, - {12652048002903177473, 172543658669764094}, - {9175230360419352987, 276069853871622551}, - {3650835473593572067, 220855883097298041}, - {17678063637842498946, 176684706477838432}, - {13527506561580357021, 282695530364541492}, - {3443307619780464970, 226156424291633194}, - {6443994910566282300, 180925139433306555}, - {5155195928453025840, 144740111546645244}, - {15627011115008661990, 231584178474632390}, - {12501608892006929592, 185267342779705912}, - {2622589484121723027, 148213874223764730}, - {4196143174594756843, 237142198758023568}, - {10735612169159626121, 189713759006418854}, - {12277838550069611220, 151771007205135083}, - {15955192865369467629, 242833611528216133}, - {1696107848069843133, 194266889222572907}, - {12424932722681605476, 155413511378058325}, - {1433148282581017146, 248661618204893321}, - {15903913885032455010, 198929294563914656}, - {9033782293284053685, 159143435651131725}, - {14454051669254485895, 254629497041810760}, - {11563241335403588716, 203703597633448608}, - {16629290697806691620, 162962878106758886}, - {781423413297334329, 260740604970814219}, - {4314487545379777786, 208592483976651375}, - {3451590036303822229, 166873987181321100}, - {5522544058086115566, 266998379490113760}, - {4418035246468892453, 213598703592091008}, - {10913125826658934609, 170878962873672806}, - {10082303693170474728, 273406340597876490}, - {8065842954536379782, 218725072478301192}, - {17520720807854834795, 174980057982640953}, - {5897060404116273733, 279968092772225526}, - {1028299508551108663, 223974474217780421}, - {15580034865808528224, 179179579374224336}, - {17549358155809824511, 286687326998758938}, - {2971440080422128639, 229349861599007151}, - {17134547323305344204, 183479889279205720}, - {13707637858644275364, 146783911423364576}, - {14553522944347019935, 234854258277383322}, - {4264120725993795302, 187883406621906658}, - {10789994210278856888, 150306725297525326}, - {9885293106962350374, 240490760476040522}, - {529536856086059653, 192392608380832418}, - {7802327114352668369, 153914086704665934}, - {1415676938738538420, 246262538727465495}, - {1132541550990830736, 197010030981972396}, - {15663428499760305882, 157608024785577916}, - {17682787970132668764, 252172839656924666}, - {10456881561364224688, 201738271725539733}, - {15744202878575200397, 161390617380431786}, - {17812026976236499989, 258224987808690858}, - {3181575136763469022, 206579990246952687}, - {13613306553636506187, 165263992197562149}, - {10713244041592678929, 264422387516099439}, - {12259944048016053467, 211537910012879551}, - {6118606423670932450, 169230328010303641}, - {2411072648389671274, 270768524816485826}, - {16686253377679378312, 216614819853188660}, - {13349002702143502650, 173291855882550928}, - {17669055508687693916, 277266969412081485}, - {14135244406950155133, 221813575529665188}, - {240149081334393137, 177450860423732151}, - {11452284974360759988, 283921376677971441}, - {5472479164746697667, 227137101342377153}, - {11756680961281178780, 181709681073901722}, - {2026647139541122378, 145367744859121378}, - {18000030682233437097, 232588391774594204}, - {18089373360528660001, 186070713419675363}, - {3403452244197197031, 148856570735740291}, - {16513570034941246220, 238170513177184465}, - {13210856027952996976, 190536410541747572}, - {3189987192878576934, 152429128433398058}, - {1414630693863812771, 243886605493436893}, - {8510402184574870864, 195109284394749514}, - {10497670562401807014, 156087427515799611}, - {9417575270359070576, 249739884025279378}, - {14912757845771077107, 199791907220223502}, - {4551508647133041040, 159833525776178802}, - {10971762650154775986, 255733641241886083}, - {16156107749607641435, 204586912993508866}, - {9235537384944202825, 163669530394807093}, - {11087511001168814197, 261871248631691349}, - {12559357615676961681, 209496998905353079}, - {13736834907283479668, 167597599124282463}, - {18289587036911657145, 268156158598851941}, - {10942320814787415393, 214524926879081553}, - {16132554281313752961, 171619941503265242}, - {11054691591134363444, 274591906405224388}, - {16222450902391311402, 219673525124179510}, - {12977960721913049122, 175738820099343608}, - {17075388340318968271, 281182112158949773}, - {2592264228029443648, 224945689727159819}, - {5763160197165465241, 179956551781727855}, - {9221056315464744386, 287930482850764568}, - {14755542681855616155, 230344386280611654}, - {15493782960226403247, 184275509024489323}, - {1326979923955391628, 147420407219591459}, - {9501865507812447252, 235872651551346334}, - {11290841220991868125, 188698121241077067}, - {1653975347309673853, 150958496992861654}, - {10025058185179298811, 241533595188578646}, - {4330697733401528726, 193226876150862917}, - {14532604630946953951, 154581500920690333}, - {1116074521063664381, 247330401473104534}, - {4582208431592841828, 197864321178483627}, - {14733813189500004432, 158291456942786901}, - {16195403473716186445, 253266331108459042}, - {5577625149489128510, 202613064886767234}, - {8151448934333213131, 162090451909413787}, - {16731667109675051333, 259344723055062059}, - {17074682502481951390, 207475778444049647}, - {6281048372501740465, 165980622755239718}, - {6360328581260874421, 265568996408383549}, - {8777611679750609860, 212455197126706839}, - {10711438158542398211, 169964157701365471}, - {9759603424184016492, 271942652322184754}, - {11497031554089123517, 217554121857747803}, - {16576322872755119460, 174043297486198242}, - {11764721337440549842, 278469275977917188}, - {16790474699436260520, 222775420782333750}, - {13432379759549008416, 178220336625867000}, - {3045063541568861850, 285152538601387201}, - {17193446092222730773, 228122030881109760}, - {13754756873778184618, 182497624704887808}, - {18382503128506368341, 145998099763910246}, - {3586563302416817083, 233596959622256395}, - {2869250641933453667, 186877567697805116}, - {17052795772514404226, 149502054158244092}, - {12527077977055405469, 239203286653190548}, - {17400360011128145022, 191362629322552438}, - {2852241564676785048, 153090103458041951}, - {15631632947708587046, 244944165532867121}, - {8815957543424959314, 195955332426293697}, - {18120812478965698421, 156764265941034957}, - {14235904707377476180, 250822825505655932}, - {4010026136418160298, 200658260404524746}, - {17965416168102169531, 160526608323619796}, - {2919224165770098987, 256842573317791675}, - {2335379332616079190, 205474058654233340}, - {1868303466092863352, 164379246923386672}, - {6678634360490491686, 263006795077418675}, - {5342907488392393349, 210405436061934940}, - {4274325990713914679, 168324348849547952}, - {10528270399884173809, 269318958159276723}, - {15801313949391159694, 215455166527421378}, - {1573004715287196786, 172364133221937103}, - {17274202803427156150, 275782613155099364}, - {17508711057483635243, 220626090524079491}, - {10317620031244997871, 176500872419263593}, - {12818843235250086271, 282401395870821749}, - {13944423402941979340, 225921116696657399}, - {14844887537095493795, 180736893357325919}, - {15565258844418305359, 144589514685860735}, - {6457670077359736959, 231343223497377177}, - {16234182506113520537, 185074578797901741}, - {9297997190148906106, 148059663038321393}, - {11187446689496339446, 236895460861314229}, - {12639306166338981880, 189516368689051383}, - {17490142562555006151, 151613094951241106}, - {2158786396894637579, 242580951921985771}, - {16484424376483351356, 194064761537588616}, - {9498190686444770762, 155251809230070893}, - {11507756283569722895, 248402894768113429}, - {12895553841597688639, 198722315814490743}, - {17695140702761971558, 158977852651592594}, - {17244178680193423523, 254364564242548151}, - {10105994129412828495, 203491651394038521}, - {4395446488788352473, 162793321115230817}, - {10722063196803274280, 260469313784369307}, - {1198952927958798777, 208375451027495446}, - {15716557601334680315, 166700360821996356}, - {17767794532651667857, 266720577315194170}, - {14214235626121334286, 213376461852155336}, - {7682039686155157106, 170701169481724269}, - {1223217053622520399, 273121871170758831}, - {15735968901865657612, 218497496936607064}, - {16278123936234436413, 174797997549285651}, - {219556594781725998, 279676796078857043}, - {7554342905309201445, 223741436863085634}, - {9732823138989271479, 178993149490468507}, - {815121763415193074, 286389039184749612}, - {11720143854957885429, 229111231347799689}, - {13065463898708218666, 183288985078239751}, - {6763022304224664610, 146631188062591801}, - {3442138057275642729, 234609900900146882}, - {13821756890046245153, 187687920720117505}, - {11057405512036996122, 150150336576094004}, - {6623802375033462826, 240240538521750407}, - {16367088344252501231, 192192430817400325}, - {13093670675402000985, 153753944653920260}, - {2503129006933649959, 246006311446272417}, - {13070549649772650937, 196805049157017933}, - {17835137349301941396, 157444039325614346}, - {2710778055689733971, 251910462920982955}, - {2168622444551787177, 201528370336786364}, - {5424246770383340065, 161222696269429091}, - {1300097203129523457, 257956314031086546}, - {15797473021471260058, 206365051224869236}, - {8948629602435097724, 165092040979895389}, - {3249760919670425388, 264147265567832623}, - {9978506365220160957, 211317812454266098}, - {15361502721659949412, 169054249963412878}, - {2442311466204457120, 270486799941460606}, - {16711244431931206989, 216389439953168484}, - {17058344360286875914, 173111551962534787}, - {12535955717491360170, 276978483140055660}, - {10028764573993088136, 221582786512044528}, - {15401709288678291155, 177266229209635622}, - {9885339602917624555, 283625966735416996}, - {4218922867592189321, 226900773388333597}, - {14443184738299482427, 181520618710666877}, - {4175850161155765295, 145216494968533502}, - {10370709072591134795, 232346391949653603}, - {15675264887556728482, 185877113559722882}, - {5161514280561562140, 148701690847778306}, - {879725219414678777, 237922705356445290}, - {703780175531743021, 190338164285156232}, - {11631070584651125387, 152270531428124985}, - {162968861732249003, 243632850284999977}, - {11198421533611530172, 194906280227999981}, - {5269388412147313814, 155925024182399985}, - {8431021459435702103, 249480038691839976}, - {3055468352806651359, 199584030953471981}, - {17201769941212962380, 159667224762777584}, - {16454785461715008838, 255467559620444135}, - {13163828369372007071, 204374047696355308}, - {17909760324981426303, 163499238157084246}, - {2830174816776909822, 261598781051334795}, - {2264139853421527858, 209279024841067836}, - {16568707141704863579, 167423219872854268}, - {4373838538276319787, 267877151796566830}, - {3499070830621055830, 214301721437253464}, - {6488605479238754987, 171441377149802771}, - {3003071137298187333, 274306203439684434}, - {6091805724580460189, 219444962751747547}, - {15941491023890099121, 175555970201398037}, - {10748990379256517301, 280889552322236860}, - {8599192303405213841, 224711641857789488}, - {14258051472207991719, 179769313486231590}, -} diff --git a/examples/gno.land/p/demo/math_eval/int32/gno.mod b/examples/gno.land/p/demo/math_eval/int32/gno.mod index de57497a699..c4e4bc8f454 100644 --- a/examples/gno.land/p/demo/math_eval/int32/gno.mod +++ b/examples/gno.land/p/demo/math_eval/int32/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/math_eval/int32 - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/membstore/gno.mod b/examples/gno.land/p/demo/membstore/gno.mod new file mode 100644 index 00000000000..007e7a5d883 --- /dev/null +++ b/examples/gno.land/p/demo/membstore/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/membstore diff --git a/examples/gno.land/p/demo/membstore/members.gno b/examples/gno.land/p/demo/membstore/members.gno new file mode 100644 index 00000000000..0bbaaaa8b04 --- /dev/null +++ b/examples/gno.land/p/demo/membstore/members.gno @@ -0,0 +1,38 @@ +package membstore + +import ( + "std" +) + +// MemberStore defines the member storage abstraction +type MemberStore interface { + // Members returns all members in the store + Members(offset, count uint64) []Member + + // Size returns the current size of the store + Size() int + + // IsMember returns a flag indicating if the given address + // belongs to a member + IsMember(address std.Address) bool + + // TotalPower returns the total voting power of the member store + TotalPower() uint64 + + // Member returns the requested member + Member(address std.Address) (Member, error) + + // AddMember adds a member to the store + AddMember(member Member) error + + // UpdateMember updates the member in the store. + // If updating a member's voting power to 0, + // the member will be removed + UpdateMember(address std.Address, member Member) error +} + +// Member holds the relevant member information +type Member struct { + Address std.Address // bech32 gno address of the member (unique) + VotingPower uint64 // the voting power of the member +} diff --git a/examples/gno.land/p/demo/membstore/membstore.gno b/examples/gno.land/p/demo/membstore/membstore.gno new file mode 100644 index 00000000000..ca721d078e6 --- /dev/null +++ b/examples/gno.land/p/demo/membstore/membstore.gno @@ -0,0 +1,209 @@ +package membstore + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" +) + +var ( + ErrAlreadyMember = errors.New("address is already a member") + ErrMissingMember = errors.New("address is not a member") + ErrInvalidAddressUpdate = errors.New("invalid address update") + ErrNotGovDAO = errors.New("caller not correct govdao instance") +) + +// maxRequestMembers is the maximum number of +// paginated members that can be requested +const maxRequestMembers = 50 + +type Option func(*MembStore) + +// WithInitialMembers initializes the member store +// with an initial member list +func WithInitialMembers(members []Member) Option { + return func(store *MembStore) { + for _, m := range members { + memberAddr := m.Address.String() + + // Check if the member already exists + if store.members.Has(memberAddr) { + panic(ufmt.Errorf("%s, %s", memberAddr, ErrAlreadyMember)) + } + + store.members.Set(memberAddr, m) + store.totalVotingPower += m.VotingPower + } + } +} + +// WithDAOPkgPath initializes the member store +// with a dao package path guard +func WithDAOPkgPath(daoPkgPath string) Option { + return func(store *MembStore) { + store.daoPkgPath = daoPkgPath + } +} + +// MembStore implements the dao.MembStore abstraction +type MembStore struct { + daoPkgPath string // active dao pkg path, if any + members *avl.Tree // std.Address -> Member + totalVotingPower uint64 // cached value for quick lookups +} + +// NewMembStore creates a new member store +func NewMembStore(opts ...Option) *MembStore { + m := &MembStore{ + members: avl.NewTree(), // empty set + daoPkgPath: "", // no dao guard + totalVotingPower: 0, + } + + // Apply the options + for _, opt := range opts { + opt(m) + } + + return m +} + +// AddMember adds member to the member store `m`. +// It fails if the caller is not GovDAO or +// if the member is already present +func (m *MembStore) AddMember(member Member) error { + if !m.isCallerDAORealm() { + return ErrNotGovDAO + } + + // Check if the member exists + if m.IsMember(member.Address) { + return ErrAlreadyMember + } + + // Add the member + m.members.Set(member.Address.String(), member) + + // Update the total voting power + m.totalVotingPower += member.VotingPower + + return nil +} + +// UpdateMember updates the member with the given address. +// Updating fails if the caller is not GovDAO. +func (m *MembStore) UpdateMember(address std.Address, member Member) error { + if !m.isCallerDAORealm() { + return ErrNotGovDAO + } + + // Get the member + oldMember, err := m.Member(address) + if err != nil { + return err + } + + // Check if this is a removal request + if member.VotingPower == 0 { + m.members.Remove(address.String()) + + // Update the total voting power + m.totalVotingPower -= oldMember.VotingPower + + return nil + } + + // Check that the member wouldn't be + // overwriting an existing one + isAddressUpdate := address != member.Address + if isAddressUpdate && m.IsMember(member.Address) { + return ErrInvalidAddressUpdate + } + + // Remove the old member info + // in case the address changed + if address != member.Address { + m.members.Remove(address.String()) + } + + // Save the new member info + m.members.Set(member.Address.String(), member) + + // Update the total voting power + difference := member.VotingPower - oldMember.VotingPower + m.totalVotingPower += difference + + return nil +} + +// IsMember returns a flag indicating if the given +// address belongs to a member of the member store +func (m *MembStore) IsMember(address std.Address) bool { + _, exists := m.members.Get(address.String()) + + return exists +} + +// Member returns the member associated with the given address +func (m *MembStore) Member(address std.Address) (Member, error) { + member, exists := m.members.Get(address.String()) + if !exists { + return Member{}, ErrMissingMember + } + + return member.(Member), nil +} + +// Members returns a paginated list of members from +// the member store. If the store is empty, an empty slice +// is returned instead +func (m *MembStore) Members(offset, count uint64) []Member { + // Calculate the left and right bounds + if count < 1 || offset >= uint64(m.members.Size()) { + return []Member{} + } + + // Limit the maximum number of returned members + if count > maxRequestMembers { + count = maxRequestMembers + } + + // Gather the members + members := make([]Member, 0) + m.members.IterateByOffset( + int(offset), + int(count), + func(_ string, val interface{}) bool { + member := val.(Member) + + // Save the member + members = append(members, member) + + return false + }) + + return members +} + +// Size returns the number of active members in the member store +func (m *MembStore) Size() int { + return m.members.Size() +} + +// TotalPower returns the total voting power +// of the member store +func (m *MembStore) TotalPower() uint64 { + return m.totalVotingPower +} + +// isCallerDAORealm returns a flag indicating if the +// current caller context is the active DAO Realm. +// We need to include a dao guard, even if the +// executor guarantees it, because +// the API of the member store is public and callable +// by anyone who has a reference to the member store instance. +func (m *MembStore) isCallerDAORealm() bool { + return m.daoPkgPath != "" && std.CurrentRealm().PkgPath() == m.daoPkgPath +} diff --git a/examples/gno.land/p/demo/membstore/membstore_test.gno b/examples/gno.land/p/demo/membstore/membstore_test.gno new file mode 100644 index 00000000000..2181adde077 --- /dev/null +++ b/examples/gno.land/p/demo/membstore/membstore_test.gno @@ -0,0 +1,317 @@ +package membstore + +import ( + "testing" + + "std" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" +) + +// generateMembers generates dummy govdao members +func generateMembers(t *testing.T, count int) []Member { + t.Helper() + + members := make([]Member, 0, count) + + for i := 0; i < count; i++ { + members = append(members, Member{ + Address: testutils.TestAddress(ufmt.Sprintf("member %d", i)), + VotingPower: 10, + }) + } + + return members +} + +func TestMembStore_GetMember(t *testing.T) { + t.Parallel() + + t.Run("member not found", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + _, err := m.Member(testutils.TestAddress("random")) + uassert.ErrorIs(t, err, ErrMissingMember) + }) + + t.Run("valid member fetched", func(t *testing.T) { + t.Parallel() + + // Create a non-empty store + members := generateMembers(t, 1) + + m := NewMembStore(WithInitialMembers(members)) + + _, err := m.Member(members[0].Address) + uassert.NoError(t, err) + }) +} + +func TestMembStore_GetMembers(t *testing.T) { + t.Parallel() + + t.Run("no members", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + members := m.Members(0, 10) + uassert.Equal(t, 0, len(members)) + }) + + t.Run("proper pagination", func(t *testing.T) { + t.Parallel() + + var ( + numMembers = maxRequestMembers * 2 + halfRange = numMembers / 2 + + members = generateMembers(t, numMembers) + m = NewMembStore(WithInitialMembers(members)) + + verifyMembersPresent = func(members, fetchedMembers []Member) { + for _, fetchedMember := range fetchedMembers { + for _, member := range members { + if member.Address != fetchedMember.Address { + continue + } + + uassert.Equal(t, member.VotingPower, fetchedMember.VotingPower) + } + } + } + ) + + urequire.Equal(t, numMembers, m.Size()) + + fetchedMembers := m.Members(0, uint64(halfRange)) + urequire.Equal(t, halfRange, len(fetchedMembers)) + + // Verify the members + verifyMembersPresent(members, fetchedMembers) + + // Fetch the other half + fetchedMembers = m.Members(uint64(halfRange), uint64(halfRange)) + urequire.Equal(t, halfRange, len(fetchedMembers)) + + // Verify the members + verifyMembersPresent(members, fetchedMembers) + }) +} + +func TestMembStore_IsMember(t *testing.T) { + t.Parallel() + + t.Run("non-existing member", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + uassert.False(t, m.IsMember(testutils.TestAddress("random"))) + }) + + t.Run("existing member", func(t *testing.T) { + t.Parallel() + + // Create a non-empty store + members := generateMembers(t, 50) + + m := NewMembStore(WithInitialMembers(members)) + + for _, member := range members { + uassert.True(t, m.IsMember(member.Address)) + } + }) +} + +func TestMembStore_AddMember(t *testing.T) { + t.Parallel() + + t.Run("caller not govdao", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore(WithDAOPkgPath("gno.land/r/gov/dao")) + + // Attempt to add a member + member := generateMembers(t, 1)[0] + uassert.ErrorIs(t, m.AddMember(member), ErrNotGovDAO) + }) + + t.Run("member already exists", func(t *testing.T) { + t.Parallel() + + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + + std.TestSetRealm(r) + + // Create a non-empty store + members := generateMembers(t, 1) + m := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members)) + + // Attempt to add a member + uassert.ErrorIs(t, m.AddMember(members[0]), ErrAlreadyMember) + }) + + t.Run("new member added", func(t *testing.T) { + t.Parallel() + + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + + std.TestSetRealm(r) + + // Create an empty store + members := generateMembers(t, 1) + m := NewMembStore(WithDAOPkgPath(daoPkgPath)) + + // Attempt to add a member + urequire.NoError(t, m.AddMember(members[0])) + + // Make sure the member is added + uassert.True(t, m.IsMember(members[0].Address)) + }) +} + +func TestMembStore_Size(t *testing.T) { + t.Parallel() + + t.Run("empty govdao", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore() + + uassert.Equal(t, 0, m.Size()) + }) + + t.Run("non-empty govdao", func(t *testing.T) { + t.Parallel() + + // Create a non-empty store + members := generateMembers(t, 50) + m := NewMembStore(WithInitialMembers(members)) + + uassert.Equal(t, len(members), m.Size()) + }) +} + +func TestMembStore_UpdateMember(t *testing.T) { + t.Parallel() + + t.Run("caller not govdao", func(t *testing.T) { + t.Parallel() + + // Create an empty store + m := NewMembStore(WithDAOPkgPath("gno.land/r/gov/dao")) + + // Attempt to update a member + member := generateMembers(t, 1)[0] + uassert.ErrorIs(t, m.UpdateMember(member.Address, member), ErrNotGovDAO) + }) + + t.Run("non-existing member", func(t *testing.T) { + t.Parallel() + + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + + std.TestSetRealm(r) + + // Create an empty store + members := generateMembers(t, 1) + m := NewMembStore(WithDAOPkgPath(daoPkgPath)) + + // Attempt to update a member + uassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[0]), ErrMissingMember) + }) + + t.Run("overwrite member attempt", func(t *testing.T) { + t.Parallel() + + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + + std.TestSetRealm(r) + + // Create a non-empty store + members := generateMembers(t, 2) + m := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members)) + + // Attempt to update a member + uassert.ErrorIs(t, m.UpdateMember(members[0].Address, members[1]), ErrInvalidAddressUpdate) + }) + + t.Run("successful update", func(t *testing.T) { + t.Parallel() + + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + + std.TestSetRealm(r) + + // Create a non-empty store + members := generateMembers(t, 1) + m := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members)) + + oldVotingPower := m.totalVotingPower + urequire.Equal(t, members[0].VotingPower, oldVotingPower) + + votingPower := uint64(300) + members[0].VotingPower = votingPower + + // Attempt to update a member + uassert.NoError(t, m.UpdateMember(members[0].Address, members[0])) + uassert.Equal(t, votingPower, m.Members(0, 10)[0].VotingPower) + urequire.Equal(t, votingPower, m.totalVotingPower) + }) + + t.Run("member removed", func(t *testing.T) { + t.Parallel() + + var ( + // Execute as the /r/gov/dao caller + daoPkgPath = "gno.land/r/gov/dao" + r = std.NewCodeRealm(daoPkgPath) + ) + + std.TestSetRealm(r) + + // Create a non-empty store + members := generateMembers(t, 1) + m := NewMembStore(WithDAOPkgPath(daoPkgPath), WithInitialMembers(members)) + + votingPower := uint64(0) + members[0].VotingPower = votingPower + + // Attempt to update a member + uassert.NoError(t, m.UpdateMember(members[0].Address, members[0])) + + // Make sure the member was removed + uassert.False(t, m.IsMember(members[0].Address)) + }) +} diff --git a/examples/gno.land/p/demo/memeland/gno.mod b/examples/gno.land/p/demo/memeland/gno.mod index 66f22d1ccee..06cc8fbf487 100644 --- a/examples/gno.land/p/demo/memeland/gno.mod +++ b/examples/gno.land/p/demo/memeland/gno.mod @@ -1,10 +1 @@ module gno.land/p/demo/memeland - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/memeland/memeland.gno b/examples/gno.land/p/demo/memeland/memeland.gno index 9c302ca365b..38f42239bec 100644 --- a/examples/gno.land/p/demo/memeland/memeland.gno +++ b/examples/gno.land/p/demo/memeland/memeland.gno @@ -160,8 +160,8 @@ func (m *Memeland) RemovePost(id string) string { panic("id cannot be empty") } - if err := m.CallerIsOwner(); err != nil { - panic(err) + if !m.CallerIsOwner() { + panic(ownable.ErrUnauthorized) } for i, post := range m.Posts { diff --git a/examples/gno.land/p/demo/merkle/merkle.gno b/examples/gno.land/p/demo/merkle/merkle.gno index 54b878bffb1..f4fcc4dad40 100644 --- a/examples/gno.land/p/demo/merkle/merkle.gno +++ b/examples/gno.land/p/demo/merkle/merkle.gno @@ -19,6 +19,17 @@ type Node struct { position uint8 } +func NewNode(hash []byte, position uint8) Node { + return Node{ + hash: hash, + position: position, + } +} + +func (n Node) Position() uint8 { + return n.position +} + func (n Node) Hash() string { return hex.EncodeToString(n.hash[:]) } diff --git a/examples/gno.land/p/demo/microblog/gno.mod b/examples/gno.land/p/demo/microblog/gno.mod index 9bbcfa19e31..a285ef5f903 100644 --- a/examples/gno.land/p/demo/microblog/gno.mod +++ b/examples/gno.land/p/demo/microblog/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/microblog - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/mux/request.gno b/examples/gno.land/p/demo/mux/request.gno index f7996fe40fe..7b5b74da91b 100644 --- a/examples/gno.land/p/demo/mux/request.gno +++ b/examples/gno.land/p/demo/mux/request.gno @@ -4,7 +4,15 @@ import "strings" // Request represents an incoming request. type Request struct { - Path string + // Path is request path name. + // + // Note: use RawPath to obtain a raw path with query string. + Path string + + // RawPath contains a whole request path, including query string. + RawPath string + + // HandlerPath is handler rule that matches a request. HandlerPath string } diff --git a/examples/gno.land/p/demo/mux/router.gno b/examples/gno.land/p/demo/mux/router.gno index a2efb3a4ebf..fe6bf70abdf 100644 --- a/examples/gno.land/p/demo/mux/router.gno +++ b/examples/gno.land/p/demo/mux/router.gno @@ -18,7 +18,8 @@ func NewRouter() *Router { // Render renders the output for the given path using the registered route handler. func (r *Router) Render(reqPath string) string { - reqParts := strings.Split(reqPath, "/") + clearPath := stripQueryString(reqPath) + reqParts := strings.Split(clearPath, "/") for _, route := range r.routes { patParts := strings.Split(route.Pattern, "/") @@ -45,7 +46,8 @@ func (r *Router) Render(reqPath string) string { } if match { req := &Request{ - Path: reqPath, + Path: clearPath, + RawPath: reqPath, HandlerPath: route.Pattern, } res := &ResponseWriter{} @@ -66,3 +68,12 @@ func (r *Router) HandleFunc(pattern string, fn HandlerFunc) { route := Handler{Pattern: pattern, Fn: fn} r.routes = append(r.routes, route) } + +func stripQueryString(reqPath string) string { + i := strings.Index(reqPath, "?") + if i == -1 { + return reqPath + } + + return reqPath[:i] +} diff --git a/examples/gno.land/p/demo/mux/router_test.gno b/examples/gno.land/p/demo/mux/router_test.gno index 13fd5b97955..cc6aad62146 100644 --- a/examples/gno.land/p/demo/mux/router_test.gno +++ b/examples/gno.land/p/demo/mux/router_test.gno @@ -1,34 +1,85 @@ package mux -import "testing" +import ( + "testing" -func TestRouter_Render(t *testing.T) { - // Define handlers and route configuration - router := NewRouter() - router.HandleFunc("hello/{name}", func(res *ResponseWriter, req *Request) { - name := req.GetVar("name") - if name != "" { - res.Write("Hello, " + name + "!") - } else { - res.Write("Hello, world!") - } - }) - router.HandleFunc("hi", func(res *ResponseWriter, req *Request) { - res.Write("Hi, earth!") - }) + "gno.land/p/demo/uassert" +) +func TestRouter_Render(t *testing.T) { cases := []struct { + label string path string expectedOutput string + setupHandler func(t *testing.T, r *Router) }{ - {"hello/Alice", "Hello, Alice!"}, - {"hi", "Hi, earth!"}, - {"hello/Bob", "Hello, Bob!"}, + { + label: "route with named parameter", + path: "hello/Alice", + expectedOutput: "Hello, Alice!", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hello/{name}", func(rw *ResponseWriter, req *Request) { + name := req.GetVar("name") + uassert.Equal(t, "Alice", name) + rw.Write("Hello, " + name + "!") + }) + }, + }, + { + label: "static route", + path: "hi", + expectedOutput: "Hi, earth!", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hi", func(rw *ResponseWriter, req *Request) { + uassert.Equal(t, req.Path, "hi") + rw.Write("Hi, earth!") + }) + }, + }, + { + label: "route with named parameter and query string", + path: "hello/foo/bar?foo=bar&baz", + expectedOutput: "foo bar", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hello/{key}/{val}", func(rw *ResponseWriter, req *Request) { + key := req.GetVar("key") + val := req.GetVar("val") + uassert.Equal(t, "foo", key) + uassert.Equal(t, "bar", val) + uassert.Equal(t, "hello/foo/bar?foo=bar&baz", req.RawPath) + uassert.Equal(t, "hello/foo/bar", req.Path) + rw.Write(key + " " + val) + }) + }, + }, + { + // TODO: finalize how router should behave with double slash in path. + label: "double slash in nested route", + path: "a/foo//", + expectedOutput: "test foo", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("a/{key}", func(rw *ResponseWriter, req *Request) { + // Assert not called + uassert.False(t, true, "unexpected handler called") + }) + + r.HandleFunc("a/{key}/{val}/", func(rw *ResponseWriter, req *Request) { + key := req.GetVar("key") + val := req.GetVar("val") + uassert.Equal(t, key, "foo") + uassert.Empty(t, val) + rw.Write("test " + key) + }) + }, + }, + // TODO: {"hello", "Hello, world!"}, // TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc } for _, tt := range cases { - t.Run(tt.path, func(t *testing.T) { + t.Run(tt.label, func(t *testing.T) { + router := NewRouter() + tt.setupHandler(t, router) output := router.Render(tt.path) if output != tt.expectedOutput { t.Errorf("Expected output %q, but got %q", tt.expectedOutput, output) diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno index f9f0ea15dd9..95bd2ac4959 100644 --- a/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno +++ b/examples/gno.land/p/demo/ownable/exts/authorizable/authorizable.gno @@ -41,7 +41,7 @@ func NewAuthorizableWithAddress(addr std.Address) *Authorizable { } func (a *Authorizable) AddToAuthList(addr std.Address) error { - if err := a.CallerIsOwner(); err != nil { + if !a.CallerIsOwner() { return ErrNotSuperuser } @@ -55,7 +55,7 @@ func (a *Authorizable) AddToAuthList(addr std.Address) error { } func (a *Authorizable) DeleteFromAuthList(addr std.Address) error { - if err := a.CallerIsOwner(); err != nil { + if !a.CallerIsOwner() { return ErrNotSuperuser } diff --git a/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod b/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod index f36823f3f71..0e8be79f130 100644 --- a/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod +++ b/examples/gno.land/p/demo/ownable/exts/authorizable/gno.mod @@ -1,9 +1 @@ module gno.land/p/demo/ownable/exts/authorizable - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/ownable/gno.mod b/examples/gno.land/p/demo/ownable/gno.mod index 00f7812f6f5..9a9abb1e661 100644 --- a/examples/gno.land/p/demo/ownable/gno.mod +++ b/examples/gno.land/p/demo/ownable/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/ownable - -require ( - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno index a77b22461a9..f565e27c0f2 100644 --- a/examples/gno.land/p/demo/ownable/ownable.gno +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -6,6 +6,7 @@ const OwnershipTransferEvent = "OwnershipTransfer" // Ownable is meant to be used as a top-level object to make your contract ownable OR // being embedded in a Gno object to manage per-object ownership. +// Ownable is safe to export as a top-level object type Ownable struct { owner std.Address } @@ -24,9 +25,8 @@ func NewWithAddress(addr std.Address) *Ownable { // TransferOwnership transfers ownership of the Ownable struct to a new address func (o *Ownable) TransferOwnership(newOwner std.Address) error { - err := o.CallerIsOwner() - if err != nil { - return err + if !o.CallerIsOwner() { + return ErrUnauthorized } if !newOwner.IsValid() { @@ -37,8 +37,8 @@ func (o *Ownable) TransferOwnership(newOwner std.Address) error { o.owner = newOwner std.Emit( OwnershipTransferEvent, - "from", string(prevOwner), - "to", string(newOwner), + "from", prevOwner.String(), + "to", newOwner.String(), ) return nil @@ -48,9 +48,8 @@ func (o *Ownable) TransferOwnership(newOwner std.Address) error { // Top-level usage: disables all only-owner actions/functions, // Embedded usage: behaves like a burn functionality, removing the owner from the struct func (o *Ownable) DropOwnership() error { - err := o.CallerIsOwner() - if err != nil { - return err + if !o.CallerIsOwner() { + return ErrUnauthorized } prevOwner := o.owner @@ -58,7 +57,7 @@ func (o *Ownable) DropOwnership() error { std.Emit( OwnershipTransferEvent, - "from", string(prevOwner), + "from", prevOwner.String(), "to", "", ) @@ -71,12 +70,8 @@ func (o Ownable) Owner() std.Address { } // CallerIsOwner checks if the caller of the function is the Realm's owner -func (o Ownable) CallerIsOwner() error { - if std.PrevRealm().Addr() == o.owner { - return nil - } - - return ErrUnauthorized +func (o Ownable) CallerIsOwner() bool { + return std.PrevRealm().Addr() == o.owner } // AssertCallerIsOwner panics if the caller is not the owner diff --git a/examples/gno.land/p/demo/ownable/ownable_test.gno b/examples/gno.land/p/demo/ownable/ownable_test.gno index a9d97154f45..f58af9642c6 100644 --- a/examples/gno.land/p/demo/ownable/ownable_test.gno +++ b/examples/gno.land/p/demo/ownable/ownable_test.gno @@ -6,6 +6,7 @@ import ( "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" ) var ( @@ -19,27 +20,14 @@ func TestNew(t *testing.T) { o := New() got := o.Owner() - if alice != got { - t.Fatalf("Expected %s, got: %s", alice, got) - } + uassert.Equal(t, got, alice) } func TestNewWithAddress(t *testing.T) { o := NewWithAddress(alice) got := o.Owner() - if alice != got { - t.Fatalf("Expected %s, got: %s", alice, got) - } -} - -func TestOwner(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(alice)) - - o := New() - expected := alice - got := o.Owner() - uassert.Equal(t, expected, got) + uassert.Equal(t, got, alice) } func TestTransferOwnership(t *testing.T) { @@ -48,14 +36,11 @@ func TestTransferOwnership(t *testing.T) { o := New() err := o.TransferOwnership(bob) - if err != nil { - t.Fatalf("TransferOwnership failed, %v", err) - } + urequire.NoError(t, err) got := o.Owner() - if bob != got { - t.Fatalf("Expected: %s, got: %s", bob, got) - } + + uassert.Equal(t, got, bob) } func TestCallerIsOwner(t *testing.T) { @@ -67,8 +52,7 @@ func TestCallerIsOwner(t *testing.T) { std.TestSetRealm(std.NewUserRealm(unauthorizedCaller)) std.TestSetOrigCaller(unauthorizedCaller) // TODO(bug): should not be needed - err := o.CallerIsOwner() - uassert.Error(t, err) // XXX: IsError(..., unauthorizedCaller) + uassert.False(t, o.CallerIsOwner()) } func TestDropOwnership(t *testing.T) { @@ -77,7 +61,7 @@ func TestDropOwnership(t *testing.T) { o := New() err := o.DropOwnership() - uassert.NoError(t, err, "DropOwnership failed") + urequire.NoError(t, err, "DropOwnership failed") owner := o.Owner() uassert.Empty(t, owner, "owner should be empty") @@ -94,13 +78,8 @@ func TestErrUnauthorized(t *testing.T) { std.TestSetRealm(std.NewUserRealm(bob)) std.TestSetOrigCaller(bob) // TODO(bug): should not be needed - err := o.TransferOwnership(alice) - if err != ErrUnauthorized { - t.Fatalf("Should've been ErrUnauthorized, was %v", err) - } - - err = o.DropOwnership() - uassert.ErrorContains(t, err, ErrUnauthorized.Error()) + uassert.ErrorContains(t, o.TransferOwnership(alice), ErrUnauthorized.Error()) + uassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error()) } func TestErrInvalidAddress(t *testing.T) { diff --git a/examples/gno.land/p/demo/pausable/gno.mod b/examples/gno.land/p/demo/pausable/gno.mod index 156875f7d85..a741342eb84 100644 --- a/examples/gno.land/p/demo/pausable/gno.mod +++ b/examples/gno.land/p/demo/pausable/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/pausable - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/pausable/pausable.gno b/examples/gno.land/p/demo/pausable/pausable.gno index eae3456ba61..e6a85771fa6 100644 --- a/examples/gno.land/p/demo/pausable/pausable.gno +++ b/examples/gno.land/p/demo/pausable/pausable.gno @@ -1,6 +1,10 @@ package pausable -import "gno.land/p/demo/ownable" +import ( + "std" + + "gno.land/p/demo/ownable" +) type Pausable struct { *ownable.Ownable @@ -30,20 +34,24 @@ func (p Pausable) IsPaused() bool { // Pause sets the state of Pausable to true, meaning all pausable functions are paused func (p *Pausable) Pause() error { - if err := p.CallerIsOwner(); err != nil { - return err + if !p.CallerIsOwner() { + return ownable.ErrUnauthorized } p.paused = true + std.Emit("Paused", "account", p.Owner().String()) + return nil } // Unpause sets the state of Pausable to false, meaning all pausable functions are resumed func (p *Pausable) Unpause() error { - if err := p.CallerIsOwner(); err != nil { - return err + if !p.CallerIsOwner() { + return ownable.ErrUnauthorized } p.paused = false + std.Emit("Unpaused", "account", p.Owner().String()) + return nil } diff --git a/examples/gno.land/p/demo/seqid/gno.mod b/examples/gno.land/p/demo/seqid/gno.mod index d1390012c3c..63e6a1fb551 100644 --- a/examples/gno.land/p/demo/seqid/gno.mod +++ b/examples/gno.land/p/demo/seqid/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/seqid - -require gno.land/p/demo/cford32 v0.0.0-latest diff --git a/examples/gno.land/p/demo/simpledao/dao.gno b/examples/gno.land/p/demo/simpledao/dao.gno new file mode 100644 index 00000000000..837f64a41d6 --- /dev/null +++ b/examples/gno.land/p/demo/simpledao/dao.gno @@ -0,0 +1,223 @@ +package simpledao + +import ( + "errors" + "std" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" + "gno.land/p/demo/ufmt" +) + +var ( + ErrInvalidExecutor = errors.New("invalid executor provided") + ErrInvalidTitle = errors.New("invalid proposal title provided") + ErrInsufficientProposalFunds = errors.New("insufficient funds for proposal") + ErrInsufficientExecuteFunds = errors.New("insufficient funds for executing proposal") + ErrProposalExecuted = errors.New("proposal already executed") + ErrProposalInactive = errors.New("proposal is inactive") + ErrProposalNotAccepted = errors.New("proposal is not accepted") +) + +var ( + minProposalFeeValue int64 = 100 * 1_000_000 // minimum gnot required for a govdao proposal (100 GNOT) + minExecuteFeeValue int64 = 500 * 1_000_000 // minimum gnot required for a govdao proposal (500 GNOT) + + minProposalFee = std.NewCoin("ugnot", minProposalFeeValue) + minExecuteFee = std.NewCoin("ugnot", minExecuteFeeValue) +) + +// SimpleDAO is a simple DAO implementation +type SimpleDAO struct { + proposals *avl.Tree // seqid.ID -> proposal + membStore membstore.MemberStore +} + +// New creates a new instance of the simpledao DAO +func New(membStore membstore.MemberStore) *SimpleDAO { + return &SimpleDAO{ + proposals: avl.NewTree(), + membStore: membStore, + } +} + +func (s *SimpleDAO) Propose(request dao.ProposalRequest) (uint64, error) { + // Make sure the executor is set + if request.Executor == nil { + return 0, ErrInvalidExecutor + } + + // Make sure the title is set + if strings.TrimSpace(request.Title) == "" { + return 0, ErrInvalidTitle + } + + var ( + caller = getDAOCaller() + sentCoins = std.GetOrigSend() // Get the sent coins, if any + canCoverFee = sentCoins.AmountOf("ugnot") >= minProposalFee.Amount + ) + + // Check if the proposal is valid + if !s.membStore.IsMember(caller) && !canCoverFee { + return 0, ErrInsufficientProposalFunds + } + + // Create the wrapped proposal + prop := &proposal{ + author: caller, + title: request.Title, + description: request.Description, + executor: request.Executor, + status: dao.Active, + tally: newTally(), + getTotalVotingPowerFn: s.membStore.TotalPower, + } + + // Add the proposal + id, err := s.addProposal(prop) + if err != nil { + return 0, ufmt.Errorf("unable to add proposal, %s", err.Error()) + } + + // Emit the proposal added event + dao.EmitProposalAdded(id, caller) + + return id, nil +} + +func (s *SimpleDAO) VoteOnProposal(id uint64, option dao.VoteOption) error { + // Verify the GOVDAO member + caller := getDAOCaller() + + member, err := s.membStore.Member(caller) + if err != nil { + return ufmt.Errorf("unable to get govdao member, %s", err.Error()) + } + + // Check if the proposal exists + propRaw, err := s.ProposalByID(id) + if err != nil { + return ufmt.Errorf("unable to get proposal %d, %s", id, err.Error()) + } + + prop := propRaw.(*proposal) + + // Check the proposal status + if prop.Status() == dao.ExecutionSuccessful || + prop.Status() == dao.ExecutionFailed { + // Proposal was already executed, nothing to vote on anymore. + // + // In fact, the proposal should stop accepting + // votes as soon as a 2/3+ majority is reached + // on either option, but leaving the ability to vote still, + // even if a proposal is accepted, or not accepted, + // leaves room for "principle" vote decisions to be recorded + return ErrProposalInactive + } + + // Cast the vote + if err = prop.tally.castVote(member, option); err != nil { + return ufmt.Errorf("unable to vote on proposal %d, %s", id, err.Error()) + } + + // Emit the vote cast event + dao.EmitVoteAdded(id, caller, option) + + // Check the votes to see if quorum is reached + var ( + totalPower = s.membStore.TotalPower() + majorityPower = (2 * totalPower) / 3 + ) + + acceptProposal := func() { + prop.status = dao.Accepted + + dao.EmitProposalAccepted(id) + } + + declineProposal := func() { + prop.status = dao.NotAccepted + + dao.EmitProposalNotAccepted(id) + } + + switch { + case prop.tally.yays > majorityPower: + // 2/3+ voted YES + acceptProposal() + case prop.tally.nays > majorityPower: + // 2/3+ voted NO + declineProposal() + case prop.tally.abstains > majorityPower: + // 2/3+ voted ABSTAIN + declineProposal() + case prop.tally.yays+prop.tally.nays+prop.tally.abstains >= totalPower: + // Everyone voted, but it's undecided, + // hence the proposal can't go through + declineProposal() + default: + // Quorum not reached + } + + return nil +} + +func (s *SimpleDAO) ExecuteProposal(id uint64) error { + var ( + caller = getDAOCaller() + sentCoins = std.GetOrigSend() // Get the sent coins, if any + canCoverFee = sentCoins.AmountOf("ugnot") >= minExecuteFee.Amount + ) + + // Check if the non-DAO member can cover the execute fee + if !s.membStore.IsMember(caller) && !canCoverFee { + return ErrInsufficientExecuteFunds + } + + // Check if the proposal exists + propRaw, err := s.ProposalByID(id) + if err != nil { + return ufmt.Errorf("unable to get proposal %d, %s", id, err.Error()) + } + + prop := propRaw.(*proposal) + + // Check if the proposal is executed + if prop.Status() == dao.ExecutionSuccessful || + prop.Status() == dao.ExecutionFailed { + // Proposal is already executed + return ErrProposalExecuted + } + + // Check the proposal status + if prop.Status() != dao.Accepted { + // Proposal is not accepted, cannot be executed + return ErrProposalNotAccepted + } + + // Emit an event when the execution finishes + defer dao.EmitProposalExecuted(id, prop.status) + + // Attempt to execute the proposal + if err = prop.executor.Execute(); err != nil { + prop.status = dao.ExecutionFailed + + return ufmt.Errorf("error during proposal %d execution, %s", id, err.Error()) + } + + // Update the proposal status + prop.status = dao.ExecutionSuccessful + + return nil +} + +// getDAOCaller returns the DAO caller. +// XXX: This is not a great way to determine the caller, and it is very unsafe. +// However, the current MsgRun context does not persist escaping the main() scope. +// Until a better solution is developed, this enables proposals to be made through a package deployment + init() +func getDAOCaller() std.Address { + return std.GetOrigCaller() +} diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno new file mode 100644 index 00000000000..46251e24dad --- /dev/null +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -0,0 +1,878 @@ +package simpledao + +import ( + "errors" + "std" + "testing" + + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" +) + +// generateMembers generates dummy govdao members +func generateMembers(t *testing.T, count int) []membstore.Member { + t.Helper() + + members := make([]membstore.Member, 0, count) + + for i := 0; i < count; i++ { + members = append(members, membstore.Member{ + Address: testutils.TestAddress(ufmt.Sprintf("member %d", i)), + VotingPower: 10, + }) + } + + return members +} + +func TestSimpleDAO_Propose(t *testing.T) { + t.Parallel() + + t.Run("invalid executor", func(t *testing.T) { + t.Parallel() + + s := New(nil) + + _, err := s.Propose(dao.ProposalRequest{}) + uassert.ErrorIs( + t, + err, + ErrInvalidExecutor, + ) + }) + + t.Run("invalid title", func(t *testing.T) { + t.Parallel() + + var ( + called = false + cb = func() error { + called = true + + return nil + } + ex = &mockExecutor{ + executeFn: cb, + } + + sentCoins = std.NewCoins( + std.NewCoin( + "ugnot", + minProposalFeeValue, + ), + ) + + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return false + }, + } + s = New(ms) + ) + + std.TestSetOrigSend(sentCoins, std.Coins{}) + + _, err := s.Propose(dao.ProposalRequest{ + Executor: ex, + Title: "", // Set invalid title + }) + uassert.ErrorIs( + t, + err, + ErrInvalidTitle, + ) + + uassert.False(t, called) + }) + + t.Run("caller cannot cover fee", func(t *testing.T) { + t.Parallel() + + var ( + called = false + cb = func() error { + called = true + + return nil + } + ex = &mockExecutor{ + executeFn: cb, + } + title = "Proposal title" + + sentCoins = std.NewCoins( + std.NewCoin( + "ugnot", + minProposalFeeValue-1, + ), + ) + + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return false + }, + } + s = New(ms) + ) + + // Set the sent coins to be lower + // than the proposal fee + std.TestSetOrigSend(sentCoins, std.Coins{}) + + _, err := s.Propose(dao.ProposalRequest{ + Executor: ex, + Title: title, + }) + uassert.ErrorIs( + t, + err, + ErrInsufficientProposalFunds, + ) + + uassert.False(t, called) + }) + + t.Run("proposal added", func(t *testing.T) { + t.Parallel() + + var ( + called = false + cb = func() error { + called = true + + return nil + } + + ex = &mockExecutor{ + executeFn: cb, + } + description = "Proposal description" + title = "Proposal title" + + proposer = testutils.TestAddress("proposer") + sentCoins = std.NewCoins( + std.NewCoin( + "ugnot", + minProposalFeeValue, // enough to cover + ), + ) + + ms = &mockMemberStore{ + isMemberFn: func(addr std.Address) bool { + return addr == proposer + }, + } + s = New(ms) + ) + + // Set the sent coins to be enough + // to cover the fee + std.TestSetOrigSend(sentCoins, std.Coins{}) + std.TestSetOrigCaller(proposer) + + // Make sure the proposal was added + id, err := s.Propose(dao.ProposalRequest{ + Title: title, + Description: description, + Executor: ex, + }) + uassert.NoError(t, err) + uassert.False(t, called) + + // Make sure the proposal exists + prop, err := s.ProposalByID(id) + uassert.NoError(t, err) + + uassert.Equal(t, proposer.String(), prop.Author().String()) + uassert.Equal(t, description, prop.Description()) + uassert.Equal(t, title, prop.Title()) + uassert.Equal(t, dao.Active.String(), prop.Status().String()) + + stats := prop.Stats() + + uassert.Equal(t, uint64(0), stats.YayVotes) + uassert.Equal(t, uint64(0), stats.NayVotes) + uassert.Equal(t, uint64(0), stats.AbstainVotes) + uassert.Equal(t, uint64(0), stats.TotalVotingPower) + }) +} + +func TestSimpleDAO_VoteOnProposal(t *testing.T) { + t.Parallel() + + t.Run("not govdao member", func(t *testing.T) { + t.Parallel() + + var ( + voter = testutils.TestAddress("voter") + fetchErr = errors.New("fetch error") + + ms = &mockMemberStore{ + memberFn: func(_ std.Address) (membstore.Member, error) { + return membstore.Member{ + Address: voter, + }, fetchErr + }, + } + s = New(ms) + ) + + std.TestSetOrigCaller(voter) + + // Attempt to vote on the proposal + uassert.ErrorContains( + t, + s.VoteOnProposal(0, dao.YesVote), + fetchErr.Error(), + ) + }) + + t.Run("missing proposal", func(t *testing.T) { + t.Parallel() + + var ( + voter = testutils.TestAddress("voter") + ms = &mockMemberStore{ + memberFn: func(a std.Address) (membstore.Member, error) { + if a != voter { + return membstore.Member{}, errors.New("not found") + } + + return membstore.Member{ + Address: voter, + }, nil + }, + } + + s = New(ms) + ) + + std.TestSetOrigCaller(voter) + + // Attempt to vote on the proposal + uassert.ErrorContains( + t, + s.VoteOnProposal(0, dao.YesVote), + ErrMissingProposal.Error(), + ) + }) + + t.Run("proposal executed", func(t *testing.T) { + t.Parallel() + + var ( + voter = testutils.TestAddress("voter") + + ms = &mockMemberStore{ + memberFn: func(a std.Address) (membstore.Member, error) { + if a != voter { + return membstore.Member{}, errors.New("not found") + } + + return membstore.Member{ + Address: voter, + }, nil + }, + } + s = New(ms) + + prop = &proposal{ + status: dao.ExecutionSuccessful, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorIs( + t, + s.VoteOnProposal(id, dao.YesVote), + ErrProposalInactive, + ) + }) + + t.Run("double vote on proposal", func(t *testing.T) { + t.Parallel() + + var ( + voter = testutils.TestAddress("voter") + member = membstore.Member{ + Address: voter, + VotingPower: 10, + } + + ms = &mockMemberStore{ + memberFn: func(a std.Address) (membstore.Member, error) { + if a != voter { + return membstore.Member{}, errors.New("not found") + } + + return member, nil + }, + } + s = New(ms) + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + tally: newTally(), + } + ) + + std.TestSetOrigCaller(voter) + + // Cast the initial vote + urequire.NoError(t, prop.tally.castVote(member, dao.YesVote)) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorContains( + t, + s.VoteOnProposal(id, dao.YesVote), + ErrAlreadyVoted.Error(), + ) + }) + + t.Run("majority accepted", func(t *testing.T) { + t.Parallel() + + var ( + members = generateMembers(t, 50) + + ms = &mockMemberStore{ + memberFn: func(address std.Address) (membstore.Member, error) { + for _, m := range members { + if m.Address == address { + return m, nil + } + } + + return membstore.Member{}, errors.New("not found") + }, + + totalPowerFn: func() uint64 { + power := uint64(0) + + for _, m := range members { + power += m.VotingPower + } + + return power + }, + } + s = New(ms) + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + tally: newTally(), + } + ) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + majorityIndex := (len(members)*2)/3 + 1 // 2/3+ + for _, m := range members[:majorityIndex] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.YesVote), + ) + } + + // Make sure the proposal was accepted + uassert.Equal(t, dao.Accepted.String(), prop.status.String()) + }) + + t.Run("majority rejected", func(t *testing.T) { + t.Parallel() + + var ( + members = generateMembers(t, 50) + + ms = &mockMemberStore{ + memberFn: func(address std.Address) (membstore.Member, error) { + for _, m := range members { + if m.Address == address { + return m, nil + } + } + + return membstore.Member{}, errors.New("member not found") + }, + + totalPowerFn: func() uint64 { + power := uint64(0) + + for _, m := range members { + power += m.VotingPower + } + + return power + }, + } + s = New(ms) + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + tally: newTally(), + } + ) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + majorityIndex := (len(members)*2)/3 + 1 // 2/3+ + for _, m := range members[:majorityIndex] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.NoVote), + ) + } + + // Make sure the proposal was not accepted + uassert.Equal(t, dao.NotAccepted.String(), prop.status.String()) + }) + + t.Run("majority abstained", func(t *testing.T) { + t.Parallel() + + var ( + members = generateMembers(t, 50) + + ms = &mockMemberStore{ + memberFn: func(address std.Address) (membstore.Member, error) { + for _, m := range members { + if m.Address == address { + return m, nil + } + } + + return membstore.Member{}, errors.New("member not found") + }, + + totalPowerFn: func() uint64 { + power := uint64(0) + + for _, m := range members { + power += m.VotingPower + } + + return power + }, + } + s = New(ms) + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + tally: newTally(), + } + ) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + majorityIndex := (len(members)*2)/3 + 1 // 2/3+ + for _, m := range members[:majorityIndex] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.AbstainVote), + ) + } + + // Make sure the proposal was not accepted + uassert.Equal(t, dao.NotAccepted.String(), prop.status.String()) + }) + + t.Run("everyone voted, undecided", func(t *testing.T) { + t.Parallel() + + var ( + members = generateMembers(t, 50) + + ms = &mockMemberStore{ + memberFn: func(address std.Address) (membstore.Member, error) { + for _, m := range members { + if m.Address == address { + return m, nil + } + } + + return membstore.Member{}, errors.New("member not found") + }, + + totalPowerFn: func() uint64 { + power := uint64(0) + + for _, m := range members { + power += m.VotingPower + } + + return power + }, + } + s = New(ms) + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + tally: newTally(), + } + ) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + // The first half votes yes + for _, m := range members[:len(members)/2] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.YesVote), + ) + } + + // The other half votes no + for _, m := range members[len(members)/2:] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.NoVote), + ) + } + + // Make sure the proposal is not active, + // since everyone voted, and it was undecided + uassert.Equal(t, dao.NotAccepted.String(), prop.status.String()) + }) + + t.Run("proposal undecided", func(t *testing.T) { + t.Parallel() + + var ( + members = generateMembers(t, 50) + + ms = &mockMemberStore{ + memberFn: func(address std.Address) (membstore.Member, error) { + for _, m := range members { + if m.Address == address { + return m, nil + } + } + + return membstore.Member{}, errors.New("member not found") + }, + + totalPowerFn: func() uint64 { + power := uint64(0) + + for _, m := range members { + power += m.VotingPower + } + + return power + }, + } + s = New(ms) + + prop = &proposal{ + status: dao.Active, + executor: &mockExecutor{}, + tally: newTally(), + } + ) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + // The first quarter votes yes + for _, m := range members[:len(members)/4] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.YesVote), + ) + } + + // The second quarter votes no + for _, m := range members[len(members)/4 : len(members)/2] { + std.TestSetOrigCaller(m.Address) + + // Attempt to vote on the proposal + urequire.NoError( + t, + s.VoteOnProposal(id, dao.NoVote), + ) + } + + // Make sure the proposal is still active, + // since there wasn't quorum reached on any decision + uassert.Equal(t, dao.Active.String(), prop.status.String()) + }) +} + +func TestSimpleDAO_ExecuteProposal(t *testing.T) { + t.Parallel() + + t.Run("caller cannot cover fee", func(t *testing.T) { + t.Parallel() + + var ( + sentCoins = std.NewCoins( + std.NewCoin( + "ugnot", + minExecuteFeeValue-1, + ), + ) + + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return false + }, + } + s = New(ms) + ) + + // Set the sent coins to be lower + // than the execute fee + std.TestSetOrigSend(sentCoins, std.Coins{}) + + uassert.ErrorIs( + t, + s.ExecuteProposal(0), + ErrInsufficientExecuteFunds, + ) + }) + + t.Run("missing proposal", func(t *testing.T) { + t.Parallel() + + var ( + sentCoins = std.NewCoins( + std.NewCoin( + "ugnot", + minExecuteFeeValue, + ), + ) + + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } + + s = New(ms) + ) + + // Set the sent coins to be enough + // so the execution can take place + std.TestSetOrigSend(sentCoins, std.Coins{}) + + uassert.ErrorContains( + t, + s.ExecuteProposal(0), + ErrMissingProposal.Error(), + ) + }) + + t.Run("proposal not accepted", func(t *testing.T) { + t.Parallel() + + var ( + voter = testutils.TestAddress("voter") + + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } + s = New(ms) + + prop = &proposal{ + status: dao.NotAccepted, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorIs( + t, + s.ExecuteProposal(id), + ErrProposalNotAccepted, + ) + }) + + t.Run("proposal already executed", func(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + status dao.ProposalStatus + }{ + { + "execution was successful", + dao.ExecutionSuccessful, + }, + { + "execution failed", + dao.ExecutionFailed, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + var ( + voter = testutils.TestAddress("voter") + + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } + s = New(ms) + + prop = &proposal{ + status: testCase.status, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorIs( + t, + s.ExecuteProposal(id), + ErrProposalExecuted, + ) + }) + } + }) + + t.Run("execution error", func(t *testing.T) { + t.Parallel() + + var ( + voter = testutils.TestAddress("voter") + + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } + + s = New(ms) + + execError = errors.New("exec error") + + mockExecutor = &mockExecutor{ + executeFn: func() error { + return execError + }, + } + + prop = &proposal{ + status: dao.Accepted, + executor: mockExecutor, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.ErrorContains( + t, + s.ExecuteProposal(id), + execError.Error(), + ) + + uassert.Equal(t, dao.ExecutionFailed.String(), prop.status.String()) + }) + + t.Run("successful execution", func(t *testing.T) { + t.Parallel() + + var ( + voter = testutils.TestAddress("voter") + + ms = &mockMemberStore{ + isMemberFn: func(_ std.Address) bool { + return true + }, + } + s = New(ms) + + called = false + mockExecutor = &mockExecutor{ + executeFn: func() error { + called = true + + return nil + }, + } + + prop = &proposal{ + status: dao.Accepted, + executor: mockExecutor, + } + ) + + std.TestSetOrigCaller(voter) + + // Add an initial proposal + id, err := s.addProposal(prop) + urequire.NoError(t, err) + + // Attempt to vote on the proposal + uassert.NoError(t, s.ExecuteProposal(id)) + uassert.Equal(t, dao.ExecutionSuccessful.String(), prop.status.String()) + uassert.True(t, called) + }) +} diff --git a/examples/gno.land/p/demo/simpledao/gno.mod b/examples/gno.land/p/demo/simpledao/gno.mod new file mode 100644 index 00000000000..51de621cbec --- /dev/null +++ b/examples/gno.land/p/demo/simpledao/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/simpledao diff --git a/examples/gno.land/p/demo/simpledao/mock_test.gno b/examples/gno.land/p/demo/simpledao/mock_test.gno new file mode 100644 index 00000000000..0cf12ccff01 --- /dev/null +++ b/examples/gno.land/p/demo/simpledao/mock_test.gno @@ -0,0 +1,97 @@ +package simpledao + +import ( + "std" + + "gno.land/p/demo/membstore" +) + +type executeDelegate func() error + +type mockExecutor struct { + executeFn executeDelegate +} + +func (m *mockExecutor) Execute() error { + if m.executeFn != nil { + return m.executeFn() + } + + return nil +} + +type ( + membersDelegate func(uint64, uint64) []membstore.Member + sizeDelegate func() int + isMemberDelegate func(std.Address) bool + totalPowerDelegate func() uint64 + memberDelegate func(std.Address) (membstore.Member, error) + addMemberDelegate func(membstore.Member) error + updateMemberDelegate func(std.Address, membstore.Member) error +) + +type mockMemberStore struct { + membersFn membersDelegate + sizeFn sizeDelegate + isMemberFn isMemberDelegate + totalPowerFn totalPowerDelegate + memberFn memberDelegate + addMemberFn addMemberDelegate + updateMemberFn updateMemberDelegate +} + +func (m *mockMemberStore) Members(offset, count uint64) []membstore.Member { + if m.membersFn != nil { + return m.membersFn(offset, count) + } + + return nil +} + +func (m *mockMemberStore) Size() int { + if m.sizeFn != nil { + return m.sizeFn() + } + + return 0 +} + +func (m *mockMemberStore) IsMember(address std.Address) bool { + if m.isMemberFn != nil { + return m.isMemberFn(address) + } + + return false +} + +func (m *mockMemberStore) TotalPower() uint64 { + if m.totalPowerFn != nil { + return m.totalPowerFn() + } + + return 0 +} + +func (m *mockMemberStore) Member(address std.Address) (membstore.Member, error) { + if m.memberFn != nil { + return m.memberFn(address) + } + + return membstore.Member{}, nil +} + +func (m *mockMemberStore) AddMember(member membstore.Member) error { + if m.addMemberFn != nil { + return m.addMemberFn(member) + } + + return nil +} + +func (m *mockMemberStore) UpdateMember(address std.Address, member membstore.Member) error { + if m.updateMemberFn != nil { + return m.updateMemberFn(address, member) + } + + return nil +} diff --git a/examples/gno.land/p/demo/simpledao/propstore.gno b/examples/gno.land/p/demo/simpledao/propstore.gno new file mode 100644 index 00000000000..91f2a883047 --- /dev/null +++ b/examples/gno.land/p/demo/simpledao/propstore.gno @@ -0,0 +1,177 @@ +package simpledao + +import ( + "errors" + "std" + "strings" + + "gno.land/p/demo/dao" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" +) + +var ErrMissingProposal = errors.New("proposal is missing") + +// maxRequestProposals is the maximum number of +// paginated proposals that can be requested +const maxRequestProposals = 10 + +// proposal is the internal simpledao proposal implementation +type proposal struct { + author std.Address // initiator of the proposal + title string // title of the proposal + description string // description of the proposal + + executor dao.Executor // executor for the proposal + status dao.ProposalStatus // status of the proposal + + tally *tally // voting tally + getTotalVotingPowerFn func() uint64 // callback for the total voting power +} + +func (p *proposal) Author() std.Address { + return p.author +} + +func (p *proposal) Title() string { + return p.title +} + +func (p *proposal) Description() string { + return p.description +} + +func (p *proposal) Status() dao.ProposalStatus { + return p.status +} + +func (p *proposal) Executor() dao.Executor { + return p.executor +} + +func (p *proposal) Stats() dao.Stats { + // Get the total voting power of the body + totalPower := p.getTotalVotingPowerFn() + + return dao.Stats{ + YayVotes: p.tally.yays, + NayVotes: p.tally.nays, + AbstainVotes: p.tally.abstains, + TotalVotingPower: totalPower, + } +} + +func (p *proposal) IsExpired() bool { + return false // this proposal never expires +} + +func (p *proposal) Render() string { + // Fetch the voting stats + stats := p.Stats() + + var out string + + out += "## Description\n\n" + if strings.TrimSpace(p.description) != "" { + out += ufmt.Sprintf("%s\n\n", p.description) + } else { + out += "No description provided.\n\n" + } + + out += "## Proposal information\n\n" + out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(p.Status().String())) + + out += ufmt.Sprintf( + "**Voting stats:**\n- YES %d (%d%%)\n- NO %d (%d%%)\n- ABSTAIN %d (%d%%)\n- MISSING VOTES %d (%d%%)\n", + stats.YayVotes, + stats.YayPercent(), + stats.NayVotes, + stats.NayPercent(), + stats.AbstainVotes, + stats.AbstainPercent(), + stats.MissingVotes(), + stats.MissingVotesPercent(), + ) + + out += "\n\n" + thresholdOut := strings.ToUpper(ufmt.Sprintf("%t", stats.YayVotes > (2*stats.TotalVotingPower)/3)) + + out += ufmt.Sprintf("**Threshold met: %s**\n\n", thresholdOut) + + return out +} + +// addProposal adds a new simpledao proposal to the store +func (s *SimpleDAO) addProposal(proposal *proposal) (uint64, error) { + // See what the next proposal number should be + nextID := uint64(s.proposals.Size()) + + // Save the proposal + s.proposals.Set(getProposalID(nextID), proposal) + + return nextID, nil +} + +func (s *SimpleDAO) Proposals(offset, count uint64) []dao.Proposal { + // Check the requested count + if count < 1 { + return []dao.Proposal{} + } + + // Limit the maximum number of returned proposals + if count > maxRequestProposals { + count = maxRequestProposals + } + + var ( + startIndex = offset + endIndex = startIndex + count + + numProposals = uint64(s.proposals.Size()) + ) + + // Check if the current offset has any proposals + if startIndex >= numProposals { + return []dao.Proposal{} + } + + // Check if the right bound is good + if endIndex > numProposals { + endIndex = numProposals + } + + props := make([]dao.Proposal, 0) + s.proposals.Iterate( + getProposalID(startIndex), + getProposalID(endIndex), + func(_ string, val interface{}) bool { + prop := val.(*proposal) + + // Save the proposal + props = append(props, prop) + + return false + }, + ) + + return props +} + +func (s *SimpleDAO) ProposalByID(id uint64) (dao.Proposal, error) { + prop, exists := s.proposals.Get(getProposalID(id)) + if !exists { + return nil, ErrMissingProposal + } + + return prop.(*proposal), nil +} + +func (s *SimpleDAO) Size() int { + return s.proposals.Size() +} + +// getProposalID generates a sequential proposal ID +// from the given ID number +func getProposalID(id uint64) string { + return seqid.ID(id).String() +} diff --git a/examples/gno.land/p/demo/simpledao/propstore_test.gno b/examples/gno.land/p/demo/simpledao/propstore_test.gno new file mode 100644 index 00000000000..5aa6ba91a1e --- /dev/null +++ b/examples/gno.land/p/demo/simpledao/propstore_test.gno @@ -0,0 +1,256 @@ +package simpledao + +import ( + "testing" + + "gno.land/p/demo/dao" + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/demo/urequire" +) + +// generateProposals generates dummy proposals +func generateProposals(t *testing.T, count int) []*proposal { + t.Helper() + + var ( + members = generateMembers(t, count) + proposals = make([]*proposal, 0, count) + ) + + for i := 0; i < count; i++ { + proposal := &proposal{ + author: members[i].Address, + description: ufmt.Sprintf("proposal %d", i), + status: dao.Active, + tally: newTally(), + getTotalVotingPowerFn: func() uint64 { + return 0 + }, + executor: nil, + } + + proposals = append(proposals, proposal) + } + + return proposals +} + +func equalProposals(t *testing.T, p1, p2 dao.Proposal) { + t.Helper() + + uassert.Equal( + t, + p1.Author().String(), + p2.Author().String(), + ) + + uassert.Equal( + t, + p1.Description(), + p2.Description(), + ) + + uassert.Equal( + t, + p1.Status().String(), + p2.Status().String(), + ) + + p1Stats := p1.Stats() + p2Stats := p2.Stats() + + uassert.Equal(t, p1Stats.YayVotes, p2Stats.YayVotes) + uassert.Equal(t, p1Stats.NayVotes, p2Stats.NayVotes) + uassert.Equal(t, p1Stats.AbstainVotes, p2Stats.AbstainVotes) + uassert.Equal(t, p1Stats.TotalVotingPower, p2Stats.TotalVotingPower) +} + +func TestProposal_Data(t *testing.T) { + t.Parallel() + + t.Run("author", func(t *testing.T) { + t.Parallel() + + p := &proposal{ + author: testutils.TestAddress("address"), + } + + uassert.Equal(t, p.author, p.Author()) + }) + + t.Run("description", func(t *testing.T) { + t.Parallel() + + p := &proposal{ + description: "example proposal description", + } + + uassert.Equal(t, p.description, p.Description()) + }) + + t.Run("status", func(t *testing.T) { + t.Parallel() + + p := &proposal{ + status: dao.ExecutionSuccessful, + } + + uassert.Equal(t, p.status.String(), p.Status().String()) + }) + + t.Run("executor", func(t *testing.T) { + t.Parallel() + + var ( + numCalled = 0 + cb = func() error { + numCalled++ + + return nil + } + + ex = &mockExecutor{ + executeFn: cb, + } + + p = &proposal{ + executor: ex, + } + ) + + urequire.NoError(t, p.executor.Execute()) + urequire.NoError(t, p.Executor().Execute()) + + uassert.Equal(t, 2, numCalled) + }) + + t.Run("no votes", func(t *testing.T) { + t.Parallel() + + p := &proposal{ + tally: newTally(), + getTotalVotingPowerFn: func() uint64 { + return 0 + }, + } + + stats := p.Stats() + + uassert.Equal(t, uint64(0), stats.YayVotes) + uassert.Equal(t, uint64(0), stats.NayVotes) + uassert.Equal(t, uint64(0), stats.AbstainVotes) + uassert.Equal(t, uint64(0), stats.TotalVotingPower) + }) + + t.Run("existing votes", func(t *testing.T) { + t.Parallel() + + var ( + members = generateMembers(t, 50) + totalPower = uint64(len(members)) * 10 + + p = &proposal{ + tally: newTally(), + getTotalVotingPowerFn: func() uint64 { + return totalPower + }, + } + ) + + for _, m := range members { + urequire.NoError(t, p.tally.castVote(m, dao.YesVote)) + } + + stats := p.Stats() + + uassert.Equal(t, totalPower, stats.YayVotes) + uassert.Equal(t, uint64(0), stats.NayVotes) + uassert.Equal(t, uint64(0), stats.AbstainVotes) + uassert.Equal(t, totalPower, stats.TotalVotingPower) + }) +} + +func TestSimpleDAO_GetProposals(t *testing.T) { + t.Parallel() + + t.Run("no proposals", func(t *testing.T) { + t.Parallel() + + s := New(nil) + + uassert.Equal(t, 0, s.Size()) + proposals := s.Proposals(0, 0) + + uassert.Equal(t, 0, len(proposals)) + }) + + t.Run("proper pagination", func(t *testing.T) { + t.Parallel() + + var ( + numProposals = 20 + halfRange = numProposals / 2 + + s = New(nil) + proposals = generateProposals(t, numProposals) + ) + + // Add initial proposals + for _, proposal := range proposals { + _, err := s.addProposal(proposal) + + urequire.NoError(t, err) + } + + uassert.Equal(t, numProposals, s.Size()) + + fetchedProposals := s.Proposals(0, uint64(halfRange)) + urequire.Equal(t, halfRange, len(fetchedProposals)) + + for index, fetchedProposal := range fetchedProposals { + equalProposals(t, proposals[index], fetchedProposal) + } + + // Fetch the other half + fetchedProposals = s.Proposals(uint64(halfRange), uint64(halfRange)) + urequire.Equal(t, halfRange, len(fetchedProposals)) + + for index, fetchedProposal := range fetchedProposals { + equalProposals(t, proposals[index+halfRange], fetchedProposal) + } + }) +} + +func TestSimpleDAO_GetProposalByID(t *testing.T) { + t.Parallel() + + t.Run("missing proposal", func(t *testing.T) { + t.Parallel() + + s := New(nil) + + _, err := s.ProposalByID(0) + uassert.ErrorIs(t, err, ErrMissingProposal) + }) + + t.Run("proposal found", func(t *testing.T) { + t.Parallel() + + var ( + s = New(nil) + proposal = generateProposals(t, 1)[0] + ) + + // Add the initial proposal + _, err := s.addProposal(proposal) + urequire.NoError(t, err) + + // Fetch the proposal + fetchedProposal, err := s.ProposalByID(0) + urequire.NoError(t, err) + + equalProposals(t, proposal, fetchedProposal) + }) +} diff --git a/examples/gno.land/p/demo/simpledao/votestore.gno b/examples/gno.land/p/demo/simpledao/votestore.gno new file mode 100644 index 00000000000..489fcaf2c0f --- /dev/null +++ b/examples/gno.land/p/demo/simpledao/votestore.gno @@ -0,0 +1,61 @@ +package simpledao + +import ( + "errors" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" +) + +var ErrAlreadyVoted = errors.New("vote already cast") + +// tally is a simple vote tally system +type tally struct { + // tally cache to keep track of active + // yes / no / abstain votes + yays uint64 + nays uint64 + abstains uint64 + + voters *avl.Tree // std.Address -> dao.VoteOption +} + +// newTally creates a new tally system instance +func newTally() *tally { + return &tally{ + voters: avl.NewTree(), + } +} + +// castVote casts a single vote in the name of the given member +func (t *tally) castVote(member membstore.Member, option dao.VoteOption) error { + // Check if the member voted already + address := member.Address.String() + + _, voted := t.voters.Get(address) + if voted { + return ErrAlreadyVoted + } + + // convert option to upper-case, like the constants are. + option = dao.VoteOption(strings.ToUpper(string(option))) + + // Update the tally + switch option { + case dao.YesVote: + t.yays += member.VotingPower + case dao.AbstainVote: + t.abstains += member.VotingPower + case dao.NoVote: + t.nays += member.VotingPower + default: + panic("invalid voting option: " + option) + } + + // Save the voting status + t.voters.Set(address, option) + + return nil +} diff --git a/examples/gno.land/p/demo/subscription/lifetime/gno.mod b/examples/gno.land/p/demo/subscription/lifetime/gno.mod index 0084aa714c5..59b6c1cf001 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/gno.mod +++ b/examples/gno.land/p/demo/subscription/lifetime/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/subscription/lifetime - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno index 8a4c10b687b..be661e70129 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno @@ -67,7 +67,7 @@ func (ls *LifetimeSubscription) HasValidSubscription(addr std.Address) error { // UpdateAmount allows the owner of the LifetimeSubscription contract to update the subscription price. func (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error { - if err := ls.CallerIsOwner(); err != nil { + if !ls.CallerIsOwner() { return ErrNotAuthorized } diff --git a/examples/gno.land/p/demo/subscription/recurring/gno.mod b/examples/gno.land/p/demo/subscription/recurring/gno.mod index d3cf8a044f8..356402978b5 100644 --- a/examples/gno.land/p/demo/subscription/recurring/gno.mod +++ b/examples/gno.land/p/demo/subscription/recurring/gno.mod @@ -1,8 +1 @@ module gno.land/p/demo/subscription/recurring - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/subscription/recurring/recurring.gno b/examples/gno.land/p/demo/subscription/recurring/recurring.gno index b5277bd716e..8f116009aa6 100644 --- a/examples/gno.land/p/demo/subscription/recurring/recurring.gno +++ b/examples/gno.land/p/demo/subscription/recurring/recurring.gno @@ -90,7 +90,7 @@ func (rs *RecurringSubscription) GetExpiration(addr std.Address) (time.Time, err // UpdateAmount allows the owner of the subscription contract to change the required subscription amount. func (rs *RecurringSubscription) UpdateAmount(newAmount int64) error { - if err := rs.CallerIsOwner(); err != nil { + if !rs.CallerIsOwner() { return ErrNotAuthorized } diff --git a/examples/gno.land/p/demo/svg/gno.mod b/examples/gno.land/p/demo/svg/gno.mod index 0af7ba0636d..b9dd7f47434 100644 --- a/examples/gno.land/p/demo/svg/gno.mod +++ b/examples/gno.land/p/demo/svg/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/svg - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/tamagotchi/gno.mod b/examples/gno.land/p/demo/tamagotchi/gno.mod index 58441284a6b..a9c6026629e 100644 --- a/examples/gno.land/p/demo/tamagotchi/gno.mod +++ b/examples/gno.land/p/demo/tamagotchi/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/tamagotchi - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno b/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno index 4b2c04b6d5c..17d6c466ed5 100644 --- a/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno +++ b/examples/gno.land/p/demo/tamagotchi/z0_filetest.gno @@ -44,6 +44,7 @@ func main() { } // Output: +// // -- INITIAL // // # Gnome 😃 diff --git a/examples/gno.land/p/demo/tests/gno.mod b/examples/gno.land/p/demo/tests/gno.mod index d3d796f76f8..a342a726f61 100644 --- a/examples/gno.land/p/demo/tests/gno.mod +++ b/examples/gno.land/p/demo/tests/gno.mod @@ -1,7 +1 @@ module gno.land/p/demo/tests - -require ( - gno.land/p/demo/tests/subtests v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/r/demo/tests v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/tests/tests.gno b/examples/gno.land/p/demo/tests/tests.gno index 43732d82dac..ffad5b8c8cd 100644 --- a/examples/gno.land/p/demo/tests/tests.gno +++ b/examples/gno.land/p/demo/tests/tests.gno @@ -4,19 +4,10 @@ import ( "std" psubtests "gno.land/p/demo/tests/subtests" - "gno.land/r/demo/tests" - rtests "gno.land/r/demo/tests" ) const World = "world" -// IncCounter demonstrates that it's possible to call a realm function from -// a package. So a package can potentially write into the store, by calling -// an other realm. -func IncCounter() { - tests.IncCounter() -} - func CurrentRealmPath() string { return std.CurrentRealm().PkgPath() } @@ -64,10 +55,6 @@ func GetPSubtestsPrevRealm() std.Realm { return psubtests.GetPrevRealm() } -func GetRTestsGetPrevRealm() std.Realm { - return rtests.GetPrevRealm() -} - // Warning: unsafe pattern. func Exec(fn func()) { fn() diff --git a/examples/gno.land/p/demo/tests/z0_filetest.gno b/examples/gno.land/p/demo/tests/z0_filetest.gno deleted file mode 100644 index b788eaf398f..00000000000 --- a/examples/gno.land/p/demo/tests/z0_filetest.gno +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - ptests "gno.land/p/demo/tests" - rtests "gno.land/r/demo/tests" -) - -func main() { - println(rtests.Counter()) - ptests.IncCounter() - println(rtests.Counter()) -} - -// Output: -// 0 -// 1 diff --git a/examples/gno.land/p/demo/testutils/crypto_test.gno b/examples/gno.land/p/demo/testutils/crypto_test.gno new file mode 100644 index 00000000000..ac77b76dadf --- /dev/null +++ b/examples/gno.land/p/demo/testutils/crypto_test.gno @@ -0,0 +1,12 @@ +package testutils + +import ( + "testing" + + "gno.land/p/demo/uassert" +) + +func TestTestAddress(t *testing.T) { + testAddr := TestAddress("author1") + uassert.Equal(t, "g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6", string(testAddr)) +} diff --git a/examples/gno.land/p/demo/todolist/gno.mod b/examples/gno.land/p/demo/todolist/gno.mod index bbccf357e3b..46d21bf0bc0 100644 --- a/examples/gno.land/p/demo/todolist/gno.mod +++ b/examples/gno.land/p/demo/todolist/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/todolist - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest -) diff --git a/examples/gno.land/p/demo/uassert/gno.mod b/examples/gno.land/p/demo/uassert/gno.mod index f22276564bf..a70e7db825d 100644 --- a/examples/gno.land/p/demo/uassert/gno.mod +++ b/examples/gno.land/p/demo/uassert/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/uassert - -require gno.land/p/demo/diff v0.0.0-latest diff --git a/examples/gno.land/p/demo/uassert/uassert.gno b/examples/gno.land/p/demo/uassert/uassert.gno index 2776e93dca9..f9c0ab3efc8 100644 --- a/examples/gno.land/p/demo/uassert/uassert.gno +++ b/examples/gno.land/p/demo/uassert/uassert.gno @@ -266,7 +266,7 @@ func NotEqual(t TestingT, expected, actual interface{}, msgs ...string) bool { if av, ok := actual.(string); ok { notEqual = ev != av ok_ = true - es, as = ev, as + es, as = ev, av } case std.Address: if av, ok := actual.(std.Address); ok { diff --git a/examples/gno.land/p/demo/ufmt/ufmt.gno b/examples/gno.land/p/demo/ufmt/ufmt.gno index 55494e32cec..c9acee1c910 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt.gno @@ -22,6 +22,8 @@ func Println(args ...interface{}) { strs = append(strs, v.String()) case error: strs = append(strs, v.Error()) + case float64: + strs = append(strs, Sprintf("%f", v)) case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: strs = append(strs, Sprintf("%d", v)) case bool: @@ -49,15 +51,28 @@ func Println(args ...interface{}) { // // The currently formatted verbs are the following: // -// %s: places a string value directly. -// If the value implements the interface interface{ String() string }, -// the String() method is called to retrieve the value. Same about Error() -// string. -// %c: formats the character represented by Unicode code point -// %d: formats an integer value using package "strconv". -// Currently supports only uint, uint64, int, int64. -// %t: formats a boolean value to "true" or "false". -// %%: outputs a literal %. Does not consume an argument. +// %s: places a string value directly. +// If the value implements the interface interface{ String() string }, +// the String() method is called to retrieve the value. Same about Error() +// string. +// %c: formats the character represented by Unicode code point +// %d: formats an integer value using package "strconv". +// Currently supports only uint, uint64, int, int64. +// %f: formats a float value, with a default precision of 6. +// %e: formats a float with scientific notation; 1.23456e+78 +// %E: formats a float with scientific notation; 1.23456E+78 +// %F: The same as %f +// %g: formats a float value with %e for large exponents, and %f with full precision for smaller numbers +// %G: formats a float value with %G for large exponents, and %F with full precision for smaller numbers +// %t: formats a boolean value to "true" or "false". +// %x: formats an integer value as a hexadecimal string. +// Currently supports only uint8, []uint8, [32]uint8. +// %c: formats a rune value as a string. +// Currently supports only rune, int. +// %q: formats a string value as a quoted string. +// %T: formats the type of the value. +// %v: formats the value with a default representation appropriate for the value's type +// %%: outputs a literal %. Does not consume an argument. func Sprintf(format string, args ...interface{}) string { // we use runes to handle multi-byte characters sTor := []rune(format) @@ -91,6 +106,51 @@ func Sprintf(format string, args ...interface{}) string { argNum++ switch verb { + case "v": + switch v := arg.(type) { + case nil: + buf += "" + case bool: + if v { + buf += "true" + } else { + buf += "false" + } + case int: + buf += strconv.Itoa(v) + case int8: + buf += strconv.Itoa(int(v)) + case int16: + buf += strconv.Itoa(int(v)) + case int32: + buf += strconv.Itoa(int(v)) + case int64: + buf += strconv.Itoa(int(v)) + case uint: + buf += strconv.FormatUint(uint64(v), 10) + case uint8: + buf += strconv.FormatUint(uint64(v), 10) + case uint16: + buf += strconv.FormatUint(uint64(v), 10) + case uint32: + buf += strconv.FormatUint(uint64(v), 10) + case uint64: + buf += strconv.FormatUint(v, 10) + case float64: + buf += strconv.FormatFloat(v, 'g', -1, 64) + case string: + buf += v + case []byte: + buf += string(v) + case []rune: + buf += string(v) + case interface{ String() string }: + buf += v.String() + case error: + buf += v.Error() + default: + buf += fallback(verb, v) + } case "s": switch v := arg.(type) { case interface{ String() string }: @@ -147,6 +207,24 @@ func Sprintf(format string, args ...interface{}) string { default: buf += fallback(verb, v) } + case "e", "E", "f", "F", "g", "G": + switch v := arg.(type) { + case float64: + switch verb { + case "e": + buf += strconv.FormatFloat(v, byte('e'), -1, 64) + case "E": + buf += strconv.FormatFloat(v, byte('E'), -1, 64) + case "f", "F": + buf += strconv.FormatFloat(v, byte('f'), 6, 64) + case "g": + buf += strconv.FormatFloat(v, byte('g'), -1, 64) + case "G": + buf += strconv.FormatFloat(v, byte('G'), -1, 64) + } + default: + buf += fallback(verb, v) + } case "t": switch v := arg.(type) { case bool: @@ -158,6 +236,53 @@ func Sprintf(format string, args ...interface{}) string { default: buf += fallback(verb, v) } + case "x": + switch v := arg.(type) { + case uint8: + buf += strconv.FormatUint(uint64(v), 16) + default: + buf += "(unhandled)" + } + case "q": + switch v := arg.(type) { + case string: + buf += strconv.Quote(v) + default: + buf += "(unhandled)" + } + case "T": + switch arg.(type) { + case bool: + buf += "bool" + case int: + buf += "int" + case int8: + buf += "int8" + case int16: + buf += "int16" + case int32: + buf += "int32" + case int64: + buf += "int64" + case uint: + buf += "uint" + case uint8: + buf += "uint8" + case uint16: + buf += "uint16" + case uint32: + buf += "uint32" + case uint64: + buf += "uint64" + case string: + buf += "string" + case []byte: + buf += "[]byte" + case []rune: + buf += "[]rune" + default: + buf += "unknown" + } // % handled before, as it does not consume an argument default: buf += "(unhandled verb: %" + verb + ")" @@ -191,6 +316,8 @@ func fallback(verb string, arg interface{}) string { case error: // note: also "string=" in Go fmt s = "string=" + v.Error() + case float64: + s = "float64=" + Sprintf("%f", v) case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: // note: rune, byte would be dups, being aliases if typename, e := typeToString(v); e != nil { diff --git a/examples/gno.land/p/demo/ufmt/ufmt_test.gno b/examples/gno.land/p/demo/ufmt/ufmt_test.gno index d53fb39bc44..1a4d4e7e6f2 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt_test.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt_test.gno @@ -20,27 +20,58 @@ func TestSprintf(t *testing.T) { expectedOutput string }{ {"hello %s!", []interface{}{"planet"}, "hello planet!"}, + {"hello %v!", []interface{}{"planet"}, "hello planet!"}, {"hi %%%s!", []interface{}{"worl%d"}, "hi %worl%d!"}, {"%s %c %d %t", []interface{}{"foo", 'α', 421, true}, "foo α 421 true"}, {"string [%s]", []interface{}{"foo"}, "string [foo]"}, {"int [%d]", []interface{}{int(42)}, "int [42]"}, + {"int [%v]", []interface{}{int(42)}, "int [42]"}, {"int8 [%d]", []interface{}{int8(8)}, "int8 [8]"}, + {"int8 [%v]", []interface{}{int8(8)}, "int8 [8]"}, {"int16 [%d]", []interface{}{int16(16)}, "int16 [16]"}, + {"int16 [%v]", []interface{}{int16(16)}, "int16 [16]"}, {"int32 [%d]", []interface{}{int32(32)}, "int32 [32]"}, + {"int32 [%v]", []interface{}{int32(32)}, "int32 [32]"}, {"int64 [%d]", []interface{}{int64(64)}, "int64 [64]"}, + {"int64 [%v]", []interface{}{int64(64)}, "int64 [64]"}, {"uint [%d]", []interface{}{uint(42)}, "uint [42]"}, + {"uint [%v]", []interface{}{uint(42)}, "uint [42]"}, {"uint8 [%d]", []interface{}{uint8(8)}, "uint8 [8]"}, + {"uint8 [%v]", []interface{}{uint8(8)}, "uint8 [8]"}, {"uint16 [%d]", []interface{}{uint16(16)}, "uint16 [16]"}, + {"uint16 [%v]", []interface{}{uint16(16)}, "uint16 [16]"}, {"uint32 [%d]", []interface{}{uint32(32)}, "uint32 [32]"}, + {"uint32 [%v]", []interface{}{uint32(32)}, "uint32 [32]"}, {"uint64 [%d]", []interface{}{uint64(64)}, "uint64 [64]"}, + {"uint64 [%v]", []interface{}{uint64(64)}, "uint64 [64]"}, + {"float64 [%e]", []interface{}{float64(64.1)}, "float64 [6.41e+01]"}, + {"float64 [%E]", []interface{}{float64(64.1)}, "float64 [6.41E+01]"}, + {"float64 [%f]", []interface{}{float64(64.1)}, "float64 [64.100000]"}, + {"float64 [%F]", []interface{}{float64(64.1)}, "float64 [64.100000]"}, + {"float64 [%g]", []interface{}{float64(64.1)}, "float64 [64.1]"}, + {"float64 [%G]", []interface{}{float64(64.1)}, "float64 [64.1]"}, {"bool [%t]", []interface{}{true}, "bool [true]"}, + {"bool [%v]", []interface{}{true}, "bool [true]"}, {"bool [%t]", []interface{}{false}, "bool [false]"}, + {"bool [%v]", []interface{}{false}, "bool [false]"}, {"no args", nil, "no args"}, {"finish with %", nil, "finish with %"}, {"stringer [%s]", []interface{}{stringer{}}, "stringer [I'm a stringer]"}, {"â", nil, "â"}, {"Hello, World! 😊", nil, "Hello, World! 😊"}, {"unicode formatting: %s", []interface{}{"😊"}, "unicode formatting: 😊"}, + {"invalid hex [%x]", []interface{}{"invalid"}, "invalid hex [(unhandled)]"}, + {"rune as character [%c]", []interface{}{rune('A')}, "rune as character [A]"}, + {"int as character [%c]", []interface{}{int('B')}, "int as character [B]"}, + {"quoted string [%q]", []interface{}{"hello"}, "quoted string [\"hello\"]"}, + {"quoted string with escape [%q]", []interface{}{"\thello\nworld\\"}, "quoted string with escape [\"\\thello\\nworld\\\\\"]"}, + {"invalid quoted string [%q]", []interface{}{123}, "invalid quoted string [(unhandled)]"}, + {"type of bool [%T]", []interface{}{true}, "type of bool [bool]"}, + {"type of int [%T]", []interface{}{123}, "type of int [int]"}, + {"type of string [%T]", []interface{}{"hello"}, "type of string [string]"}, + {"type of []byte [%T]", []interface{}{[]byte{1, 2, 3}}, "type of []byte [[]byte]"}, + {"type of []rune [%T]", []interface{}{[]rune{'a', 'b', 'c'}}, "type of []rune [[]rune]"}, + {"type of unknown [%T]", []interface{}{struct{}{}}, "type of unknown [unknown]"}, // mismatch printing {"%s", []interface{}{nil}, "%!s()"}, {"%s", []interface{}{421}, "%!s(int=421)"}, diff --git a/examples/gno.land/p/demo/uint256/arithmetic_test.gno b/examples/gno.land/p/demo/uint256/arithmetic_test.gno index 9f45a507754..addd33db997 100644 --- a/examples/gno.land/p/demo/uint256/arithmetic_test.gno +++ b/examples/gno.land/p/demo/uint256/arithmetic_test.gno @@ -1,6 +1,8 @@ package uint256 -import "testing" +import ( + "testing" +) type binOp2Test struct { x, y, want string @@ -16,30 +18,45 @@ func TestAdd(t *testing.T) { {"18446744073709551615", "18446744073709551615", "36893488147419103230"}, // uint64 overflow } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } + for _, tt := range tests { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } + want := MustFromDecimal(tt.want) + got := new(Uint).Add(x, y) - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue + if got.Neq(want) { + t.Errorf("Add(%s, %s) = %v, want %v", tt.x, tt.y, got.String(), want.String()) } + } +} - got := &Uint{} - got.Add(x, y) +func TestAddOverflow(t *testing.T) { + tests := []struct { + x, y string + want string + overflow bool + }{ + {"0", "1", "1", false}, + {"1", "0", "1", false}, + {"1", "1", "2", false}, + {"10", "10", "20", false}, + {"18446744073709551615", "18446744073709551615", "36893488147419103230", false}, // uint64 overflow, but not Uint256 overflow + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "0", true}, // 2^256 - 1 + 1, should overflow + {"57896044618658097711785492504343953926634992332820282019728792003956564819967", "57896044618658097711785492504343953926634992332820282019728792003956564819968", "115792089237316195423570985008687907853269984665640564039457584007913129639935", false}, // (2^255 - 1) + 2^255, no overflow + {"57896044618658097711785492504343953926634992332820282019728792003956564819967", "57896044618658097711785492504343953926634992332820282019728792003956564819969", "0", true}, // (2^255 - 1) + (2^255 + 1), should overflow + } - if got.Neq(want) { - t.Errorf("Add(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + for _, tt := range tests { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + want, _ := FromDecimal(tt.want) + + got, overflow := new(Uint).AddOverflow(x, y) + + if got.Cmp(want) != 0 || overflow != tt.overflow { + t.Errorf("AddOverflow(%s, %s) = (%s, %v), want (%s, %v)", + tt.x, tt.y, got.String(), overflow, tt.want, tt.overflow) } } } @@ -50,33 +67,53 @@ func TestSub(t *testing.T) { {"1", "1", "0"}, {"10", "10", "0"}, {"31337", "1337", "30000"}, - {"2", "3", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, // underflow + {"2", "3", twoPow256Sub1}, // underflow } for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } + want := MustFromDecimal(tc.want) - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue + got := new(Uint).Sub(x, y) + + if got.Neq(want) { + t.Errorf( + "Sub(%s, %s) = %v, want %v", + tc.x, tc.y, got.String(), want.String(), + ) } + } +} - got := &Uint{} - got.Sub(x, y) +func TestSubOverflow(t *testing.T) { + tests := []struct { + x, y string + want string + overflow bool + }{ + {"1", "0", "1", false}, + {"1", "1", "0", false}, + {"10", "10", "0", false}, + {"31337", "1337", "30000", false}, + {"0", "1", "115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, // 0 - 1, should underflow + {"57896044618658097711785492504343953926634992332820282019728792003956564819968", "1", "57896044618658097711785492504343953926634992332820282019728792003956564819967", false}, // 2^255 - 1, no underflow + {"57896044618658097711785492504343953926634992332820282019728792003956564819968", "57896044618658097711785492504343953926634992332820282019728792003956564819969", "115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, // 2^255 - (2^255 + 1), should underflow + } - if got.Neq(want) { - t.Errorf("Sub(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + for _, tc := range tests { + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) + want := MustFromDecimal(tc.want) + + got, overflow := new(Uint).SubOverflow(x, y) + + if got.Cmp(want) != 0 || overflow != tc.overflow { + t.Errorf( + "SubOverflow(%s, %s) = (%s, %v), want (%s, %v)", + tc.x, tc.y, got.String(), overflow, tc.want, tc.overflow, + ) } } } @@ -89,30 +126,50 @@ func TestMul(t *testing.T) { {"18446744073709551615", "2", "36893488147419103230"}, // uint64 overflow } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } + for _, tt := range tests { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + want := MustFromDecimal(tt.want) + got := new(Uint).Mul(x, y) - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue + if got.Neq(want) { + t.Errorf("Mul(%s, %s) = %v, want %v", tt.x, tt.y, got.String(), want.String()) } + } +} - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } +func TestMulOverflow(t *testing.T) { + tests := []struct { + x string + y string + wantZ string + wantOver bool + }{ + {"0x1", "0x1", "0x1", false}, + {"0x0", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x0", false}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x2", "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", true}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x1", true}, + {"0x8000000000000000000000000000000000000000000000000000000000000000", "0x2", "0x0", true}, + {"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x2", "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", false}, + {"0x100000000000000000", "0x100000000000000000", "0x10000000000000000000000000000000000", false}, + {"0x10000000000000000000000000000000", "0x10000000000000000000000000000000", "0x100000000000000000000000000000000000000000000000000000000000000", false}, + } - got := &Uint{} - got.Mul(x, y) + for _, tt := range tests { + x := MustFromHex(tt.x) + y := MustFromHex(tt.y) + wantZ := MustFromHex(tt.wantZ) - if got.Neq(want) { - t.Errorf("Mul(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + gotZ, gotOver := new(Uint).MulOverflow(x, y) + + if gotZ.Neq(wantZ) { + t.Errorf( + "MulOverflow(%s, %s) = %s, want %s", + tt.x, tt.y, gotZ.String(), wantZ.String(), + ) + } + if gotOver != tt.wantOver { + t.Errorf("MulOverflow(%s, %s) = %v, want %v", tt.x, tt.y, gotOver, tt.wantOver) } } } @@ -123,32 +180,19 @@ func TestDiv(t *testing.T) { {"31337", "0", "0"}, {"0", "31337", "0"}, {"1", "1", "1"}, + {"1000000000000000000", "3", "333333333333333333"}, + {twoPow256Sub1, "2", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } + for _, tt := range tests { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + want := MustFromDecimal(tt.want) - got := &Uint{} - got.Div(x, y) + got := new(Uint).Div(x, y) if got.Neq(want) { - t.Errorf("Div(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Div(%s, %s) = %v, want %v", tt.x, tt.y, got.String(), want.String()) } } } @@ -160,32 +204,56 @@ func TestMod(t *testing.T) { {"0", "31337", "0"}, {"2", "31337", "2"}, {"1", "1", "0"}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "2", "1"}, // 2^256 - 1 mod 2 + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "3", "0"}, // 2^256 - 1 mod 3 + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "57896044618658097711785492504343953926634992332820282019728792003956564819968", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, // 2^256 - 1 mod 2^255 } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } + for _, tt := range tests { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + want := MustFromDecimal(tt.want) - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } + got := new(Uint).Mod(x, y) - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue + if got.Neq(want) { + t.Errorf("Mod(%s, %s) = %v, want %v", tt.x, tt.y, got.String(), want.String()) } + } +} - got := &Uint{} - got.Mod(x, y) +func TestMulMod(t *testing.T) { + tests := []struct { + x string + y string + m string + want string + }{ + {"0x1", "0x1", "0x2", "0x1"}, + {"0x10", "0x10", "0x7", "0x4"}, + {"0x100", "0x100", "0x17", "0x9"}, + {"0x31337", "0x31337", "0x31338", "0x1"}, + {"0x0", "0x31337", "0x31338", "0x0"}, + {"0x31337", "0x0", "0x31338", "0x0"}, + {"0x2", "0x3", "0x5", "0x1"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0x0"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe", "0x1"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", "0xffffffffffffffffffffffffffffffff", "0x0"}, + } + + for _, tt := range tests { + x := MustFromHex(tt.x) + y := MustFromHex(tt.y) + m := MustFromHex(tt.m) + want := MustFromHex(tt.want) + + got := new(Uint).MulMod(x, y, m) if got.Neq(want) { - t.Errorf("Mod(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf( + "MulMod(%s, %s, %s) = %s, want %s", + tt.x, tt.y, tt.m, got.String(), want.String(), + ) } } } @@ -206,30 +274,11 @@ func TestDivMod(t *testing.T) { {"2", "31337", "0", "2"}, } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - wantDiv, err := FromDecimal(tc.wantDiv) - if err != nil { - t.Error(err) - continue - } - - wantMod, err := FromDecimal(tc.wantMod) - if err != nil { - t.Error(err) - continue - } + for _, tt := range tests { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + wantDiv := MustFromDecimal(tt.wantDiv) + wantMod := MustFromDecimal(tt.wantMod) gotDiv := new(Uint) gotMod := new(Uint) @@ -237,13 +286,13 @@ func TestDivMod(t *testing.T) { for i := range gotDiv.arr { if gotDiv.arr[i] != wantDiv.arr[i] { - t.Errorf("DivMod(%s, %s) got Div %v, want Div %v", tc.x, tc.y, gotDiv, wantDiv) + t.Errorf("DivMod(%s, %s) got Div %v, want Div %v", tt.x, tt.y, gotDiv, wantDiv) break } } for i := range gotMod.arr { if gotMod.arr[i] != wantMod.arr[i] { - t.Errorf("DivMod(%s, %s) got Mod %v, want Mod %v", tc.x, tc.y, gotMod, wantMod) + t.Errorf("DivMod(%s, %s) got Mod %v, want Mod %v", tt.x, tt.y, gotMod, wantMod) break } } @@ -259,27 +308,17 @@ func TestNeg(t *testing.T) { {"115792089237316195423570985008687907853269984665640564039457584007913129608599", "31337"}, {"0", "0"}, {"2", "115792089237316195423570985008687907853269984665640564039457584007913129639934"}, - {"1", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + {"1", twoPow256Sub1}, } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } + for _, tt := range tests { + x := MustFromDecimal(tt.x) + want := MustFromDecimal(tt.want) - got := &Uint{} - got.Neg(x) + got := new(Uint).Neg(x) if got.Neq(want) { - t.Errorf("Neg(%s) = %v, want %v", tc.x, got.ToString(), want.ToString()) + t.Errorf("Neg(%s) = %v, want %v", tt.x, got.String(), want.String()) } } } @@ -297,30 +336,57 @@ func TestExp(t *testing.T) { {"2", "256", "0"}, // overflow } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } + for _, tt := range tests { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + want := MustFromDecimal(tt.want) - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } + got := new(Uint).Exp(x, y) - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue + if got.Neq(want) { + t.Errorf( + "Exp(%s, %s) = %v, want %v", + tt.x, tt.y, got.String(), want.String(), + ) } + } +} + +func TestExp_LargeExponent(t *testing.T) { + tests := []struct { + name string + base string + exponent string + expected string + }{ + { + name: "2^129", + base: "2", + exponent: "680564733841876926926749214863536422912", + expected: "0", + }, + { + name: "2^193", + base: "2", + exponent: "12379400392853802746563808384000000000000000000", + expected: "0", + }, + } - got := &Uint{} - got.Exp(x, y) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + base := MustFromDecimal(tt.base) + exponent := MustFromDecimal(tt.exponent) + expected := MustFromDecimal(tt.expected) - if got.Neq(want) { - t.Errorf("Exp(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } + result := new(Uint).Exp(base, exponent) + + if result.Neq(expected) { + t.Errorf( + "Test %s failed. Expected %s, got %s", + tt.name, expected.String(), result.String(), + ) + } + }) } } diff --git a/examples/gno.land/p/demo/uint256/bitwise_test.gno b/examples/gno.land/p/demo/uint256/bitwise_test.gno index aba89edfabf..45118af0b0f 100644 --- a/examples/gno.land/p/demo/uint256/bitwise_test.gno +++ b/examples/gno.land/p/demo/uint256/bitwise_test.gno @@ -37,11 +37,14 @@ func TestOr(t *testing.T) { }, } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - res := new(Uint).Or(&tc.x, &tc.y) - if *res != tc.want { - t.Errorf("Or(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := new(Uint).Or(&tt.x, &tt.y) + if *res != tt.want { + t.Errorf( + "Or(%s, %s) = %s, want %s", + tt.x.String(), tt.y.String(), res.String(), (tt.want).String(), + ) } }) } @@ -93,11 +96,14 @@ func TestAnd(t *testing.T) { }, } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - res := new(Uint).And(&tc.x, &tc.y) - if *res != tc.want { - t.Errorf("And(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := new(Uint).And(&tt.x, &tt.y) + if *res != tt.want { + t.Errorf( + "And(%s, %s) = %s, want %s", + tt.x.String(), tt.y.String(), res.String(), (tt.want).String(), + ) } }) } @@ -126,11 +132,14 @@ func TestNot(t *testing.T) { }, } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - res := new(Uint).Not(&tc.x) - if *res != tc.want { - t.Errorf("Not(%s) = %s, want %s", tc.x.ToString(), res.ToString(), (tc.want).ToString()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := new(Uint).Not(&tt.x) + if *res != tt.want { + t.Errorf( + "Not(%s) = %s, want %s", + tt.x.String(), res.String(), (tt.want).String(), + ) } }) } @@ -182,11 +191,14 @@ func TestAndNot(t *testing.T) { }, } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - res := new(Uint).AndNot(&tc.x, &tc.y) - if *res != tc.want { - t.Errorf("AndNot(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := new(Uint).AndNot(&tt.x, &tt.y) + if *res != tt.want { + t.Errorf( + "AndNot(%s, %s) = %s, want %s", + tt.x.String(), tt.y.String(), res.String(), (tt.want).String(), + ) } }) } @@ -238,11 +250,14 @@ func TestXor(t *testing.T) { }, } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - res := new(Uint).Xor(&tc.x, &tc.y) - if *res != tc.want { - t.Errorf("Xor(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := new(Uint).Xor(&tt.x, &tt.y) + if *res != tt.want { + t.Errorf( + "Xor(%s, %s) = %s, want %s", + tt.x.String(), tt.y.String(), res.String(), (tt.want).String(), + ) } }) } @@ -272,26 +287,31 @@ func TestLsh(t *testing.T) { {"31337", 193, "393411074163624830192644266310117284962799025126338899061243904"}, {"31337", 255, "57896044618658097711785492504343953926634992332820282019728792003956564819968"}, {"31337", 256, "0"}, - } + // 64 < n < 128 + {"1", 65, "36893488147419103232"}, + {"31337", 100, "39724366859352024754702188346867712"}, - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } + // 128 < n < 192 + {"1", 129, "680564733841876926926749214863536422912"}, + {"31337", 150, "44725660946326664792723507424638829088826130956288"}, - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } + // 192 < n < 256 + {"1", 193, "12554203470773361527671578846415332832204710888928069025792"}, + {"31337", 200, "50356617492943978264658466087695012475238275216171379079839219712"}, - got := &Uint{} - got.Lsh(x, tc.y) + // n > 256 + {"1", 257, "0"}, + {"31337", 300, "0"}, + } + + for _, tt := range tests { + x := MustFromDecimal(tt.x) + want := MustFromDecimal(tt.want) + + got := new(Uint).Lsh(x, tt.y) if got.Neq(want) { - t.Errorf("Lsh(%s, %d) = %s, want %s", tc.x, tc.y, got.ToString(), want.ToString()) + t.Errorf("Lsh(%s, %d) = %s, want %s", tt.x, tt.y, got.String(), want.String()) } } } @@ -319,26 +339,85 @@ func TestRsh(t *testing.T) { {"196705537081812415096322133155058642481399512563169449530621952", 192, "31337"}, {"10663428532201448629551770073089320442396672", 128, "31337"}, {"578065619037836218990592", 64, "31337"}, + {twoPow256Sub1, 256, "0"}, + // outliers + {"340282366920938463463374607431768211455", 129, "0"}, + {"18446744073709551615", 65, "0"}, + {twoPow256Sub1, 1, "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, + + // n > 256 + {"1", 257, "0"}, + {"31337", 300, "0"}, } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } + for _, tt := range tests { + x := MustFromDecimal(tt.x) + + want := MustFromDecimal(tt.want) + got := new(Uint).Rsh(x, tt.y) - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue + if got.Neq(want) { + t.Errorf("Rsh(%s, %d) = %s, want %s", tt.x, tt.y, got.String(), want.String()) } + } +} + +func TestSRsh(t *testing.T) { + tests := []struct { + x string + y uint + want string + }{ + // Positive numbers (behaves like Rsh) + {"0x0", 0, "0x0"}, + {"0x0", 1, "0x0"}, + {"0x1", 0, "0x1"}, + {"0x1", 1, "0x0"}, + {"0x31337", 0, "0x31337"}, + {"0x31337", 4, "0x3133"}, + {"0x31337", 8, "0x313"}, + {"0x31337", 16, "0x3"}, + {"0x10000000000000000", 64, "0x1"}, // 2^64 >> 64 - got := &Uint{} - got.Rsh(x, tc.y) + // // Numbers with MSB set (negative numbers in two's complement) + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 0, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 1, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 4, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 64, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 128, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 192, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 255, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, - if got.Neq(want) { - t.Errorf("Rsh(%s, %d) = %s, want %s", tc.x, tc.y, got.ToString(), want.ToString()) + // Large positive number close to max value + {"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 1, "0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 2, "0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 64, "0x7fffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 128, "0x7fffffffffffffffffffffffffffffff"}, + {"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 192, "0x7fffffffffffffff"}, + {"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 255, "0x0"}, + + // Specific cases + {"0x8000000000000000000000000000000000000000000000000000000000000000", 1, "0xc000000000000000000000000000000000000000000000000000000000000000"}, + {"0x8000000000000000000000000000000000000000000000000000000000000001", 1, "0xc000000000000000000000000000000000000000000000000000000000000000"}, + + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 65, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 127, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 129, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 193, "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}, + + // n > 256 + {"0x1", 257, "0x0"}, + {"0x31337", 300, "0x0"}, + } + + for _, tt := range tests { + x := MustFromHex(tt.x) + want := MustFromHex(tt.want) + + got := new(Uint).SRsh(x, tt.y) + + if !got.Eq(want) { + t.Errorf("SRsh(%s, %d) = %s, want %s", tt.x, tt.y, got.String(), want.String()) } } } diff --git a/examples/gno.land/p/demo/uint256/cmp_test.gno b/examples/gno.land/p/demo/uint256/cmp_test.gno index 930079f70f0..05243290271 100644 --- a/examples/gno.land/p/demo/uint256/cmp_test.gno +++ b/examples/gno.land/p/demo/uint256/cmp_test.gno @@ -5,6 +5,39 @@ import ( "testing" ) +func TestSign(t *testing.T) { + tests := []struct { + input *Uint + expected int + }{ + { + input: NewUint(0), + expected: 0, + }, + { + input: NewUint(1), + expected: 1, + }, + { + input: NewUint(0x7fffffffffffffff), + expected: 1, + }, + { + input: NewUint(0x8000000000000000), + expected: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.input.String(), func(t *testing.T) { + result := tt.input.Sign() + if result != tt.expected { + t.Errorf("Sign() = %d; want %d", result, tt.expected) + } + }) + } +} + func TestCmp(t *testing.T) { tests := []struct { x, y string @@ -20,17 +53,8 @@ func TestCmp(t *testing.T) { } for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) got := x.Cmp(y) if got != tc.want { @@ -49,16 +73,12 @@ func TestIsZero(t *testing.T) { {"10", false}, } - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } + for _, tt := range tests { + x := MustFromDecimal(tt.x) got := x.IsZero() - if got != tc.want { - t.Errorf("IsZero(%s) = %v, want %v", tc.x, got, tc.want) + if got != tt.want { + t.Errorf("IsZero(%s) = %v, want %v", tt.x, got, tt.want) } } } @@ -77,31 +97,53 @@ func TestLtUint64(t *testing.T) { } for _, tc := range tests { - var x *Uint - var err error - - if strings.HasPrefix(tc.x, "0x") { - x, err = FromHex(tc.x) - if err != nil { - t.Error(err) - continue - } - } else { - x, err = FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - } + x := parseTestString(t, tc.x) got := x.LtUint64(tc.y) - if got != tc.want { t.Errorf("LtUint64(%s, %d) = %v, want %v", tc.x, tc.y, got, tc.want) } } } +func TestUint_GtUint64(t *testing.T) { + tests := []struct { + name string + z string + n uint64 + want bool + }{ + { + name: "z > n", + z: "1", + n: 0, + want: true, + }, + { + name: "z < n", + z: "18446744073709551615", + n: 0xFFFFFFFFFFFFFFFF, + want: false, + }, + { + name: "z == n", + z: "18446744073709551615", + n: 0xFFFFFFFFFFFFFFFF, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + z := MustFromDecimal(tt.z) + + if got := z.GtUint64(tt.n); got != tt.want { + t.Errorf("Uint.GtUint64() = %v, want %v", got, tt.want) + } + }) + } +} + func TestSGT(t *testing.T) { x := MustFromHex("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe") y := MustFromHex("0x0") @@ -127,37 +169,83 @@ func TestEq(t *testing.T) { {"0xffffffffffffffff", "18446744073709551615", true}, {"0x10000000000000000", "18446744073709551616", true}, {"0", "0", true}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, + {twoPow256Sub1, twoPow256Sub1, true}, } - for i, tc := range tests { - var x *Uint - var err error + for _, tt := range tests { + x := parseTestString(t, tt.x) - if strings.HasPrefix(tc.x, "0x") { - x, err = FromHex(tc.x) - if err != nil { - t.Error(err) - continue - } - } else { - x, err = FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } + y, err := FromDecimal(tt.y) + if err != nil { + t.Error(err) + continue } - y, err := FromDecimal(tc.y) + got := x.Eq(y) + + if got != tt.want { + t.Errorf("Eq(%s, %s) = %v, want %v", tt.x, tt.y, got, tt.want) + } + } +} + +func TestUint_Lte(t *testing.T) { + tests := []struct { + z, x string + want bool + }{ + {"10", "20", true}, + {"20", "10", false}, + {"10", "10", true}, + {"0", "0", true}, + } + + for _, tt := range tests { + z, err := FromDecimal(tt.z) if err != nil { t.Error(err) continue } + x, err := FromDecimal(tt.x) + if err != nil { + t.Error(err) + continue + } + if got := z.Lte(x); got != tt.want { + t.Errorf("Uint.Lte(%v, %v) = %v, want %v", tt.z, tt.x, got, tt.want) + } + } +} - got := x.Eq(y) +func TestUint_Gte(t *testing.T) { + tests := []struct { + z, x string + want bool + }{ + {"20", "10", true}, + {"10", "20", false}, + {"10", "10", true}, + {"0", "0", true}, + } - if got != tc.want { - t.Errorf("Eq(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + for _, tt := range tests { + z := parseTestString(t, tt.z) + x := parseTestString(t, tt.x) + + if got := z.Gte(x); got != tt.want { + t.Errorf("Uint.Gte(%v, %v) = %v, want %v", tt.z, tt.x, got, tt.want) } } } + +func parseTestString(_ *testing.T, s string) *Uint { + var x *Uint + + if strings.HasPrefix(s, "0x") { + x = MustFromHex(s) + } else { + x = MustFromDecimal(s) + } + + return x +} diff --git a/examples/gno.land/p/demo/uint256/conversion.gno b/examples/gno.land/p/demo/uint256/conversion.gno index 4ef90602ab3..c2f228f314c 100644 --- a/examples/gno.land/p/demo/uint256/conversion.gno +++ b/examples/gno.land/p/demo/uint256/conversion.gno @@ -130,7 +130,7 @@ func (z *Uint) scanScientificFromString(src string) error { // ToString returns the decimal string representation of z. It returns an empty string if z is nil. // OBS: doesn't exist from holiman's uint256 -func (z *Uint) ToString() string { +func (z *Uint) String() string { if z == nil { return "" } diff --git a/examples/gno.land/p/demo/uint256/conversion_test.gno b/examples/gno.land/p/demo/uint256/conversion_test.gno index ee3aad0f819..3942a102511 100644 --- a/examples/gno.land/p/demo/uint256/conversion_test.gno +++ b/examples/gno.land/p/demo/uint256/conversion_test.gno @@ -14,18 +14,18 @@ func TestIsUint64(t *testing.T) { {"0x10000000000000000", false}, } - for _, tc := range tests { - x := MustFromHex(tc.x) + for _, tt := range tests { + x := MustFromHex(tt.x) got := x.IsUint64() - if got != tc.want { - t.Errorf("IsUint64(%s) = %v, want %v", tc.x, got, tc.want) + if got != tt.want { + t.Errorf("IsUint64(%s) = %v, want %v", tt.x, got, tt.want) } } } func TestDec(t *testing.T) { - testCases := []struct { + tests := []struct { name string z Uint want string @@ -43,16 +43,133 @@ func TestDec(t *testing.T) { { name: "max possible value", z: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - want: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + want: twoPow256Sub1, }, } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := tc.z.Dec() - if result != tc.want { - t.Errorf("Dec(%v) = %s, want %s", tc.z, result, tc.want) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.z.Dec() + if result != tt.want { + t.Errorf("Dec(%v) = %s, want %s", tt.z, result, tt.want) } }) } } + +func TestUint_Scan(t *testing.T) { + tests := []struct { + name string + input interface{} + want *Uint + wantErr bool + }{ + { + name: "nil", + input: nil, + want: NewUint(0), + }, + { + name: "valid scientific notation", + input: "1e4", + want: NewUint(10000), + }, + { + name: "valid decimal string", + input: "12345", + want: NewUint(12345), + }, + { + name: "valid byte slice", + input: []byte("12345"), + want: NewUint(12345), + }, + { + name: "invalid string", + input: "invalid", + wantErr: true, + }, + { + name: "out of range", + input: "115792089237316195423570985008687907853269984665640564039457584007913129639936", // 2^256 + wantErr: true, + }, + { + name: "unsupported type", + input: 123, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + z := new(Uint) + err := z.Scan(tt.input) + + if tt.wantErr { + if err == nil { + t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr) + } + } else { + if err != nil { + t.Errorf("Scan() error = %v, wantErr %v", err, tt.wantErr) + } + if !z.Eq(tt.want) { + t.Errorf("Scan() = %v, want %v", z, tt.want) + } + } + }) + } +} + +func TestSetBytes(t *testing.T) { + tests := []struct { + input []byte + expected string + }{ + {[]byte{}, "0"}, + {[]byte{0x01}, "1"}, + {[]byte{0x12, 0x34}, "4660"}, + {[]byte{0x12, 0x34, 0x56}, "1193046"}, + {[]byte{0x12, 0x34, 0x56, 0x78}, "305419896"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a}, "78187493530"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, "20015998343868"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, "5124095576030430"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, "1311768467463790320"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, "335812727670730321938"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, "85968058283706962416180"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, "22007822920628982378542166"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, "5634002667681019488906794616"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, "1442304682926340989160139421850"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, "369229998829143293224995691993788"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde}, "94522879700260683065598897150409950"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, "24197857203266734864793317670504947440"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12}, "6194651444036284125387089323649266544658"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34}, "1585830769673288736099094866854212235432500"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56}, "405972677036361916441368285914678332270720086"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}, "103929005321308650608990281194157653061304342136"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a}, "26605825362255014555901511985704359183693911586970"}, + {[]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc}, "6811091292737283726310787068340315951025641366264508"}, + {[]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"}, + {[]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"}, + {[]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"}, + {[]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"}, + {[]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"}, + {[]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"}, + {[]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"}, + {[]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"}, + {[]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"}, + {[]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"}, + // over 32 bytes (last 32 bytes are used) + {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"}, + } + + for _, test := range tests { + z := new(Uint) + z.SetBytes(test.input) + expected := MustFromDecimal(test.expected) + if z.Cmp(expected) != 0 { + t.Errorf("SetBytes(%x) = %s, expected %s", test.input, z.String(), test.expected) + } + } +} diff --git a/examples/gno.land/p/demo/uint256/uint256.gno b/examples/gno.land/p/demo/uint256/uint256.gno index 80da0ba882b..3d183362992 100644 --- a/examples/gno.land/p/demo/uint256/uint256.gno +++ b/examples/gno.land/p/demo/uint256/uint256.gno @@ -5,6 +5,7 @@ package uint256 import ( "errors" "math/bits" + "strconv" ) const ( @@ -143,10 +144,10 @@ func (z *Uint) fromDecimal(bs string) error { if remaining <= 0 { return nil // Done } else if remaining > 19 { - num, err = parseUint(bs[remaining-19:remaining], 10, 64) + num, err = strconv.ParseUint(bs[remaining-19:remaining], 10, 64) } else { // Final round - num, err = parseUint(bs, 10, 64) + num, err = strconv.ParseUint(bs, 10, 64) } if err != nil { return err diff --git a/examples/gno.land/p/demo/uint256/uint256_test.gno b/examples/gno.land/p/demo/uint256/uint256_test.gno new file mode 100644 index 00000000000..ae8129b6e27 --- /dev/null +++ b/examples/gno.land/p/demo/uint256/uint256_test.gno @@ -0,0 +1,127 @@ +package uint256 + +import ( + "testing" +) + +func TestSetAllOne(t *testing.T) { + z := Zero() + z.SetAllOne() + if z.String() != twoPow256Sub1 { + t.Errorf("Expected all ones, got %s", z.String()) + } +} + +func TestByte(t *testing.T) { + tests := []struct { + input string + position uint64 + expected byte + }{ + {"0x1000000000000000000000000000000000000000000000000000000000000000", 0, 16}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 0, 255}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 31, 255}, + } + + for i, tt := range tests { + z, _ := FromHex(tt.input) + n := NewUint(tt.position) + result := z.Byte(n) + + if result.arr[0] != uint64(tt.expected) { + t.Errorf("Test case %d failed. Input: %s, Position: %d, Expected: %d, Got: %d", + i, tt.input, tt.position, tt.expected, result.arr[0]) + } + + // check other array elements are 0 + if result.arr[1] != 0 || result.arr[2] != 0 || result.arr[3] != 0 { + t.Errorf("Test case %d failed. Non-zero values in upper bytes", i) + } + } + + // overflow + z, _ := FromHex("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") + n := NewUint(32) + result := z.Byte(n) + + if !result.IsZero() { + t.Errorf("Expected zero for position >= 32, got %v", result) + } +} + +func TestBitLen(t *testing.T) { + tests := []struct { + input string + expected int + }{ + {"0x0", 0}, + {"0x1", 1}, + {"0xff", 8}, + {"0x100", 9}, + {"0xffff", 16}, + {"0x10000", 17}, + {"0xffffffffffffffff", 64}, + {"0x10000000000000000", 65}, + {"0xffffffffffffffffffffffffffffffff", 128}, + {"0x100000000000000000000000000000000", 129}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 256}, + } + + for i, tt := range tests { + z, _ := FromHex(tt.input) + result := z.BitLen() + + if result != tt.expected { + t.Errorf("Test case %d failed. Input: %s, Expected: %d, Got: %d", + i, tt.input, tt.expected, result) + } + } +} + +func TestByteLen(t *testing.T) { + tests := []struct { + input string + expected int + }{ + {"0x0", 0}, + {"0x1", 1}, + {"0xff", 1}, + {"0x100", 2}, + {"0xffff", 2}, + {"0x10000", 3}, + {"0xffffffffffffffff", 8}, + {"0x10000000000000000", 9}, + {"0xffffffffffffffffffffffffffffffff", 16}, + {"0x100000000000000000000000000000000", 17}, + {"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 32}, + } + + for i, tt := range tests { + z, _ := FromHex(tt.input) + result := z.ByteLen() + + if result != tt.expected { + t.Errorf("Test case %d failed. Input: %s, Expected: %d, Got: %d", + i, tt.input, tt.expected, result) + } + } +} + +func TestClone(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"0x1", "1"}, + {"0x100", "256"}, + {"0x10000000000000000", "18446744073709551616"}, + } + + for _, tt := range tests { + z, _ := FromHex(tt.input) + result := z.Clone() + if result.String() != tt.expected { + t.Errorf("Test %s failed. Expected %s, got %s", tt.input, tt.expected, result.String()) + } + } +} diff --git a/examples/gno.land/p/demo/uint256/utils.gno b/examples/gno.land/p/demo/uint256/utils.gno index 969728f3369..bcc7bb283e0 100644 --- a/examples/gno.land/p/demo/uint256/utils.gno +++ b/examples/gno.land/p/demo/uint256/utils.gno @@ -1,63 +1,5 @@ package uint256 -// lower(c) is a lower-case letter if and only if -// c is either that lower-case letter or the equivalent upper-case letter. -// Instead of writing c == 'x' || c == 'X' one can write lower(c) == 'x'. -// Note that lower of non-letters can produce other non-letters. -func lower(c byte) byte { - return c | ('x' - 'X') -} - -// underscoreOK reports whether the underscores in s are allowed. -// Checking them in this one function lets all the parsers skip over them simply. -// Underscore must appear only between digits or between a base prefix and a digit. -func underscoreOK(s string) bool { - // saw tracks the last character (class) we saw: - // ^ for beginning of number, - // 0 for a digit or base prefix, - // _ for an underscore, - // ! for none of the above. - saw := '^' - i := 0 - - // Optional sign. - if len(s) >= 1 && (s[0] == '-' || s[0] == '+') { - s = s[1:] - } - - // Optional base prefix. - hex := false - if len(s) >= 2 && s[0] == '0' && (lower(s[1]) == 'b' || lower(s[1]) == 'o' || lower(s[1]) == 'x') { - i = 2 - saw = '0' // base prefix counts as a digit for "underscore as digit separator" - hex = lower(s[1]) == 'x' - } - - // Number proper. - for ; i < len(s); i++ { - // Digits are always okay. - if '0' <= s[i] && s[i] <= '9' || hex && 'a' <= lower(s[i]) && lower(s[i]) <= 'f' { - saw = '0' - continue - } - // Underscore must follow digit. - if s[i] == '_' { - if saw != '0' { - return false - } - saw = '_' - continue - } - // Underscore must also be followed by digit. - if saw == '_' { - return false - } - // Saw non-digit, non-underscore. - saw = '!' - } - return saw != '_' -} - func checkNumberS(input string) error { const fn = "UnmarshalText" l := len(input) @@ -76,105 +18,3 @@ func checkNumberS(input string) error { } return nil } - -// ParseUint is like ParseUint but for unsigned numbers. -// -// A sign prefix is not permitted. -func parseUint(s string, base int, bitSize int) (uint64, error) { - const fnParseUint = "ParseUint" - - if s == "" { - return 0, errSyntax(fnParseUint, s) - } - - base0 := base == 0 - - s0 := s - switch { - case 2 <= base && base <= 36: - // valid base; nothing to do - - case base == 0: - // Look for octal, hex prefix. - base = 10 - if s[0] == '0' { - switch { - case len(s) >= 3 && lower(s[1]) == 'b': - base = 2 - s = s[2:] - case len(s) >= 3 && lower(s[1]) == 'o': - base = 8 - s = s[2:] - case len(s) >= 3 && lower(s[1]) == 'x': - base = 16 - s = s[2:] - default: - base = 8 - s = s[1:] - } - } - - default: - return 0, errInvalidBase(fnParseUint, base) - } - - if bitSize == 0 { - bitSize = uintSize - } else if bitSize < 0 || bitSize > 64 { - return 0, errInvalidBitSize(fnParseUint, bitSize) - } - - // Cutoff is the smallest number such that cutoff*base > maxUint64. - // Use compile-time constants for common cases. - var cutoff uint64 - switch base { - case 10: - cutoff = MaxUint64/10 + 1 - case 16: - cutoff = MaxUint64/16 + 1 - default: - cutoff = MaxUint64/uint64(base) + 1 - } - - maxVal := uint64(1)<= byte(base) { - return 0, errSyntax(fnParseUint, s0) - } - - if n >= cutoff { - // n*base overflows - return maxVal, errRange(fnParseUint, s0) - } - n *= uint64(base) - - n1 := n + uint64(d) - if n1 < n || n1 > maxVal { - // n+d overflows - return maxVal, errRange(fnParseUint, s0) - } - n = n1 - } - - if underscores && !underscoreOK(s0) { - return 0, errSyntax(fnParseUint, s0) - } - - return n, nil -} diff --git a/examples/gno.land/p/demo/urequire/gno.mod b/examples/gno.land/p/demo/urequire/gno.mod index 9689a2222ac..e5336b2c80d 100644 --- a/examples/gno.land/p/demo/urequire/gno.mod +++ b/examples/gno.land/p/demo/urequire/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/urequire - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/demo/watchdog/gno.mod b/examples/gno.land/p/demo/watchdog/gno.mod index 29005441401..96fba14451b 100644 --- a/examples/gno.land/p/demo/watchdog/gno.mod +++ b/examples/gno.land/p/demo/watchdog/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/watchdog - -require gno.land/p/demo/uassert v0.0.0-latest diff --git a/examples/gno.land/p/gov/executor/callback.gno b/examples/gno.land/p/gov/executor/callback.gno new file mode 100644 index 00000000000..5d46a97cd69 --- /dev/null +++ b/examples/gno.land/p/gov/executor/callback.gno @@ -0,0 +1,39 @@ +package executor + +import ( + "errors" + "std" +) + +var errInvalidCaller = errors.New("invalid executor caller") + +// NewCallbackExecutor creates a new callback executor with the provided callback function +func NewCallbackExecutor(callback func() error, path string) *CallbackExecutor { + return &CallbackExecutor{ + callback: callback, + daoPkgPath: path, + } +} + +// CallbackExecutor is an implementation of the dao.Executor interface, +// based on a specific callback. +// The given callback should verify the validity of the govdao call +type CallbackExecutor struct { + callback func() error // the callback to be executed + daoPkgPath string // the active pkg path of the govdao +} + +// Execute runs the executor's callback function. +func (exec *CallbackExecutor) Execute() error { + // Verify the caller is an adequate Realm + caller := std.CurrentRealm().PkgPath() + if caller != exec.daoPkgPath { + return errInvalidCaller + } + + if exec.callback != nil { + return exec.callback() + } + + return nil +} diff --git a/examples/gno.land/p/gov/executor/context.gno b/examples/gno.land/p/gov/executor/context.gno new file mode 100644 index 00000000000..158e3b1e0be --- /dev/null +++ b/examples/gno.land/p/gov/executor/context.gno @@ -0,0 +1,75 @@ +package executor + +import ( + "errors" + "std" + + "gno.land/p/demo/context" +) + +type propContextKey string + +func (k propContextKey) String() string { return string(k) } + +const ( + statusContextKey = propContextKey("govdao-prop-status") + approvedStatus = "approved" +) + +var errNotApproved = errors.New("not approved by govdao") + +// CtxExecutor is an implementation of the dao.Executor interface, +// based on the given context. +// It utilizes the given context to assert the validity of the govdao call +type CtxExecutor struct { + callbackCtx func(ctx context.Context) error // the callback ctx fn, if any + daoPkgPath string // the active pkg path of the govdao +} + +// NewCtxExecutor creates a new executor with the provided callback function. +func NewCtxExecutor(callback func(ctx context.Context) error, path string) *CtxExecutor { + return &CtxExecutor{ + callbackCtx: callback, + daoPkgPath: path, + } +} + +// Execute runs the executor's callback function +func (exec *CtxExecutor) Execute() error { + // Verify the caller is an adequate Realm + caller := std.CurrentRealm().PkgPath() + if caller != exec.daoPkgPath { + return errInvalidCaller + } + + // Create the context + ctx := context.WithValue( + context.Empty(), + statusContextKey, + approvedStatus, + ) + + return exec.callbackCtx(ctx) +} + +// IsApprovedByGovdaoContext asserts that the govdao approved the context +func IsApprovedByGovdaoContext(ctx context.Context) bool { + v := ctx.Value(statusContextKey) + if v == nil { + return false + } + + vs, ok := v.(string) + + return ok && vs == approvedStatus +} + +// AssertContextApprovedByGovDAO asserts the given context +// was approved by GOVDAO +func AssertContextApprovedByGovDAO(ctx context.Context) { + if IsApprovedByGovdaoContext(ctx) { + return + } + + panic(errNotApproved) +} diff --git a/examples/gno.land/p/gov/executor/gno.mod b/examples/gno.land/p/gov/executor/gno.mod new file mode 100644 index 00000000000..5dbb6f7f85e --- /dev/null +++ b/examples/gno.land/p/gov/executor/gno.mod @@ -0,0 +1 @@ +module gno.land/p/gov/executor diff --git a/examples/gno.land/p/gov/executor/proposal_test.gno b/examples/gno.land/p/gov/executor/proposal_test.gno new file mode 100644 index 00000000000..3a70fc40596 --- /dev/null +++ b/examples/gno.land/p/gov/executor/proposal_test.gno @@ -0,0 +1,180 @@ +package executor + +import ( + "errors" + "std" + "testing" + + "gno.land/p/demo/context" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestExecutor_Callback(t *testing.T) { + t.Parallel() + + t.Run("govdao not caller", func(t *testing.T) { + t.Parallel() + + var ( + called = false + + cb = func() error { + called = true + + return nil + } + ) + + // Create the executor + e := NewCallbackExecutor(cb, "gno.land/r/gov/dao") + + // Execute as not the /r/gov/dao caller + uassert.ErrorIs(t, e.Execute(), errInvalidCaller) + uassert.False(t, called, "expected proposal to not execute") + }) + + t.Run("execution successful", func(t *testing.T) { + t.Parallel() + + var ( + called = false + + cb = func() error { + called = true + + return nil + } + ) + + // Create the executor + daoPkgPath := "gno.land/r/gov/dao" + e := NewCallbackExecutor(cb, daoPkgPath) + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + uassert.NoError(t, e.Execute()) + uassert.True(t, called, "expected proposal to execute") + }) + + t.Run("execution unsuccessful", func(t *testing.T) { + t.Parallel() + + var ( + called = false + expectedErr = errors.New("unexpected") + + cb = func() error { + called = true + + return expectedErr + } + ) + + // Create the executor + daoPkgPath := "gno.land/r/gov/dao" + e := NewCallbackExecutor(cb, daoPkgPath) + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + uassert.ErrorIs(t, e.Execute(), expectedErr) + uassert.True(t, called, "expected proposal to execute") + }) +} + +func TestExecutor_Context(t *testing.T) { + t.Parallel() + + t.Run("govdao not caller", func(t *testing.T) { + t.Parallel() + + var ( + called = false + + cb = func(ctx context.Context) error { + if !IsApprovedByGovdaoContext(ctx) { + t.Fatal("not govdao caller") + } + + called = true + + return nil + } + ) + + // Create the executor + e := NewCtxExecutor(cb, "gno.land/r/gov/dao") + + // Execute as not the /r/gov/dao caller + uassert.ErrorIs(t, e.Execute(), errInvalidCaller) + uassert.False(t, called, "expected proposal to not execute") + }) + + t.Run("execution successful", func(t *testing.T) { + t.Parallel() + + var ( + called = false + + cb = func(ctx context.Context) error { + if !IsApprovedByGovdaoContext(ctx) { + t.Fatal("not govdao caller") + } + + called = true + + return nil + } + ) + + // Create the executor + daoPkgPath := "gno.land/r/gov/dao" + e := NewCtxExecutor(cb, daoPkgPath) + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + urequire.NoError(t, e.Execute()) + uassert.True(t, called, "expected proposal to execute") + }) + + t.Run("execution unsuccessful", func(t *testing.T) { + t.Parallel() + + var ( + called = false + expectedErr = errors.New("unexpected") + + cb = func(ctx context.Context) error { + if !IsApprovedByGovdaoContext(ctx) { + t.Fatal("not govdao caller") + } + + called = true + + return expectedErr + } + ) + + // Create the executor + daoPkgPath := "gno.land/r/gov/dao" + e := NewCtxExecutor(cb, daoPkgPath) + + // Execute as the /r/gov/dao caller + r := std.NewCodeRealm(daoPkgPath) + std.TestSetRealm(r) + + uassert.NotPanics(t, func() { + err := e.Execute() + + uassert.ErrorIs(t, err, expectedErr) + }) + + uassert.True(t, called, "expected proposal to execute") + }) +} diff --git a/examples/gno.land/p/gov/proposal/gno.mod b/examples/gno.land/p/gov/proposal/gno.mod deleted file mode 100644 index 3f6ef34a759..00000000000 --- a/examples/gno.land/p/gov/proposal/gno.mod +++ /dev/null @@ -1,7 +0,0 @@ -module gno.land/p/gov/proposal - -require ( - gno.land/p/demo/context v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/p/gov/proposal/proposal.gno b/examples/gno.land/p/gov/proposal/proposal.gno deleted file mode 100644 index ca1767228c9..00000000000 --- a/examples/gno.land/p/gov/proposal/proposal.gno +++ /dev/null @@ -1,106 +0,0 @@ -// Package proposal provides a structure for executing proposals. -package proposal - -import ( - "errors" - "std" - - "gno.land/p/demo/context" -) - -var errNotGovDAO = errors.New("only r/gov/dao can be the caller") - -// NewExecutor creates a new executor with the provided callback function. -func NewExecutor(callback func() error) Executor { - return &executorImpl{ - callback: callback, - done: false, - } -} - -// NewCtxExecutor creates a new executor with the provided callback function. -func NewCtxExecutor(callback func(ctx context.Context) error) Executor { - return &executorImpl{ - callbackCtx: callback, - done: false, - } -} - -// executorImpl is an implementation of the Executor interface. -type executorImpl struct { - callback func() error - callbackCtx func(ctx context.Context) error - done bool - success bool -} - -// Execute runs the executor's callback function. -func (exec *executorImpl) Execute() error { - if exec.done { - return ErrAlreadyDone - } - - // Verify the executor is r/gov/dao - assertCalledByGovdao() - - var err error - if exec.callback != nil { - err = exec.callback() - } else if exec.callbackCtx != nil { - ctx := context.WithValue(context.Empty(), statusContextKey, approvedStatus) - err = exec.callbackCtx(ctx) - } - exec.done = true - exec.success = err == nil - - return err -} - -// IsDone returns whether the executor has been executed. -func (exec *executorImpl) IsDone() bool { - return exec.done -} - -// IsSuccessful returns whether the execution was successful. -func (exec *executorImpl) IsSuccessful() bool { - return exec.success -} - -// IsExpired returns whether the execution had expired or not. -// This implementation never expires. -func (exec *executorImpl) IsExpired() bool { - return false -} - -func IsApprovedByGovdaoContext(ctx context.Context) bool { - v := ctx.Value(statusContextKey) - if v == nil { - return false - } - vs, ok := v.(string) - return ok && vs == approvedStatus -} - -func AssertContextApprovedByGovDAO(ctx context.Context) { - if !IsApprovedByGovdaoContext(ctx) { - panic("not approved by govdao") - } -} - -// assertCalledByGovdao asserts that the calling Realm is /r/gov/dao -func assertCalledByGovdao() { - caller := std.CurrentRealm().PkgPath() - - if caller != daoPkgPath { - panic(errNotGovDAO) - } -} - -type propContextKey string - -func (k propContextKey) String() string { return string(k) } - -const ( - statusContextKey = propContextKey("govdao-prop-status") - approvedStatus = "approved" -) diff --git a/examples/gno.land/p/gov/proposal/proposal_test.gno b/examples/gno.land/p/gov/proposal/proposal_test.gno deleted file mode 100644 index 536871e644d..00000000000 --- a/examples/gno.land/p/gov/proposal/proposal_test.gno +++ /dev/null @@ -1,156 +0,0 @@ -package proposal - -import ( - "errors" - "std" - "testing" - - "gno.land/p/demo/uassert" - "gno.land/p/demo/urequire" -) - -func TestExecutor(t *testing.T) { - t.Parallel() - - verifyProposalFailed := func(e Executor) { - uassert.True(t, e.IsDone(), "expected proposal to be done") - uassert.False(t, e.IsSuccessful(), "expected proposal to fail") - } - - verifyProposalSucceeded := func(e Executor) { - uassert.True(t, e.IsDone(), "expected proposal to be done") - uassert.True(t, e.IsSuccessful(), "expected proposal to be successful") - } - - t.Run("govdao not caller", func(t *testing.T) { - t.Parallel() - - var ( - called = false - - cb = func() error { - called = true - - return nil - } - ) - - // Create the executor - e := NewExecutor(cb) - - urequire.False(t, e.IsDone(), "expected status to be NotExecuted") - - // Execute as not the /r/gov/dao caller - uassert.PanicsWithMessage(t, errNotGovDAO.Error(), func() { - _ = e.Execute() - }) - - uassert.False(t, called, "expected proposal to not execute") - }) - - t.Run("execution successful", func(t *testing.T) { - t.Parallel() - - var ( - called = false - - cb = func() error { - called = true - - return nil - } - ) - - // Create the executor - e := NewExecutor(cb) - - urequire.False(t, e.IsDone(), "expected status to be NotExecuted") - - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) - std.TestSetRealm(r) - - uassert.NotPanics(t, func() { - err := e.Execute() - - uassert.NoError(t, err) - }) - - uassert.True(t, called, "expected proposal to execute") - - // Make sure the execution params are correct - verifyProposalSucceeded(e) - }) - - t.Run("execution unsuccessful", func(t *testing.T) { - t.Parallel() - - var ( - called = false - expectedErr = errors.New("unexpected") - - cb = func() error { - called = true - - return expectedErr - } - ) - - // Create the executor - e := NewExecutor(cb) - - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) - std.TestSetRealm(r) - - uassert.NotPanics(t, func() { - err := e.Execute() - - uassert.ErrorIs(t, err, expectedErr) - }) - - uassert.True(t, called, "expected proposal to execute") - - // Make sure the execution params are correct - verifyProposalFailed(e) - }) - - t.Run("proposal already executed", func(t *testing.T) { - t.Parallel() - - var ( - called = false - - cb = func() error { - called = true - - return nil - } - ) - - // Create the executor - e := NewExecutor(cb) - - urequire.False(t, e.IsDone(), "expected status to be NotExecuted") - - // Execute as the /r/gov/dao caller - r := std.NewCodeRealm(daoPkgPath) - std.TestSetRealm(r) - - uassert.NotPanics(t, func() { - uassert.NoError(t, e.Execute()) - }) - - uassert.True(t, called, "expected proposal to execute") - - // Make sure the execution params are correct - verifyProposalSucceeded(e) - - // Attempt to execute the proposal again - uassert.NotPanics(t, func() { - err := e.Execute() - - uassert.ErrorIs(t, err, ErrAlreadyDone) - }) - }) -} diff --git a/examples/gno.land/p/gov/proposal/types.gno b/examples/gno.land/p/gov/proposal/types.gno deleted file mode 100644 index 6cd2da9ccfe..00000000000 --- a/examples/gno.land/p/gov/proposal/types.gno +++ /dev/null @@ -1,37 +0,0 @@ -// Package proposal defines types for proposal execution. -package proposal - -import "errors" - -// Executor represents a minimal closure-oriented proposal design. -// It is intended to be used by a govdao governance proposal (v1, v2, etc). -type Executor interface { - // Execute executes the given proposal, and returns any error encountered - // during the execution - Execute() error - - // IsDone returns a flag indicating if the proposal was executed - IsDone() bool - - // IsSuccessful returns a flag indicating if the proposal was executed - // and is successful - IsSuccessful() bool // IsDone() && !err - - // IsExpired returns whether the execution had expired or not. - IsExpired() bool -} - -// ErrAlreadyDone is the error returned when trying to execute an already -// executed proposal. -var ErrAlreadyDone = errors.New("already executed") - -// Status enum. -type Status string - -const ( - NotExecuted Status = "not_executed" - Succeeded Status = "succeeded" - Failed Status = "failed" -) - -const daoPkgPath = "gno.land/r/gov/dao" // TODO: make sure this is configurable through r/sys/vars diff --git a/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno b/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno new file mode 100644 index 00000000000..bf80964a9a0 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno @@ -0,0 +1,103 @@ +// Package datasource defines generic interfaces for datasources. +// +// Datasources contain a set of records which can optionally be +// taggable. Tags can optionally be used to filter records by taxonomy. +// +// Datasources can help in cases where the data sent during +// communication between different realms needs to be generic +// to avoid direct dependencies. +package datasource + +import "errors" + +// ErrInvalidRecord indicates that a datasource contains invalid records. +var ErrInvalidRecord = errors.New("datasource records is not valid") + +type ( + // Fields defines an interface for read-only fields. + Fields interface { + // Has checks whether a field exists. + Has(name string) bool + + // Get retrieves the value associated with the given field. + Get(name string) (value interface{}, found bool) + } + + // Record defines a datasource record. + Record interface { + // ID returns the unique record's identifier. + ID() string + + // String returns a string representation of the record. + String() string + + // Fields returns record fields and values. + Fields() (Fields, error) + } + + // TaggableRecord defines a datasource record that supports tags. + // Tags can be used to build a taxonomy to filter records by category. + TaggableRecord interface { + // Tags returns a list of tags for the record. + Tags() []string + } + + // ContentRecord defines a datasource record that can return content. + ContentRecord interface { + // Content returns the record content. + Content() (string, error) + } + + // Iterator defines an iterator of datasource records. + Iterator interface { + // Next returns true when a new record is available. + Next() bool + + // Err returns any error raised when reading records. + Err() error + + // Record returns the current record. + Record() Record + } + + // Datasource defines a generic datasource. + Datasource interface { + // Records returns a new datasource records iterator. + Records(Query) Iterator + + // Size returns the total number of records in the datasource. + // When -1 is returned it means datasource doesn't support size. + Size() int + + // Record returns a single datasource record. + Record(id string) (Record, error) + } +) + +// NewIterator returns a new record iterator for a datasource query. +func NewIterator(ds Datasource, options ...QueryOption) Iterator { + return ds.Records(NewQuery(options...)) +} + +// QueryRecords return a slice of records for a datasource query. +func QueryRecords(ds Datasource, options ...QueryOption) ([]Record, error) { + var ( + records []Record + query = NewQuery(options...) + iter = ds.Records(query) + ) + + for i := 0; i < query.Count && iter.Next(); i++ { + r := iter.Record() + if r == nil { + return nil, ErrInvalidRecord + } + + records = append(records, r) + } + + if err := iter.Err(); err != nil { + return nil, err + } + return records, nil +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno b/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno new file mode 100644 index 00000000000..304a311ced7 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno @@ -0,0 +1,171 @@ +package datasource + +import ( + "errors" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestNewIterator(t *testing.T) { + cases := []struct { + name string + records []Record + err error + }{ + { + name: "ok", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + }, + { + name: "error", + err: errors.New("test"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + ds := testDatasource{ + records: tc.records, + err: tc.err, + } + + // Act + iter := NewIterator(ds) + + // Assert + if tc.err != nil { + uassert.ErrorIs(t, tc.err, iter.Err()) + return + } + + uassert.NoError(t, iter.Err()) + + for i := 0; iter.Next(); i++ { + r := iter.Record() + urequire.NotEqual(t, nil, r, "valid record") + urequire.True(t, i < len(tc.records), "iteration count") + uassert.Equal(t, tc.records[i].ID(), r.ID()) + } + }) + } +} + +func TestQueryRecords(t *testing.T) { + cases := []struct { + name string + records []Record + recordCount int + options []QueryOption + err error + }{ + { + name: "ok", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + recordCount: 3, + }, + { + name: "with count", + options: []QueryOption{WithCount(2)}, + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + recordCount: 2, + }, + { + name: "invalid record", + records: []Record{ + testRecord{id: "1"}, + nil, + testRecord{id: "3"}, + }, + err: ErrInvalidRecord, + }, + { + name: "iterator error", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "3"}, + }, + err: errors.New("test"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + ds := testDatasource{ + records: tc.records, + err: tc.err, + } + + // Act + records, err := QueryRecords(ds, tc.options...) + + // Assert + if tc.err != nil { + uassert.ErrorIs(t, tc.err, err) + return + } + + uassert.NoError(t, err) + + urequire.Equal(t, tc.recordCount, len(records), "record count") + for i, r := range records { + urequire.NotEqual(t, nil, r, "valid record") + uassert.Equal(t, tc.records[i].ID(), r.ID()) + } + }) + } +} + +type testDatasource struct { + records []Record + err error +} + +func (testDatasource) Size() int { return -1 } +func (testDatasource) Record(string) (Record, error) { return nil, nil } +func (ds testDatasource) Records(Query) Iterator { return &testIter{records: ds.records, err: ds.err} } + +type testRecord struct { + id string + fields Fields + err error +} + +func (r testRecord) ID() string { return r.id } +func (r testRecord) String() string { return "str" + r.id } +func (r testRecord) Fields() (Fields, error) { return r.fields, r.err } + +type testIter struct { + index int + records []Record + current Record + err error +} + +func (it testIter) Err() error { return it.err } +func (it testIter) Record() Record { return it.current } + +func (it *testIter) Next() bool { + count := len(it.records) + if it.err != nil || count == 0 || it.index >= count { + return false + } + it.current = it.records[it.index] + it.index++ + return true +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/gno.mod b/examples/gno.land/p/jeronimoalbi/datasource/gno.mod new file mode 100644 index 00000000000..3b398971b41 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/gno.mod @@ -0,0 +1 @@ +module gno.land/p/jeronimoalbi/datasource diff --git a/examples/gno.land/p/jeronimoalbi/datasource/query.gno b/examples/gno.land/p/jeronimoalbi/datasource/query.gno new file mode 100644 index 00000000000..f971f9c64db --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/query.gno @@ -0,0 +1,70 @@ +package datasource + +import "gno.land/p/demo/avl" + +// DefaultQueryRecords defines the default number of records returned by queries. +const DefaultQueryRecords = 50 + +var defaultQuery = Query{Count: DefaultQueryRecords} + +type ( + // QueryOption configures datasource queries. + QueryOption func(*Query) + + // Query contains datasource query options. + Query struct { + // Offset of the first record to return during iteration. + Offset int + + // Count contains the number to records that query should return. + Count int + + // Tag contains a tag to use as filter for the records. + Tag string + + // Filters contains optional query filters by field value. + Filters avl.Tree + } +) + +// WithOffset configures query to return records starting from an offset. +func WithOffset(offset int) QueryOption { + return func(q *Query) { + q.Offset = offset + } +} + +// WithCount configures the number of records that query returns. +func WithCount(count int) QueryOption { + return func(q *Query) { + if count < 1 { + count = DefaultQueryRecords + } + q.Count = count + } +} + +// ByTag configures query to filter by tag. +func ByTag(tag string) QueryOption { + return func(q *Query) { + q.Tag = tag + } +} + +// WithFilter assigns a new filter argument to a query. +// This option can be used multiple times if more than one +// filter has to be given to the query. +func WithFilter(field string, value interface{}) QueryOption { + return func(q *Query) { + q.Filters.Set(field, value) + } +} + +// NewQuery creates a new datasource query. +func NewQuery(options ...QueryOption) Query { + q := defaultQuery + for _, apply := range options { + apply(&q) + } + return q +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno b/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno new file mode 100644 index 00000000000..6f78d41bb35 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno @@ -0,0 +1,104 @@ +package datasource + +import ( + "fmt" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestNewQuery(t *testing.T) { + cases := []struct { + name string + options []QueryOption + setup func() Query + }{ + { + name: "default", + setup: func() Query { + return Query{Count: DefaultQueryRecords} + }, + }, + { + name: "with offset", + options: []QueryOption{WithOffset(100)}, + setup: func() Query { + return Query{ + Offset: 100, + Count: DefaultQueryRecords, + } + }, + }, + { + name: "with count", + options: []QueryOption{WithCount(10)}, + setup: func() Query { + return Query{Count: 10} + }, + }, + { + name: "with invalid count", + options: []QueryOption{WithCount(0)}, + setup: func() Query { + return Query{Count: DefaultQueryRecords} + }, + }, + { + name: "by tag", + options: []QueryOption{ByTag("foo")}, + setup: func() Query { + return Query{ + Tag: "foo", + Count: DefaultQueryRecords, + } + }, + }, + { + name: "with filter", + options: []QueryOption{WithFilter("foo", 42)}, + setup: func() Query { + q := Query{Count: DefaultQueryRecords} + q.Filters.Set("foo", 42) + return q + }, + }, + { + name: "with multiple filters", + options: []QueryOption{ + WithFilter("foo", 42), + WithFilter("bar", "baz"), + }, + setup: func() Query { + q := Query{Count: DefaultQueryRecords} + q.Filters.Set("foo", 42) + q.Filters.Set("bar", "baz") + return q + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + want := tc.setup() + + // Act + q := NewQuery(tc.options...) + + // Assert + uassert.Equal(t, want.Offset, q.Offset) + uassert.Equal(t, want.Count, q.Count) + uassert.Equal(t, want.Tag, q.Tag) + uassert.Equal(t, want.Filters.Size(), q.Filters.Size()) + + want.Filters.Iterate("", "", func(k string, v interface{}) bool { + got, exists := q.Filters.Get(k) + uassert.True(t, exists) + if exists { + uassert.Equal(t, fmt.Sprint(v), fmt.Sprint(got)) + } + return false + }) + }) + } +} diff --git a/examples/gno.land/p/moul/addrset/addrset.gno b/examples/gno.land/p/moul/addrset/addrset.gno new file mode 100644 index 00000000000..0bb8165f9fe --- /dev/null +++ b/examples/gno.land/p/moul/addrset/addrset.gno @@ -0,0 +1,100 @@ +// Package addrset provides a specialized set data structure for managing unique Gno addresses. +// +// It is built on top of an AVL tree for efficient operations and maintains addresses in sorted order. +// This package is particularly useful when you need to: +// - Track a collection of unique addresses (e.g., for whitelists, participants, etc.) +// - Efficiently check address membership +// - Support pagination when displaying addresses +// +// Example usage: +// +// import ( +// "std" +// "gno.land/p/moul/addrset" +// ) +// +// func MyHandler() { +// // Create a new address set +// var set addrset.Set +// +// // Add some addresses +// addr1 := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") +// addr2 := std.Address("g1sss5g0rkqr88k4u648yd5d3l9t4d8vvqwszqth") +// +// set.Add(addr1) // returns true (newly added) +// set.Add(addr2) // returns true (newly added) +// set.Add(addr1) // returns false (already exists) +// +// // Check membership +// if set.Has(addr1) { +// // addr1 is in the set +// } +// +// // Get size +// size := set.Size() // returns 2 +// +// // Iterate with pagination (10 items per page, starting at offset 0) +// set.IterateByOffset(0, 10, func(addr std.Address) bool { +// // Process addr +// return false // continue iteration +// }) +// +// // Remove an address +// set.Remove(addr1) // returns true (was present) +// set.Remove(addr1) // returns false (not present) +// } +package addrset + +import ( + "std" + + "gno.land/p/demo/avl" +) + +type Set struct { + tree avl.Tree +} + +// Add inserts an address into the set. +// Returns true if the address was newly added, false if it already existed. +func (s *Set) Add(addr std.Address) bool { + return !s.tree.Set(string(addr), nil) +} + +// Remove deletes an address from the set. +// Returns true if the address was found and removed, false if it didn't exist. +func (s *Set) Remove(addr std.Address) bool { + _, removed := s.tree.Remove(string(addr)) + return removed +} + +// Has checks if an address exists in the set. +func (s *Set) Has(addr std.Address) bool { + return s.tree.Has(string(addr)) +} + +// Size returns the number of addresses in the set. +func (s *Set) Size() int { + return s.tree.Size() +} + +// IterateByOffset walks through addresses starting at the given offset. +// The callback should return true to stop iteration. +func (s *Set) IterateByOffset(offset int, count int, cb func(addr std.Address) bool) { + s.tree.IterateByOffset(offset, count, func(key string, _ interface{}) bool { + return cb(std.Address(key)) + }) +} + +// ReverseIterateByOffset walks through addresses in reverse order starting at the given offset. +// The callback should return true to stop iteration. +func (s *Set) ReverseIterateByOffset(offset int, count int, cb func(addr std.Address) bool) { + s.tree.ReverseIterateByOffset(offset, count, func(key string, _ interface{}) bool { + return cb(std.Address(key)) + }) +} + +// Tree returns the underlying AVL tree for advanced usage. +func (s *Set) Tree() avl.ITree { + return &s.tree +} diff --git a/examples/gno.land/p/moul/addrset/addrset_test.gno b/examples/gno.land/p/moul/addrset/addrset_test.gno new file mode 100644 index 00000000000..c3e27eab1df --- /dev/null +++ b/examples/gno.land/p/moul/addrset/addrset_test.gno @@ -0,0 +1,174 @@ +package addrset + +import ( + "std" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestSet(t *testing.T) { + addr1 := std.Address("addr1") + addr2 := std.Address("addr2") + addr3 := std.Address("addr3") + + tests := []struct { + name string + actions func(s *Set) + size int + has map[std.Address]bool + addrs []std.Address // for iteration checks + }{ + { + name: "empty set", + actions: func(s *Set) {}, + size: 0, + has: map[std.Address]bool{addr1: false}, + }, + { + name: "single address", + actions: func(s *Set) { + s.Add(addr1) + }, + size: 1, + has: map[std.Address]bool{ + addr1: true, + addr2: false, + }, + addrs: []std.Address{addr1}, + }, + { + name: "multiple addresses", + actions: func(s *Set) { + s.Add(addr1) + s.Add(addr2) + s.Add(addr3) + }, + size: 3, + has: map[std.Address]bool{ + addr1: true, + addr2: true, + addr3: true, + }, + addrs: []std.Address{addr1, addr2, addr3}, + }, + { + name: "remove address", + actions: func(s *Set) { + s.Add(addr1) + s.Add(addr2) + s.Remove(addr1) + }, + size: 1, + has: map[std.Address]bool{ + addr1: false, + addr2: true, + }, + addrs: []std.Address{addr2}, + }, + { + name: "duplicate adds", + actions: func(s *Set) { + uassert.True(t, s.Add(addr1)) // first add returns true + uassert.False(t, s.Add(addr1)) // second add returns false + uassert.True(t, s.Remove(addr1)) // remove existing returns true + uassert.False(t, s.Remove(addr1)) // remove non-existing returns false + }, + size: 0, + has: map[std.Address]bool{ + addr1: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var set Set + + // Execute test actions + tt.actions(&set) + + // Check size + uassert.Equal(t, tt.size, set.Size()) + + // Check existence + for addr, expected := range tt.has { + uassert.Equal(t, expected, set.Has(addr)) + } + + // Check iteration if addresses are specified + if tt.addrs != nil { + collected := []std.Address{} + set.IterateByOffset(0, 10, func(addr std.Address) bool { + collected = append(collected, addr) + return false + }) + + // Check length + uassert.Equal(t, len(tt.addrs), len(collected)) + + // Check each address + for i, addr := range tt.addrs { + uassert.Equal(t, addr, collected[i]) + } + } + }) + } +} + +func TestSetIterationLimits(t *testing.T) { + tests := []struct { + name string + addrs []std.Address + offset int + limit int + expected int + }{ + { + name: "zero offset full list", + addrs: []std.Address{"a1", "a2", "a3"}, + offset: 0, + limit: 10, + expected: 3, + }, + { + name: "offset with limit", + addrs: []std.Address{"a1", "a2", "a3", "a4"}, + offset: 1, + limit: 2, + expected: 2, + }, + { + name: "offset beyond size", + addrs: []std.Address{"a1", "a2"}, + offset: 3, + limit: 1, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var set Set + for _, addr := range tt.addrs { + set.Add(addr) + } + + // Test forward iteration + count := 0 + set.IterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool { + count++ + return false + }) + uassert.Equal(t, tt.expected, count) + + // Test reverse iteration + count = 0 + set.ReverseIterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool { + count++ + return false + }) + uassert.Equal(t, tt.expected, count) + }) + } +} diff --git a/examples/gno.land/p/moul/addrset/gno.mod b/examples/gno.land/p/moul/addrset/gno.mod new file mode 100644 index 00000000000..45bb53b399c --- /dev/null +++ b/examples/gno.land/p/moul/addrset/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/addrset diff --git a/examples/gno.land/p/moul/collection/collection.gno b/examples/gno.land/p/moul/collection/collection.gno new file mode 100644 index 00000000000..f6d26e6a3ee --- /dev/null +++ b/examples/gno.land/p/moul/collection/collection.gno @@ -0,0 +1,509 @@ +// Package collection provides a generic collection implementation with support for +// multiple indexes, including unique indexes and case-insensitive indexes. +// It is designed to be used with any type and allows efficient lookups using +// different fields or computed values. +// +// Example usage: +// +// // Define a data type +// type User struct { +// Name string +// Email string +// Age int +// Username string +// Tags []string +// } +// +// // Create a new collection +// c := collection.New() +// +// // Add indexes with different options +// c.AddIndex("name", func(v interface{}) string { +// return v.(*User).Name +// }, UniqueIndex) +// +// c.AddIndex("email", func(v interface{}) string { +// return v.(*User).Email +// }, UniqueIndex|CaseInsensitiveIndex) +// +// c.AddIndex("age", func(v interface{}) string { +// return strconv.Itoa(v.(*User).Age) +// }, DefaultIndex) // Non-unique index +// +// c.AddIndex("username", func(v interface{}) string { +// return v.(*User).Username +// }, UniqueIndex|SparseIndex) // Allow empty usernames +// +// // For tags, we index all tags for the user +// c.AddIndex("tag", func(v interface{}) []string { +// return v.(*User).Tags +// }, DefaultIndex) // Non-unique to allow multiple users with same tag +// +// // Store an object +// id := c.Set(&User{ +// Name: "Alice", +// Email: "alice@example.com", +// Age: 30, +// Tags: []string{"admin", "moderator"}, // User can have multiple tags +// }) +// +// // Retrieve by any index +// entry := c.GetFirst("email", "alice@example.com") +// adminUsers := c.GetAll("tag", "admin") // Find all users with admin tag +// modUsers := c.GetAll("tag", "moderator") // Find all users with moderator tag +// +// Index options can be combined using the bitwise OR operator. +// Available options: +// - DefaultIndex: Regular index with no special behavior +// - UniqueIndex: Ensures values are unique within the index +// - CaseInsensitiveIndex: Makes string comparisons case-insensitive +// - SparseIndex: Skips indexing empty values (nil or empty string) +// +// Example: UniqueIndex|CaseInsensitiveIndex for a case-insensitive unique index +package collection + +import ( + "errors" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" +) + +// New creates a new Collection instance with an initialized ID index. +// The ID index is a special unique index that is always present and +// serves as the primary key for all objects in the collection. +func New() *Collection { + c := &Collection{ + indexes: make(map[string]*Index), + idGen: seqid.ID(0), + } + // Initialize _id index + c.indexes[IDIndex] = &Index{ + options: UniqueIndex, + tree: avl.NewTree(), + } + return c +} + +// Collection represents a collection of objects with multiple indexes +type Collection struct { + indexes map[string]*Index + idGen seqid.ID +} + +const ( + // IDIndex is the reserved name for the primary key index + IDIndex = "_id" +) + +// IndexOption represents configuration options for an index using bit flags +type IndexOption uint64 + +const ( + // DefaultIndex is a basic index with no special options + DefaultIndex IndexOption = 0 + + // UniqueIndex ensures no duplicate values are allowed + UniqueIndex IndexOption = 1 << iota + + // CaseInsensitiveIndex automatically converts string values to lowercase + CaseInsensitiveIndex + + // SparseIndex only indexes non-empty values + SparseIndex +) + +// Index represents an index with its configuration and data. +// The index function can return either: +// - string: for single-value indexes +// - []string: for multi-value indexes where one object can be indexed under multiple keys +// +// The backing tree stores either a single ID or []string for multiple IDs per key. +type Index struct { + fn interface{} + options IndexOption + tree avl.ITree +} + +// AddIndex adds a new index to the collection with the specified options +// +// Parameters: +// - name: the unique name of the index (e.g., "tags") +// - indexFn: a function that extracts either a string or []string from an object +// - options: bit flags for index configuration (e.g., UniqueIndex) +func (c *Collection) AddIndex(name string, indexFn interface{}, options IndexOption) { + if name == IDIndex { + panic("_id is a reserved index name") + } + c.indexes[name] = &Index{ + fn: indexFn, + options: options, + tree: avl.NewTree(), + } +} + +// storeIndex handles how we store an ID in the index tree +func (idx *Index) store(key string, idStr string) { + stored, exists := idx.tree.Get(key) + if !exists { + // First entry for this key + idx.tree.Set(key, idStr) + return + } + + // Handle existing entries + switch existing := stored.(type) { + case string: + if existing == idStr { + return // Already stored + } + // Convert to array + idx.tree.Set(key, []string{existing, idStr}) + case []string: + // Check if ID already exists + for _, id := range existing { + if id == idStr { + return + } + } + // Append new ID + idx.tree.Set(key, append(existing, idStr)) + } +} + +// removeIndex handles how we remove an ID from the index tree +func (idx *Index) remove(key string, idStr string) { + stored, exists := idx.tree.Get(key) + if !exists { + return + } + + switch existing := stored.(type) { + case string: + if existing == idStr { + idx.tree.Remove(key) + } + case []string: + newIds := make([]string, 0, len(existing)) + for _, id := range existing { + if id != idStr { + newIds = append(newIds, id) + } + } + if len(newIds) == 0 { + idx.tree.Remove(key) + } else if len(newIds) == 1 { + idx.tree.Set(key, newIds[0]) + } else { + idx.tree.Set(key, newIds) + } + } +} + +// generateKeys extracts one or more keys from an object for a given index. +func generateKeys(idx *Index, obj interface{}) ([]string, bool) { + if obj == nil { + return nil, false + } + + switch fnTyped := idx.fn.(type) { + case func(interface{}) string: + // Single-value index + key := fnTyped(obj) + return []string{key}, true + case func(interface{}) []string: + // Multi-value index + keys := fnTyped(obj) + return keys, true + default: + panic("invalid index function type") + } +} + +// Set adds or updates an object in the collection. +// Returns a positive ID if successful. +// Returns 0 if: +// - The object is nil +// - A uniqueness constraint would be violated +// - Index generation fails for any index +func (c *Collection) Set(obj interface{}) uint64 { + if obj == nil { + return 0 + } + + // Generate new ID + id := c.idGen.Next() + idStr := id.String() + + // Check uniqueness constraints first + for name, idx := range c.indexes { + if name == IDIndex { + continue + } + keys, ok := generateKeys(idx, obj) + if !ok { + return 0 + } + + for _, key := range keys { + // Skip empty values for sparse indexes + if idx.options&SparseIndex != 0 && key == "" { + continue + } + if idx.options&CaseInsensitiveIndex != 0 { + key = strings.ToLower(key) + } + // Only check uniqueness for unique + single-value indexes + // (UniqueIndex is ambiguous; skipping that scenario) + if idx.options&UniqueIndex != 0 { + if existing, exists := idx.tree.Get(key); exists && existing != nil { + return 0 + } + } + } + } + + // Store in _id index first (the actual object) + c.indexes[IDIndex].tree.Set(idStr, obj) + + // Store in all other indexes + for name, idx := range c.indexes { + if name == IDIndex { + continue + } + keys, ok := generateKeys(idx, obj) + if !ok { + // Rollback: remove from _id index + c.indexes[IDIndex].tree.Remove(idStr) + return 0 + } + + for _, key := range keys { + if idx.options&SparseIndex != 0 && key == "" { + continue + } + if idx.options&CaseInsensitiveIndex != 0 { + key = strings.ToLower(key) + } + idx.store(key, idStr) + } + } + + return uint64(id) +} + +// Get retrieves entries matching the given key in the specified index. +// Returns an iterator over the matching entries. +func (c *Collection) Get(indexName string, key string) EntryIterator { + idx, exists := c.indexes[indexName] + if !exists { + return EntryIterator{err: errors.New("index not found: " + indexName)} + } + + if idx.options&CaseInsensitiveIndex != 0 { + key = strings.ToLower(key) + } + + if indexName == IDIndex { + // For ID index, validate the ID format first + _, err := seqid.FromString(key) + if err != nil { + return EntryIterator{err: err} + } + } + + return EntryIterator{ + collection: c, + indexName: indexName, + key: key, + } +} + +// GetFirst returns the first matching entry or nil if none found +func (c *Collection) GetFirst(indexName, key string) *Entry { + iter := c.Get(indexName, key) + if iter.Next() { + return iter.Value() + } + return nil +} + +// Delete removes an object by its ID and returns true if something was deleted +func (c *Collection) Delete(id uint64) bool { + idStr := seqid.ID(id).String() + + // Get the object first to clean up other indexes + obj, exists := c.indexes[IDIndex].tree.Get(idStr) + if !exists { + return false + } + + // Remove from all indexes + for name, idx := range c.indexes { + if name == IDIndex { + idx.tree.Remove(idStr) + continue + } + keys, ok := generateKeys(idx, obj) + if !ok { + continue + } + for _, key := range keys { + if idx.options&CaseInsensitiveIndex != 0 { + key = strings.ToLower(key) + } + idx.remove(key, idStr) + } + } + return true +} + +// Update updates an existing object and returns true if successful +// Returns true if the update was successful. +// Returns false if: +// - The object is nil +// - The ID doesn't exist +// - A uniqueness constraint would be violated +// - Index generation fails for any index +// +// If the update fails, the collection remains unchanged. +func (c *Collection) Update(id uint64, obj interface{}) bool { + if obj == nil { + return false + } + idStr := seqid.ID(id).String() + oldObj, exists := c.indexes[IDIndex].tree.Get(idStr) + if !exists { + return false + } + + // Check unique constraints + for name, idx := range c.indexes { + if name == IDIndex { + continue + } + + if idx.options&UniqueIndex != 0 { + newKeys, newOk := generateKeys(idx, obj) + _, oldOk := generateKeys(idx, oldObj) + if !newOk || !oldOk { + return false + } + + for _, newKey := range newKeys { + if idx.options&CaseInsensitiveIndex != 0 { + newKey = strings.ToLower(newKey) + } + + found, _ := idx.tree.Get(newKey) + if found != nil { + if storedID, ok := found.(string); !ok || storedID != idStr { + return false + } + } + } + } + } + + // Store old index entries for potential rollback + oldEntries := make(map[string][]string) + for name, idx := range c.indexes { + if name == IDIndex { + continue + } + oldKeys, ok := generateKeys(idx, oldObj) + if !ok { + continue + } + var adjusted []string + for _, okey := range oldKeys { + if idx.options&CaseInsensitiveIndex != 0 { + okey = strings.ToLower(okey) + } + // Remove the oldObj from the index right away + idx.remove(okey, idStr) + adjusted = append(adjusted, okey) + } + oldEntries[name] = adjusted + } + + // Update the object in the _id index + c.indexes[IDIndex].tree.Set(idStr, obj) + + // Add new index entries + for name, idx := range c.indexes { + if name == IDIndex { + continue + } + newKeys, ok := generateKeys(idx, obj) + if !ok { + // Rollback: restore old object and old index entries + c.indexes[IDIndex].tree.Set(idStr, oldObj) + for idxName, keys := range oldEntries { + for _, oldKey := range keys { + c.indexes[idxName].store(oldKey, idStr) + } + } + return false + } + for _, nkey := range newKeys { + if idx.options&CaseInsensitiveIndex != 0 { + nkey = strings.ToLower(nkey) + } + idx.store(nkey, idStr) + } + } + + return true +} + +// GetAll retrieves all entries matching the given key in the specified index. +func (c *Collection) GetAll(indexName string, key string) []Entry { + idx, exists := c.indexes[indexName] + if !exists { + return nil + } + + if idx.options&CaseInsensitiveIndex != 0 { + key = strings.ToLower(key) + } + + if indexName == IDIndex { + if obj, exists := idx.tree.Get(key); exists { + return []Entry{{ID: key, Obj: obj}} + } + return nil + } + + idData, exists := idx.tree.Get(key) + if !exists { + return nil + } + + // Handle both single and multi-value cases based on the actual data type + switch stored := idData.(type) { + case []string: + result := make([]Entry, 0, len(stored)) + for _, idStr := range stored { + if obj, exists := c.indexes[IDIndex].tree.Get(idStr); exists { + result = append(result, Entry{ID: idStr, Obj: obj}) + } + } + return result + case string: + if obj, exists := c.indexes[IDIndex].tree.Get(stored); exists { + return []Entry{{ID: stored, Obj: obj}} + } + } + return nil +} + +// GetIndex returns the underlying tree for an index +func (c *Collection) GetIndex(name string) avl.ITree { + idx, exists := c.indexes[name] + if !exists { + return nil + } + return idx.tree +} diff --git a/examples/gno.land/p/moul/collection/collection_test.gno b/examples/gno.land/p/moul/collection/collection_test.gno new file mode 100644 index 00000000000..3e03d222ce8 --- /dev/null +++ b/examples/gno.land/p/moul/collection/collection_test.gno @@ -0,0 +1,987 @@ +package collection + +import ( + "errors" + "strconv" + "strings" + "testing" + + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" +) + +type Person struct { + Name string + Age int + Email string + Username string + Tags []string +} + +func (p Person) String() string { + return ufmt.Sprintf("name=%s age=%d email=%s username=%s tags=%s", + p.Name, p.Age, p.Email, p.Username, strings.Join(p.Tags, ",")) +} + +// TestOperation represents a single operation in a test sequence +type TestOperation struct { + op string // "set" or "update" + person *Person + id uint64 // for updates + wantID uint64 + wantErr bool +} + +// TestCase represents a complete test case with setup and operations +type TestCase struct { + name string + setupIndex func(*Collection) + operations []TestOperation +} + +func TestBasicOperations(t *testing.T) { + c := New() + + // Add indexes + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex) + c.AddIndex("age", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }, DefaultIndex) + + // Test basic Set and Get + p1 := &Person{Name: "Alice", Age: 30, Email: "alice@test.com"} + id1 := c.Set(p1) + if id1 == 0 { + t.Error("Failed to set first object") + } + + // Get by ID + iter := c.Get(IDIndex, seqid.ID(id1).String()) + if !iter.Next() { + t.Error("Failed to get object by ID") + } + entry := iter.Value() + if entry.Obj.(*Person).Name != "Alice" { + t.Error("Got wrong object") + } +} + +func TestUniqueConstraints(t *testing.T) { + tests := []struct { + name string + setup func(*Collection) uint64 + wantID bool + }{ + { + name: "First person", + setup: func(c *Collection) uint64 { + return c.Set(&Person{Name: "Alice"}) + }, + wantID: true, + }, + { + name: "Duplicate name", + setup: func(c *Collection) uint64 { + c.Set(&Person{Name: "Alice"}) + return c.Set(&Person{Name: "Alice"}) + }, + wantID: false, + }, + { + name: "Same age (non-unique index)", + setup: func(c *Collection) uint64 { + c.AddIndex("age", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }, DefaultIndex) + c.Set(&Person{Name: "Alice", Age: 30}) + return c.Set(&Person{Name: "Bob", Age: 30}) + }, + wantID: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := New() + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex) + + id := tt.setup(c) + if (id != 0) != tt.wantID { + t.Errorf("Set() got id = %v, want non-zero: %v", id, tt.wantID) + } + }) + } +} + +func TestUpdates(t *testing.T) { + c := New() + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex) + c.AddIndex("username", func(v interface{}) string { + return strings.ToLower(v.(*Person).Username) + }, UniqueIndex|CaseInsensitiveIndex) + + // Initial setup + p1 := &Person{Name: "Alice", Username: "alice123"} + p2 := &Person{Name: "Bob", Username: "bob456"} + + id1 := c.Set(p1) + id2 := c.Set(p2) + + tests := []struct { + name string + id uint64 + newPerson *Person + wantRet bool + }{ + { + name: "Update to non-conflicting values", + id: id1, + newPerson: &Person{Name: "Alice2", Username: "alice1234"}, + wantRet: true, + }, + { + name: "Update to conflicting username", + id: id1, + newPerson: &Person{Name: "Alice2", Username: "bob456"}, + wantRet: false, + }, + { + name: "Update non-existent ID", + id: 99999, + newPerson: &Person{Name: "Test", Username: "test"}, + wantRet: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotID := c.Update(tt.id, tt.newPerson) + if gotID != tt.wantRet { + t.Errorf("Update() got = %v, want %v", gotID, tt.wantRet) + } + }) + } +} + +func TestDelete(t *testing.T) { + c := New() + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex) + + p1 := &Person{Name: "Alice"} + id1 := c.Set(p1) + + tests := []struct { + name string + id uint64 + wantRet bool + }{ + { + name: "Delete existing object", + id: id1, + wantRet: true, + }, + { + name: "Delete non-existent object", + id: 99999, + wantRet: false, + }, + { + name: "Delete already deleted object", + id: id1, + wantRet: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotID := c.Delete(tt.id) + if gotID != tt.wantRet { + t.Errorf("Delete() got = %v, want %v", gotID, tt.wantRet) + } + }) + } +} + +func TestEdgeCases(t *testing.T) { + c := New() + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex) + + tests := []struct { + name string + operation func() bool + wantPanic bool + }{ + { + name: "Set nil object", + operation: func() bool { + return c.Set(nil) != 0 + }, + wantPanic: false, + }, + { + name: "Set wrong type", + operation: func() bool { + return c.Set("not a person") != 0 + }, + wantPanic: true, + }, + { + name: "Update with nil", + operation: func() bool { + id := c.Set(&Person{Name: "Test"}) + return c.Update(id, nil) + }, + wantPanic: false, + }, + { + name: "Get with invalid index name", + operation: func() bool { + iter := c.Get("invalid_index", "key") + if iter.Empty() { + return false + } + entry := iter.Value() + if entry == nil { + return false + } + id, err := seqid.FromString(entry.ID) + if err != nil { + return false + } + return true + }, + wantPanic: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got bool + panicked := false + + func() { + defer func() { + if r := recover(); r != nil { + panicked = true + } + }() + got = tt.operation() + }() + + if panicked != tt.wantPanic { + t.Errorf("Operation panicked = %v, want panic = %v", panicked, tt.wantPanic) + } + if !panicked && got != false { + t.Errorf("Operation returned %v, want 0", got) + } + }) + } +} + +func TestIndexTypes(t *testing.T) { + c := New() + + // Test different types of indexes + c.AddIndex("composite", func(v interface{}) string { + p := v.(*Person) + return p.Name + ":" + strconv.Itoa(p.Age) + }, UniqueIndex) + + c.AddIndex("case_insensitive", func(v interface{}) string { + return strings.ToLower(v.(*Person).Username) + }, UniqueIndex|CaseInsensitiveIndex) + + // Test composite index + p1 := &Person{Name: "Alice", Age: 30, Username: "Alice123"} + id1 := c.Set(p1) + if id1 == 0 { + t.Error("Failed to set object with composite index") + } + + // Test case-insensitive index + p2 := &Person{Name: "Bob", Age: 25, Username: "alice123"} + id2 := c.Set(p2) + if id2 != 0 { + t.Error("Case-insensitive index failed to prevent duplicate") + } +} + +func TestIndexOptions(t *testing.T) { + tests := []struct { + name string + setup func(*Collection) uint64 + wantID bool + wantErr bool + }{ + { + name: "Unique case-sensitive index", + setup: func(c *Collection) uint64 { + c.AddIndex("username", func(v interface{}) string { + return v.(*Person).Username + }, UniqueIndex) + + id1 := c.Set(&Person{Username: "Alice"}) + return c.Set(&Person{Username: "Alice"}) // Should fail + }, + wantID: false, + }, + { + name: "Unique case-insensitive index", + setup: func(c *Collection) uint64 { + c.AddIndex("email", func(v interface{}) string { + return v.(*Person).Email + }, UniqueIndex|CaseInsensitiveIndex) + + id1 := c.Set(&Person{Email: "test@example.com"}) + return c.Set(&Person{Email: "TEST@EXAMPLE.COM"}) // Should fail + }, + wantID: false, + }, + { + name: "Default index", + setup: func(c *Collection) uint64 { + c.AddIndex("age", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }, DefaultIndex) + + // First person with age 30 + id1 := c.Set(&Person{Age: 30}) + if id1 == 0 { + t.Error("Failed to set first person") + } + + // Second person with same age should succeed + return c.Set(&Person{Age: 30}) + }, + wantID: true, + }, + { + name: "Multiple options", + setup: func(c *Collection) uint64 { + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex|CaseInsensitiveIndex|SparseIndex) + + id1 := c.Set(&Person{Name: "Alice"}) + return c.Set(&Person{Name: "ALICE"}) // Should fail + }, + wantID: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := New() // Create new collection for each test + id := tt.setup(c) + if (id != 0) != tt.wantID { + t.Errorf("got id = %v, want non-zero: %v", id, tt.wantID) + } + }) + } +} + +func TestConcurrentOperations(t *testing.T) { + c := New() + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex) + + p1 := &Person{Name: "Alice"} + id1 := c.Set(p1) + iter := c.Get("_id", seqid.ID(id1).String()) + success := c.Update(id1, &Person{Name: "Alice2"}) + + if iter.Empty() || !success { + t.Error("Concurrent operations failed") + } +} + +func TestSparseIndexBehavior(t *testing.T) { + c := New() + c.AddIndex("optional_field", func(v interface{}) string { + return v.(*Person).Username + }, SparseIndex) + + tests := []struct { + name string + person *Person + wantID bool + }{ + { + name: "Empty optional field", + person: &Person{Name: "Alice", Email: "alice@test.com"}, + wantID: true, + }, + { + name: "Populated optional field", + person: &Person{Name: "Bob", Email: "bob@test.com", Username: "bobby"}, + wantID: true, + }, + { + name: "Multiple empty fields", + person: &Person{Name: "Charlie"}, + wantID: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := c.Set(tt.person) + if (id != 0) != tt.wantID { + t.Errorf("Set() got id = %v, want non-zero: %v", id, tt.wantID) + } + }) + } +} + +func TestIndexKeyGeneration(t *testing.T) { + c := New() + c.AddIndex("composite", func(v interface{}) string { + p := v.(*Person) + return p.Name + ":" + strconv.Itoa(p.Age) + }, UniqueIndex) + + tests := []struct { + name string + person *Person + wantID bool + }{ + { + name: "Valid composite key", + person: &Person{Name: "Alice", Age: 30}, + wantID: true, + }, + { + name: "Duplicate composite key", + person: &Person{Name: "Alice", Age: 30}, + wantID: false, + }, + { + name: "Different composite key", + person: &Person{Name: "Alice", Age: 31}, + wantID: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id := c.Set(tt.person) + if (id != 0) != tt.wantID { + t.Errorf("Set() got id = %v, want non-zero: %v", id, tt.wantID) + } + }) + } +} + +func TestGetIndex(t *testing.T) { + c := New() + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex) + + tests := []struct { + name string + indexName string + wantNil bool + }{ + { + name: "Get existing index", + indexName: "name", + wantNil: false, + }, + { + name: "Get _id index", + indexName: IDIndex, + wantNil: false, + }, + { + name: "Get non-existent index", + indexName: "invalid", + wantNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tree := c.GetIndex(tt.indexName) + if (tree == nil) != tt.wantNil { + t.Errorf("GetIndex() got nil = %v, want nil = %v", tree == nil, tt.wantNil) + } + }) + } +} + +func TestAddIndexPanic(t *testing.T) { + c := New() + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic when adding _id index") + } + }() + + c.AddIndex(IDIndex, func(v interface{}) string { + return "" + }, DefaultIndex) +} + +func TestCaseInsensitiveOperations(t *testing.T) { + c := New() + c.AddIndex("email", func(v interface{}) string { + return v.(*Person).Email + }, UniqueIndex|CaseInsensitiveIndex) + + p := &Person{Email: "Test@Example.com"} + id := c.Set(p) + + tests := []struct { + name string + key string + wantObj bool + operation string // "get" or "getAll" + wantCount int + }{ + {"Get exact match", "Test@Example.com", true, "get", 1}, + {"Get different case", "test@example.COM", true, "get", 1}, + {"Get non-existent", "other@example.com", false, "get", 0}, + {"GetAll exact match", "Test@Example.com", true, "getAll", 1}, + {"GetAll different case", "test@example.COM", true, "getAll", 1}, + {"GetAll non-existent", "other@example.com", false, "getAll", 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.operation == "get" { + iter := c.Get("email", tt.key) + if iter.Empty() { + if tt.wantObj { + t.Error("Expected iterator to not be empty") + } + return + } + hasValue := iter.Next() + if hasValue != tt.wantObj { + t.Errorf("Get() got object = %v, want object = %v", hasValue, tt.wantObj) + } + if hasValue { + entry := iter.Value() + if entry.ID != seqid.ID(id).String() { + t.Errorf("Get() got id = %v, want id = %v", entry.ID, seqid.ID(id).String()) + } + } + } else { + entries := c.GetAll("email", tt.key) + if len(entries) != tt.wantCount { + t.Errorf("GetAll() returned %d entries, want %d", len(entries), tt.wantCount) + } + if tt.wantCount > 0 { + entry := entries[0] + if entry.ID != seqid.ID(id).String() { + t.Errorf("GetAll() returned ID %s, want %s", entry.ID, seqid.ID(id).String()) + } + } + } + }) + } +} + +func TestGetInvalidID(t *testing.T) { + c := New() + iter := c.Get(IDIndex, "not-a-valid-id") + if !iter.Empty() { + t.Errorf("Get() with invalid ID format got an entry, want nil") + } +} + +func TestGetAll(t *testing.T) { + c := New() + c.AddIndex("tags", func(v interface{}) []string { + return v.(*Person).Tags + }, DefaultIndex) + c.AddIndex("age", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }, DefaultIndex) + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex) + + // Create test data + people := []*Person{ + {Name: "Alice", Age: 30, Tags: []string{"dev", "go"}}, + {Name: "Bob", Age: 30, Tags: []string{"dev", "python"}}, + {Name: "Charlie", Age: 25, Tags: []string{"dev", "rust"}}, + } + + ids := make([]uint64, len(people)) + for i, p := range people { + ids[i] = c.Set(p) + if ids[i] == 0 { + t.Fatalf("Failed to set person %s", p.Name) + } + } + + tests := []struct { + name string + indexName string + key string + wantCount int + }{ + { + name: "Multi-value index with multiple matches", + indexName: "tags", + key: "dev", + wantCount: 3, + }, + { + name: "Multi-value index with single match", + indexName: "tags", + key: "go", + wantCount: 1, + }, + { + name: "Multi-value index with no matches", + indexName: "tags", + key: "java", + wantCount: 0, + }, + { + name: "Single-value non-unique index with multiple matches", + indexName: "age", + key: "30", + wantCount: 2, + }, + { + name: "Single-value unique index", + indexName: "name", + key: "Alice", + wantCount: 1, + }, + { + name: "Non-existent index", + indexName: "invalid", + key: "value", + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + iter := c.Get(tt.indexName, tt.key) + count := 0 + for iter.Next() { + entry := iter.Value() + if entry.ID == "" { + t.Error("Got entry with empty ID") + } + if entry.Obj == nil { + t.Error("Got entry with nil Obj") + } + count++ + } + if count != tt.wantCount { + t.Errorf("Got %d entries, want %d", count, tt.wantCount) + } + }) + } +} + +func TestIndexOperations(t *testing.T) { + tests := []struct { + name string + setup func(*Collection) (uint64, error) + verify func(*Collection, uint64) error + wantErr bool + }{ + { + name: "Basic set and get", + setup: func(c *Collection) (uint64, error) { + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex) + return c.Set(&Person{Name: "Alice", Age: 30}), nil + }, + verify: func(c *Collection, id uint64) error { + iter := c.Get(IDIndex, seqid.ID(id).String()) + if !iter.Next() { + return errors.New("failed to get object by ID") + } + entry := iter.Value() + if entry.Obj.(*Person).Name != "Alice" { + return errors.New("got wrong object") + } + return nil + }, + }, + { + name: "Composite index", + setup: func(c *Collection) (uint64, error) { + c.AddIndex("composite", func(v interface{}) string { + p := v.(*Person) + return p.Name + ":" + strconv.Itoa(p.Age) + }, UniqueIndex) + return c.Set(&Person{Name: "Alice", Age: 30}), nil + }, + verify: func(c *Collection, id uint64) error { + iter := c.Get("composite", "Alice:30") + if !iter.Next() { + return errors.New("failed to get object by composite index") + } + return nil + }, + }, + // Add more test cases combining unique scenarios from original tests + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := New() + id, err := tt.setup(c) + if (err != nil) != tt.wantErr { + t.Errorf("setup error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil { + if err := tt.verify(c, id); err != nil { + t.Errorf("verification failed: %v", err) + } + } + }) + } +} + +func TestMultiValueIndexes(t *testing.T) { + c := New() + c.AddIndex("tags", func(v interface{}) []string { + return v.(*Person).Tags + }, DefaultIndex) + + tests := []struct { + name string + setup []*Person + searchTag string + wantCount int + }{ + { + name: "Multiple tags, multiple matches", + setup: []*Person{ + {Name: "Alice", Tags: []string{"dev", "go"}}, + {Name: "Bob", Tags: []string{"dev", "python"}}, + {Name: "Charlie", Tags: []string{"dev", "rust"}}, + }, + searchTag: "dev", + wantCount: 3, + }, + { + name: "Single tag match", + setup: []*Person{ + {Name: "Alice", Tags: []string{"dev", "go"}}, + {Name: "Bob", Tags: []string{"dev", "python"}}, + }, + searchTag: "go", + wantCount: 1, + }, + { + name: "No matches", + setup: []*Person{ + {Name: "Alice", Tags: []string{"dev", "go"}}, + {Name: "Bob", Tags: []string{"dev", "python"}}, + }, + searchTag: "java", + wantCount: 0, + }, + { + name: "Empty tags", + setup: []*Person{ + {Name: "Alice", Tags: []string{}}, + {Name: "Bob", Tags: nil}, + }, + searchTag: "dev", + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := New() + c.AddIndex("tags", func(v interface{}) []string { + return v.(*Person).Tags + }, DefaultIndex) + + // Setup test data + for _, p := range tt.setup { + if id := c.Set(p); id == 0 { + t.Fatalf("Failed to set person %s", p.Name) + } + } + + // Test Get operation + iter := c.Get("tags", tt.searchTag) + count := 0 + for iter.Next() { + count++ + } + if count != tt.wantCount { + t.Errorf("Get() got %d matches, want %d", count, tt.wantCount) + } + }) + } +} + +func TestGetOperations(t *testing.T) { + c := New() + c.AddIndex("name", func(v interface{}) string { + return v.(*Person).Name + }, UniqueIndex) + c.AddIndex("age", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }, DefaultIndex) + + // Setup test data + testPeople := []*Person{ + {Name: "Alice", Age: 30}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 25}, + } + + ids := make([]uint64, len(testPeople)) + for i, p := range testPeople { + ids[i] = c.Set(p) + if ids[i] == 0 { + t.Fatalf("Failed to set person %s", p.Name) + } + } + + tests := []struct { + name string + indexName string + key string + wantCount int + wantErr bool + }{ + { + name: "Get by ID", + indexName: IDIndex, + key: seqid.ID(ids[0]).String(), + wantCount: 1, + wantErr: false, + }, + { + name: "Get by unique index", + indexName: "name", + key: "Alice", + wantCount: 1, + wantErr: false, + }, + { + name: "Get by non-unique index", + indexName: "age", + key: "30", + wantCount: 2, + wantErr: false, + }, + { + name: "Get with invalid index", + indexName: "invalid_index", + key: "value", + wantCount: 0, + wantErr: true, + }, + { + name: "Get with invalid ID format", + indexName: IDIndex, + key: "not-a-valid-id", + wantCount: 0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + iter := c.Get(tt.indexName, tt.key) + if iter.Empty() { + if !tt.wantErr { + t.Errorf("Get() returned empty iterator, wanted %d results", tt.wantCount) + } + return + } + + count := 0 + for iter.Next() { + entry := iter.Value() + if entry.ID == "" { + t.Error("Got entry with empty ID") + } + if entry.Obj == nil { + t.Error("Got entry with nil Obj") + } + count++ + } + + if count != tt.wantCount { + t.Errorf("Get() returned %d results, want %d", count, tt.wantCount) + } + }) + } +} + +func TestEntryString(t *testing.T) { + tests := []struct { + name string + entry *Entry + expected string + }{ + { + name: "Nil entry", + entry: nil, + expected: "", + }, + { + name: "Person entry", + entry: &Entry{ + ID: "123", + Obj: &Person{Name: "Alice", Age: 30}, + }, + expected: `Entry{ID: 123, Obj: name=Alice age=30 email= username= tags=}`, + }, + { + name: "Entry with nil object", + entry: &Entry{ + ID: "456", + Obj: nil, + }, + expected: `Entry{ID: 456, Obj: }`, + }, + { + name: "Entry with complete person", + entry: &Entry{ + ID: "789", + Obj: &Person{ + Name: "Bob", + Age: 25, + Email: "bob@example.com", + Username: "bobby", + Tags: []string{"dev", "go"}, + }, + }, + expected: `Entry{ID: 789, Obj: name=Bob age=25 email=bob@example.com username=bobby tags=dev,go}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.entry.String() + if got != tt.expected { + t.Errorf("Entry.String() = %q, want %q", got, tt.expected) + } + }) + } +} diff --git a/examples/gno.land/p/moul/collection/entry.gno b/examples/gno.land/p/moul/collection/entry.gno new file mode 100644 index 00000000000..8daa893b61d --- /dev/null +++ b/examples/gno.land/p/moul/collection/entry.gno @@ -0,0 +1,149 @@ +package collection + +import "gno.land/p/demo/ufmt" + +// Entry represents a single object in the collection with its ID +type Entry struct { + ID string + Obj interface{} +} + +// String returns a string representation of the Entry +func (e *Entry) String() string { + if e == nil { + return "" + } + return ufmt.Sprintf("Entry{ID: %s, Obj: %v}", e.ID, e.Obj) +} + +// EntryIterator provides iteration over collection entries +type EntryIterator struct { + collection *Collection + indexName string + key string + currentID string + currentObj interface{} + err error + closed bool + + // For multi-value cases + ids []string + currentIdx int +} + +func (ei *EntryIterator) Close() error { + ei.closed = true + ei.currentID = "" + ei.currentObj = nil + ei.ids = nil + return nil +} + +func (ei *EntryIterator) Next() bool { + if ei == nil || ei.closed || ei.err != nil { + return false + } + + // Handle ID index specially + if ei.indexName == IDIndex { + if ei.currentID != "" { // We've already returned the single value + return false + } + obj, exists := ei.collection.indexes[IDIndex].tree.Get(ei.key) + if !exists { + return false + } + ei.currentID = ei.key + ei.currentObj = obj + return true + } + + // Get the index + idx, exists := ei.collection.indexes[ei.indexName] + if !exists { + return false + } + + // Initialize ids slice if needed + if ei.ids == nil { + idData, exists := idx.tree.Get(ei.key) + if !exists { + return false + } + + switch stored := idData.(type) { + case []string: + ei.ids = stored + ei.currentIdx = -1 + case string: + ei.ids = []string{stored} + ei.currentIdx = -1 + default: + return false + } + } + + // Move to next ID + ei.currentIdx++ + if ei.currentIdx >= len(ei.ids) { + return false + } + + // Fetch the actual object + ei.currentID = ei.ids[ei.currentIdx] + obj, exists := ei.collection.indexes[IDIndex].tree.Get(ei.currentID) + if !exists { + // Skip invalid entries + return ei.Next() + } + ei.currentObj = obj + return true +} + +func (ei *EntryIterator) Error() error { + return ei.err +} + +func (ei *EntryIterator) Value() *Entry { + if ei == nil || ei.closed || ei.currentID == "" { + return nil + } + return &Entry{ + ID: ei.currentID, + Obj: ei.currentObj, + } +} + +func (ei *EntryIterator) Empty() bool { + if ei == nil || ei.closed || ei.err != nil { + return true + } + + // Handle ID index specially + if ei.indexName == IDIndex { + _, exists := ei.collection.indexes[IDIndex].tree.Get(ei.key) + return !exists + } + + // Get the index + idx, exists := ei.collection.indexes[ei.indexName] + if !exists { + return true + } + + // Check if key exists in index + idData, exists := idx.tree.Get(ei.key) + if !exists { + return true + } + + // Check if there are any valid IDs + switch stored := idData.(type) { + case []string: + return len(stored) == 0 + case string: + return stored == "" + default: + return true + } +} diff --git a/examples/gno.land/p/moul/collection/gno.mod b/examples/gno.land/p/moul/collection/gno.mod new file mode 100644 index 00000000000..a6eeca36837 --- /dev/null +++ b/examples/gno.land/p/moul/collection/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/collection diff --git a/examples/gno.land/p/moul/debug/debug.gno b/examples/gno.land/p/moul/debug/debug.gno new file mode 100644 index 00000000000..9ba3dd36a98 --- /dev/null +++ b/examples/gno.land/p/moul/debug/debug.gno @@ -0,0 +1,92 @@ +// Package debug provides utilities for logging and displaying debug information +// within Gno realms. It supports conditional rendering of logs and metadata, +// toggleable via query parameters. +// +// Key Features: +// - Log collection and display using Markdown formatting. +// - Metadata display for realm path, address, and height. +// - Collapsible debug section for cleaner presentation. +// - Query-based debug toggle using `?debug=1`. +package debug + +import ( + "std" + "time" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/moul/realmpath" +) + +// Debug encapsulates debug information, including logs and metadata. +type Debug struct { + Logs []string + HideMetadata bool +} + +// Log appends a new line of debug information to the Logs slice. +func (d *Debug) Log(line string) { + d.Logs = append(d.Logs, line) +} + +// Render generates the debug content as a collapsible Markdown section. +// It conditionally renders logs and metadata if enabled via the `?debug=1` query parameter. +func (d Debug) Render(path string) string { + if realmpath.Parse(path).Query.Get("debug") != "1" { + return "" + } + + var content string + + if d.Logs != nil { + content += md.H3("Logs") + content += md.BulletList(d.Logs) + } + + if !d.HideMetadata { + content += md.H3("Metadata") + table := mdtable.Table{ + Headers: []string{"Key", "Value"}, + } + table.Append([]string{"`std.CurrentRealm().PkgPath()`", string(std.CurrentRealm().PkgPath())}) + table.Append([]string{"`std.CurrentRealm().Addr()`", string(std.CurrentRealm().Addr())}) + table.Append([]string{"`std.PrevRealm().PkgPath()`", string(std.PrevRealm().PkgPath())}) + table.Append([]string{"`std.PrevRealm().Addr()`", string(std.PrevRealm().Addr())}) + table.Append([]string{"`std.GetHeight()`", ufmt.Sprintf("%d", std.GetHeight())}) + table.Append([]string{"`time.Now().Format(time.RFC3339)`", time.Now().Format(time.RFC3339)}) + content += table.String() + } + + if content == "" { + return "" + } + + return md.CollapsibleSection("debug", content) +} + +// Render displays metadata about the current realm but does not display logs. +// This function uses a default Debug struct with metadata enabled and no logs. +func Render(path string) string { + return Debug{}.Render(path) +} + +// IsEnabled checks if the `?debug=1` query parameter is set in the given path. +// Returns true if debugging is enabled, otherwise false. +func IsEnabled(path string) bool { + req := realmpath.Parse(path) + return req.Query.Get("debug") == "1" +} + +// ToggleURL modifies the given path's query string to toggle the `?debug=1` parameter. +// If debugging is currently enabled, it removes the parameter. +// If debugging is disabled, it adds the parameter. +func ToggleURL(path string) string { + req := realmpath.Parse(path) + if IsEnabled(path) { + req.Query.Del("debug") + } else { + req.Query.Add("debug", "1") + } + return req.String() +} diff --git a/examples/gno.land/p/moul/debug/gno.mod b/examples/gno.land/p/moul/debug/gno.mod new file mode 100644 index 00000000000..eb48ed292ca --- /dev/null +++ b/examples/gno.land/p/moul/debug/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/debug diff --git a/examples/gno.land/p/moul/debug/z1_filetest.gno b/examples/gno.land/p/moul/debug/z1_filetest.gno new file mode 100644 index 00000000000..8203749d3c7 --- /dev/null +++ b/examples/gno.land/p/moul/debug/z1_filetest.gno @@ -0,0 +1,31 @@ +package main + +import "gno.land/p/moul/debug" + +func main() { + println("---") + println(debug.Render("")) + println("---") + println(debug.Render("?debug=1")) + println("---") +} + +// Output: +// --- +// +// --- +//
debug +// +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | | +// | `std.CurrentRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | +// +//
+// +// --- diff --git a/examples/gno.land/p/moul/debug/z2_filetest.gno b/examples/gno.land/p/moul/debug/z2_filetest.gno new file mode 100644 index 00000000000..32c2fe49951 --- /dev/null +++ b/examples/gno.land/p/moul/debug/z2_filetest.gno @@ -0,0 +1,37 @@ +package main + +import "gno.land/p/moul/debug" + +func main() { + var d debug.Debug + d.Log("hello world!") + d.Log("foobar") + println("---") + println(d.Render("")) + println("---") + println(d.Render("?debug=1")) + println("---") +} + +// Output: +// --- +// +// --- +//
debug +// +// ### Logs +// - hello world! +// - foobar +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | | +// | `std.CurrentRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | +// +//
+// +// --- diff --git a/examples/gno.land/p/moul/fp/fp.gno b/examples/gno.land/p/moul/fp/fp.gno new file mode 100644 index 00000000000..b2811c77d5a --- /dev/null +++ b/examples/gno.land/p/moul/fp/fp.gno @@ -0,0 +1,270 @@ +// Package fp provides functional programming utilities for Gno, enabling +// transformations, filtering, and other operations on slices of interface{}. +// +// Example of chaining operations: +// +// numbers := []interface{}{1, 2, 3, 4, 5, 6} +// +// // Define predicates, mappers and reducers +// isEven := func(v interface{}) bool { return v.(int)%2 == 0 } +// double := func(v interface{}) interface{} { return v.(int) * 2 } +// sum := func(a, b interface{}) interface{} { return a.(int) + b.(int) } +// +// // Chain operations: filter even numbers, double them, then sum +// evenNums := Filter(numbers, isEven) // [2, 4, 6] +// doubled := Map(evenNums, double) // [4, 8, 12] +// result := Reduce(doubled, sum, 0) // 24 +// +// // Alternative: group by even/odd, then get even numbers +// byMod2 := func(v interface{}) interface{} { return v.(int) % 2 } +// grouped := GroupBy(numbers, byMod2) // {0: [2,4,6], 1: [1,3,5]} +// evens := grouped[0] // [2,4,6] +package fp + +// Mapper is a function type that maps an element to another element. +type Mapper func(interface{}) interface{} + +// Predicate is a function type that evaluates a condition on an element. +type Predicate func(interface{}) bool + +// Reducer is a function type that reduces two elements to a single value. +type Reducer func(interface{}, interface{}) interface{} + +// Filter filters elements from the slice that satisfy the given predicate. +// +// Example: +// +// numbers := []interface{}{-1, 0, 1, 2} +// isPositive := func(v interface{}) bool { return v.(int) > 0 } +// result := Filter(numbers, isPositive) // [1, 2] +func Filter(values []interface{}, fn Predicate) []interface{} { + result := []interface{}{} + for _, v := range values { + if fn(v) { + result = append(result, v) + } + } + return result +} + +// Map applies a function to each element in the slice. +// +// Example: +// +// numbers := []interface{}{1, 2, 3} +// toString := func(v interface{}) interface{} { return fmt.Sprintf("%d", v) } +// result := Map(numbers, toString) // ["1", "2", "3"] +func Map(values []interface{}, fn Mapper) []interface{} { + result := make([]interface{}, len(values)) + for i, v := range values { + result[i] = fn(v) + } + return result +} + +// Reduce reduces a slice to a single value by applying a function. +// +// Example: +// +// numbers := []interface{}{1, 2, 3, 4} +// sum := func(a, b interface{}) interface{} { return a.(int) + b.(int) } +// result := Reduce(numbers, sum, 0) // 10 +func Reduce(values []interface{}, fn Reducer, initial interface{}) interface{} { + acc := initial + for _, v := range values { + acc = fn(acc, v) + } + return acc +} + +// FlatMap maps each element to a collection and flattens the results. +// +// Example: +// +// words := []interface{}{"hello", "world"} +// split := func(v interface{}) interface{} { +// chars := []interface{}{} +// for _, c := range v.(string) { +// chars = append(chars, string(c)) +// } +// return chars +// } +// result := FlatMap(words, split) // ["h","e","l","l","o","w","o","r","l","d"] +func FlatMap(values []interface{}, fn Mapper) []interface{} { + result := []interface{}{} + for _, v := range values { + inner := fn(v).([]interface{}) + result = append(result, inner...) + } + return result +} + +// All returns true if all elements satisfy the predicate. +// +// Example: +// +// numbers := []interface{}{2, 4, 6, 8} +// isEven := func(v interface{}) bool { return v.(int)%2 == 0 } +// result := All(numbers, isEven) // true +func All(values []interface{}, fn Predicate) bool { + for _, v := range values { + if !fn(v) { + return false + } + } + return true +} + +// Any returns true if at least one element satisfies the predicate. +// +// Example: +// +// numbers := []interface{}{1, 3, 4, 7} +// isEven := func(v interface{}) bool { return v.(int)%2 == 0 } +// result := Any(numbers, isEven) // true (4 is even) +func Any(values []interface{}, fn Predicate) bool { + for _, v := range values { + if fn(v) { + return true + } + } + return false +} + +// None returns true if no elements satisfy the predicate. +// +// Example: +// +// numbers := []interface{}{1, 3, 5, 7} +// isEven := func(v interface{}) bool { return v.(int)%2 == 0 } +// result := None(numbers, isEven) // true (no even numbers) +func None(values []interface{}, fn Predicate) bool { + for _, v := range values { + if fn(v) { + return false + } + } + return true +} + +// Chunk splits a slice into chunks of the given size. +// +// Example: +// +// numbers := []interface{}{1, 2, 3, 4, 5} +// result := Chunk(numbers, 2) // [[1,2], [3,4], [5]] +func Chunk(values []interface{}, size int) [][]interface{} { + if size <= 0 { + return nil + } + var chunks [][]interface{} + for i := 0; i < len(values); i += size { + end := i + size + if end > len(values) { + end = len(values) + } + chunks = append(chunks, values[i:end]) + } + return chunks +} + +// Find returns the first element that satisfies the predicate and a boolean indicating if an element was found. +// +// Example: +// +// numbers := []interface{}{1, 2, 3, 4} +// isEven := func(v interface{}) bool { return v.(int)%2 == 0 } +// result, found := Find(numbers, isEven) // 2, true +func Find(values []interface{}, fn Predicate) (interface{}, bool) { + for _, v := range values { + if fn(v) { + return v, true + } + } + return nil, false +} + +// Reverse reverses the order of elements in a slice. +// +// Example: +// +// numbers := []interface{}{1, 2, 3} +// result := Reverse(numbers) // [3, 2, 1] +func Reverse(values []interface{}) []interface{} { + result := make([]interface{}, len(values)) + for i, v := range values { + result[len(values)-1-i] = v + } + return result +} + +// Zip combines two slices into a slice of pairs. If the slices have different lengths, +// extra elements from the longer slice are ignored. +// +// Example: +// +// a := []interface{}{1, 2, 3} +// b := []interface{}{"a", "b", "c"} +// result := Zip(a, b) // [[1,"a"], [2,"b"], [3,"c"]] +func Zip(a, b []interface{}) [][2]interface{} { + length := min(len(a), len(b)) + result := make([][2]interface{}, length) + for i := 0; i < length; i++ { + result[i] = [2]interface{}{a[i], b[i]} + } + return result +} + +// Unzip splits a slice of pairs into two separate slices. +// +// Example: +// +// pairs := [][2]interface{}{{1,"a"}, {2,"b"}, {3,"c"}} +// numbers, letters := Unzip(pairs) // [1,2,3], ["a","b","c"] +func Unzip(pairs [][2]interface{}) ([]interface{}, []interface{}) { + a := make([]interface{}, len(pairs)) + b := make([]interface{}, len(pairs)) + for i, pair := range pairs { + a[i] = pair[0] + b[i] = pair[1] + } + return a, b +} + +// GroupBy groups elements based on a key returned by a Mapper. +// +// Example: +// +// numbers := []interface{}{1, 2, 3, 4, 5, 6} +// byMod3 := func(v interface{}) interface{} { return v.(int) % 3 } +// result := GroupBy(numbers, byMod3) // {0: [3,6], 1: [1,4], 2: [2,5]} +func GroupBy(values []interface{}, fn Mapper) map[interface{}][]interface{} { + result := make(map[interface{}][]interface{}) + for _, v := range values { + key := fn(v) + result[key] = append(result[key], v) + } + return result +} + +// Flatten flattens a slice of slices into a single slice. +// +// Example: +// +// nested := [][]interface{}{{1,2}, {3,4}, {5}} +// result := Flatten(nested) // [1,2,3,4,5] +func Flatten(values [][]interface{}) []interface{} { + result := []interface{}{} + for _, v := range values { + result = append(result, v...) + } + return result +} + +// Helper functions +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/examples/gno.land/p/moul/fp/fp_test.gno b/examples/gno.land/p/moul/fp/fp_test.gno new file mode 100644 index 00000000000..00957486fe9 --- /dev/null +++ b/examples/gno.land/p/moul/fp/fp_test.gno @@ -0,0 +1,666 @@ +package fp + +import ( + "fmt" + "testing" +) + +func TestMap(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) interface{} + expected []interface{} + }{ + { + name: "multiply numbers by 2", + input: []interface{}{1, 2, 3}, + fn: func(v interface{}) interface{} { return v.(int) * 2 }, + expected: []interface{}{2, 4, 6}, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(v interface{}) interface{} { return v.(int) * 2 }, + expected: []interface{}{}, + }, + { + name: "convert numbers to strings", + input: []interface{}{1, 2, 3}, + fn: func(v interface{}) interface{} { return fmt.Sprintf("%d", v.(int)) }, + expected: []interface{}{"1", "2", "3"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Map(tt.input, tt.fn) + if !equalSlices(result, tt.expected) { + t.Errorf("Map failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestFilter(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) bool + expected []interface{} + }{ + { + name: "filter even numbers", + input: []interface{}{1, 2, 3, 4}, + fn: func(v interface{}) bool { return v.(int)%2 == 0 }, + expected: []interface{}{2, 4}, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(v interface{}) bool { return v.(int)%2 == 0 }, + expected: []interface{}{}, + }, + { + name: "no matches", + input: []interface{}{1, 3, 5}, + fn: func(v interface{}) bool { return v.(int)%2 == 0 }, + expected: []interface{}{}, + }, + { + name: "all matches", + input: []interface{}{2, 4, 6}, + fn: func(v interface{}) bool { return v.(int)%2 == 0 }, + expected: []interface{}{2, 4, 6}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Filter(tt.input, tt.fn) + if !equalSlices(result, tt.expected) { + t.Errorf("Filter failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestReduce(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}, interface{}) interface{} + initial interface{} + expected interface{} + }{ + { + name: "sum numbers", + input: []interface{}{1, 2, 3}, + fn: func(a, b interface{}) interface{} { return a.(int) + b.(int) }, + initial: 0, + expected: 6, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(a, b interface{}) interface{} { return a.(int) + b.(int) }, + initial: 0, + expected: 0, + }, + { + name: "concatenate strings", + input: []interface{}{"a", "b", "c"}, + fn: func(a, b interface{}) interface{} { return a.(string) + b.(string) }, + initial: "", + expected: "abc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Reduce(tt.input, tt.fn, tt.initial) + if result != tt.expected { + t.Errorf("Reduce failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestFlatMap(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) interface{} + expected []interface{} + }{ + { + name: "split words into chars", + input: []interface{}{"go", "fn"}, + fn: func(word interface{}) interface{} { + chars := []interface{}{} + for _, c := range word.(string) { + chars = append(chars, string(c)) + } + return chars + }, + expected: []interface{}{"g", "o", "f", "n"}, + }, + { + name: "empty string handling", + input: []interface{}{"", "a", ""}, + fn: func(word interface{}) interface{} { + chars := []interface{}{} + for _, c := range word.(string) { + chars = append(chars, string(c)) + } + return chars + }, + expected: []interface{}{"a"}, + }, + { + name: "nil handling", + input: []interface{}{nil, "a", nil}, + fn: func(word interface{}) interface{} { + if word == nil { + return []interface{}{} + } + return []interface{}{word} + }, + expected: []interface{}{"a"}, + }, + { + name: "empty slice result", + input: []interface{}{"", "", ""}, + fn: func(word interface{}) interface{} { + return []interface{}{} + }, + expected: []interface{}{}, + }, + { + name: "nested array flattening", + input: []interface{}{1, 2, 3}, + fn: func(n interface{}) interface{} { + return []interface{}{n, n} + }, + expected: []interface{}{1, 1, 2, 2, 3, 3}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FlatMap(tt.input, tt.fn) + if !equalSlices(result, tt.expected) { + t.Errorf("FlatMap failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestAllAnyNone(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) bool + expectedAll bool + expectedAny bool + expectedNone bool + }{ + { + name: "all even numbers", + input: []interface{}{2, 4, 6, 8}, + fn: func(x interface{}) bool { return x.(int)%2 == 0 }, + expectedAll: true, + expectedAny: true, + expectedNone: false, + }, + { + name: "no even numbers", + input: []interface{}{1, 3, 5, 7}, + fn: func(x interface{}) bool { return x.(int)%2 == 0 }, + expectedAll: false, + expectedAny: false, + expectedNone: true, + }, + { + name: "mixed even/odd numbers", + input: []interface{}{1, 2, 3, 4}, + fn: func(x interface{}) bool { return x.(int)%2 == 0 }, + expectedAll: false, + expectedAny: true, + expectedNone: false, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(x interface{}) bool { return x.(int)%2 == 0 }, + expectedAll: true, // vacuously true + expectedAny: false, // vacuously false + expectedNone: true, // vacuously true + }, + { + name: "nil predicate handling", + input: []interface{}{nil, nil, nil}, + fn: func(x interface{}) bool { return x == nil }, + expectedAll: true, + expectedAny: true, + expectedNone: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resultAll := All(tt.input, tt.fn) + if resultAll != tt.expectedAll { + t.Errorf("All failed, expected %v, got %v", tt.expectedAll, resultAll) + } + + resultAny := Any(tt.input, tt.fn) + if resultAny != tt.expectedAny { + t.Errorf("Any failed, expected %v, got %v", tt.expectedAny, resultAny) + } + + resultNone := None(tt.input, tt.fn) + if resultNone != tt.expectedNone { + t.Errorf("None failed, expected %v, got %v", tt.expectedNone, resultNone) + } + }) + } +} + +func TestChunk(t *testing.T) { + tests := []struct { + name string + input []interface{} + size int + expected [][]interface{} + }{ + { + name: "normal chunks", + input: []interface{}{1, 2, 3, 4, 5}, + size: 2, + expected: [][]interface{}{{1, 2}, {3, 4}, {5}}, + }, + { + name: "empty slice", + input: []interface{}{}, + size: 2, + expected: [][]interface{}{}, + }, + { + name: "chunk size equals length", + input: []interface{}{1, 2, 3}, + size: 3, + expected: [][]interface{}{{1, 2, 3}}, + }, + { + name: "chunk size larger than length", + input: []interface{}{1, 2}, + size: 3, + expected: [][]interface{}{{1, 2}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Chunk(tt.input, tt.size) + if !equalNestedSlices(result, tt.expected) { + t.Errorf("Chunk failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestFind(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) bool + expected interface{} + shouldFound bool + }{ + { + name: "find first number greater than 2", + input: []interface{}{1, 2, 3, 4}, + fn: func(v interface{}) bool { return v.(int) > 2 }, + expected: 3, + shouldFound: true, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(v interface{}) bool { return v.(int) > 2 }, + expected: nil, + shouldFound: false, + }, + { + name: "no match", + input: []interface{}{1, 2}, + fn: func(v interface{}) bool { return v.(int) > 10 }, + expected: nil, + shouldFound: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, found := Find(tt.input, tt.fn) + if found != tt.shouldFound { + t.Errorf("Find failed, expected found=%v, got found=%v", tt.shouldFound, found) + } + if found && result != tt.expected { + t.Errorf("Find failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestReverse(t *testing.T) { + tests := []struct { + name string + input []interface{} + expected []interface{} + }{ + { + name: "normal sequence", + input: []interface{}{1, 2, 3, 4}, + expected: []interface{}{4, 3, 2, 1}, + }, + { + name: "empty slice", + input: []interface{}{}, + expected: []interface{}{}, + }, + { + name: "single element", + input: []interface{}{1}, + expected: []interface{}{1}, + }, + { + name: "mixed types", + input: []interface{}{1, "a", true, 2.5}, + expected: []interface{}{2.5, true, "a", 1}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Reverse(tt.input) + if !equalSlices(result, tt.expected) { + t.Errorf("Reverse failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestZipUnzip(t *testing.T) { + tests := []struct { + name string + a []interface{} + b []interface{} + expectedZip [][2]interface{} + expectedA []interface{} + expectedB []interface{} + }{ + { + name: "normal case", + a: []interface{}{1, 2, 3}, + b: []interface{}{"a", "b", "c"}, + expectedZip: [][2]interface{}{{1, "a"}, {2, "b"}, {3, "c"}}, + expectedA: []interface{}{1, 2, 3}, + expectedB: []interface{}{"a", "b", "c"}, + }, + { + name: "empty slices", + a: []interface{}{}, + b: []interface{}{}, + expectedZip: [][2]interface{}{}, + expectedA: []interface{}{}, + expectedB: []interface{}{}, + }, + { + name: "different lengths - a shorter", + a: []interface{}{1, 2}, + b: []interface{}{"a", "b", "c"}, + expectedZip: [][2]interface{}{{1, "a"}, {2, "b"}}, + expectedA: []interface{}{1, 2}, + expectedB: []interface{}{"a", "b"}, + }, + { + name: "different lengths - b shorter", + a: []interface{}{1, 2, 3}, + b: []interface{}{"a"}, + expectedZip: [][2]interface{}{{1, "a"}}, + expectedA: []interface{}{1}, + expectedB: []interface{}{"a"}, + }, + { + name: "mixed types", + a: []interface{}{1, true, "x"}, + b: []interface{}{2.5, false, "y"}, + expectedZip: [][2]interface{}{{1, 2.5}, {true, false}, {"x", "y"}}, + expectedA: []interface{}{1, true, "x"}, + expectedB: []interface{}{2.5, false, "y"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + zipped := Zip(tt.a, tt.b) + if len(zipped) != len(tt.expectedZip) { + t.Errorf("Zip failed, expected length %v, got %v", len(tt.expectedZip), len(zipped)) + } + for i, pair := range zipped { + if pair[0] != tt.expectedZip[i][0] || pair[1] != tt.expectedZip[i][1] { + t.Errorf("Zip failed at index %d, expected %v, got %v", i, tt.expectedZip[i], pair) + } + } + + unzippedA, unzippedB := Unzip(zipped) + if !equalSlices(unzippedA, tt.expectedA) { + t.Errorf("Unzip failed for slice A, expected %v, got %v", tt.expectedA, unzippedA) + } + if !equalSlices(unzippedB, tt.expectedB) { + t.Errorf("Unzip failed for slice B, expected %v, got %v", tt.expectedB, unzippedB) + } + }) + } +} + +func TestGroupBy(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) interface{} + expected map[interface{}][]interface{} + }{ + { + name: "group by even/odd", + input: []interface{}{1, 2, 3, 4, 5, 6}, + fn: func(v interface{}) interface{} { return v.(int) % 2 }, + expected: map[interface{}][]interface{}{ + 0: {2, 4, 6}, + 1: {1, 3, 5}, + }, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(v interface{}) interface{} { return v.(int) % 2 }, + expected: map[interface{}][]interface{}{}, + }, + { + name: "single group", + input: []interface{}{2, 4, 6}, + fn: func(v interface{}) interface{} { return v.(int) % 2 }, + expected: map[interface{}][]interface{}{ + 0: {2, 4, 6}, + }, + }, + { + name: "group by type", + input: []interface{}{1, "a", 2, "b", true}, + fn: func(v interface{}) interface{} { + switch v.(type) { + case int: + return "int" + case string: + return "string" + default: + return "other" + } + }, + expected: map[interface{}][]interface{}{ + "int": {1, 2}, + "string": {"a", "b"}, + "other": {true}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GroupBy(tt.input, tt.fn) + if len(result) != len(tt.expected) { + t.Errorf("GroupBy failed, expected %d groups, got %d", len(tt.expected), len(result)) + } + for k, v := range tt.expected { + if !equalSlices(result[k], v) { + t.Errorf("GroupBy failed for key %v, expected %v, got %v", k, v, result[k]) + } + } + }) + } +} + +func TestFlatten(t *testing.T) { + tests := []struct { + name string + input [][]interface{} + expected []interface{} + }{ + { + name: "normal nested slices", + input: [][]interface{}{{1, 2}, {3, 4}, {5}}, + expected: []interface{}{1, 2, 3, 4, 5}, + }, + { + name: "empty outer slice", + input: [][]interface{}{}, + expected: []interface{}{}, + }, + { + name: "empty inner slices", + input: [][]interface{}{{}, {}, {}}, + expected: []interface{}{}, + }, + { + name: "mixed types", + input: [][]interface{}{{1, "a"}, {true, 2.5}, {nil}}, + expected: []interface{}{1, "a", true, 2.5, nil}, + }, + { + name: "single element slices", + input: [][]interface{}{{1}, {2}, {3}}, + expected: []interface{}{1, 2, 3}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Flatten(tt.input) + if !equalSlices(result, tt.expected) { + t.Errorf("Flatten failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestContains(t *testing.T) { + tests := []struct { + name string + slice []interface{} + item interface{} + expected bool + }{ + { + name: "contains integer", + slice: []interface{}{1, 2, 3}, + item: 2, + expected: true, + }, + { + name: "does not contain integer", + slice: []interface{}{1, 2, 3}, + item: 4, + expected: false, + }, + { + name: "contains string", + slice: []interface{}{"a", "b", "c"}, + item: "b", + expected: true, + }, + { + name: "empty slice", + slice: []interface{}{}, + item: 1, + expected: false, + }, + { + name: "contains nil", + slice: []interface{}{1, nil, 3}, + item: nil, + expected: true, + }, + { + name: "mixed types", + slice: []interface{}{1, "a", true}, + item: true, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := contains(tt.slice, tt.item) + if result != tt.expected { + t.Errorf("contains failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +// Helper function for testing +func contains(slice []interface{}, item interface{}) bool { + for _, v := range slice { + if v == item { + return true + } + } + return false +} + +// Helper functions for comparing slices +func equalSlices(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func equalNestedSlices(a, b [][]interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !equalSlices(a[i], b[i]) { + return false + } + } + return true +} diff --git a/examples/gno.land/p/moul/fp/gno.mod b/examples/gno.land/p/moul/fp/gno.mod new file mode 100644 index 00000000000..905fa0f1c0e --- /dev/null +++ b/examples/gno.land/p/moul/fp/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/fp diff --git a/examples/gno.land/p/moul/helplink/gno.mod b/examples/gno.land/p/moul/helplink/gno.mod new file mode 100644 index 00000000000..cb070b79d6a --- /dev/null +++ b/examples/gno.land/p/moul/helplink/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/helplink diff --git a/examples/gno.land/p/moul/helplink/helplink.gno b/examples/gno.land/p/moul/helplink/helplink.gno new file mode 100644 index 00000000000..14b44622a1e --- /dev/null +++ b/examples/gno.land/p/moul/helplink/helplink.gno @@ -0,0 +1,79 @@ +// Package helplink provides utilities for creating help page links compatible +// with Gnoweb, Gnobro, and other clients that support the Gno contracts' +// flavored Markdown format. +// +// This package simplifies the generation of dynamic, context-sensitive help +// links, enabling users to navigate relevant documentation seamlessly within +// the Gno ecosystem. +// +// For a more lightweight alternative, consider using p/moul/txlink. +// +// The primary functions — Func, FuncURL, and Home — are intended for use with +// the "relative realm". When specifying a custom Realm, you can create links +// that utilize either the current realm path or a fully qualified path to +// another realm. +package helplink + +import ( + "strings" + + "gno.land/p/moul/txlink" +) + +const chainDomain = "gno.land" // XXX: std.ChainDomain (#2911) + +// Func returns a markdown link for the specific function with optional +// key-value arguments, for the current realm. +func Func(title string, fn string, args ...string) string { + return Realm("").Func(title, fn, args...) +} + +// FuncURL returns a URL for the specified function with optional key-value +// arguments, for the current realm. +func FuncURL(fn string, args ...string) string { + return Realm("").FuncURL(fn, args...) +} + +// Home returns the URL for the help homepage of the current realm. +func Home() string { + return Realm("").Home() +} + +// Realm represents a specific realm for generating help links. +type Realm string + +// prefix returns the URL prefix for the realm. +func (r Realm) prefix() string { + // relative + if r == "" { + return "" + } + + // local realm -> /realm + realm := string(r) + if strings.Contains(realm, chainDomain) { + return strings.TrimPrefix(realm, chainDomain) + } + + // remote realm -> https://remote.land/realm + return "https://" + string(r) +} + +// Func returns a markdown link for the specified function with optional +// key-value arguments. +func (r Realm) Func(title string, fn string, args ...string) string { + // XXX: escape title + return "[" + title + "](" + r.FuncURL(fn, args...) + ")" +} + +// FuncURL returns a URL for the specified function with optional key-value +// arguments. +func (r Realm) FuncURL(fn string, args ...string) string { + tlr := txlink.Realm(r) + return tlr.Call(fn, args...) +} + +// Home returns the base help URL for the specified realm. +func (r Realm) Home() string { + return r.prefix() + "$help" +} diff --git a/examples/gno.land/p/moul/helplink/helplink_test.gno b/examples/gno.land/p/moul/helplink/helplink_test.gno new file mode 100644 index 00000000000..29cfd02eb67 --- /dev/null +++ b/examples/gno.land/p/moul/helplink/helplink_test.gno @@ -0,0 +1,78 @@ +package helplink + +import ( + "testing" + + "gno.land/p/demo/urequire" +) + +func TestFunc(t *testing.T) { + tests := []struct { + title string + fn string + args []string + want string + realm Realm + }{ + {"Example", "foo", []string{"bar", "1", "baz", "2"}, "[Example]($help&func=foo&bar=1&baz=2)", ""}, + {"Realm Example", "foo", []string{"bar", "1", "baz", "2"}, "[Realm Example](/r/lorem/ipsum$help&func=foo&bar=1&baz=2)", "gno.land/r/lorem/ipsum"}, + {"Single Arg", "testFunc", []string{"key", "value"}, "[Single Arg]($help&func=testFunc&key=value)", ""}, + {"No Args", "noArgsFunc", []string{}, "[No Args]($help&func=noArgsFunc)", ""}, + {"Odd Args", "oddArgsFunc", []string{"key"}, "[Odd Args]($help&func=oddArgsFunc)", ""}, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + got := tt.realm.Func(tt.title, tt.fn, tt.args...) + urequire.Equal(t, tt.want, got) + }) + } +} + +func TestFuncURL(t *testing.T) { + tests := []struct { + fn string + args []string + want string + realm Realm + }{ + {"foo", []string{"bar", "1", "baz", "2"}, "$help&func=foo&bar=1&baz=2", ""}, + {"testFunc", []string{"key", "value"}, "$help&func=testFunc&key=value", ""}, + {"noArgsFunc", []string{}, "$help&func=noArgsFunc", ""}, + {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc", ""}, + {"foo", []string{"bar", "1", "baz", "2"}, "/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.land/r/lorem/ipsum"}, + {"testFunc", []string{"key", "value"}, "/r/lorem/ipsum$help&func=testFunc&key=value", "gno.land/r/lorem/ipsum"}, + {"noArgsFunc", []string{}, "/r/lorem/ipsum$help&func=noArgsFunc", "gno.land/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc", "gno.land/r/lorem/ipsum"}, + {"foo", []string{"bar", "1", "baz", "2"}, "https://gno.world/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.world/r/lorem/ipsum"}, + {"testFunc", []string{"key", "value"}, "https://gno.world/r/lorem/ipsum$help&func=testFunc&key=value", "gno.world/r/lorem/ipsum"}, + {"noArgsFunc", []string{}, "https://gno.world/r/lorem/ipsum$help&func=noArgsFunc", "gno.world/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc", "gno.world/r/lorem/ipsum"}, + } + + for _, tt := range tests { + title := tt.fn + t.Run(title, func(t *testing.T) { + got := tt.realm.FuncURL(tt.fn, tt.args...) + urequire.Equal(t, tt.want, got) + }) + } +} + +func TestHome(t *testing.T) { + tests := []struct { + realm Realm + want string + }{ + {"", "$help"}, + {"gno.land/r/lorem/ipsum", "/r/lorem/ipsum$help"}, + {"gno.world/r/lorem/ipsum", "https://gno.world/r/lorem/ipsum$help"}, + } + + for _, tt := range tests { + t.Run(string(tt.realm), func(t *testing.T) { + got := tt.realm.Home() + urequire.Equal(t, tt.want, got) + }) + } +} diff --git a/examples/gno.land/p/moul/md/gno.mod b/examples/gno.land/p/moul/md/gno.mod new file mode 100644 index 00000000000..55d124d9e6b --- /dev/null +++ b/examples/gno.land/p/moul/md/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/md diff --git a/examples/gno.land/p/moul/md/md.gno b/examples/gno.land/p/moul/md/md.gno new file mode 100644 index 00000000000..61d6948b997 --- /dev/null +++ b/examples/gno.land/p/moul/md/md.gno @@ -0,0 +1,242 @@ +// Package md provides helper functions for generating Markdown content programmatically. +// +// It includes utilities for text formatting, creating lists, blockquotes, code blocks, +// links, images, and more. +// +// Highlights: +// - Supports basic Markdown syntax such as bold, italic, strikethrough, headers, and lists. +// - Manages multiline support in lists (e.g., bullet, ordered, and todo lists). +// - Includes advanced helpers like inline images with links and nested list prefixes. +package md + +import ( + "strconv" + "strings" +) + +// Bold returns bold text for markdown. +// Example: Bold("foo") => "**foo**" +func Bold(text string) string { + return "**" + text + "**" +} + +// Italic returns italicized text for markdown. +// Example: Italic("foo") => "*foo*" +func Italic(text string) string { + return "*" + text + "*" +} + +// Strikethrough returns strikethrough text for markdown. +// Example: Strikethrough("foo") => "~~foo~~" +func Strikethrough(text string) string { + return "~~" + text + "~~" +} + +// H1 returns a level 1 header for markdown. +// Example: H1("foo") => "# foo\n" +func H1(text string) string { + return "# " + text + "\n" +} + +// H2 returns a level 2 header for markdown. +// Example: H2("foo") => "## foo\n" +func H2(text string) string { + return "## " + text + "\n" +} + +// H3 returns a level 3 header for markdown. +// Example: H3("foo") => "### foo\n" +func H3(text string) string { + return "### " + text + "\n" +} + +// H4 returns a level 4 header for markdown. +// Example: H4("foo") => "#### foo\n" +func H4(text string) string { + return "#### " + text + "\n" +} + +// H5 returns a level 5 header for markdown. +// Example: H5("foo") => "##### foo\n" +func H5(text string) string { + return "##### " + text + "\n" +} + +// H6 returns a level 6 header for markdown. +// Example: H6("foo") => "###### foo\n" +func H6(text string) string { + return "###### " + text + "\n" +} + +// BulletList returns a bullet list for markdown. +// Example: BulletList([]string{"foo", "bar"}) => "- foo\n- bar\n" +func BulletList(items []string) string { + var sb strings.Builder + for _, item := range items { + sb.WriteString(BulletItem(item)) + } + return sb.String() +} + +// BulletItem returns a bullet item for markdown. +// Example: BulletItem("foo") => "- foo\n" +func BulletItem(item string) string { + var sb strings.Builder + lines := strings.Split(item, "\n") + sb.WriteString("- " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + return sb.String() +} + +// OrderedList returns an ordered list for markdown. +// Example: OrderedList([]string{"foo", "bar"}) => "1. foo\n2. bar\n" +func OrderedList(items []string) string { + var sb strings.Builder + for i, item := range items { + lines := strings.Split(item, "\n") + sb.WriteString(strconv.Itoa(i+1) + ". " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + } + return sb.String() +} + +// TodoList returns a list of todo items with checkboxes for markdown. +// Example: TodoList([]string{"foo", "bar\nmore bar"}, []bool{true, false}) => "- [x] foo\n- [ ] bar\n more bar\n" +func TodoList(items []string, done []bool) string { + var sb strings.Builder + for i, item := range items { + sb.WriteString(TodoItem(item, done[i])) + } + return sb.String() +} + +// TodoItem returns a todo item with checkbox for markdown. +// Example: TodoItem("foo", true) => "- [x] foo\n" +func TodoItem(item string, done bool) string { + var sb strings.Builder + checkbox := " " + if done { + checkbox = "x" + } + lines := strings.Split(item, "\n") + sb.WriteString("- [" + checkbox + "] " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + return sb.String() +} + +// Nested prefixes each line with a given prefix, enabling nested lists. +// Example: Nested("- foo\n- bar", " ") => " - foo\n - bar\n" +func Nested(content, prefix string) string { + lines := strings.Split(content, "\n") + for i := range lines { + if strings.TrimSpace(lines[i]) != "" { + lines[i] = prefix + lines[i] + } + } + return strings.Join(lines, "\n") +} + +// Blockquote returns a blockquote for markdown. +// Example: Blockquote("foo\nbar") => "> foo\n> bar\n" +func Blockquote(text string) string { + lines := strings.Split(text, "\n") + var sb strings.Builder + for _, line := range lines { + sb.WriteString("> " + line + "\n") + } + return sb.String() +} + +// InlineCode returns inline code for markdown. +// Example: InlineCode("foo") => "`foo`" +func InlineCode(code string) string { + return "`" + strings.ReplaceAll(code, "`", "\\`") + "`" +} + +// CodeBlock creates a markdown code block. +// Example: CodeBlock("foo") => "```\nfoo\n```" +func CodeBlock(content string) string { + return "```\n" + strings.ReplaceAll(content, "```", "\\```") + "\n```" +} + +// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting. +// Example: LanguageCodeBlock("go", "foo") => "```go\nfoo\n```" +func LanguageCodeBlock(language, content string) string { + return "```" + language + "\n" + strings.ReplaceAll(content, "```", "\\```") + "\n```" +} + +// HorizontalRule returns a horizontal rule for markdown. +// Example: HorizontalRule() => "---\n" +func HorizontalRule() string { + return "---\n" +} + +// Link returns a hyperlink for markdown. +// Example: Link("foo", "http://example.com") => "[foo](http://example.com)" +func Link(text, url string) string { + return "[" + EscapeText(text) + "](" + url + ")" +} + +// InlineImageWithLink creates an inline image wrapped in a hyperlink for markdown. +// Example: InlineImageWithLink("alt text", "image-url", "link-url") => "[![alt text](image-url)](link-url)" +func InlineImageWithLink(altText, imageUrl, linkUrl string) string { + return "[" + Image(altText, imageUrl) + "](" + linkUrl + ")" +} + +// Image returns an image for markdown. +// Example: Image("foo", "http://example.com") => "![foo](http://example.com)" +func Image(altText, url string) string { + return "![" + EscapeText(altText) + "](" + url + ")" +} + +// Footnote returns a footnote for markdown. +// Example: Footnote("foo", "bar") => "[foo]: bar" +func Footnote(reference, text string) string { + return "[" + EscapeText(reference) + "]: " + text +} + +// Paragraph wraps the given text in a Markdown paragraph. +// Example: Paragraph("foo") => "foo\n" +func Paragraph(content string) string { + return content + "\n\n" +} + +// CollapsibleSection creates a collapsible section for markdown using +// HTML
and tags. +// Example: +// CollapsibleSection("Click to expand", "Hidden content") +// => +//
Click to expand +// +// Hidden content +//
+func CollapsibleSection(title, content string) string { + return "
" + EscapeText(title) + "\n\n" + content + "\n
\n" +} + +// EscapeText escapes special Markdown characters in regular text where needed. +func EscapeText(text string) string { + replacer := strings.NewReplacer( + `*`, `\*`, + `_`, `\_`, + `[`, `\[`, + `]`, `\]`, + `(`, `\(`, + `)`, `\)`, + `~`, `\~`, + `>`, `\>`, + `|`, `\|`, + `-`, `\-`, + `+`, `\+`, + ".", `\.`, + "!", `\!`, + "`", "\\`", + ) + return replacer.Replace(text) +} diff --git a/examples/gno.land/p/moul/md/md_test.gno b/examples/gno.land/p/moul/md/md_test.gno new file mode 100644 index 00000000000..144ae58d918 --- /dev/null +++ b/examples/gno.land/p/moul/md/md_test.gno @@ -0,0 +1,88 @@ +package md + +import ( + "testing" + + "gno.land/p/moul/md" +) + +func TestHelpers(t *testing.T) { + tests := []struct { + name string + function func() string + expected string + }{ + {"Bold", func() string { return md.Bold("foo") }, "**foo**"}, + {"Italic", func() string { return md.Italic("foo") }, "*foo*"}, + {"Strikethrough", func() string { return md.Strikethrough("foo") }, "~~foo~~"}, + {"H1", func() string { return md.H1("foo") }, "# foo\n"}, + {"HorizontalRule", md.HorizontalRule, "---\n"}, + {"InlineCode", func() string { return md.InlineCode("foo") }, "`foo`"}, + {"CodeBlock", func() string { return md.CodeBlock("foo") }, "```\nfoo\n```"}, + {"LanguageCodeBlock", func() string { return md.LanguageCodeBlock("go", "foo") }, "```go\nfoo\n```"}, + {"Link", func() string { return md.Link("foo", "http://example.com") }, "[foo](http://example.com)"}, + {"Image", func() string { return md.Image("foo", "http://example.com") }, "![foo](http://example.com)"}, + {"InlineImageWithLink", func() string { return md.InlineImageWithLink("alt", "image-url", "link-url") }, "[![alt](image-url)](link-url)"}, + {"Footnote", func() string { return md.Footnote("foo", "bar") }, "[foo]: bar"}, + {"Paragraph", func() string { return md.Paragraph("foo") }, "foo\n\n"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.function() + if result != tt.expected { + t.Errorf("%s() = %q, want %q", tt.name, result, tt.expected) + } + }) + } +} + +func TestLists(t *testing.T) { + t.Run("BulletList", func(t *testing.T) { + items := []string{"foo", "bar"} + expected := "- foo\n- bar\n" + result := md.BulletList(items) + if result != expected { + t.Errorf("BulletList(%q) = %q, want %q", items, result, expected) + } + }) + + t.Run("OrderedList", func(t *testing.T) { + items := []string{"foo", "bar"} + expected := "1. foo\n2. bar\n" + result := md.OrderedList(items) + if result != expected { + t.Errorf("OrderedList(%q) = %q, want %q", items, result, expected) + } + }) + + t.Run("TodoList", func(t *testing.T) { + items := []string{"foo", "bar\nmore bar"} + done := []bool{true, false} + expected := "- [x] foo\n- [ ] bar\n more bar\n" + result := md.TodoList(items, done) + if result != expected { + t.Errorf("TodoList(%q, %q) = %q, want %q", items, done, result, expected) + } + }) +} + +func TestNested(t *testing.T) { + t.Run("Nested Single Level", func(t *testing.T) { + content := "- foo\n- bar" + expected := " - foo\n - bar" + result := md.Nested(content, " ") + if result != expected { + t.Errorf("Nested(%q) = %q, want %q", content, result, expected) + } + }) + + t.Run("Nested Double Level", func(t *testing.T) { + content := " - foo\n - bar" + expected := " - foo\n - bar" + result := md.Nested(content, " ") + if result != expected { + t.Errorf("Nested(%q) = %q, want %q", content, result, expected) + } + }) +} diff --git a/examples/gno.land/p/moul/md/z1_filetest.gno b/examples/gno.land/p/moul/md/z1_filetest.gno new file mode 100644 index 00000000000..077e1732bcb --- /dev/null +++ b/examples/gno.land/p/moul/md/z1_filetest.gno @@ -0,0 +1,87 @@ +package main + +import "gno.land/p/moul/md" + +func main() { + println(md.H1("Header 1")) + println(md.H2("Header 2")) + println(md.H3("Header 3")) + println(md.H4("Header 4")) + println(md.H5("Header 5")) + println(md.H6("Header 6")) + println(md.Bold("bold")) + println(md.Italic("italic")) + println(md.Strikethrough("strikethrough")) + println(md.BulletList([]string{ + "Item 1", + "Item 2\nMore details for item 2", + })) + println(md.OrderedList([]string{"Step 1", "Step 2"})) + println(md.TodoList([]string{"Task 1", "Task 2\nSubtask 2"}, []bool{true, false})) + println(md.Nested(md.BulletList([]string{"Parent Item", md.OrderedList([]string{"Child 1", "Child 2"})}), " ")) + println(md.Blockquote("This is a blockquote\nSpanning multiple lines")) + println(md.InlineCode("inline `code`")) + println(md.CodeBlock("line1\nline2")) + println(md.LanguageCodeBlock("go", "func main() {\nprintln(\"Hello, world!\")\n}")) + println(md.HorizontalRule()) + println(md.Link("Gno", "http://gno.land")) + println(md.Image("Alt Text", "http://example.com/image.png")) + println(md.InlineImageWithLink("Alt Text", "http://example.com/image.png", "http://example.com")) + println(md.Footnote("ref", "This is a footnote")) + println(md.Paragraph("This is a paragraph.")) +} + +// Output: +// # Header 1 +// +// ## Header 2 +// +// ### Header 3 +// +// #### Header 4 +// +// ##### Header 5 +// +// ###### Header 6 +// +// **bold** +// *italic* +// ~~strikethrough~~ +// - Item 1 +// - Item 2 +// More details for item 2 +// +// 1. Step 1 +// 2. Step 2 +// +// - [x] Task 1 +// - [ ] Task 2 +// Subtask 2 +// +// - Parent Item +// - 1. Child 1 +// 2. Child 2 +// +// +// > This is a blockquote +// > Spanning multiple lines +// +// `inline \`code\`` +// ``` +// line1 +// line2 +// ``` +// ```go +// func main() { +// println("Hello, world!") +// } +// ``` +// --- +// +// [Gno](http://gno.land) +// ![Alt Text](http://example.com/image.png) +// [![Alt Text](http://example.com/image.png)](http://example.com) +// [ref]: This is a footnote +// This is a paragraph. +// +// diff --git a/examples/gno.land/p/moul/mdtable/gno.mod b/examples/gno.land/p/moul/mdtable/gno.mod new file mode 100644 index 00000000000..079c935a874 --- /dev/null +++ b/examples/gno.land/p/moul/mdtable/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/mdtable diff --git a/examples/gno.land/p/moul/mdtable/mdtable.gno b/examples/gno.land/p/moul/mdtable/mdtable.gno new file mode 100644 index 00000000000..13812bd973d --- /dev/null +++ b/examples/gno.land/p/moul/mdtable/mdtable.gno @@ -0,0 +1,66 @@ +// Package mdtable provides a simple way to create Markdown tables. +// +// Example usage: +// +// import "gno.land/p/moul/mdtable" +// +// func Render(path string) string { +// table := mdtable.Table{ +// Headers: []string{"ID", "Title", "Status", "Date"}, +// } +// table.Append([]string{"#1", "Add a new validator", "succeed", "2024-01-01"}) +// table.Append([]string{"#2", "Change parameter", "timed out", "2024-01-02"}) +// return table.String() +// } +// +// Output: +// +// | ID | Title | Status | Date | +// | --- | --- | --- | --- | +// | #1 | Add a new validator | succeed | 2024-01-01 | +// | #2 | Change parameter | timed out | 2024-01-02 | +package mdtable + +import ( + "strings" +) + +type Table struct { + Headers []string + Rows [][]string + // XXX: optional headers alignment. +} + +func (t *Table) Append(row []string) { + t.Rows = append(t.Rows, row) +} + +func (t Table) String() string { + // XXX: switch to using text/tabwriter when porting to Gno to support + // better-formatted raw Markdown output. + + if len(t.Headers) == 0 && len(t.Rows) == 0 { + return "" + } + + var sb strings.Builder + + if len(t.Headers) == 0 { + t.Headers = make([]string, len(t.Rows[0])) + } + + // Print header. + sb.WriteString("| " + strings.Join(t.Headers, " | ") + " |\n") + sb.WriteString("|" + strings.Repeat(" --- |", len(t.Headers)) + "\n") + + // Print rows. + for _, row := range t.Rows { + escapedRow := make([]string, len(row)) + for i, cell := range row { + escapedRow[i] = strings.ReplaceAll(cell, "|", "|") // Escape pipe characters. + } + sb.WriteString("| " + strings.Join(escapedRow, " | ") + " |\n") + } + + return sb.String() +} diff --git a/examples/gno.land/p/moul/mdtable/mdtable_test.gno b/examples/gno.land/p/moul/mdtable/mdtable_test.gno new file mode 100644 index 00000000000..87836a3ab11 --- /dev/null +++ b/examples/gno.land/p/moul/mdtable/mdtable_test.gno @@ -0,0 +1,158 @@ +package mdtable_test + +import ( + "testing" + + "gno.land/p/demo/urequire" + "gno.land/p/moul/mdtable" +) + +// XXX: switch to `func Example() {}` when supported. +func TestExample(t *testing.T) { + table := mdtable.Table{ + Headers: []string{"ID", "Title", "Status"}, + Rows: [][]string{ + {"#1", "Add a new validator", "succeed"}, + {"#2", "Change parameter", "timed out"}, + {"#3", "Fill pool", "active"}, + }, + } + + got := table.String() + expected := `| ID | Title | Status | +| --- | --- | --- | +| #1 | Add a new validator | succeed | +| #2 | Change parameter | timed out | +| #3 | Fill pool | active | +` + + urequire.Equal(t, got, expected) +} + +func TestTableString(t *testing.T) { + tests := []struct { + name string + table mdtable.Table + expected string + }{ + { + name: "With Headers and Rows", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + Rows: [][]string{ + {"#1", "Add a new validator", "succeed", "2024-01-01"}, + {"#2", "Change parameter", "timed out", "2024-01-02"}, + }, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Add a new validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +`, + }, + { + name: "Without Headers", + table: mdtable.Table{ + Rows: [][]string{ + {"#1", "Add a new validator", "succeed", "2024-01-01"}, + {"#2", "Change parameter", "timed out", "2024-01-02"}, + }, + }, + expected: `| | | | | +| --- | --- | --- | --- | +| #1 | Add a new validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +`, + }, + { + name: "Without Rows", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +`, + }, + { + name: "With Pipe Character in Content", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + Rows: [][]string{ + {"#1", "Add a new | validator", "succeed", "2024-01-01"}, + {"#2", "Change parameter", "timed out", "2024-01-02"}, + }, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Add a new | validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +`, + }, + { + name: "With Varying Row Sizes", // XXX: should we have a different behavior? + table: mdtable.Table{ + Headers: []string{"ID", "Title"}, + Rows: [][]string{ + {"#1", "Add a new validator"}, + {"#2", "Change parameter", "Extra Column"}, + {"#3", "Fill pool"}, + }, + }, + expected: `| ID | Title | +| --- | --- | +| #1 | Add a new validator | +| #2 | Change parameter | Extra Column | +| #3 | Fill pool | +`, + }, + { + name: "With UTF-8 Characters", + table: mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + Rows: [][]string{ + {"#1", "Café", "succeed", "2024-01-01"}, + {"#2", "München", "timed out", "2024-01-02"}, + {"#3", "São Paulo", "active", "2024-01-03"}, + }, + }, + expected: `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Café | succeed | 2024-01-01 | +| #2 | München | timed out | 2024-01-02 | +| #3 | São Paulo | active | 2024-01-03 | +`, + }, + { + name: "With no Headers and no Rows", + table: mdtable.Table{}, + expected: ``, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.table.String() + urequire.Equal(t, got, tt.expected) + }) + } +} + +func TestTableAppend(t *testing.T) { + table := mdtable.Table{ + Headers: []string{"ID", "Title", "Status", "Date"}, + } + + // Use the Append method to add rows to the table + table.Append([]string{"#1", "Add a new validator", "succeed", "2024-01-01"}) + table.Append([]string{"#2", "Change parameter", "timed out", "2024-01-02"}) + table.Append([]string{"#3", "Fill pool", "active", "2024-01-03"}) + got := table.String() + + expected := `| ID | Title | Status | Date | +| --- | --- | --- | --- | +| #1 | Add a new validator | succeed | 2024-01-01 | +| #2 | Change parameter | timed out | 2024-01-02 | +| #3 | Fill pool | active | 2024-01-03 | +` + urequire.Equal(t, got, expected) +} diff --git a/examples/gno.land/p/moul/memo/gno.mod b/examples/gno.land/p/moul/memo/gno.mod new file mode 100644 index 00000000000..4a9948c30f7 --- /dev/null +++ b/examples/gno.land/p/moul/memo/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/memo diff --git a/examples/gno.land/p/moul/memo/memo.gno b/examples/gno.land/p/moul/memo/memo.gno new file mode 100644 index 00000000000..e31f13aab15 --- /dev/null +++ b/examples/gno.land/p/moul/memo/memo.gno @@ -0,0 +1,134 @@ +// Package memo provides a simple memoization utility to cache function results. +// +// The package offers a Memoizer type that can cache function results based on keys, +// with optional validation of cached values. This is useful for expensive computations +// that need to be cached and potentially invalidated based on custom conditions. +// +// /!\ Important Warning for Gno Usage: +// In Gno, storage updates only persist during transactions. This means: +// - Cache entries created during queries will NOT persist +// - Creating cache entries during queries will actually decrease performance +// as it wastes resources trying to save data that won't be saved +// +// Best Practices: +// - Use this pattern in transaction-driven contexts rather than query/render scenarios +// - Consider controlled cache updates, e.g., by specific accounts (like oracles) +// - Ideal for cases where cache updates happen every N blocks or on specific events +// - Carefully evaluate if caching will actually improve performance in your use case +// +// Basic usage example: +// +// m := memo.New() +// +// // Cache expensive computation +// result := m.Memoize("key", func() interface{} { +// // expensive operation +// return "computed-value" +// }) +// +// // Subsequent calls with same key return cached result +// result = m.Memoize("key", func() interface{} { +// // function won't be called, cached value is returned +// return "computed-value" +// }) +// +// Example with validation: +// +// type TimestampedValue struct { +// Value string +// Timestamp time.Time +// } +// +// m := memo.New() +// +// // Cache value with timestamp +// result := m.MemoizeWithValidator( +// "key", +// func() interface{} { +// return TimestampedValue{ +// Value: "data", +// Timestamp: time.Now(), +// } +// }, +// func(cached interface{}) bool { +// // Validate that the cached value is not older than 1 hour +// if tv, ok := cached.(TimestampedValue); ok { +// return time.Since(tv.Timestamp) < time.Hour +// } +// return false +// }, +// ) +package memo + +import ( + "gno.land/p/demo/btree" + "gno.land/p/demo/ufmt" +) + +// Record implements the btree.Record interface for our cache entries +type cacheEntry struct { + key interface{} + value interface{} +} + +// Less implements btree.Record interface +func (e cacheEntry) Less(than btree.Record) bool { + // Convert the other record to cacheEntry + other := than.(cacheEntry) + // Compare string representations of keys for consistent ordering + return ufmt.Sprintf("%v", e.key) < ufmt.Sprintf("%v", other.key) +} + +// Memoizer is a structure to handle memoization of function results. +type Memoizer struct { + cache *btree.BTree +} + +// New creates a new Memoizer instance. +func New() *Memoizer { + return &Memoizer{ + cache: btree.New(), + } +} + +// Memoize ensures the result of the given function is cached for the specified key. +func (m *Memoizer) Memoize(key interface{}, fn func() interface{}) interface{} { + entry := cacheEntry{key: key} + if found := m.cache.Get(entry); found != nil { + return found.(cacheEntry).value + } + + value := fn() + m.cache.Insert(cacheEntry{key: key, value: value}) + return value +} + +// MemoizeWithValidator ensures the result is cached and valid according to the validator function. +func (m *Memoizer) MemoizeWithValidator(key interface{}, fn func() interface{}, isValid func(interface{}) bool) interface{} { + entry := cacheEntry{key: key} + if found := m.cache.Get(entry); found != nil { + cachedEntry := found.(cacheEntry) + if isValid(cachedEntry.value) { + return cachedEntry.value + } + } + + value := fn() + m.cache.Insert(cacheEntry{key: key, value: value}) + return value +} + +// Invalidate removes the cached value for the specified key. +func (m *Memoizer) Invalidate(key interface{}) { + m.cache.Delete(cacheEntry{key: key}) +} + +// Clear clears all cached values. +func (m *Memoizer) Clear() { + m.cache.Clear(true) +} + +// Size returns the number of items currently in the cache. +func (m *Memoizer) Size() int { + return m.cache.Len() +} diff --git a/examples/gno.land/p/moul/memo/memo_test.gno b/examples/gno.land/p/moul/memo/memo_test.gno new file mode 100644 index 00000000000..44dde5df640 --- /dev/null +++ b/examples/gno.land/p/moul/memo/memo_test.gno @@ -0,0 +1,449 @@ +package memo + +import ( + "std" + "testing" + "time" +) + +type timestampedValue struct { + value interface{} + timestamp time.Time +} + +// complexKey is used to test struct keys +type complexKey struct { + ID int + Name string +} + +func TestMemoize(t *testing.T) { + tests := []struct { + name string + key interface{} + value interface{} + callCount *int + }{ + { + name: "string key and value", + key: "test-key", + value: "test-value", + callCount: new(int), + }, + { + name: "int key and value", + key: 42, + value: 123, + callCount: new(int), + }, + { + name: "mixed types", + key: "number", + value: 42, + callCount: new(int), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := New() + if m.Size() != 0 { + t.Errorf("Initial size = %d, want 0", m.Size()) + } + + fn := func() interface{} { + *tt.callCount++ + return tt.value + } + + // First call should compute + result := m.Memoize(tt.key, fn) + if result != tt.value { + t.Errorf("Memoize() = %v, want %v", result, tt.value) + } + if *tt.callCount != 1 { + t.Errorf("Function called %d times, want 1", *tt.callCount) + } + if m.Size() != 1 { + t.Errorf("Size after first call = %d, want 1", m.Size()) + } + + // Second call should use cache + result = m.Memoize(tt.key, fn) + if result != tt.value { + t.Errorf("Memoize() second call = %v, want %v", result, tt.value) + } + if *tt.callCount != 1 { + t.Errorf("Function called %d times, want 1", *tt.callCount) + } + if m.Size() != 1 { + t.Errorf("Size after second call = %d, want 1", m.Size()) + } + }) + } +} + +func TestMemoizeWithValidator(t *testing.T) { + tests := []struct { + name string + key interface{} + value interface{} + validDuration time.Duration + waitDuration time.Duration + expectedCalls int + shouldRecompute bool + }{ + { + name: "valid cache", + key: "key1", + value: "value1", + validDuration: time.Hour, + waitDuration: time.Millisecond, + expectedCalls: 1, + shouldRecompute: false, + }, + { + name: "expired cache", + key: "key2", + value: "value2", + validDuration: time.Millisecond, + waitDuration: time.Millisecond * 2, + expectedCalls: 2, + shouldRecompute: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := New() + callCount := 0 + + fn := func() interface{} { + callCount++ + return timestampedValue{ + value: tt.value, + timestamp: time.Now(), + } + } + + isValid := func(cached interface{}) bool { + if tv, ok := cached.(timestampedValue); ok { + return time.Since(tv.timestamp) < tt.validDuration + } + return false + } + + // First call + result := m.MemoizeWithValidator(tt.key, fn, isValid) + if tv, ok := result.(timestampedValue); !ok || tv.value != tt.value { + t.Errorf("MemoizeWithValidator() = %v, want value %v", result, tt.value) + } + + // Wait + std.TestSkipHeights(10) + + // Second call + result = m.MemoizeWithValidator(tt.key, fn, isValid) + if tv, ok := result.(timestampedValue); !ok || tv.value != tt.value { + t.Errorf("MemoizeWithValidator() second call = %v, want value %v", result, tt.value) + } + + if callCount != tt.expectedCalls { + t.Errorf("Function called %d times, want %d", callCount, tt.expectedCalls) + } + }) + } +} + +func TestInvalidate(t *testing.T) { + tests := []struct { + name string + key interface{} + value interface{} + callCount *int + }{ + { + name: "invalidate existing key", + key: "test-key", + value: "test-value", + callCount: new(int), + }, + { + name: "invalidate non-existing key", + key: "missing-key", + value: "test-value", + callCount: new(int), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := New() + fn := func() interface{} { + *tt.callCount++ + return tt.value + } + + // First call + m.Memoize(tt.key, fn) + if m.Size() != 1 { + t.Errorf("Size after first call = %d, want 1", m.Size()) + } + + // Invalidate + m.Invalidate(tt.key) + if m.Size() != 0 { + t.Errorf("Size after invalidate = %d, want 0", m.Size()) + } + + // Call again should recompute + result := m.Memoize(tt.key, fn) + if result != tt.value { + t.Errorf("Memoize() after invalidate = %v, want %v", result, tt.value) + } + if *tt.callCount != 2 { + t.Errorf("Function called %d times, want 2", *tt.callCount) + } + if m.Size() != 1 { + t.Errorf("Size after recompute = %d, want 1", m.Size()) + } + }) + } +} + +func TestClear(t *testing.T) { + m := New() + callCount := 0 + + fn := func() interface{} { + callCount++ + return "value" + } + + // Cache some values + m.Memoize("key1", fn) + m.Memoize("key2", fn) + + if callCount != 2 { + t.Errorf("Initial calls = %d, want 2", callCount) + } + if m.Size() != 2 { + t.Errorf("Size after initial calls = %d, want 2", m.Size()) + } + + // Clear cache + m.Clear() + if m.Size() != 0 { + t.Errorf("Size after clear = %d, want 0", m.Size()) + } + + // Recompute values + m.Memoize("key1", fn) + m.Memoize("key2", fn) + + if callCount != 4 { + t.Errorf("Calls after clear = %d, want 4", callCount) + } + if m.Size() != 2 { + t.Errorf("Size after recompute = %d, want 2", m.Size()) + } +} + +func TestSize(t *testing.T) { + m := New() + + if m.Size() != 0 { + t.Errorf("Initial size = %d, want 0", m.Size()) + } + + callCount := 0 + fn := func() interface{} { + callCount++ + return "value" + } + + // Add items + m.Memoize("key1", fn) + if m.Size() != 1 { + t.Errorf("Size after first insert = %d, want 1", m.Size()) + } + + m.Memoize("key2", fn) + if m.Size() != 2 { + t.Errorf("Size after second insert = %d, want 2", m.Size()) + } + + // Duplicate key should not increase size + m.Memoize("key1", fn) + if m.Size() != 2 { + t.Errorf("Size after duplicate insert = %d, want 2", m.Size()) + } + + // Remove item + m.Invalidate("key1") + if m.Size() != 1 { + t.Errorf("Size after invalidate = %d, want 1", m.Size()) + } + + // Clear all + m.Clear() + if m.Size() != 0 { + t.Errorf("Size after clear = %d, want 0", m.Size()) + } +} + +func TestMemoizeWithDifferentKeyTypes(t *testing.T) { + tests := []struct { + name string + keys []interface{} // Now an array of keys + values []string // Corresponding values + callCount *int + }{ + { + name: "integer keys", + keys: []interface{}{42, 43}, + values: []string{"value-for-42", "value-for-43"}, + callCount: new(int), + }, + { + name: "float keys", + keys: []interface{}{3.14, 2.718}, + values: []string{"value-for-pi", "value-for-e"}, + callCount: new(int), + }, + { + name: "bool keys", + keys: []interface{}{true, false}, + values: []string{"value-for-true", "value-for-false"}, + callCount: new(int), + }, + /* + { + name: "struct keys", + keys: []interface{}{ + complexKey{ID: 1, Name: "test1"}, + complexKey{ID: 2, Name: "test2"}, + }, + values: []string{"value-for-struct1", "value-for-struct2"}, + callCount: new(int), + }, + { + name: "nil and empty interface keys", + keys: []interface{}{nil, interface{}(nil)}, + values: []string{"value-for-nil", "value-for-empty-interface"}, + callCount: new(int), + }, + */ + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := New() + + // Test both keys + for i, key := range tt.keys { + value := tt.values[i] + fn := func() interface{} { + *tt.callCount++ + return value + } + + // First call should compute + result := m.Memoize(key, fn) + if result != value { + t.Errorf("Memoize() for key %v = %v, want %v", key, result, value) + } + if *tt.callCount != i+1 { + t.Errorf("Function called %d times, want %d", *tt.callCount, i+1) + } + } + + // Verify size includes both entries + if m.Size() != 2 { + t.Errorf("Size after both inserts = %d, want 2", m.Size()) + } + + // Second call for each key should use cache + for i, key := range tt.keys { + initialCount := *tt.callCount + result := m.Memoize(key, func() interface{} { + *tt.callCount++ + return "should-not-be-called" + }) + + if result != tt.values[i] { + t.Errorf("Memoize() second call for key %v = %v, want %v", key, result, tt.values[i]) + } + if *tt.callCount != initialCount { + t.Errorf("Cache miss for key %v", key) + } + } + + // Test invalidate for each key + for i, key := range tt.keys { + m.Invalidate(key) + if m.Size() != 1-i { + t.Errorf("Size after invalidate %d = %d, want %d", i+1, m.Size(), 1-i) + } + } + }) + } +} + +func TestMultipleKeyTypes(t *testing.T) { + m := New() + callCount := 0 + + // Insert different key types simultaneously (two of each type) + keys := []interface{}{ + 42, 43, // ints + "string-key1", "string-key2", // strings + 3.14, 2.718, // floats + true, false, // bools + } + + for i, key := range keys { + value := i + m.Memoize(key, func() interface{} { + callCount++ + return value + }) + } + + // Verify size includes all entries + if m.Size() != len(keys) { + t.Errorf("Size = %d, want %d", m.Size(), len(keys)) + } + + // Verify all values are cached correctly + for i, key := range keys { + initialCount := callCount + result := m.Memoize(key, func() interface{} { + callCount++ + return -1 // Should never be returned if cache works + }) + + if result != i { + t.Errorf("Memoize(%v) = %v, want %v", key, result, i) + } + if callCount != initialCount { + t.Errorf("Cache miss for key %v", key) + } + } + + // Test invalidation of pairs + for i := 0; i < len(keys); i += 2 { + m.Invalidate(keys[i]) + m.Invalidate(keys[i+1]) + expectedSize := len(keys) - (i + 2) + if m.Size() != expectedSize { + t.Errorf("Size after invalidating pair %d = %d, want %d", i/2, m.Size(), expectedSize) + } + } + + // Clear and verify + m.Clear() + if m.Size() != 0 { + t.Errorf("Size after clear = %d, want 0", m.Size()) + } +} diff --git a/examples/gno.land/p/moul/printfdebugging/color.gno b/examples/gno.land/p/moul/printfdebugging/color.gno new file mode 100644 index 00000000000..b3bf647b9b5 --- /dev/null +++ b/examples/gno.land/p/moul/printfdebugging/color.gno @@ -0,0 +1,81 @@ +package printfdebugging + +// consts copied from https://github.com/fatih/color/blob/main/color.go + +// Attribute defines a single SGR Code +type Attribute int + +const Escape = "\x1b" + +// Base attributes +const ( + Reset Attribute = iota + Bold + Faint + Italic + Underline + BlinkSlow + BlinkRapid + ReverseVideo + Concealed + CrossedOut +) + +const ( + ResetBold Attribute = iota + 22 + ResetItalic + ResetUnderline + ResetBlinking + _ + ResetReversed + ResetConcealed + ResetCrossedOut +) + +// Foreground text colors +const ( + FgBlack Attribute = iota + 30 + FgRed + FgGreen + FgYellow + FgBlue + FgMagenta + FgCyan + FgWhite +) + +// Foreground Hi-Intensity text colors +const ( + FgHiBlack Attribute = iota + 90 + FgHiRed + FgHiGreen + FgHiYellow + FgHiBlue + FgHiMagenta + FgHiCyan + FgHiWhite +) + +// Background text colors +const ( + BgBlack Attribute = iota + 40 + BgRed + BgGreen + BgYellow + BgBlue + BgMagenta + BgCyan + BgWhite +) + +// Background Hi-Intensity text colors +const ( + BgHiBlack Attribute = iota + 100 + BgHiRed + BgHiGreen + BgHiYellow + BgHiBlue + BgHiMagenta + BgHiCyan + BgHiWhite +) diff --git a/examples/gno.land/p/moul/printfdebugging/gno.mod b/examples/gno.land/p/moul/printfdebugging/gno.mod new file mode 100644 index 00000000000..4b8d0f3256c --- /dev/null +++ b/examples/gno.land/p/moul/printfdebugging/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/printfdebugging diff --git a/examples/gno.land/p/moul/printfdebugging/printfdebugging.gno b/examples/gno.land/p/moul/printfdebugging/printfdebugging.gno new file mode 100644 index 00000000000..a12a3dfadd2 --- /dev/null +++ b/examples/gno.land/p/moul/printfdebugging/printfdebugging.gno @@ -0,0 +1,19 @@ +// this package is a joke... or not. +package printfdebugging + +import ( + "strings" + + "gno.land/p/demo/ufmt" +) + +func BigRedLine(args ...string) { + println(ufmt.Sprintf("%s[%dm####################################%s[%dm %s", + Escape, int(BgRed), Escape, int(Reset), + strings.Join(args, " "), + )) +} + +func Success() { + println(" \033[31mS\033[33mU\033[32mC\033[36mC\033[34mE\033[35mS\033[31mS\033[0m ") +} diff --git a/examples/gno.land/p/moul/realmpath/gno.mod b/examples/gno.land/p/moul/realmpath/gno.mod new file mode 100644 index 00000000000..0c012a0c3ae --- /dev/null +++ b/examples/gno.land/p/moul/realmpath/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/realmpath diff --git a/examples/gno.land/p/moul/realmpath/realmpath.gno b/examples/gno.land/p/moul/realmpath/realmpath.gno new file mode 100644 index 00000000000..c46c97b4bed --- /dev/null +++ b/examples/gno.land/p/moul/realmpath/realmpath.gno @@ -0,0 +1,100 @@ +// Package realmpath is a lightweight Render.path parsing and link generation +// library with an idiomatic API, closely resembling that of net/url. +// +// This package provides utilities for parsing request paths and query +// parameters, allowing you to extract path segments and manipulate query +// values. +// +// Example usage: +// +// import "gno.land/p/moul/realmpath" +// +// func Render(path string) string { +// // Parsing a sample path with query parameters +// path = "hello/world?foo=bar&baz=foobar" +// req := realmpath.Parse(path) +// +// // Accessing parsed path and query parameters +// println(req.Path) // Output: hello/world +// println(req.PathPart(0)) // Output: hello +// println(req.PathPart(1)) // Output: world +// println(req.Query.Get("foo")) // Output: bar +// println(req.Query.Get("baz")) // Output: foobar +// +// // Rebuilding the URL +// println(req.String()) // Output: /r/current/realm:hello/world?baz=foobar&foo=bar +// } +package realmpath + +import ( + "net/url" + "std" + "strings" +) + +const chainDomain = "gno.land" // XXX: std.ChainDomain (#2911) + +// Request represents a parsed request. +type Request struct { + Path string // The path of the request + Query url.Values // The parsed query parameters + Realm string // The realm associated with the request +} + +// Parse takes a raw path string and returns a Request object. +// It splits the path into its components and parses any query parameters. +func Parse(rawPath string) *Request { + // Split the raw path into path and query components + path, query := splitPathAndQuery(rawPath) + + // Parse the query string into url.Values + queryValues, _ := url.ParseQuery(query) + + return &Request{ + Path: path, // Set the path + Query: queryValues, // Set the parsed query values + } +} + +// PathParts returns the segments of the path as a slice of strings. +// It trims leading and trailing slashes and splits the path by slashes. +func (r *Request) PathParts() []string { + return strings.Split(strings.Trim(r.Path, "/"), "/") +} + +// PathPart returns the specified part of the path. +// If the index is out of bounds, it returns an empty string. +func (r *Request) PathPart(index int) string { + parts := r.PathParts() // Get the path segments + if index < 0 || index >= len(parts) { + return "" // Return empty if index is out of bounds + } + return parts[index] // Return the specified path part +} + +// String rebuilds the URL from the path and query values. +// If the Realm is not set, it automatically retrieves the current realm path. +func (r *Request) String() string { + // Automatically set the Realm if it is not already defined + if r.Realm == "" { + r.Realm = std.CurrentRealm().PkgPath() // Get the current realm path + } + + // Rebuild the path using the realm and path parts + relativePkgPath := strings.TrimPrefix(r.Realm, chainDomain) // Trim the chain domain prefix + reconstructedPath := relativePkgPath + ":" + strings.Join(r.PathParts(), "/") + + // Rebuild the query string + queryString := r.Query.Encode() // Encode the query parameters + if queryString != "" { + return reconstructedPath + "?" + queryString // Return the full URL with query + } + return reconstructedPath // Return the path without query parameters +} + +func splitPathAndQuery(rawPath string) (string, string) { + if idx := strings.Index(rawPath, "?"); idx != -1 { + return rawPath[:idx], rawPath[idx+1:] // Split at the first '?' found + } + return rawPath, "" // No query string present +} diff --git a/examples/gno.land/p/moul/realmpath/realmpath_test.gno b/examples/gno.land/p/moul/realmpath/realmpath_test.gno new file mode 100644 index 00000000000..a638b40d3ca --- /dev/null +++ b/examples/gno.land/p/moul/realmpath/realmpath_test.gno @@ -0,0 +1,151 @@ +package realmpath_test + +import ( + "net/url" + "std" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" + "gno.land/p/moul/realmpath" +) + +func TestExample(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/lorem/ipsum")) + + // initial parsing + path := "hello/world?foo=bar&baz=foobar" + req := realmpath.Parse(path) + urequire.False(t, req == nil, "req should not be nil") + uassert.Equal(t, req.Path, "hello/world") + uassert.Equal(t, req.Query.Get("foo"), "bar") + uassert.Equal(t, req.Query.Get("baz"), "foobar") + uassert.Equal(t, req.String(), "/r/lorem/ipsum:hello/world?baz=foobar&foo=bar") + + // alter query + req.Query.Set("hey", "salut") + uassert.Equal(t, req.String(), "/r/lorem/ipsum:hello/world?baz=foobar&foo=bar&hey=salut") + + // alter path + req.Path = "bye/ciao" + uassert.Equal(t, req.String(), "/r/lorem/ipsum:bye/ciao?baz=foobar&foo=bar&hey=salut") +} + +func TestParse(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/lorem/ipsum")) + + tests := []struct { + rawPath string + realm string // optional + expectedPath string + expectedQuery url.Values + expectedString string + }{ + { + rawPath: "hello/world?foo=bar&baz=foobar", + expectedPath: "hello/world", + expectedQuery: url.Values{ + "foo": []string{"bar"}, + "baz": []string{"foobar"}, + }, + expectedString: "/r/lorem/ipsum:hello/world?baz=foobar&foo=bar", + }, + { + rawPath: "api/v1/resource?search=test&limit=10", + expectedPath: "api/v1/resource", + expectedQuery: url.Values{ + "search": []string{"test"}, + "limit": []string{"10"}, + }, + expectedString: "/r/lorem/ipsum:api/v1/resource?limit=10&search=test", + }, + { + rawPath: "singlepath", + expectedPath: "singlepath", + expectedQuery: url.Values{}, + expectedString: "/r/lorem/ipsum:singlepath", + }, + { + rawPath: "path/with/trailing/slash/", + expectedPath: "path/with/trailing/slash/", + expectedQuery: url.Values{}, + expectedString: "/r/lorem/ipsum:path/with/trailing/slash", + }, + { + rawPath: "emptyquery?", + expectedPath: "emptyquery", + expectedQuery: url.Values{}, + expectedString: "/r/lorem/ipsum:emptyquery", + }, + { + rawPath: "path/with/special/characters/?key=val%20ue&anotherKey=with%21special%23chars", + expectedPath: "path/with/special/characters/", + expectedQuery: url.Values{ + "key": []string{"val ue"}, + "anotherKey": []string{"with!special#chars"}, + }, + expectedString: "/r/lorem/ipsum:path/with/special/characters?anotherKey=with%21special%23chars&key=val+ue", + }, + { + rawPath: "path/with/empty/key?keyEmpty&=valueEmpty", + expectedPath: "path/with/empty/key", + expectedQuery: url.Values{ + "keyEmpty": []string{""}, + "": []string{"valueEmpty"}, + }, + expectedString: "/r/lorem/ipsum:path/with/empty/key?=valueEmpty&keyEmpty=", + }, + { + rawPath: "path/with/multiple/empty/keys?=empty1&=empty2", + expectedPath: "path/with/multiple/empty/keys", + expectedQuery: url.Values{ + "": []string{"empty1", "empty2"}, + }, + expectedString: "/r/lorem/ipsum:path/with/multiple/empty/keys?=empty1&=empty2", + }, + { + rawPath: "path/with/percent-encoded/%20space?query=hello%20world", + expectedPath: "path/with/percent-encoded/%20space", // XXX: should we decode? + expectedQuery: url.Values{ + "query": []string{"hello world"}, + }, + expectedString: "/r/lorem/ipsum:path/with/percent-encoded/%20space?query=hello+world", + }, + { + rawPath: "path/with/very/long/query?key1=value1&key2=value2&key3=value3&key4=value4&key5=value5&key6=value6", + expectedPath: "path/with/very/long/query", + expectedQuery: url.Values{ + "key1": []string{"value1"}, + "key2": []string{"value2"}, + "key3": []string{"value3"}, + "key4": []string{"value4"}, + "key5": []string{"value5"}, + "key6": []string{"value6"}, + }, + expectedString: "/r/lorem/ipsum:path/with/very/long/query?key1=value1&key2=value2&key3=value3&key4=value4&key5=value5&key6=value6", + }, + { + rawPath: "custom/realm?foo=bar&baz=foobar", + realm: "gno.land/r/foo/bar", + expectedPath: "custom/realm", + expectedQuery: url.Values{ + "foo": []string{"bar"}, + "baz": []string{"foobar"}, + }, + expectedString: "/r/foo/bar:custom/realm?baz=foobar&foo=bar", + }, + } + + for _, tt := range tests { + t.Run(tt.rawPath, func(t *testing.T) { + req := realmpath.Parse(tt.rawPath) + req.Realm = tt.realm // set optional realm + urequire.False(t, req == nil, "req should not be nil") + uassert.Equal(t, req.Path, tt.expectedPath) + urequire.Equal(t, len(req.Query), len(tt.expectedQuery)) + uassert.Equal(t, req.Query.Encode(), tt.expectedQuery.Encode()) + // XXX: uassert.Equal(t, req.Query, tt.expectedQuery) + uassert.Equal(t, req.String(), tt.expectedString) + }) + } +} diff --git a/examples/gno.land/p/moul/txlink/gno.mod b/examples/gno.land/p/moul/txlink/gno.mod new file mode 100644 index 00000000000..ed16b8b74fd --- /dev/null +++ b/examples/gno.land/p/moul/txlink/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/txlink diff --git a/examples/gno.land/p/moul/txlink/txlink.gno b/examples/gno.land/p/moul/txlink/txlink.gno new file mode 100644 index 00000000000..65edda6911e --- /dev/null +++ b/examples/gno.land/p/moul/txlink/txlink.gno @@ -0,0 +1,74 @@ +// Package txlink provides utilities for creating transaction-related links +// compatible with Gnoweb, Gnobro, and other clients within the Gno ecosystem. +// +// This package is optimized for generating lightweight transaction links with +// flexible arguments, allowing users to build dynamic links that integrate +// seamlessly with various Gno clients. +// +// The primary function, Call, is designed to produce markdown links for +// transaction functions in the current "relative realm". By specifying a custom +// Realm, you can generate links that either use the current realm path or a +// fully qualified path for another realm. +// +// This package is a streamlined alternative to helplink, providing similar +// functionality for transaction links without the full feature set of helplink. +package txlink + +import ( + "std" + "strings" +) + +const chainDomain = "gno.land" // XXX: std.ChainDomain (#2911) + +// Call returns a URL for the specified function with optional key-value +// arguments, for the current realm. +func Call(fn string, args ...string) string { + return Realm("").Call(fn, args...) +} + +// Realm represents a specific realm for generating tx links. +type Realm string + +// prefix returns the URL prefix for the realm. +func (r Realm) prefix() string { + // relative + if r == "" { + curPath := std.CurrentRealm().PkgPath() + return strings.TrimPrefix(curPath, chainDomain) + } + + // local realm -> /realm + realm := string(r) + if strings.Contains(realm, chainDomain) { + return strings.TrimPrefix(realm, chainDomain) + } + + // remote realm -> https://remote.land/realm + return "https://" + string(r) +} + +// Call returns a URL for the specified function with optional key-value +// arguments. +func (r Realm) Call(fn string, args ...string) string { + // Start with the base query + url := r.prefix() + "$help&func=" + fn + + // Check if args length is even + if len(args)%2 != 0 { + // If not even, we can choose to handle the error here. + // For example, we can just return the URL without appending + // more args. + return url + } + + // Append key-value pairs to the URL + for i := 0; i < len(args); i += 2 { + key := args[i] + value := args[i+1] + // XXX: escape keys and args + url += "&" + key + "=" + value + } + + return url +} diff --git a/examples/gno.land/p/moul/txlink/txlink_test.gno b/examples/gno.land/p/moul/txlink/txlink_test.gno new file mode 100644 index 00000000000..61b532270d4 --- /dev/null +++ b/examples/gno.land/p/moul/txlink/txlink_test.gno @@ -0,0 +1,37 @@ +package txlink + +import ( + "testing" + + "gno.land/p/demo/urequire" +) + +func TestCall(t *testing.T) { + tests := []struct { + fn string + args []string + want string + realm Realm + }{ + {"foo", []string{"bar", "1", "baz", "2"}, "$help&func=foo&bar=1&baz=2", ""}, + {"testFunc", []string{"key", "value"}, "$help&func=testFunc&key=value", ""}, + {"noArgsFunc", []string{}, "$help&func=noArgsFunc", ""}, + {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc", ""}, + {"foo", []string{"bar", "1", "baz", "2"}, "/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.land/r/lorem/ipsum"}, + {"testFunc", []string{"key", "value"}, "/r/lorem/ipsum$help&func=testFunc&key=value", "gno.land/r/lorem/ipsum"}, + {"noArgsFunc", []string{}, "/r/lorem/ipsum$help&func=noArgsFunc", "gno.land/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc", "gno.land/r/lorem/ipsum"}, + {"foo", []string{"bar", "1", "baz", "2"}, "https://gno.world/r/lorem/ipsum$help&func=foo&bar=1&baz=2", "gno.world/r/lorem/ipsum"}, + {"testFunc", []string{"key", "value"}, "https://gno.world/r/lorem/ipsum$help&func=testFunc&key=value", "gno.world/r/lorem/ipsum"}, + {"noArgsFunc", []string{}, "https://gno.world/r/lorem/ipsum$help&func=noArgsFunc", "gno.world/r/lorem/ipsum"}, + {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc", "gno.world/r/lorem/ipsum"}, + } + + for _, tt := range tests { + title := tt.fn + t.Run(title, func(t *testing.T) { + got := tt.realm.Call(tt.fn, tt.args...) + urequire.Equal(t, tt.want, got) + }) + } +} diff --git a/examples/gno.land/p/moul/typeutil/gno.mod b/examples/gno.land/p/moul/typeutil/gno.mod new file mode 100644 index 00000000000..4f9c432456b --- /dev/null +++ b/examples/gno.land/p/moul/typeutil/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/typeutil diff --git a/examples/gno.land/p/moul/typeutil/typeutil.gno b/examples/gno.land/p/moul/typeutil/typeutil.gno new file mode 100644 index 00000000000..1fa79b94549 --- /dev/null +++ b/examples/gno.land/p/moul/typeutil/typeutil.gno @@ -0,0 +1,715 @@ +// Package typeutil provides utility functions for converting between different types +// and checking their states. It aims to provide consistent behavior across different +// types while remaining lightweight and dependency-free. +package typeutil + +import ( + "errors" + "sort" + "std" + "strconv" + "strings" + "time" +) + +// stringer is the interface that wraps the String method. +type stringer interface { + String() string +} + +// ToString converts any value to its string representation. +// It supports a wide range of Go types including: +// - Basic: string, bool +// - Numbers: int, int8-64, uint, uint8-64, float32, float64 +// - Special: time.Time, std.Address, []byte +// - Slices: []T for most basic types +// - Maps: map[string]string, map[string]interface{} +// - Interface: types implementing String() string +// +// Example usage: +// +// str := typeutil.ToString(42) // "42" +// str = typeutil.ToString([]int{1, 2}) // "[1 2]" +// str = typeutil.ToString(map[string]string{ // "map[a:1 b:2]" +// "a": "1", +// "b": "2", +// }) +func ToString(val interface{}) string { + if val == nil { + return "" + } + + // First check if value implements Stringer interface + if s, ok := val.(interface{ String() string }); ok { + return s.String() + } + + switch v := val.(type) { + // Pointer types - dereference and recurse + case *string: + if v == nil { + return "" + } + return *v + case *int: + if v == nil { + return "" + } + return strconv.Itoa(*v) + case *bool: + if v == nil { + return "" + } + return strconv.FormatBool(*v) + case *time.Time: + if v == nil { + return "" + } + return v.String() + case *std.Address: + if v == nil { + return "" + } + return string(*v) + + // String types + case string: + return v + case stringer: + return v.String() + + // Special types + case time.Time: + return v.String() + case std.Address: + return string(v) + case []byte: + return string(v) + case struct{}: + return "{}" + + // Integer types + case int: + return strconv.Itoa(v) + case int8: + return strconv.FormatInt(int64(v), 10) + case int16: + return strconv.FormatInt(int64(v), 10) + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case uint: + return strconv.FormatUint(uint64(v), 10) + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint16: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + + // Float types + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + + // Boolean + case bool: + if v { + return "true" + } + return "false" + + // Slice types + case []string: + return join(v) + case []int: + return join(v) + case []int32: + return join(v) + case []int64: + return join(v) + case []float32: + return join(v) + case []float64: + return join(v) + case []interface{}: + return join(v) + case []time.Time: + return joinTimes(v) + case []stringer: + return join(v) + case []std.Address: + return joinAddresses(v) + case [][]byte: + return joinBytes(v) + + // Map types with various key types + case map[interface{}]interface{}, map[string]interface{}, map[string]string, map[string]int: + var b strings.Builder + b.WriteString("map[") + first := true + + switch m := v.(type) { + case map[interface{}]interface{}: + // Convert all keys to strings for consistent ordering + keys := make([]string, 0) + keyMap := make(map[string]interface{}) + + for k := range m { + keyStr := ToString(k) + keys = append(keys, keyStr) + keyMap[keyStr] = k + } + sort.Strings(keys) + + for _, keyStr := range keys { + if !first { + b.WriteString(" ") + } + origKey := keyMap[keyStr] + b.WriteString(keyStr) + b.WriteString(":") + b.WriteString(ToString(m[origKey])) + first = false + } + + case map[string]interface{}: + keys := make([]string, 0) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + if !first { + b.WriteString(" ") + } + b.WriteString(k) + b.WriteString(":") + b.WriteString(ToString(m[k])) + first = false + } + + case map[string]string: + keys := make([]string, 0) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + if !first { + b.WriteString(" ") + } + b.WriteString(k) + b.WriteString(":") + b.WriteString(m[k]) + first = false + } + + case map[string]int: + keys := make([]string, 0) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + if !first { + b.WriteString(" ") + } + b.WriteString(k) + b.WriteString(":") + b.WriteString(strconv.Itoa(m[k])) + first = false + } + } + b.WriteString("]") + return b.String() + + // Default + default: + return "" + } +} + +func join(slice interface{}) string { + if IsZero(slice) { + return "[]" + } + + items := ToInterfaceSlice(slice) + if items == nil { + return "[]" + } + + var b strings.Builder + b.WriteString("[") + for i, item := range items { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(ToString(item)) + } + b.WriteString("]") + return b.String() +} + +func joinTimes(slice []time.Time) string { + if len(slice) == 0 { + return "[]" + } + var b strings.Builder + b.WriteString("[") + for i, t := range slice { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(t.String()) + } + b.WriteString("]") + return b.String() +} + +func joinAddresses(slice []std.Address) string { + if len(slice) == 0 { + return "[]" + } + var b strings.Builder + b.WriteString("[") + for i, addr := range slice { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(string(addr)) + } + b.WriteString("]") + return b.String() +} + +func joinBytes(slice [][]byte) string { + if len(slice) == 0 { + return "[]" + } + var b strings.Builder + b.WriteString("[") + for i, bytes := range slice { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(string(bytes)) + } + b.WriteString("]") + return b.String() +} + +// ToBool converts any value to a boolean based on common programming conventions. +// For example: +// - Numbers: 0 is false, any other number is true +// - Strings: "", "0", "false", "f", "no", "n", "off" are false, others are true +// - Slices/Maps: empty is false, non-empty is true +// - nil: always false +// - bool: direct value +func ToBool(val interface{}) bool { + if IsZero(val) { + return false + } + + // Handle special string cases + if str, ok := val.(string); ok { + str = strings.ToLower(strings.TrimSpace(str)) + return str != "" && str != "0" && str != "false" && str != "f" && str != "no" && str != "n" && str != "off" + } + + return true +} + +// IsZero returns true if the value represents a "zero" or "empty" state for its type. +// For example: +// - Numbers: 0 +// - Strings: "" +// - Slices/Maps: empty +// - nil: true +// - bool: false +// - time.Time: IsZero() +// - std.Address: empty string +func IsZero(val interface{}) bool { + if val == nil { + return true + } + + switch v := val.(type) { + // Pointer types - nil pointer is zero, otherwise check pointed value + case *bool: + return v == nil || !*v + case *string: + return v == nil || *v == "" + case *int: + return v == nil || *v == 0 + case *time.Time: + return v == nil || v.IsZero() + case *std.Address: + return v == nil || string(*v) == "" + + // Bool + case bool: + return !v + + // String types + case string: + return v == "" + case stringer: + return v.String() == "" + + // Integer types + case int: + return v == 0 + case int8: + return v == 0 + case int16: + return v == 0 + case int32: + return v == 0 + case int64: + return v == 0 + case uint: + return v == 0 + case uint8: + return v == 0 + case uint16: + return v == 0 + case uint32: + return v == 0 + case uint64: + return v == 0 + + // Float types + case float32: + return v == 0 + case float64: + return v == 0 + + // Special types + case []byte: + return len(v) == 0 + case time.Time: + return v.IsZero() + case std.Address: + return string(v) == "" + + // Slices (check if empty) + case []string: + return len(v) == 0 + case []int: + return len(v) == 0 + case []int32: + return len(v) == 0 + case []int64: + return len(v) == 0 + case []float32: + return len(v) == 0 + case []float64: + return len(v) == 0 + case []interface{}: + return len(v) == 0 + case []time.Time: + return len(v) == 0 + case []std.Address: + return len(v) == 0 + case [][]byte: + return len(v) == 0 + case []stringer: + return len(v) == 0 + + // Maps (check if empty) + case map[string]string: + return len(v) == 0 + case map[string]interface{}: + return len(v) == 0 + + default: + return false // non-nil unknown types are considered non-zero + } +} + +// ToInterfaceSlice converts various slice types to []interface{} +func ToInterfaceSlice(val interface{}) []interface{} { + switch v := val.(type) { + case []interface{}: + return v + case []string: + result := make([]interface{}, len(v)) + for i, s := range v { + result[i] = s + } + return result + case []int: + result := make([]interface{}, len(v)) + for i, n := range v { + result[i] = n + } + return result + case []int32: + result := make([]interface{}, len(v)) + for i, n := range v { + result[i] = n + } + return result + case []int64: + result := make([]interface{}, len(v)) + for i, n := range v { + result[i] = n + } + return result + case []float32: + result := make([]interface{}, len(v)) + for i, n := range v { + result[i] = n + } + return result + case []float64: + result := make([]interface{}, len(v)) + for i, n := range v { + result[i] = n + } + return result + case []bool: + result := make([]interface{}, len(v)) + for i, b := range v { + result[i] = b + } + return result + default: + return nil + } +} + +// ToMapStringInterface converts a map with string keys and any value type to map[string]interface{} +func ToMapStringInterface(m interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + switch v := m.(type) { + case map[string]interface{}: + return v, nil + case map[string]string: + for k, val := range v { + result[k] = val + } + case map[string]int: + for k, val := range v { + result[k] = val + } + case map[string]int64: + for k, val := range v { + result[k] = val + } + case map[string]float64: + for k, val := range v { + result[k] = val + } + case map[string]bool: + for k, val := range v { + result[k] = val + } + case map[string][]string: + for k, val := range v { + result[k] = ToInterfaceSlice(val) + } + case map[string][]int: + for k, val := range v { + result[k] = ToInterfaceSlice(val) + } + case map[string][]interface{}: + for k, val := range v { + result[k] = val + } + case map[string]map[string]interface{}: + for k, val := range v { + result[k] = val + } + case map[string]map[string]string: + for k, val := range v { + if converted, err := ToMapStringInterface(val); err == nil { + result[k] = converted + } else { + return nil, errors.New("failed to convert nested map at key: " + k) + } + } + default: + return nil, errors.New("unsupported map type: " + ToString(m)) + } + + return result, nil +} + +// ToMapIntInterface converts a map with int keys and any value type to map[int]interface{} +func ToMapIntInterface(m interface{}) (map[int]interface{}, error) { + result := make(map[int]interface{}) + + switch v := m.(type) { + case map[int]interface{}: + return v, nil + case map[int]string: + for k, val := range v { + result[k] = val + } + case map[int]int: + for k, val := range v { + result[k] = val + } + case map[int]int64: + for k, val := range v { + result[k] = val + } + case map[int]float64: + for k, val := range v { + result[k] = val + } + case map[int]bool: + for k, val := range v { + result[k] = val + } + case map[int][]string: + for k, val := range v { + result[k] = ToInterfaceSlice(val) + } + case map[int][]int: + for k, val := range v { + result[k] = ToInterfaceSlice(val) + } + case map[int][]interface{}: + for k, val := range v { + result[k] = val + } + case map[int]map[string]interface{}: + for k, val := range v { + result[k] = val + } + case map[int]map[int]interface{}: + for k, val := range v { + result[k] = val + } + default: + return nil, errors.New("unsupported map type: " + ToString(m)) + } + + return result, nil +} + +// ToStringSlice converts various slice types to []string +func ToStringSlice(val interface{}) []string { + switch v := val.(type) { + case []string: + return v + case []interface{}: + result := make([]string, len(v)) + for i, item := range v { + result[i] = ToString(item) + } + return result + case []int: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.Itoa(n) + } + return result + case []int32: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatInt(int64(n), 10) + } + return result + case []int64: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatInt(n, 10) + } + return result + case []float32: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatFloat(float64(n), 'f', -1, 32) + } + return result + case []float64: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatFloat(n, 'f', -1, 64) + } + return result + case []bool: + result := make([]string, len(v)) + for i, b := range v { + result[i] = strconv.FormatBool(b) + } + return result + case []time.Time: + result := make([]string, len(v)) + for i, t := range v { + result[i] = t.String() + } + return result + case []std.Address: + result := make([]string, len(v)) + for i, addr := range v { + result[i] = string(addr) + } + return result + case [][]byte: + result := make([]string, len(v)) + for i, b := range v { + result[i] = string(b) + } + return result + case []stringer: + result := make([]string, len(v)) + for i, s := range v { + result[i] = s.String() + } + return result + case []uint: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatUint(uint64(n), 10) + } + return result + case []uint8: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatUint(uint64(n), 10) + } + return result + case []uint16: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatUint(uint64(n), 10) + } + return result + case []uint32: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatUint(uint64(n), 10) + } + return result + case []uint64: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatUint(n, 10) + } + return result + default: + // Try to convert using reflection if it's a slice + if slice := ToInterfaceSlice(val); slice != nil { + result := make([]string, len(slice)) + for i, item := range slice { + result[i] = ToString(item) + } + return result + } + return nil + } +} diff --git a/examples/gno.land/p/moul/typeutil/typeutil_test.gno b/examples/gno.land/p/moul/typeutil/typeutil_test.gno new file mode 100644 index 00000000000..543ea1deec4 --- /dev/null +++ b/examples/gno.land/p/moul/typeutil/typeutil_test.gno @@ -0,0 +1,1075 @@ +package typeutil + +import ( + "std" + "strings" + "testing" + "time" +) + +type testStringer struct { + value string +} + +func (t testStringer) String() string { + return "test:" + t.value +} + +func TestToString(t *testing.T) { + // setup test data + str := "hello" + num := 42 + b := true + now := time.Now() + addr := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + stringer := testStringer{value: "hello"} + + type testCase struct { + name string + input interface{} + expected string + } + + tests := []testCase{ + // basic types + {"string", "hello", "hello"}, + {"empty_string", "", ""}, + {"nil", nil, ""}, + + // integer types + {"int", 42, "42"}, + {"int8", int8(8), "8"}, + {"int16", int16(16), "16"}, + {"int32", int32(32), "32"}, + {"int64", int64(64), "64"}, + {"uint", uint(42), "42"}, + {"uint8", uint8(8), "8"}, + {"uint16", uint16(16), "16"}, + {"uint32", uint32(32), "32"}, + {"uint64", uint64(64), "64"}, + + // float types + {"float32", float32(3.14), "3.14"}, + {"float64", 3.14159, "3.14159"}, + + // boolean + {"bool_true", true, "true"}, + {"bool_false", false, "false"}, + + // special types + {"time", now, now.String()}, + {"address", addr, string(addr)}, + {"bytes", []byte("hello"), "hello"}, + {"stringer", stringer, "test:hello"}, + + // slices + {"empty_slice", []string{}, "[]"}, + {"string_slice", []string{"a", "b"}, "[a b]"}, + {"int_slice", []int{1, 2}, "[1 2]"}, + {"int32_slice", []int32{1, 2}, "[1 2]"}, + {"int64_slice", []int64{1, 2}, "[1 2]"}, + {"float32_slice", []float32{1.1, 2.2}, "[1.1 2.2]"}, + {"float64_slice", []float64{1.1, 2.2}, "[1.1 2.2]"}, + {"bytes_slice", [][]byte{[]byte("a"), []byte("b")}, "[a b]"}, + {"time_slice", []time.Time{now, now}, "[" + now.String() + " " + now.String() + "]"}, + {"address_slice", []std.Address{addr, addr}, "[" + string(addr) + " " + string(addr) + "]"}, + {"interface_slice", []interface{}{1, "a", true}, "[1 a true]"}, + + // empty slices + {"empty_string_slice", []string{}, "[]"}, + {"empty_int_slice", []int{}, "[]"}, + {"empty_int32_slice", []int32{}, "[]"}, + {"empty_int64_slice", []int64{}, "[]"}, + {"empty_float32_slice", []float32{}, "[]"}, + {"empty_float64_slice", []float64{}, "[]"}, + {"empty_bytes_slice", [][]byte{}, "[]"}, + {"empty_time_slice", []time.Time{}, "[]"}, + {"empty_address_slice", []std.Address{}, "[]"}, + {"empty_interface_slice", []interface{}{}, "[]"}, + + // maps + {"empty_string_map", map[string]string{}, "map[]"}, + {"string_map", map[string]string{"a": "1", "b": "2"}, "map[a:1 b:2]"}, + {"empty_interface_map", map[string]interface{}{}, "map[]"}, + {"interface_map", map[string]interface{}{"a": 1, "b": "2"}, "map[a:1 b:2]"}, + + // edge cases + {"empty_bytes", []byte{}, ""}, + {"nil_interface", interface{}(nil), ""}, + {"empty_struct", struct{}{}, "{}"}, + {"unknown_type", struct{ foo string }{}, ""}, + + // pointer types + {"nil_string_ptr", (*string)(nil), ""}, + {"string_ptr", &str, "hello"}, + {"nil_int_ptr", (*int)(nil), ""}, + {"int_ptr", &num, "42"}, + {"nil_bool_ptr", (*bool)(nil), ""}, + {"bool_ptr", &b, "true"}, + // {"nil_time_ptr", (*time.Time)(nil), ""}, // TODO: fix this + {"time_ptr", &now, now.String()}, + // {"nil_address_ptr", (*std.Address)(nil), ""}, // TODO: fix this + {"address_ptr", &addr, string(addr)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToString(tt.input) + if got != tt.expected { + t.Errorf("%s: ToString(%v) = %q, want %q", tt.name, tt.input, got, tt.expected) + } + }) + } +} + +func TestToBool(t *testing.T) { + str := "true" + num := 42 + b := true + now := time.Now() + addr := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + zero := 0 + empty := "" + falseVal := false + + type testCase struct { + name string + input interface{} + expected bool + } + + tests := []testCase{ + // basic types + {"true", true, true}, + {"false", false, false}, + {"nil", nil, false}, + + // strings + {"empty_string", "", false}, + {"zero_string", "0", false}, + {"false_string", "false", false}, + {"f_string", "f", false}, + {"no_string", "no", false}, + {"n_string", "n", false}, + {"off_string", "off", false}, + {"space_string", " ", false}, + {"true_string", "true", true}, + {"yes_string", "yes", true}, + {"random_string", "hello", true}, + + // numbers + {"zero_int", 0, false}, + {"positive_int", 1, true}, + {"negative_int", -1, true}, + {"zero_float", 0.0, false}, + {"positive_float", 0.1, true}, + {"negative_float", -0.1, true}, + + // special types + {"empty_bytes", []byte{}, false}, + {"non_empty_bytes", []byte{1}, true}, + /*{"zero_time", time.Time{}, false},*/ // TODO: fix this + {"empty_address", std.Address(""), false}, + + // slices + {"empty_slice", []string{}, false}, + {"non_empty_slice", []string{"a"}, true}, + + // maps + {"empty_map", map[string]string{}, false}, + {"non_empty_map", map[string]string{"a": "b"}, true}, + + // pointer types + {"nil_bool_ptr", (*bool)(nil), false}, + {"true_ptr", &b, true}, + {"false_ptr", &falseVal, false}, + {"nil_string_ptr", (*string)(nil), false}, + {"string_ptr", &str, true}, + {"empty_string_ptr", &empty, false}, + {"nil_int_ptr", (*int)(nil), false}, + {"int_ptr", &num, true}, + {"zero_int_ptr", &zero, false}, + // {"nil_time_ptr", (*time.Time)(nil), false}, // TODO: fix this + {"time_ptr", &now, true}, + // {"nil_address_ptr", (*std.Address)(nil), false}, // TODO: fix this + {"address_ptr", &addr, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToBool(tt.input) + if got != tt.expected { + t.Errorf("%s: ToBool(%v) = %v, want %v", tt.name, tt.input, got, tt.expected) + } + }) + } +} + +func TestIsZero(t *testing.T) { + str := "hello" + num := 42 + b := true + now := time.Now() + addr := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + zero := 0 + empty := "" + falseVal := false + + type testCase struct { + name string + input interface{} + expected bool + } + + tests := []testCase{ + // basic types + {"true", true, false}, + {"false", false, true}, + {"nil", nil, true}, + + // strings + {"empty_string", "", true}, + {"non_empty_string", "hello", false}, + + // numbers + {"zero_int", 0, true}, + {"non_zero_int", 1, false}, + {"zero_float", 0.0, true}, + {"non_zero_float", 0.1, false}, + + // special types + {"empty_bytes", []byte{}, true}, + {"non_empty_bytes", []byte{1}, false}, + /*{"zero_time", time.Time{}, true},*/ // TODO: fix this + {"empty_address", std.Address(""), true}, + + // slices + {"empty_slice", []string{}, true}, + {"non_empty_slice", []string{"a"}, false}, + + // maps + {"empty_map", map[string]string{}, true}, + {"non_empty_map", map[string]string{"a": "b"}, false}, + + // pointer types + {"nil_bool_ptr", (*bool)(nil), true}, + {"false_ptr", &falseVal, true}, + {"true_ptr", &b, false}, + {"nil_string_ptr", (*string)(nil), true}, + {"empty_string_ptr", &empty, true}, + {"string_ptr", &str, false}, + {"nil_int_ptr", (*int)(nil), true}, + {"zero_int_ptr", &zero, true}, + {"int_ptr", &num, false}, + // {"nil_time_ptr", (*time.Time)(nil), true}, // TODO: fix this + {"time_ptr", &now, false}, + // {"nil_address_ptr", (*std.Address)(nil), true}, // TODO: fix this + {"address_ptr", &addr, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsZero(tt.input) + if got != tt.expected { + t.Errorf("%s: IsZero(%v) = %v, want %v", tt.name, tt.input, got, tt.expected) + } + }) + } +} + +func TestToInterfaceSlice(t *testing.T) { + now := time.Now() + addr := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + str := testStringer{value: "hello"} + + tests := []struct { + name string + input interface{} + expected []interface{} + compare func([]interface{}, []interface{}) bool + }{ + { + name: "nil", + input: nil, + expected: nil, + compare: compareNil, + }, + { + name: "empty_interface_slice", + input: []interface{}{}, + expected: []interface{}{}, + compare: compareEmpty, + }, + { + name: "interface_slice", + input: []interface{}{1, "two", true}, + expected: []interface{}{1, "two", true}, + compare: compareInterfaces, + }, + { + name: "string_slice", + input: []string{"a", "b", "c"}, + expected: []interface{}{"a", "b", "c"}, + compare: compareStrings, + }, + { + name: "int_slice", + input: []int{1, 2, 3}, + expected: []interface{}{1, 2, 3}, + compare: compareInts, + }, + { + name: "int32_slice", + input: []int32{1, 2, 3}, + expected: []interface{}{int32(1), int32(2), int32(3)}, + compare: compareInt32s, + }, + { + name: "int64_slice", + input: []int64{1, 2, 3}, + expected: []interface{}{int64(1), int64(2), int64(3)}, + compare: compareInt64s, + }, + { + name: "float32_slice", + input: []float32{1.1, 2.2, 3.3}, + expected: []interface{}{float32(1.1), float32(2.2), float32(3.3)}, + compare: compareFloat32s, + }, + { + name: "float64_slice", + input: []float64{1.1, 2.2, 3.3}, + expected: []interface{}{1.1, 2.2, 3.3}, + compare: compareFloat64s, + }, + { + name: "bool_slice", + input: []bool{true, false, true}, + expected: []interface{}{true, false, true}, + compare: compareBools, + }, + /* { + name: "time_slice", + input: []time.Time{now}, + expected: []interface{}{now}, + compare: compareTimes, + }, */ // TODO: fix this + /* { + name: "address_slice", + input: []std.Address{addr}, + expected: []interface{}{addr}, + compare: compareAddresses, + },*/ // TODO: fix this + /* { + name: "bytes_slice", + input: [][]byte{[]byte("hello"), []byte("world")}, + expected: []interface{}{[]byte("hello"), []byte("world")}, + compare: compareBytes, + },*/ // TODO: fix this + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToInterfaceSlice(tt.input) + if !tt.compare(got, tt.expected) { + t.Errorf("ToInterfaceSlice() = %v, want %v", got, tt.expected) + } + }) + } +} + +func compareNil(a, b []interface{}) bool { + return a == nil && b == nil +} + +func compareEmpty(a, b []interface{}) bool { + return len(a) == 0 && len(b) == 0 +} + +func compareInterfaces(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func compareStrings(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + as, ok1 := a[i].(string) + bs, ok2 := b[i].(string) + if !ok1 || !ok2 || as != bs { + return false + } + } + return true +} + +func compareInts(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ai, ok1 := a[i].(int) + bi, ok2 := b[i].(int) + if !ok1 || !ok2 || ai != bi { + return false + } + } + return true +} + +func compareInt32s(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ai, ok1 := a[i].(int32) + bi, ok2 := b[i].(int32) + if !ok1 || !ok2 || ai != bi { + return false + } + } + return true +} + +func compareInt64s(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ai, ok1 := a[i].(int64) + bi, ok2 := b[i].(int64) + if !ok1 || !ok2 || ai != bi { + return false + } + } + return true +} + +func compareFloat32s(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ai, ok1 := a[i].(float32) + bi, ok2 := b[i].(float32) + if !ok1 || !ok2 || ai != bi { + return false + } + } + return true +} + +func compareFloat64s(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ai, ok1 := a[i].(float64) + bi, ok2 := b[i].(float64) + if !ok1 || !ok2 || ai != bi { + return false + } + } + return true +} + +func compareBools(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ab, ok1 := a[i].(bool) + bb, ok2 := b[i].(bool) + if !ok1 || !ok2 || ab != bb { + return false + } + } + return true +} + +func compareTimes(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + at, ok1 := a[i].(time.Time) + bt, ok2 := b[i].(time.Time) + if !ok1 || !ok2 || !at.Equal(bt) { + return false + } + } + return true +} + +func compareAddresses(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + aa, ok1 := a[i].(std.Address) + ba, ok2 := b[i].(std.Address) + if !ok1 || !ok2 || aa != ba { + return false + } + } + return true +} + +func compareBytes(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ab, ok1 := a[i].([]byte) + bb, ok2 := b[i].([]byte) + if !ok1 || !ok2 || string(ab) != string(bb) { + return false + } + } + return true +} + +// compareStringInterfaceMaps compares two map[string]interface{} for equality +func compareStringInterfaceMaps(a, b map[string]interface{}) bool { + if len(a) != len(b) { + return false + } + for k, v1 := range a { + v2, ok := b[k] + if !ok { + return false + } + // Compare values based on their type + switch val1 := v1.(type) { + case string: + val2, ok := v2.(string) + if !ok || val1 != val2 { + return false + } + case int: + val2, ok := v2.(int) + if !ok || val1 != val2 { + return false + } + case float64: + val2, ok := v2.(float64) + if !ok || val1 != val2 { + return false + } + case bool: + val2, ok := v2.(bool) + if !ok || val1 != val2 { + return false + } + case []interface{}: + val2, ok := v2.([]interface{}) + if !ok || len(val1) != len(val2) { + return false + } + for i := range val1 { + if val1[i] != val2[i] { + return false + } + } + case map[string]interface{}: + val2, ok := v2.(map[string]interface{}) + if !ok || !compareStringInterfaceMaps(val1, val2) { + return false + } + default: + return false + } + } + return true +} + +func TestToMapStringInterface(t *testing.T) { + tests := []struct { + name string + input interface{} + expected map[string]interface{} + wantErr bool + }{ + { + name: "map[string]interface{}", + input: map[string]interface{}{ + "key1": "value1", + "key2": 42, + }, + expected: map[string]interface{}{ + "key1": "value1", + "key2": 42, + }, + wantErr: false, + }, + { + name: "map[string]string", + input: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + expected: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + wantErr: false, + }, + { + name: "map[string]int", + input: map[string]int{ + "key1": 1, + "key2": 2, + }, + expected: map[string]interface{}{ + "key1": 1, + "key2": 2, + }, + wantErr: false, + }, + { + name: "map[string]float64", + input: map[string]float64{ + "key1": 1.1, + "key2": 2.2, + }, + expected: map[string]interface{}{ + "key1": 1.1, + "key2": 2.2, + }, + wantErr: false, + }, + { + name: "map[string]bool", + input: map[string]bool{ + "key1": true, + "key2": false, + }, + expected: map[string]interface{}{ + "key1": true, + "key2": false, + }, + wantErr: false, + }, + { + name: "map[string][]string", + input: map[string][]string{ + "key1": {"a", "b"}, + "key2": {"c", "d"}, + }, + expected: map[string]interface{}{ + "key1": []interface{}{"a", "b"}, + "key2": []interface{}{"c", "d"}, + }, + wantErr: false, + }, + { + name: "nested map[string]map[string]string", + input: map[string]map[string]string{ + "key1": {"nested1": "value1"}, + "key2": {"nested2": "value2"}, + }, + expected: map[string]interface{}{ + "key1": map[string]interface{}{"nested1": "value1"}, + "key2": map[string]interface{}{"nested2": "value2"}, + }, + wantErr: false, + }, + { + name: "unsupported type", + input: 42, // not a map + expected: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ToMapStringInterface(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ToMapStringInterface() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if !compareStringInterfaceMaps(got, tt.expected) { + t.Errorf("ToMapStringInterface() = %v, expected %v", got, tt.expected) + } + } + }) + } +} + +// Test error messages +func TestToMapStringInterfaceErrors(t *testing.T) { + _, err := ToMapStringInterface(42) + if err == nil || !strings.Contains(err.Error(), "unsupported map type") { + t.Errorf("Expected error containing 'unsupported map type', got %v", err) + } +} + +// compareIntInterfaceMaps compares two map[int]interface{} for equality +func compareIntInterfaceMaps(a, b map[int]interface{}) bool { + if len(a) != len(b) { + return false + } + for k, v1 := range a { + v2, ok := b[k] + if !ok { + return false + } + // Compare values based on their type + switch val1 := v1.(type) { + case string: + val2, ok := v2.(string) + if !ok || val1 != val2 { + return false + } + case int: + val2, ok := v2.(int) + if !ok || val1 != val2 { + return false + } + case float64: + val2, ok := v2.(float64) + if !ok || val1 != val2 { + return false + } + case bool: + val2, ok := v2.(bool) + if !ok || val1 != val2 { + return false + } + case []interface{}: + val2, ok := v2.([]interface{}) + if !ok || len(val1) != len(val2) { + return false + } + for i := range val1 { + if val1[i] != val2[i] { + return false + } + } + case map[string]interface{}: + val2, ok := v2.(map[string]interface{}) + if !ok || !compareStringInterfaceMaps(val1, val2) { + return false + } + default: + return false + } + } + return true +} + +func TestToMapIntInterface(t *testing.T) { + tests := []struct { + name string + input interface{} + expected map[int]interface{} + wantErr bool + }{ + { + name: "map[int]interface{}", + input: map[int]interface{}{ + 1: "value1", + 2: 42, + }, + expected: map[int]interface{}{ + 1: "value1", + 2: 42, + }, + wantErr: false, + }, + { + name: "map[int]string", + input: map[int]string{ + 1: "value1", + 2: "value2", + }, + expected: map[int]interface{}{ + 1: "value1", + 2: "value2", + }, + wantErr: false, + }, + { + name: "map[int]int", + input: map[int]int{ + 1: 10, + 2: 20, + }, + expected: map[int]interface{}{ + 1: 10, + 2: 20, + }, + wantErr: false, + }, + { + name: "map[int]float64", + input: map[int]float64{ + 1: 1.1, + 2: 2.2, + }, + expected: map[int]interface{}{ + 1: 1.1, + 2: 2.2, + }, + wantErr: false, + }, + { + name: "map[int]bool", + input: map[int]bool{ + 1: true, + 2: false, + }, + expected: map[int]interface{}{ + 1: true, + 2: false, + }, + wantErr: false, + }, + { + name: "map[int][]string", + input: map[int][]string{ + 1: {"a", "b"}, + 2: {"c", "d"}, + }, + expected: map[int]interface{}{ + 1: []interface{}{"a", "b"}, + 2: []interface{}{"c", "d"}, + }, + wantErr: false, + }, + { + name: "map[int]map[string]interface{}", + input: map[int]map[string]interface{}{ + 1: {"nested1": "value1"}, + 2: {"nested2": "value2"}, + }, + expected: map[int]interface{}{ + 1: map[string]interface{}{"nested1": "value1"}, + 2: map[string]interface{}{"nested2": "value2"}, + }, + wantErr: false, + }, + { + name: "unsupported type", + input: 42, // not a map + expected: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ToMapIntInterface(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ToMapIntInterface() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if !compareIntInterfaceMaps(got, tt.expected) { + t.Errorf("ToMapIntInterface() = %v, expected %v", got, tt.expected) + } + } + }) + } +} + +func TestToStringSlice(t *testing.T) { + tests := []struct { + name string + input interface{} + expected []string + }{ + { + name: "nil input", + input: nil, + expected: nil, + }, + { + name: "empty slice", + input: []string{}, + expected: []string{}, + }, + { + name: "string slice", + input: []string{"a", "b", "c"}, + expected: []string{"a", "b", "c"}, + }, + { + name: "int slice", + input: []int{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "int32 slice", + input: []int32{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "int64 slice", + input: []int64{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "uint slice", + input: []uint{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "uint8 slice", + input: []uint8{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "uint16 slice", + input: []uint16{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "uint32 slice", + input: []uint32{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "uint64 slice", + input: []uint64{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "float32 slice", + input: []float32{1.1, 2.2, 3.3}, + expected: []string{"1.1", "2.2", "3.3"}, + }, + { + name: "float64 slice", + input: []float64{1.1, 2.2, 3.3}, + expected: []string{"1.1", "2.2", "3.3"}, + }, + { + name: "bool slice", + input: []bool{true, false, true}, + expected: []string{"true", "false", "true"}, + }, + { + name: "[]byte slice", + input: [][]byte{[]byte("hello"), []byte("world")}, + expected: []string{"hello", "world"}, + }, + { + name: "interface slice", + input: []interface{}{1, "hello", true}, + expected: []string{"1", "hello", "true"}, + }, + { + name: "time slice", + input: []time.Time{time.Time{}, time.Time{}}, + expected: []string{"0001-01-01 00:00:00 +0000 UTC", "0001-01-01 00:00:00 +0000 UTC"}, + }, + { + name: "address slice", + input: []std.Address{"addr1", "addr2"}, + expected: []string{"addr1", "addr2"}, + }, + { + name: "non-slice input", + input: 42, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ToStringSlice(tt.input) + if !slicesEqual(result, tt.expected) { + t.Errorf("ToStringSlice(%v) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} + +// Helper function to compare string slices +func slicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestToStringAdvanced(t *testing.T) { + tests := []struct { + name string + input interface{} + expected string + }{ + { + name: "slice with mixed basic types", + input: []interface{}{ + 42, + "hello", + true, + 3.14, + }, + expected: "[42 hello true 3.14]", + }, + { + name: "map with basic types", + input: map[string]interface{}{ + "int": 42, + "str": "hello", + "bool": true, + "float": 3.14, + }, + expected: "map[bool:true float:3.14 int:42 str:hello]", + }, + { + name: "mixed types map", + input: map[interface{}]interface{}{ + 42: "number", + "string": 123, + true: []int{1, 2, 3}, + struct{}{}: "empty", + }, + expected: "map[42:number string:123 true:[1 2 3] {}:empty]", + }, + { + name: "nested maps", + input: map[string]interface{}{ + "a": map[string]int{ + "x": 1, + "y": 2, + }, + "b": []interface{}{1, "two", true}, + }, + expected: "map[a:map[x:1 y:2] b:[1 two true]]", + }, + { + name: "empty struct", + input: struct{}{}, + expected: "{}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ToString(tt.input) + if result != tt.expected { + t.Errorf("\nToString(%v) =\n%v\nwant:\n%v", tt.input, result, tt.expected) + } + }) + } +} diff --git a/examples/gno.land/p/moul/ulist/gno.mod b/examples/gno.land/p/moul/ulist/gno.mod new file mode 100644 index 00000000000..077f8c556f3 --- /dev/null +++ b/examples/gno.land/p/moul/ulist/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/ulist diff --git a/examples/gno.land/p/moul/ulist/ulist.gno b/examples/gno.land/p/moul/ulist/ulist.gno new file mode 100644 index 00000000000..507a02a4e45 --- /dev/null +++ b/examples/gno.land/p/moul/ulist/ulist.gno @@ -0,0 +1,437 @@ +// Package ulist provides an append-only list implementation using a binary tree structure, +// optimized for scenarios requiring sequential inserts with auto-incrementing indices. +// +// The implementation uses a binary tree where new elements are added by following a path +// determined by the binary representation of the index. This provides automatic balancing +// for append operations without requiring any balancing logic. +// +// Unlike the AVL tree-based list implementation (p/demo/avl/list), ulist is specifically +// designed for append-only operations and does not require rebalancing. This makes it more +// efficient for sequential inserts but less flexible for general-purpose list operations. +// +// Key differences from AVL list: +// * Append-only design (no arbitrary inserts) +// * No tree rebalancing needed +// * Simpler implementation +// * More memory efficient for sequential operations +// * Less flexible than AVL (no arbitrary inserts/reordering) +// +// Key characteristics: +// * O(log n) append and access operations +// * Perfect balance for power-of-2 sizes +// * No balancing needed +// * Memory efficient +// * Natural support for range queries +// * Support for soft deletion of elements +// * Forward and reverse iteration capabilities +// * Offset-based iteration with count control +package ulist + +// TODO: Use this ulist in moul/collection for the primary index. +// TODO: Benchmarks. + +import ( + "errors" +) + +// List represents an append-only binary tree list +type List struct { + root *treeNode + totalSize int + activeSize int +} + +// Entry represents a key-value pair in the list, where Index is the position +// and Value is the stored data +type Entry struct { + Index int + Value interface{} +} + +// treeNode represents a node in the binary tree +type treeNode struct { + data interface{} + left *treeNode + right *treeNode +} + +// Error variables +var ( + ErrOutOfBounds = errors.New("index out of bounds") + ErrDeleted = errors.New("element already deleted") +) + +// New creates a new empty List instance +func New() *List { + return &List{} +} + +// Append adds one or more values to the end of the list. +// Values are added sequentially, and the list grows automatically. +func (l *List) Append(values ...interface{}) { + for _, value := range values { + index := l.totalSize + node := l.findNode(index, true) + node.data = value + l.totalSize++ + l.activeSize++ + } +} + +// Get retrieves the value at the specified index. +// Returns nil if the index is out of bounds or if the element was deleted. +func (l *List) Get(index int) interface{} { + node := l.findNode(index, false) + if node == nil { + return nil + } + return node.data +} + +// Delete marks the elements at the specified indices as deleted. +// Returns ErrOutOfBounds if any index is invalid or ErrDeleted if +// the element was already deleted. +func (l *List) Delete(indices ...int) error { + if len(indices) == 0 { + return nil + } + if l == nil || l.totalSize == 0 { + return ErrOutOfBounds + } + + for _, index := range indices { + if index < 0 || index >= l.totalSize { + return ErrOutOfBounds + } + + node := l.findNode(index, false) + if node == nil || node.data == nil { + return ErrDeleted + } + node.data = nil + l.activeSize-- + } + + return nil +} + +// Set updates or restores a value at the specified index if within bounds +// Returns ErrOutOfBounds if the index is invalid +func (l *List) Set(index int, value interface{}) error { + if l == nil || index < 0 || index >= l.totalSize { + return ErrOutOfBounds + } + + node := l.findNode(index, false) + if node == nil { + return ErrOutOfBounds + } + + // If this is restoring a deleted element + if value != nil && node.data == nil { + l.activeSize++ + } + + // If this is deleting an element + if value == nil && node.data != nil { + l.activeSize-- + } + + node.data = value + return nil +} + +// Size returns the number of active (non-deleted) elements in the list +func (l *List) Size() int { + if l == nil { + return 0 + } + return l.activeSize +} + +// TotalSize returns the total number of elements ever added to the list, +// including deleted elements +func (l *List) TotalSize() int { + if l == nil { + return 0 + } + return l.totalSize +} + +// IterCbFn is a callback function type used in iteration methods. +// Return true to stop iteration, false to continue. +type IterCbFn func(index int, value interface{}) bool + +// Iterator performs iteration between start and end indices, calling cb for each entry. +// If start > end, iteration is performed in reverse order. +// Returns true if iteration was stopped early by the callback returning true. +// Skips deleted elements. +func (l *List) Iterator(start, end int, cb IterCbFn) bool { + // For empty list or invalid range + if l == nil || l.totalSize == 0 { + return false + } + if start < 0 && end < 0 { + return false + } + if start >= l.totalSize && end >= l.totalSize { + return false + } + + // Normalize indices + if start < 0 { + start = 0 + } + if end < 0 { + end = 0 + } + if end >= l.totalSize { + end = l.totalSize - 1 + } + if start >= l.totalSize { + start = l.totalSize - 1 + } + + // Handle reverse iteration + if start > end { + for i := start; i >= end; i-- { + val := l.Get(i) + if val != nil { + if cb(i, val) { + return true + } + } + } + return false + } + + // Handle forward iteration + for i := start; i <= end; i++ { + val := l.Get(i) + if val != nil { + if cb(i, val) { + return true + } + } + } + return false +} + +// IteratorByOffset performs iteration starting from offset for count elements. +// If count is positive, iterates forward; if negative, iterates backward. +// The iteration stops after abs(count) elements or when reaching list bounds. +// Skips deleted elements. +func (l *List) IteratorByOffset(offset int, count int, cb IterCbFn) bool { + if count == 0 || l == nil || l.totalSize == 0 { + return false + } + + // Normalize offset + if offset < 0 { + offset = 0 + } + if offset >= l.totalSize { + offset = l.totalSize - 1 + } + + // Determine end based on count direction + var end int + if count > 0 { + end = l.totalSize - 1 + } else { + end = 0 + } + + wrapperReturned := false + + // Wrap the callback to limit iterations + remaining := abs(count) + wrapper := func(index int, value interface{}) bool { + if remaining <= 0 { + wrapperReturned = true + return true + } + remaining-- + return cb(index, value) + } + ret := l.Iterator(offset, end, wrapper) + if wrapperReturned { + return false + } + return ret +} + +// abs returns the absolute value of x +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +// findNode locates or creates a node at the given index in the binary tree. +// The tree is structured such that the path to a node is determined by the binary +// representation of the index. For example, a tree with 15 elements would look like: +// +// 0 +// / \ +// 1 2 +// / \ / \ +// 3 4 5 6 +// / \ / \ / \ / \ +// 7 8 9 10 11 12 13 14 +// +// To find index 13 (binary 1101): +// 1. Start at root (0) +// 2. Calculate bits needed (4 bits for index 13) +// 3. Skip the highest bit position and start from bits-2 +// 4. Read bits from left to right: +// - 1 -> go right to 2 +// - 1 -> go right to 6 +// - 0 -> go left to 13 +// +// Special cases: +// - Index 0 always returns the root node +// - For create=true, missing nodes are created along the path +// - For create=false, returns nil if any node is missing +func (l *List) findNode(index int, create bool) *treeNode { + // For read operations, check bounds strictly + if !create && (l == nil || index < 0 || index >= l.totalSize) { + return nil + } + + // For create operations, allow index == totalSize for append + if create && (l == nil || index < 0 || index > l.totalSize) { + return nil + } + + // Initialize root if needed + if l.root == nil { + if !create { + return nil + } + l.root = &treeNode{} + return l.root + } + + node := l.root + + // Special case for root node + if index == 0 { + return node + } + + // Calculate the number of bits needed (inline highestBit logic) + bits := 0 + n := index + 1 + for n > 0 { + n >>= 1 + bits++ + } + + // Start from the second highest bit + for level := bits - 2; level >= 0; level-- { + bit := (index & (1 << uint(level))) != 0 + + if bit { + if node.right == nil { + if !create { + return nil + } + node.right = &treeNode{} + } + node = node.right + } else { + if node.left == nil { + if !create { + return nil + } + node.left = &treeNode{} + } + node = node.left + } + } + + return node +} + +// MustDelete deletes elements at the specified indices. +// Panics if any index is invalid or if any element was already deleted. +func (l *List) MustDelete(indices ...int) { + if err := l.Delete(indices...); err != nil { + panic(err) + } +} + +// MustGet retrieves the value at the specified index. +// Panics if the index is out of bounds or if the element was deleted. +func (l *List) MustGet(index int) interface{} { + if l == nil || index < 0 || index >= l.totalSize { + panic(ErrOutOfBounds) + } + value := l.Get(index) + if value == nil { + panic(ErrDeleted) + } + return value +} + +// MustSet updates or restores a value at the specified index. +// Panics if the index is out of bounds. +func (l *List) MustSet(index int, value interface{}) { + if err := l.Set(index, value); err != nil { + panic(err) + } +} + +// GetRange returns a slice of Entry containing elements between start and end indices. +// If start > end, elements are returned in reverse order. +// Deleted elements are skipped. +func (l *List) GetRange(start, end int) []Entry { + var entries []Entry + l.Iterator(start, end, func(index int, value interface{}) bool { + entries = append(entries, Entry{Index: index, Value: value}) + return false + }) + return entries +} + +// GetByOffset returns a slice of Entry starting from offset for count elements. +// If count is positive, returns elements forward; if negative, returns elements backward. +// The operation stops after abs(count) elements or when reaching list bounds. +// Deleted elements are skipped. +func (l *List) GetByOffset(offset int, count int) []Entry { + var entries []Entry + l.IteratorByOffset(offset, count, func(index int, value interface{}) bool { + entries = append(entries, Entry{Index: index, Value: value}) + return false + }) + return entries +} + +// IList defines the interface for an ulist.List compatible structure. +type IList interface { + // Basic operations + Append(values ...interface{}) + Get(index int) interface{} + Delete(indices ...int) error + Size() int + TotalSize() int + Set(index int, value interface{}) error + + // Must variants that panic instead of returning errors + MustDelete(indices ...int) + MustGet(index int) interface{} + MustSet(index int, value interface{}) + + // Range operations + GetRange(start, end int) []Entry + GetByOffset(offset int, count int) []Entry + + // Iterator operations + Iterator(start, end int, cb IterCbFn) bool + IteratorByOffset(offset int, count int, cb IterCbFn) bool +} + +// Verify that List implements IList +var _ IList = (*List)(nil) diff --git a/examples/gno.land/p/moul/ulist/ulist_test.gno b/examples/gno.land/p/moul/ulist/ulist_test.gno new file mode 100644 index 00000000000..f098731a7db --- /dev/null +++ b/examples/gno.land/p/moul/ulist/ulist_test.gno @@ -0,0 +1,1422 @@ +package ulist + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/typeutil" +) + +func TestNew(t *testing.T) { + l := New() + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, 0, l.TotalSize()) +} + +func TestListAppendAndGet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + expected interface{} + }{ + { + name: "empty list", + setup: func() *List { + return New() + }, + index: 0, + expected: nil, + }, + { + name: "single append and get", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + expected: 42, + }, + { + name: "multiple appends and get first", + setup: func() *List { + l := New() + l.Append(1) + l.Append(2) + l.Append(3) + return l + }, + index: 0, + expected: 1, + }, + { + name: "multiple appends and get last", + setup: func() *List { + l := New() + l.Append(1) + l.Append(2) + l.Append(3) + return l + }, + index: 2, + expected: 3, + }, + { + name: "get with invalid index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + expected: nil, + }, + { + name: "31 items get first", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 0, + expected: 0, + }, + { + name: "31 items get last", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 30, + expected: 30, + }, + { + name: "31 items get middle", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 15, + expected: 15, + }, + { + name: "values around power of 2 boundary", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 15, + expected: 15, + }, + { + name: "values at power of 2", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 16, + expected: 16, + }, + { + name: "values after power of 2", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 17, + expected: 17, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + got := l.Get(tt.index) + if got != tt.expected { + t.Errorf("List.Get() = %v, want %v", got, tt.expected) + } + }) + } +} + +// generateSequence creates a slice of integers from 0 to n-1 +func generateSequence(n int) []interface{} { + result := make([]interface{}, n) + for i := 0; i < n; i++ { + result[i] = i + } + return result +} + +func TestListDelete(t *testing.T) { + tests := []struct { + name string + setup func() *List + deleteIndices []int + expectedErr error + expectedSize int + }{ + { + name: "delete single element", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + deleteIndices: []int{1}, + expectedErr: nil, + expectedSize: 2, + }, + { + name: "delete multiple elements", + setup: func() *List { + l := New() + l.Append(1, 2, 3, 4, 5) + return l + }, + deleteIndices: []int{0, 2, 4}, + expectedErr: nil, + expectedSize: 2, + }, + { + name: "delete with negative index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + deleteIndices: []int{-1}, + expectedErr: ErrOutOfBounds, + expectedSize: 1, + }, + { + name: "delete beyond size", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + deleteIndices: []int{1}, + expectedErr: ErrOutOfBounds, + expectedSize: 1, + }, + { + name: "delete already deleted element", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + deleteIndices: []int{0}, + expectedErr: ErrDeleted, + expectedSize: 0, + }, + { + name: "delete multiple elements in reverse", + setup: func() *List { + l := New() + l.Append(1, 2, 3, 4, 5) + return l + }, + deleteIndices: []int{4, 2, 0}, + expectedErr: nil, + expectedSize: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + initialSize := l.Size() + err := l.Delete(tt.deleteIndices...) + if err != nil && tt.expectedErr != nil { + uassert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + uassert.Equal(t, tt.expectedErr, err) + } + uassert.Equal(t, tt.expectedSize, l.Size(), + ufmt.Sprintf("Expected size %d after deleting %d elements from size %d, got %d", + tt.expectedSize, len(tt.deleteIndices), initialSize, l.Size())) + }) + } +} + +func TestListSizeAndTotalSize(t *testing.T) { + t.Run("empty list", func(t *testing.T) { + list := New() + uassert.Equal(t, 0, list.Size()) + uassert.Equal(t, 0, list.TotalSize()) + }) + + t.Run("list with elements", func(t *testing.T) { + list := New() + list.Append(1) + list.Append(2) + list.Append(3) + uassert.Equal(t, 3, list.Size()) + uassert.Equal(t, 3, list.TotalSize()) + }) + + t.Run("list with deleted elements", func(t *testing.T) { + list := New() + list.Append(1) + list.Append(2) + list.Append(3) + list.Delete(1) + uassert.Equal(t, 2, list.Size()) + uassert.Equal(t, 3, list.TotalSize()) + }) +} + +func TestIterator(t *testing.T) { + tests := []struct { + name string + values []interface{} + start int + end int + expected []Entry + wantStop bool + stopAfter int // stop after N elements, -1 for no stop + }{ + { + name: "empty list", + values: []interface{}{}, + start: 0, + end: 10, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "nil list", + values: nil, + start: 0, + end: 0, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "single element forward", + values: []interface{}{42}, + start: 0, + end: 0, + expected: []Entry{ + {Index: 0, Value: 42}, + }, + stopAfter: -1, + }, + { + name: "multiple elements forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + {Index: 4, Value: 5}, + }, + stopAfter: -1, + }, + { + name: "multiple elements reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 4, + end: 0, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "partial range forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 1, + end: 3, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + }, + stopAfter: -1, + }, + { + name: "partial range reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 3, + end: 1, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + stopAfter: -1, + }, + { + name: "stop iteration early", + values: []interface{}{1, 2, 3, 4, 5}, + start: 0, + end: 4, + wantStop: true, + stopAfter: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + }, + { + name: "negative start", + values: []interface{}{1, 2, 3}, + start: -1, + end: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + stopAfter: -1, + }, + { + name: "negative end", + values: []interface{}{1, 2, 3}, + start: 0, + end: -2, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "start beyond size", + values: []interface{}{1, 2, 3}, + start: 5, + end: 6, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "end beyond size", + values: []interface{}{1, 2, 3}, + start: 0, + end: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + stopAfter: -1, + }, + { + name: "with deleted elements", + values: []interface{}{1, 2, nil, 4, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 3, Value: 4}, + {Index: 4, Value: 5}, + }, + stopAfter: -1, + }, + { + name: "with deleted elements reverse", + values: []interface{}{1, nil, 3, nil, 5}, + start: 4, + end: 0, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 2, Value: 3}, + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "start equals end", + values: []interface{}{1, 2, 3}, + start: 1, + end: 1, + expected: []Entry{{Index: 1, Value: 2}}, + stopAfter: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + var result []Entry + stopped := list.Iterator(tt.start, tt.end, func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return tt.stopAfter >= 0 && len(result) >= tt.stopAfter + }) + + uassert.Equal(t, len(result), len(tt.expected), "comparing length") + + for i := range result { + uassert.Equal(t, result[i].Index, tt.expected[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(result[i].Value), typeutil.ToString(tt.expected[i].Value), "comparing value") + } + + uassert.Equal(t, stopped, tt.wantStop, "comparing stopped") + }) + } +} + +func TestLargeListAppendGetAndDelete(t *testing.T) { + l := New() + size := 100 + + // Append values from 0 to 99 + for i := 0; i < size; i++ { + l.Append(i) + val := l.Get(i) + uassert.Equal(t, i, val) + } + + // Verify size + uassert.Equal(t, size, l.Size()) + uassert.Equal(t, size, l.TotalSize()) + + // Get and verify each value + for i := 0; i < size; i++ { + val := l.Get(i) + uassert.Equal(t, i, val) + } + + // Get and verify each value + for i := 0; i < size; i++ { + err := l.Delete(i) + uassert.Equal(t, nil, err) + } + + // Verify size + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, size, l.TotalSize()) + + // Get and verify each value + for i := 0; i < size; i++ { + val := l.Get(i) + uassert.Equal(t, nil, val) + } +} + +func TestEdgeCases(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + { + name: "nil list operations", + test: func(t *testing.T) { + var l *List + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, 0, l.TotalSize()) + uassert.Equal(t, nil, l.Get(0)) + err := l.Delete(0) + uassert.Equal(t, ErrOutOfBounds.Error(), err.Error()) + }, + }, + { + name: "delete empty indices slice", + test: func(t *testing.T) { + l := New() + l.Append(1) + err := l.Delete() + uassert.Equal(t, nil, err) + uassert.Equal(t, 1, l.Size()) + }, + }, + { + name: "append nil values", + test: func(t *testing.T) { + l := New() + l.Append(nil, nil) + uassert.Equal(t, 2, l.Size()) + uassert.Equal(t, nil, l.Get(0)) + uassert.Equal(t, nil, l.Get(1)) + }, + }, + { + name: "delete same index multiple times", + test: func(t *testing.T) { + l := New() + l.Append(1, 2, 3) + err := l.Delete(1) + uassert.Equal(t, nil, err) + err = l.Delete(1) + uassert.Equal(t, ErrDeleted.Error(), err.Error()) + }, + }, + { + name: "iterator with all deleted elements", + test: func(t *testing.T) { + l := New() + l.Append(1, 2, 3) + l.Delete(0, 1, 2) + var count int + l.Iterator(0, 2, func(index int, value interface{}) bool { + count++ + return false + }) + uassert.Equal(t, 0, count) + }, + }, + { + name: "append after delete", + test: func(t *testing.T) { + l := New() + l.Append(1, 2) + l.Delete(1) + l.Append(3) + uassert.Equal(t, 2, l.Size()) + uassert.Equal(t, 3, l.TotalSize()) + uassert.Equal(t, 1, l.Get(0)) + uassert.Equal(t, nil, l.Get(1)) + uassert.Equal(t, 3, l.Get(2)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.test(t) + }) + } +} + +func TestIteratorByOffset(t *testing.T) { + tests := []struct { + name string + values []interface{} + offset int + count int + expected []Entry + wantStop bool + }{ + { + name: "empty list", + values: []interface{}{}, + offset: 0, + count: 5, + expected: []Entry{}, + wantStop: false, + }, + { + name: "positive count forward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 1, + count: 2, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "negative count backward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 3, + count: -2, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "count exceeds available elements forward", + values: []interface{}{1, 2, 3}, + offset: 1, + count: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "count exceeds available elements backward", + values: []interface{}{1, 2, 3}, + offset: 1, + count: -5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "zero count", + values: []interface{}{1, 2, 3}, + offset: 0, + count: 0, + expected: []Entry{}, + wantStop: false, + }, + { + name: "negative offset", + values: []interface{}{1, 2, 3}, + offset: -1, + count: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + wantStop: false, + }, + { + name: "offset beyond size", + values: []interface{}{1, 2, 3}, + offset: 5, + count: -2, + expected: []Entry{ + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + wantStop: false, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + offset: 0, + count: 3, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + wantStop: false, + }, + { + name: "early stop in forward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 0, + count: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + wantStop: true, // The callback will return true after 2 elements + }, + { + name: "early stop in backward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 4, + count: -5, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 3, Value: 4}, + }, + wantStop: true, // The callback will return true after 2 elements + }, + { + name: "nil list", + values: nil, + offset: 0, + count: 5, + expected: []Entry{}, + wantStop: false, + }, + { + name: "single element forward", + values: []interface{}{1}, + offset: 0, + count: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "single element backward", + values: []interface{}{1}, + offset: 0, + count: -5, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "all deleted elements", + values: []interface{}{nil, nil, nil}, + offset: 0, + count: 3, + expected: []Entry{}, + wantStop: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + var result []Entry + var cb IterCbFn + if tt.wantStop { + cb = func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return len(result) >= 2 // Stop after 2 elements for early stop tests + } + } else { + cb = func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return false + } + } + + stopped := list.IteratorByOffset(tt.offset, tt.count, cb) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + uassert.Equal(t, tt.wantStop, stopped, "comparing stopped") + }) + } +} + +func TestMustDelete(t *testing.T) { + tests := []struct { + name string + setup func() *List + indices []int + shouldPanic bool + panicMsg string + }{ + { + name: "successful delete", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + indices: []int{1}, + shouldPanic: false, + }, + { + name: "out of bounds", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + indices: []int{1}, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "already deleted", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + indices: []int{0}, + shouldPanic: true, + panicMsg: ErrDeleted.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + l.MustDelete(tt.indices...) + if tt.shouldPanic { + t.Error("Expected panic") + } + }) + } +} + +func TestMustGet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + expected interface{} + shouldPanic bool + panicMsg string + }{ + { + name: "successful get", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + expected: 42, + shouldPanic: false, + }, + { + name: "out of bounds negative", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: -1, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "out of bounds positive", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "deleted element", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + index: 0, + shouldPanic: true, + panicMsg: ErrDeleted.Error(), + }, + { + name: "nil list", + setup: func() *List { + return nil + }, + index: 0, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + result := l.MustGet(tt.index) + if tt.shouldPanic { + t.Error("Expected panic") + } + uassert.Equal(t, typeutil.ToString(tt.expected), typeutil.ToString(result)) + }) + } +} + +func TestGetRange(t *testing.T) { + tests := []struct { + name string + values []interface{} + start int + end int + expected []Entry + }{ + { + name: "empty list", + values: []interface{}{}, + start: 0, + end: 10, + expected: []Entry{}, + }, + { + name: "single element", + values: []interface{}{42}, + start: 0, + end: 0, + expected: []Entry{ + {Index: 0, Value: 42}, + }, + }, + { + name: "multiple elements forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 1, + end: 3, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + }, + }, + { + name: "multiple elements reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 3, + end: 1, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + }, + { + name: "nil list", + values: nil, + start: 0, + end: 5, + expected: []Entry{}, + }, + { + name: "negative indices", + values: []interface{}{1, 2, 3}, + start: -1, + end: -2, + expected: []Entry{}, + }, + { + name: "indices beyond size", + values: []interface{}{1, 2, 3}, + start: 1, + end: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + result := list.GetRange(tt.start, tt.end) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + }) + } +} + +func TestGetByOffset(t *testing.T) { + tests := []struct { + name string + values []interface{} + offset int + count int + expected []Entry + }{ + { + name: "empty list", + values: []interface{}{}, + offset: 0, + count: 5, + expected: []Entry{}, + }, + { + name: "positive count forward", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 1, + count: 2, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + { + name: "negative count backward", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 3, + count: -2, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + }, + }, + { + name: "count exceeds available elements", + values: []interface{}{1, 2, 3}, + offset: 1, + count: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + { + name: "zero count", + values: []interface{}{1, 2, 3}, + offset: 0, + count: 0, + expected: []Entry{}, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + offset: 0, + count: 3, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + }, + { + name: "negative offset", + values: []interface{}{1, 2, 3}, + offset: -1, + count: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + }, + { + name: "offset beyond size", + values: []interface{}{1, 2, 3}, + offset: 5, + count: -2, + expected: []Entry{ + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + }, + { + name: "nil list", + values: nil, + offset: 0, + count: 5, + expected: []Entry{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + result := list.GetByOffset(tt.offset, tt.count) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + }) + } +} + +func TestMustSet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + value interface{} + shouldPanic bool + panicMsg string + }{ + { + name: "successful set", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + value: 99, + shouldPanic: false, + }, + { + name: "restore deleted element", + setup: func() *List { + l := New() + l.Append(42) + l.Delete(0) + return l + }, + index: 0, + value: 99, + shouldPanic: false, + }, + { + name: "out of bounds negative", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: -1, + value: 99, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "out of bounds positive", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + value: 99, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "nil list", + setup: func() *List { + return nil + }, + index: 0, + value: 99, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + l.MustSet(tt.index, tt.value) + if tt.shouldPanic { + t.Error("Expected panic") + } + // Verify the value was set correctly for non-panic cases + if !tt.shouldPanic { + result := l.Get(tt.index) + uassert.Equal(t, typeutil.ToString(tt.value), typeutil.ToString(result)) + } + }) + } +} + +func TestSet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + value interface{} + expectedErr error + verify func(t *testing.T, l *List) + }{ + { + name: "set value in empty list", + setup: func() *List { + return New() + }, + index: 0, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 0, l.Size()) + }, + }, + { + name: "set value at valid index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 0, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(0)) + uassert.Equal(t, 1, l.Size()) + uassert.Equal(t, 1, l.TotalSize()) + }, + }, + { + name: "set value at negative index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: -1, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 1, l.Get(0)) + }, + }, + { + name: "set value beyond size", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 1, l.Get(0)) + uassert.Equal(t, 1, l.Size()) + }, + }, + { + name: "set nil value", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 0, + value: nil, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, nil, l.Get(0)) + uassert.Equal(t, 0, l.Size()) + }, + }, + { + name: "set value at deleted index", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + l.Delete(1) + return l + }, + index: 1, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(1)) + uassert.Equal(t, 3, l.Size()) + uassert.Equal(t, 3, l.TotalSize()) + }, + }, + { + name: "set value in nil list", + setup: func() *List { + return nil + }, + index: 0, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 0, l.Size()) + }, + }, + { + name: "set multiple values at same index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 0, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(0)) + err := l.Set(0, 99) + uassert.Equal(t, nil, err) + uassert.Equal(t, 99, l.Get(0)) + uassert.Equal(t, 1, l.Size()) + }, + }, + { + name: "set value at last index", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + index: 2, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(2)) + uassert.Equal(t, 3, l.Size()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + err := l.Set(tt.index, tt.value) + + if tt.expectedErr != nil { + uassert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + uassert.Equal(t, nil, err) + } + + tt.verify(t, l) + }) + } +} diff --git a/examples/gno.land/p/moul/web25/gno.mod b/examples/gno.land/p/moul/web25/gno.mod new file mode 100644 index 00000000000..f27bc793bf7 --- /dev/null +++ b/examples/gno.land/p/moul/web25/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/web25 diff --git a/examples/gno.land/p/moul/web25/web25.gno b/examples/gno.land/p/moul/web25/web25.gno new file mode 100644 index 00000000000..46d564b70ad --- /dev/null +++ b/examples/gno.land/p/moul/web25/web25.gno @@ -0,0 +1,51 @@ +// Pacakge web25 provides an opinionated way to register an external web2 +// frontend to provide a "better" web2.5 experience. +package web25 + +import ( + "strings" + + "gno.land/p/moul/realmpath" +) + +type Config struct { + CID string + URL string + Text string +} + +func (c *Config) SetRemoteFrontendByURL(url string) { + c.CID = "" + c.URL = url +} + +func (c *Config) SetRemoteFrontendByCID(cid string) { + c.CID = cid + c.URL = "" +} + +func (c Config) GetLink() string { + if c.CID != "" { + return "https://ipfs.io/ipfs/" + c.CID + } + return c.URL +} + +const DefaultText = "Click [here]({link}) to visit the full rendering experience.\n" + +// Render displays a frontend link at the top of your realm's Render function in +// a concistent way to help gno visitors to have a consistent experience. +// +// if query is not nil, then it will check if it's not disable by ?no-web25, so +// that you can call the render function from an external point of view. +func (c Config) Render(path string) string { + if realmpath.Parse(path).Query.Get("no-web25") == "1" { + return "" + } + text := c.Text + if text == "" { + text = DefaultText + } + text = strings.ReplaceAll(text, "{link}", c.GetLink()) + return text +} diff --git a/examples/gno.land/p/moul/web25/web25_test.gno b/examples/gno.land/p/moul/web25/web25_test.gno new file mode 100644 index 00000000000..6d58a586595 --- /dev/null +++ b/examples/gno.land/p/moul/web25/web25_test.gno @@ -0,0 +1 @@ +package web25 diff --git a/examples/gno.land/p/moul/xmath/generate.go b/examples/gno.land/p/moul/xmath/generate.go new file mode 100644 index 00000000000..ad70adb06bd --- /dev/null +++ b/examples/gno.land/p/moul/xmath/generate.go @@ -0,0 +1,3 @@ +package xmath + +//go:generate go run generator.go diff --git a/examples/gno.land/p/moul/xmath/generator.go b/examples/gno.land/p/moul/xmath/generator.go new file mode 100644 index 00000000000..afe5a4341fa --- /dev/null +++ b/examples/gno.land/p/moul/xmath/generator.go @@ -0,0 +1,184 @@ +//go:build ignore + +package main + +import ( + "bytes" + "fmt" + "go/format" + "log" + "os" + "strings" + "text/template" +) + +type Type struct { + Name string + ZeroValue string + Signed bool + Float bool +} + +var types = []Type{ + {"Int8", "0", true, false}, + {"Int16", "0", true, false}, + {"Int32", "0", true, false}, + {"Int64", "0", true, false}, + {"Int", "0", true, false}, + {"Uint8", "0", false, false}, + {"Uint16", "0", false, false}, + {"Uint32", "0", false, false}, + {"Uint64", "0", false, false}, + {"Uint", "0", false, false}, + {"Float32", "0.0", true, true}, + {"Float64", "0.0", true, true}, +} + +const sourceTpl = `// Code generated by generator.go; DO NOT EDIT. +package xmath + +{{ range .Types }} +// {{.Name}} helpers +func Max{{.Name}}(a, b {{.Name | lower}}) {{.Name | lower}} { + if a > b { + return a + } + return b +} + +func Min{{.Name}}(a, b {{.Name | lower}}) {{.Name | lower}} { + if a < b { + return a + } + return b +} + +func Clamp{{.Name}}(value, min, max {{.Name | lower}}) {{.Name | lower}} { + if value < min { + return min + } + if value > max { + return max + } + return value +} +{{if .Signed}} +func Abs{{.Name}}(x {{.Name | lower}}) {{.Name | lower}} { + if x < 0 { + return -x + } + return x +} + +func Sign{{.Name}}(x {{.Name | lower}}) {{.Name | lower}} { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} +{{end}} +{{end}} +` + +const testTpl = `package xmath + +import "testing" + +{{range .Types}} +func Test{{.Name}}Helpers(t *testing.T) { + // Test Max{{.Name}} + if Max{{.Name}}(1, 2) != 2 { + t.Error("Max{{.Name}}(1, 2) should be 2") + } + {{if .Signed}}if Max{{.Name}}(-1, -2) != -1 { + t.Error("Max{{.Name}}(-1, -2) should be -1") + }{{end}} + + // Test Min{{.Name}} + if Min{{.Name}}(1, 2) != 1 { + t.Error("Min{{.Name}}(1, 2) should be 1") + } + {{if .Signed}}if Min{{.Name}}(-1, -2) != -2 { + t.Error("Min{{.Name}}(-1, -2) should be -2") + }{{end}} + + // Test Clamp{{.Name}} + if Clamp{{.Name}}(5, 1, 3) != 3 { + t.Error("Clamp{{.Name}}(5, 1, 3) should be 3") + } + if Clamp{{.Name}}(0, 1, 3) != 1 { + t.Error("Clamp{{.Name}}(0, 1, 3) should be 1") + } + if Clamp{{.Name}}(2, 1, 3) != 2 { + t.Error("Clamp{{.Name}}(2, 1, 3) should be 2") + } + {{if .Signed}} + // Test Abs{{.Name}} + if Abs{{.Name}}(-5) != 5 { + t.Error("Abs{{.Name}}(-5) should be 5") + } + if Abs{{.Name}}(5) != 5 { + t.Error("Abs{{.Name}}(5) should be 5") + } + + // Test Sign{{.Name}} + if Sign{{.Name}}(-5) != -1 { + t.Error("Sign{{.Name}}(-5) should be -1") + } + if Sign{{.Name}}(5) != 1 { + t.Error("Sign{{.Name}}(5) should be 1") + } + if Sign{{.Name}}({{.ZeroValue}}) != 0 { + t.Error("Sign{{.Name}}({{.ZeroValue}}) should be 0") + } + {{end}} +} +{{end}} +` + +func main() { + funcMap := template.FuncMap{ + "lower": strings.ToLower, + } + + // Generate source file + sourceTmpl := template.Must(template.New("source").Funcs(funcMap).Parse(sourceTpl)) + var sourceOut bytes.Buffer + if err := sourceTmpl.Execute(&sourceOut, struct{ Types []Type }{types}); err != nil { + log.Fatal(err) + } + + // Format the generated code + formattedSource, err := format.Source(sourceOut.Bytes()) + if err != nil { + log.Fatal(err) + } + + // Write source file + if err := os.WriteFile("xmath.gen.gno", formattedSource, 0644); err != nil { + log.Fatal(err) + } + + // Generate test file + testTmpl := template.Must(template.New("test").Parse(testTpl)) + var testOut bytes.Buffer + if err := testTmpl.Execute(&testOut, struct{ Types []Type }{types}); err != nil { + log.Fatal(err) + } + + // Format the generated test code + formattedTest, err := format.Source(testOut.Bytes()) + if err != nil { + log.Fatal(err) + } + + // Write test file + if err := os.WriteFile("xmath.gen_test.gno", formattedTest, 0644); err != nil { + log.Fatal(err) + } + + fmt.Println("Generated xmath.gen.gno and xmath.gen_test.gno") +} diff --git a/examples/gno.land/p/moul/xmath/gno.mod b/examples/gno.land/p/moul/xmath/gno.mod new file mode 100644 index 00000000000..63b782c88f2 --- /dev/null +++ b/examples/gno.land/p/moul/xmath/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/xmath diff --git a/examples/gno.land/p/moul/xmath/xmath.gen.gno b/examples/gno.land/p/moul/xmath/xmath.gen.gno new file mode 100644 index 00000000000..266c77e1e84 --- /dev/null +++ b/examples/gno.land/p/moul/xmath/xmath.gen.gno @@ -0,0 +1,421 @@ +// Code generated by generator.go; DO NOT EDIT. +package xmath + +// Int8 helpers +func MaxInt8(a, b int8) int8 { + if a > b { + return a + } + return b +} + +func MinInt8(a, b int8) int8 { + if a < b { + return a + } + return b +} + +func ClampInt8(value, min, max int8) int8 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt8(x int8) int8 { + if x < 0 { + return -x + } + return x +} + +func SignInt8(x int8) int8 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int16 helpers +func MaxInt16(a, b int16) int16 { + if a > b { + return a + } + return b +} + +func MinInt16(a, b int16) int16 { + if a < b { + return a + } + return b +} + +func ClampInt16(value, min, max int16) int16 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt16(x int16) int16 { + if x < 0 { + return -x + } + return x +} + +func SignInt16(x int16) int16 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int32 helpers +func MaxInt32(a, b int32) int32 { + if a > b { + return a + } + return b +} + +func MinInt32(a, b int32) int32 { + if a < b { + return a + } + return b +} + +func ClampInt32(value, min, max int32) int32 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt32(x int32) int32 { + if x < 0 { + return -x + } + return x +} + +func SignInt32(x int32) int32 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int64 helpers +func MaxInt64(a, b int64) int64 { + if a > b { + return a + } + return b +} + +func MinInt64(a, b int64) int64 { + if a < b { + return a + } + return b +} + +func ClampInt64(value, min, max int64) int64 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt64(x int64) int64 { + if x < 0 { + return -x + } + return x +} + +func SignInt64(x int64) int64 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int helpers +func MaxInt(a, b int) int { + if a > b { + return a + } + return b +} + +func MinInt(a, b int) int { + if a < b { + return a + } + return b +} + +func ClampInt(value, min, max int) int { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt(x int) int { + if x < 0 { + return -x + } + return x +} + +func SignInt(x int) int { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Uint8 helpers +func MaxUint8(a, b uint8) uint8 { + if a > b { + return a + } + return b +} + +func MinUint8(a, b uint8) uint8 { + if a < b { + return a + } + return b +} + +func ClampUint8(value, min, max uint8) uint8 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint16 helpers +func MaxUint16(a, b uint16) uint16 { + if a > b { + return a + } + return b +} + +func MinUint16(a, b uint16) uint16 { + if a < b { + return a + } + return b +} + +func ClampUint16(value, min, max uint16) uint16 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint32 helpers +func MaxUint32(a, b uint32) uint32 { + if a > b { + return a + } + return b +} + +func MinUint32(a, b uint32) uint32 { + if a < b { + return a + } + return b +} + +func ClampUint32(value, min, max uint32) uint32 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint64 helpers +func MaxUint64(a, b uint64) uint64 { + if a > b { + return a + } + return b +} + +func MinUint64(a, b uint64) uint64 { + if a < b { + return a + } + return b +} + +func ClampUint64(value, min, max uint64) uint64 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint helpers +func MaxUint(a, b uint) uint { + if a > b { + return a + } + return b +} + +func MinUint(a, b uint) uint { + if a < b { + return a + } + return b +} + +func ClampUint(value, min, max uint) uint { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Float32 helpers +func MaxFloat32(a, b float32) float32 { + if a > b { + return a + } + return b +} + +func MinFloat32(a, b float32) float32 { + if a < b { + return a + } + return b +} + +func ClampFloat32(value, min, max float32) float32 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsFloat32(x float32) float32 { + if x < 0 { + return -x + } + return x +} + +func SignFloat32(x float32) float32 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Float64 helpers +func MaxFloat64(a, b float64) float64 { + if a > b { + return a + } + return b +} + +func MinFloat64(a, b float64) float64 { + if a < b { + return a + } + return b +} + +func ClampFloat64(value, min, max float64) float64 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsFloat64(x float64) float64 { + if x < 0 { + return -x + } + return x +} + +func SignFloat64(x float64) float64 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} diff --git a/examples/gno.land/p/moul/xmath/xmath.gen_test.gno b/examples/gno.land/p/moul/xmath/xmath.gen_test.gno new file mode 100644 index 00000000000..16c80fc983d --- /dev/null +++ b/examples/gno.land/p/moul/xmath/xmath.gen_test.gno @@ -0,0 +1,466 @@ +package xmath + +import "testing" + +func TestInt8Helpers(t *testing.T) { + // Test MaxInt8 + if MaxInt8(1, 2) != 2 { + t.Error("MaxInt8(1, 2) should be 2") + } + if MaxInt8(-1, -2) != -1 { + t.Error("MaxInt8(-1, -2) should be -1") + } + + // Test MinInt8 + if MinInt8(1, 2) != 1 { + t.Error("MinInt8(1, 2) should be 1") + } + if MinInt8(-1, -2) != -2 { + t.Error("MinInt8(-1, -2) should be -2") + } + + // Test ClampInt8 + if ClampInt8(5, 1, 3) != 3 { + t.Error("ClampInt8(5, 1, 3) should be 3") + } + if ClampInt8(0, 1, 3) != 1 { + t.Error("ClampInt8(0, 1, 3) should be 1") + } + if ClampInt8(2, 1, 3) != 2 { + t.Error("ClampInt8(2, 1, 3) should be 2") + } + + // Test AbsInt8 + if AbsInt8(-5) != 5 { + t.Error("AbsInt8(-5) should be 5") + } + if AbsInt8(5) != 5 { + t.Error("AbsInt8(5) should be 5") + } + + // Test SignInt8 + if SignInt8(-5) != -1 { + t.Error("SignInt8(-5) should be -1") + } + if SignInt8(5) != 1 { + t.Error("SignInt8(5) should be 1") + } + if SignInt8(0) != 0 { + t.Error("SignInt8(0) should be 0") + } + +} + +func TestInt16Helpers(t *testing.T) { + // Test MaxInt16 + if MaxInt16(1, 2) != 2 { + t.Error("MaxInt16(1, 2) should be 2") + } + if MaxInt16(-1, -2) != -1 { + t.Error("MaxInt16(-1, -2) should be -1") + } + + // Test MinInt16 + if MinInt16(1, 2) != 1 { + t.Error("MinInt16(1, 2) should be 1") + } + if MinInt16(-1, -2) != -2 { + t.Error("MinInt16(-1, -2) should be -2") + } + + // Test ClampInt16 + if ClampInt16(5, 1, 3) != 3 { + t.Error("ClampInt16(5, 1, 3) should be 3") + } + if ClampInt16(0, 1, 3) != 1 { + t.Error("ClampInt16(0, 1, 3) should be 1") + } + if ClampInt16(2, 1, 3) != 2 { + t.Error("ClampInt16(2, 1, 3) should be 2") + } + + // Test AbsInt16 + if AbsInt16(-5) != 5 { + t.Error("AbsInt16(-5) should be 5") + } + if AbsInt16(5) != 5 { + t.Error("AbsInt16(5) should be 5") + } + + // Test SignInt16 + if SignInt16(-5) != -1 { + t.Error("SignInt16(-5) should be -1") + } + if SignInt16(5) != 1 { + t.Error("SignInt16(5) should be 1") + } + if SignInt16(0) != 0 { + t.Error("SignInt16(0) should be 0") + } + +} + +func TestInt32Helpers(t *testing.T) { + // Test MaxInt32 + if MaxInt32(1, 2) != 2 { + t.Error("MaxInt32(1, 2) should be 2") + } + if MaxInt32(-1, -2) != -1 { + t.Error("MaxInt32(-1, -2) should be -1") + } + + // Test MinInt32 + if MinInt32(1, 2) != 1 { + t.Error("MinInt32(1, 2) should be 1") + } + if MinInt32(-1, -2) != -2 { + t.Error("MinInt32(-1, -2) should be -2") + } + + // Test ClampInt32 + if ClampInt32(5, 1, 3) != 3 { + t.Error("ClampInt32(5, 1, 3) should be 3") + } + if ClampInt32(0, 1, 3) != 1 { + t.Error("ClampInt32(0, 1, 3) should be 1") + } + if ClampInt32(2, 1, 3) != 2 { + t.Error("ClampInt32(2, 1, 3) should be 2") + } + + // Test AbsInt32 + if AbsInt32(-5) != 5 { + t.Error("AbsInt32(-5) should be 5") + } + if AbsInt32(5) != 5 { + t.Error("AbsInt32(5) should be 5") + } + + // Test SignInt32 + if SignInt32(-5) != -1 { + t.Error("SignInt32(-5) should be -1") + } + if SignInt32(5) != 1 { + t.Error("SignInt32(5) should be 1") + } + if SignInt32(0) != 0 { + t.Error("SignInt32(0) should be 0") + } + +} + +func TestInt64Helpers(t *testing.T) { + // Test MaxInt64 + if MaxInt64(1, 2) != 2 { + t.Error("MaxInt64(1, 2) should be 2") + } + if MaxInt64(-1, -2) != -1 { + t.Error("MaxInt64(-1, -2) should be -1") + } + + // Test MinInt64 + if MinInt64(1, 2) != 1 { + t.Error("MinInt64(1, 2) should be 1") + } + if MinInt64(-1, -2) != -2 { + t.Error("MinInt64(-1, -2) should be -2") + } + + // Test ClampInt64 + if ClampInt64(5, 1, 3) != 3 { + t.Error("ClampInt64(5, 1, 3) should be 3") + } + if ClampInt64(0, 1, 3) != 1 { + t.Error("ClampInt64(0, 1, 3) should be 1") + } + if ClampInt64(2, 1, 3) != 2 { + t.Error("ClampInt64(2, 1, 3) should be 2") + } + + // Test AbsInt64 + if AbsInt64(-5) != 5 { + t.Error("AbsInt64(-5) should be 5") + } + if AbsInt64(5) != 5 { + t.Error("AbsInt64(5) should be 5") + } + + // Test SignInt64 + if SignInt64(-5) != -1 { + t.Error("SignInt64(-5) should be -1") + } + if SignInt64(5) != 1 { + t.Error("SignInt64(5) should be 1") + } + if SignInt64(0) != 0 { + t.Error("SignInt64(0) should be 0") + } + +} + +func TestIntHelpers(t *testing.T) { + // Test MaxInt + if MaxInt(1, 2) != 2 { + t.Error("MaxInt(1, 2) should be 2") + } + if MaxInt(-1, -2) != -1 { + t.Error("MaxInt(-1, -2) should be -1") + } + + // Test MinInt + if MinInt(1, 2) != 1 { + t.Error("MinInt(1, 2) should be 1") + } + if MinInt(-1, -2) != -2 { + t.Error("MinInt(-1, -2) should be -2") + } + + // Test ClampInt + if ClampInt(5, 1, 3) != 3 { + t.Error("ClampInt(5, 1, 3) should be 3") + } + if ClampInt(0, 1, 3) != 1 { + t.Error("ClampInt(0, 1, 3) should be 1") + } + if ClampInt(2, 1, 3) != 2 { + t.Error("ClampInt(2, 1, 3) should be 2") + } + + // Test AbsInt + if AbsInt(-5) != 5 { + t.Error("AbsInt(-5) should be 5") + } + if AbsInt(5) != 5 { + t.Error("AbsInt(5) should be 5") + } + + // Test SignInt + if SignInt(-5) != -1 { + t.Error("SignInt(-5) should be -1") + } + if SignInt(5) != 1 { + t.Error("SignInt(5) should be 1") + } + if SignInt(0) != 0 { + t.Error("SignInt(0) should be 0") + } + +} + +func TestUint8Helpers(t *testing.T) { + // Test MaxUint8 + if MaxUint8(1, 2) != 2 { + t.Error("MaxUint8(1, 2) should be 2") + } + + // Test MinUint8 + if MinUint8(1, 2) != 1 { + t.Error("MinUint8(1, 2) should be 1") + } + + // Test ClampUint8 + if ClampUint8(5, 1, 3) != 3 { + t.Error("ClampUint8(5, 1, 3) should be 3") + } + if ClampUint8(0, 1, 3) != 1 { + t.Error("ClampUint8(0, 1, 3) should be 1") + } + if ClampUint8(2, 1, 3) != 2 { + t.Error("ClampUint8(2, 1, 3) should be 2") + } + +} + +func TestUint16Helpers(t *testing.T) { + // Test MaxUint16 + if MaxUint16(1, 2) != 2 { + t.Error("MaxUint16(1, 2) should be 2") + } + + // Test MinUint16 + if MinUint16(1, 2) != 1 { + t.Error("MinUint16(1, 2) should be 1") + } + + // Test ClampUint16 + if ClampUint16(5, 1, 3) != 3 { + t.Error("ClampUint16(5, 1, 3) should be 3") + } + if ClampUint16(0, 1, 3) != 1 { + t.Error("ClampUint16(0, 1, 3) should be 1") + } + if ClampUint16(2, 1, 3) != 2 { + t.Error("ClampUint16(2, 1, 3) should be 2") + } + +} + +func TestUint32Helpers(t *testing.T) { + // Test MaxUint32 + if MaxUint32(1, 2) != 2 { + t.Error("MaxUint32(1, 2) should be 2") + } + + // Test MinUint32 + if MinUint32(1, 2) != 1 { + t.Error("MinUint32(1, 2) should be 1") + } + + // Test ClampUint32 + if ClampUint32(5, 1, 3) != 3 { + t.Error("ClampUint32(5, 1, 3) should be 3") + } + if ClampUint32(0, 1, 3) != 1 { + t.Error("ClampUint32(0, 1, 3) should be 1") + } + if ClampUint32(2, 1, 3) != 2 { + t.Error("ClampUint32(2, 1, 3) should be 2") + } + +} + +func TestUint64Helpers(t *testing.T) { + // Test MaxUint64 + if MaxUint64(1, 2) != 2 { + t.Error("MaxUint64(1, 2) should be 2") + } + + // Test MinUint64 + if MinUint64(1, 2) != 1 { + t.Error("MinUint64(1, 2) should be 1") + } + + // Test ClampUint64 + if ClampUint64(5, 1, 3) != 3 { + t.Error("ClampUint64(5, 1, 3) should be 3") + } + if ClampUint64(0, 1, 3) != 1 { + t.Error("ClampUint64(0, 1, 3) should be 1") + } + if ClampUint64(2, 1, 3) != 2 { + t.Error("ClampUint64(2, 1, 3) should be 2") + } + +} + +func TestUintHelpers(t *testing.T) { + // Test MaxUint + if MaxUint(1, 2) != 2 { + t.Error("MaxUint(1, 2) should be 2") + } + + // Test MinUint + if MinUint(1, 2) != 1 { + t.Error("MinUint(1, 2) should be 1") + } + + // Test ClampUint + if ClampUint(5, 1, 3) != 3 { + t.Error("ClampUint(5, 1, 3) should be 3") + } + if ClampUint(0, 1, 3) != 1 { + t.Error("ClampUint(0, 1, 3) should be 1") + } + if ClampUint(2, 1, 3) != 2 { + t.Error("ClampUint(2, 1, 3) should be 2") + } + +} + +func TestFloat32Helpers(t *testing.T) { + // Test MaxFloat32 + if MaxFloat32(1, 2) != 2 { + t.Error("MaxFloat32(1, 2) should be 2") + } + if MaxFloat32(-1, -2) != -1 { + t.Error("MaxFloat32(-1, -2) should be -1") + } + + // Test MinFloat32 + if MinFloat32(1, 2) != 1 { + t.Error("MinFloat32(1, 2) should be 1") + } + if MinFloat32(-1, -2) != -2 { + t.Error("MinFloat32(-1, -2) should be -2") + } + + // Test ClampFloat32 + if ClampFloat32(5, 1, 3) != 3 { + t.Error("ClampFloat32(5, 1, 3) should be 3") + } + if ClampFloat32(0, 1, 3) != 1 { + t.Error("ClampFloat32(0, 1, 3) should be 1") + } + if ClampFloat32(2, 1, 3) != 2 { + t.Error("ClampFloat32(2, 1, 3) should be 2") + } + + // Test AbsFloat32 + if AbsFloat32(-5) != 5 { + t.Error("AbsFloat32(-5) should be 5") + } + if AbsFloat32(5) != 5 { + t.Error("AbsFloat32(5) should be 5") + } + + // Test SignFloat32 + if SignFloat32(-5) != -1 { + t.Error("SignFloat32(-5) should be -1") + } + if SignFloat32(5) != 1 { + t.Error("SignFloat32(5) should be 1") + } + if SignFloat32(0.0) != 0 { + t.Error("SignFloat32(0.0) should be 0") + } + +} + +func TestFloat64Helpers(t *testing.T) { + // Test MaxFloat64 + if MaxFloat64(1, 2) != 2 { + t.Error("MaxFloat64(1, 2) should be 2") + } + if MaxFloat64(-1, -2) != -1 { + t.Error("MaxFloat64(-1, -2) should be -1") + } + + // Test MinFloat64 + if MinFloat64(1, 2) != 1 { + t.Error("MinFloat64(1, 2) should be 1") + } + if MinFloat64(-1, -2) != -2 { + t.Error("MinFloat64(-1, -2) should be -2") + } + + // Test ClampFloat64 + if ClampFloat64(5, 1, 3) != 3 { + t.Error("ClampFloat64(5, 1, 3) should be 3") + } + if ClampFloat64(0, 1, 3) != 1 { + t.Error("ClampFloat64(0, 1, 3) should be 1") + } + if ClampFloat64(2, 1, 3) != 2 { + t.Error("ClampFloat64(2, 1, 3) should be 2") + } + + // Test AbsFloat64 + if AbsFloat64(-5) != 5 { + t.Error("AbsFloat64(-5) should be 5") + } + if AbsFloat64(5) != 5 { + t.Error("AbsFloat64(5) should be 5") + } + + // Test SignFloat64 + if SignFloat64(-5) != -1 { + t.Error("SignFloat64(-5) should be -1") + } + if SignFloat64(5) != 1 { + t.Error("SignFloat64(5) should be 1") + } + if SignFloat64(0.0) != 0 { + t.Error("SignFloat64(0.0) should be 0") + } + +} diff --git a/examples/gno.land/p/n2p5/chonk/chonk.gno b/examples/gno.land/p/n2p5/chonk/chonk.gno new file mode 100644 index 00000000000..8b7425eafd0 --- /dev/null +++ b/examples/gno.land/p/n2p5/chonk/chonk.gno @@ -0,0 +1,84 @@ +// Package chonk provides a simple way to store arbitrarily large strings +// in a linked list across transactions for efficient storage and retrieval. +// A Chonk support three operations: Add, Flush, and Scanner. +// - Add appends a string to the Chonk. +// - Flush clears the Chonk. +// - Scanner is used to iterate over the chunks in the Chonk. +package chonk + +// Chonk is a linked list string storage and +// retrieval system for fine bois. +type Chonk struct { + first *chunk + last *chunk +} + +// chunk is a linked list node for Chonk +type chunk struct { + text string + next *chunk +} + +// New creates a reference to a new Chonk +func New() *Chonk { + return &Chonk{} +} + +// Add appends a string to the Chonk. If the Chonk is empty, +// the string will be the first and last chunk. Otherwise, +// the string will be appended to the end of the Chonk. +func (c *Chonk) Add(text string) { + next := &chunk{text: text} + if c.first == nil { + c.first = next + c.last = next + return + } + c.last.next = next + c.last = next +} + +// Flush clears the Chonk by setting the first and last +// chunks to nil. This will allow the garbage collector to +// free the memory used by the Chonk. +func (c *Chonk) Flush() { + c.first = nil + c.last = nil +} + +// Scanner returns a new Scanner for the Chonk. The Scanner +// is used to iterate over the chunks in the Chonk. +func (c *Chonk) Scanner() *Scanner { + return &Scanner{ + next: c.first, + } +} + +// Scanner is a simple string scanner for Chonk. It is used +// to iterate over the chunks in a Chonk from first to last. +type Scanner struct { + current *chunk + next *chunk +} + +// Scan advances the scanner to the next chunk. It returns +// true if there is a next chunk, and false if there is not. +func (s *Scanner) Scan() bool { + if s.next != nil { + s.current = s.next + s.next = s.next.next + return true + } + return false +} + +// Text returns the current chunk. It is only valid to call +// this method after a call to Scan returns true. Expected usage: +// +// scanner := chonk.Scanner() +// for scanner.Scan() { +// fmt.Println(scanner.Text()) +// } +func (s *Scanner) Text() string { + return s.current.text +} diff --git a/examples/gno.land/p/n2p5/chonk/chonk_test.gno b/examples/gno.land/p/n2p5/chonk/chonk_test.gno new file mode 100644 index 00000000000..7caf1012d39 --- /dev/null +++ b/examples/gno.land/p/n2p5/chonk/chonk_test.gno @@ -0,0 +1,54 @@ +package chonk + +import ( + "testing" +) + +func TestChonk(t *testing.T) { + t.Parallel() + c := New() + testTable := []struct { + name string + chunks []string + }{ + { + name: "empty", + chunks: []string{}, + }, + { + name: "single chunk", + chunks: []string{"a"}, + }, + { + name: "multiple chunks", + chunks: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + }, + { + name: "multiline chunks", + chunks: []string{"1a\nb\nc\n\n", "d\ne\nf", "g\nh\ni", "j\nk\nl\n\n\n\n"}, + }, + { + name: "empty", + chunks: []string{}, + }, + } + testChonk := func(t *testing.T, c *Chonk, chunks []string) { + for _, chunk := range chunks { + c.Add(chunk) + } + scanner := c.Scanner() + i := 0 + for scanner.Scan() { + if scanner.Text() != chunks[i] { + t.Errorf("expected %s, got %s", chunks[i], scanner.Text()) + } + i++ + } + } + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + testChonk(t, c, test.chunks) + c.Flush() + }) + } +} diff --git a/examples/gno.land/p/n2p5/chonk/gno.mod b/examples/gno.land/p/n2p5/chonk/gno.mod new file mode 100644 index 00000000000..b0dee537b0e --- /dev/null +++ b/examples/gno.land/p/n2p5/chonk/gno.mod @@ -0,0 +1 @@ +module gno.land/p/n2p5/chonk diff --git a/examples/gno.land/p/n2p5/haystack/gno.mod b/examples/gno.land/p/n2p5/haystack/gno.mod new file mode 100644 index 00000000000..987d62d4565 --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/gno.mod @@ -0,0 +1 @@ +module gno.land/p/n2p5/haystack diff --git a/examples/gno.land/p/n2p5/haystack/haystack.gno b/examples/gno.land/p/n2p5/haystack/haystack.gno new file mode 100644 index 00000000000..0ab4953acb6 --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/haystack.gno @@ -0,0 +1,99 @@ +package haystack + +import ( + "encoding/hex" + "errors" + + "gno.land/p/demo/avl" + "gno.land/p/n2p5/haystack/needle" +) + +var ( + // ErrorNeedleNotFound is returned when a needle is not found in the haystack. + ErrorNeedleNotFound = errors.New("needle not found") + // ErrorNeedleLength is returned when a needle is not the correct length. + ErrorNeedleLength = errors.New("invalid needle length") + // ErrorHashLength is returned when a needle hash is not the correct length. + ErrorHashLength = errors.New("invalid hash length") + // ErrorDuplicateNeedle is returned when a needle already exists in the haystack. + ErrorDuplicateNeedle = errors.New("needle already exists") + // ErrorHashMismatch is returned when a needle hash does not match the needle. This should + // never happen and indicates a critical internal storage error. + ErrorHashMismatch = errors.New("storage error: hash mismatch") + // ErrorValueInvalidType is returned when a needle value is not a byte slice. This should + // never happen and indicates a critical internal storage error. + ErrorValueInvalidType = errors.New("storage error: invalid value type, expected []byte") +) + +const ( + // EncodedHashLength is the length of the hex-encoded needle hash. + EncodedHashLength = needle.HashLength * 2 + // EncodedPayloadLength is the length of the hex-encoded needle payload. + EncodedPayloadLength = needle.PayloadLength * 2 + // EncodedNeedleLength is the length of the hex-encoded needle. + EncodedNeedleLength = EncodedHashLength + EncodedPayloadLength +) + +// Haystack is a permissionless, append-only, content-addressed key-value store for fix +// length messages known as needles. A needle is a 192 byte byte slice with a 32 byte +// hash (sha256) and a 160 byte payload. +type Haystack struct{ internal *avl.Tree } + +// New creates a new instance of a Haystack key-value store. +func New() *Haystack { + return &Haystack{ + internal: avl.NewTree(), + } +} + +// Add takes a fixed-length hex-encoded needle bytes and adds it to the haystack key-value +// store. The key is the first 32 bytes of the needle hash (64 bytes hex-encoded) of the +// sha256 sum of the payload. The value is the 160 byte byte slice of the needle payload. +// An error is returned if the needle is found to be invalid. +func (h *Haystack) Add(needleHex string) error { + if len(needleHex) != EncodedNeedleLength { + return ErrorNeedleLength + } + b, err := hex.DecodeString(needleHex) + if err != nil { + return err + } + n, err := needle.FromBytes(b) + if err != nil { + return err + } + if h.internal.Has(needleHex[:EncodedHashLength]) { + return ErrorDuplicateNeedle + } + h.internal.Set(needleHex[:EncodedHashLength], n.Payload()) + return nil +} + +// Get takes a hex-encoded needle hash and returns the complete hex-encoded needle bytes +// and an error. Errors covers errors that span from the needle not being found, internal +// storage error inconsistencies, and invalid value types. +func (h *Haystack) Get(hash string) (string, error) { + if len(hash) != EncodedHashLength { + return "", ErrorHashLength + } + if _, err := hex.DecodeString(hash); err != nil { + return "", err + } + v, ok := h.internal.Get(hash) + if !ok { + return "", ErrorNeedleNotFound + } + b, ok := v.([]byte) + if !ok { + return "", ErrorValueInvalidType + } + n, err := needle.New(b) + if err != nil { + return "", err + } + needleHash := hex.EncodeToString(n.Hash()) + if needleHash != hash { + return "", ErrorHashMismatch + } + return hex.EncodeToString(n.Bytes()), nil +} diff --git a/examples/gno.land/p/n2p5/haystack/haystack_test.gno b/examples/gno.land/p/n2p5/haystack/haystack_test.gno new file mode 100644 index 00000000000..8291a101d73 --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/haystack_test.gno @@ -0,0 +1,94 @@ +package haystack + +import ( + "encoding/hex" + "testing" + + "gno.land/p/n2p5/haystack/needle" +) + +func TestHaystack(t *testing.T) { + t.Parallel() + + t.Run("New", func(t *testing.T) { + t.Parallel() + h := New() + if h == nil { + t.Error("New returned nil") + } + }) + + t.Run("Add", func(t *testing.T) { + t.Parallel() + h := New() + n, _ := needle.New(make([]byte, needle.PayloadLength)) + validNeedleHex := hex.EncodeToString(n.Bytes()) + + testTable := []struct { + needleHex string + err error + }{ + {validNeedleHex, nil}, + {validNeedleHex, ErrorDuplicateNeedle}, + {"bad" + validNeedleHex[3:], needle.ErrorInvalidHash}, + {"XXX" + validNeedleHex[3:], hex.InvalidByteError('X')}, + {validNeedleHex[:len(validNeedleHex)-2], ErrorNeedleLength}, + {validNeedleHex + "00", ErrorNeedleLength}, + {"000", ErrorNeedleLength}, + } + for _, tt := range testTable { + err := h.Add(tt.needleHex) + if err != tt.err { + t.Error(tt.needleHex, err.Error(), "!=", tt.err.Error()) + } + } + }) + + t.Run("Get", func(t *testing.T) { + t.Parallel() + h := New() + + // genNeedleHex returns a hex-encoded needle and its hash for a given index. + genNeedleHex := func(i int) (string, string) { + b := make([]byte, needle.PayloadLength) + b[0] = byte(i) + n, _ := needle.New(b) + return hex.EncodeToString(n.Bytes()), hex.EncodeToString(n.Hash()) + } + + // Add a valid needle to the haystack. + validNeedleHex, validHash := genNeedleHex(0) + h.Add(validNeedleHex) + + // Add a needle and break the value type. + _, brokenHashValueType := genNeedleHex(1) + h.internal.Set(brokenHashValueType, 0) + + // Add a needle with invalid hash. + _, invalidHash := genNeedleHex(2) + h.internal.Set(invalidHash, make([]byte, needle.PayloadLength)) + + testTable := []struct { + hash string + expected string + err error + }{ + {validHash, validNeedleHex, nil}, + {validHash[:len(validHash)-2], "", ErrorHashLength}, + {validHash + "00", "", ErrorHashLength}, + {"XXX" + validHash[3:], "", hex.InvalidByteError('X')}, + {"bad" + validHash[3:], "", ErrorNeedleNotFound}, + {brokenHashValueType, "", ErrorValueInvalidType}, + {invalidHash, "", ErrorHashMismatch}, + } + for _, tt := range testTable { + actual, err := h.Get(tt.hash) + if err != tt.err { + t.Error(tt.hash, err.Error(), "!=", tt.err.Error()) + } + if actual != tt.expected { + t.Error(tt.hash, actual, "!=", tt.expected) + } + } + }) +} diff --git a/examples/gno.land/p/n2p5/haystack/needle/gno.mod b/examples/gno.land/p/n2p5/haystack/needle/gno.mod new file mode 100644 index 00000000000..91f489282cf --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/needle/gno.mod @@ -0,0 +1 @@ +module gno.land/p/n2p5/haystack/needle diff --git a/examples/gno.land/p/n2p5/haystack/needle/needle.gno b/examples/gno.land/p/n2p5/haystack/needle/needle.gno new file mode 100644 index 00000000000..971bc31599a --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/needle/needle.gno @@ -0,0 +1,91 @@ +package needle + +import ( + "bytes" + "crypto/sha256" + "errors" +) + +const ( + // HashLength is the length in bytes of the hash prefix in any message + HashLength = 32 + // PayloadLength is the length of the remaining bytes of the message. + PayloadLength = 160 + // NeedleLength is the number of bytes required for a valid needle. + NeedleLength = HashLength + PayloadLength +) + +// Needle is a container for a 160 byte payload +// and a 32 byte sha256 hash of the payload. +type Needle struct { + hash [HashLength]byte + payload [PayloadLength]byte +} + +var ( + // ErrorInvalidHash is an error for in invalid hash + ErrorInvalidHash = errors.New("invalid hash") + // ErrorByteSliceLength is an error for an invalid byte slice length passed in to New or FromBytes + ErrorByteSliceLength = errors.New("invalid byte slice length") +) + +// New creates a Needle used for submitting a payload to a Haystack sever. It takes a Payload +// byte slice that is 160 bytes in length and returns a reference to a +// Needle and an error. The purpose of this function is to make it +// easy to create a new Needle from a payload. This function handles creating a sha256 +// hash of the payload, which is used by the Needle to submit to a haystack server. +func New(p []byte) (*Needle, error) { + if len(p) != PayloadLength { + return nil, ErrorByteSliceLength + } + var n Needle + sum := sha256.Sum256(p) + copy(n.hash[:], sum[:]) + copy(n.payload[:], p) + return &n, nil +} + +// FromBytes is intended convert raw bytes (from UDP or storage) into a Needle. +// It takes a byte slice and expects it to be exactly the length of NeedleLength. +// The byte slice should consist of the first 32 bytes being the sha256 hash of the +// payload and the payload bytes. This function verifies the length of the byte slice, +// copies the bytes into a private [192]byte array, and validates the Needle. It returns +// a reference to a Needle and an error. +func FromBytes(b []byte) (*Needle, error) { + if len(b) != NeedleLength { + return nil, ErrorByteSliceLength + } + var n Needle + copy(n.hash[:], b[:HashLength]) + copy(n.payload[:], b[HashLength:]) + if err := n.validate(); err != nil { + return nil, err + } + return &n, nil +} + +// Hash returns a copy of the bytes of the sha256 256 hash of the Needle payload. +func (n *Needle) Hash() []byte { + return n.Bytes()[:HashLength] +} + +// Payload returns a byte slice of the Needle payload +func (n *Needle) Payload() []byte { + return n.Bytes()[HashLength:] +} + +// Bytes returns a byte slice of the entire 192 byte hash + payload +func (n *Needle) Bytes() []byte { + b := make([]byte, NeedleLength) + copy(b, n.hash[:]) + copy(b[HashLength:], n.payload[:]) + return b +} + +// validate checks that a Needle has a valid hash, it returns either nil or an error. +func (n *Needle) validate() error { + if hash := sha256.Sum256(n.Payload()); !bytes.Equal(n.Hash(), hash[:]) { + return ErrorInvalidHash + } + return nil +} diff --git a/examples/gno.land/p/n2p5/haystack/needle/needle_test.gno b/examples/gno.land/p/n2p5/haystack/needle/needle_test.gno new file mode 100644 index 00000000000..aa81750fc00 --- /dev/null +++ b/examples/gno.land/p/n2p5/haystack/needle/needle_test.gno @@ -0,0 +1,157 @@ +package needle + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "testing" +) + +func TestNeedle(t *testing.T) { + t.Parallel() + t.Run("Bytes", func(t *testing.T) { + t.Parallel() + p, _ := hex.DecodeString("40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + n, _ := New(p) + b := n.Bytes() + b[0], b[1], b[2], b[3] = 0, 0, 0, 0 + if bytes.Equal(n.Bytes(), b) { + t.Error("mutating Bytes() changed needle bytes") + } + }) + t.Run("Payload", func(t *testing.T) { + t.Parallel() + p, _ := hex.DecodeString("40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + n, _ := New(p) + payload := n.Payload() + if !bytes.Equal(p, payload) { + t.Error("payload imported by New does not match needle.Payload()") + } + payload[0] = 0 + pl := n.Payload() + if bytes.Equal(pl, payload) { + t.Error("mutating Payload() changed needle payload") + } + }) + t.Run("Hash", func(t *testing.T) { + t.Parallel() + p, _ := hex.DecodeString("40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + n, _ := New(p) + hash := n.Hash() + h := sha256.Sum256(p) + if !bytes.Equal(h[:], hash) { + t.Error("exported hash is invalid") + } + hash[0] = 0 + h2 := n.Hash() + if bytes.Equal(h2, hash) { + t.Error("mutating Hash() changed needle hash") + } + }) +} + +func TestNew(t *testing.T) { + t.Parallel() + + p, _ := hex.DecodeString("40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + expected, _ := hex.DecodeString("f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + + testTable := []struct { + payload []byte + expected []byte + hasError bool + description string + }{ + { + payload: p, + expected: expected, + hasError: false, + description: "expected payload", + }, + { + payload: p[:PayloadLength-1], + expected: nil, + hasError: true, + description: "payload invalid length (too small)", + }, + { + payload: append(p, byte(1)), + expected: nil, + hasError: true, + description: "payload invalid length (too large)", + }, + } + + for _, test := range testTable { + n, err := New(test.payload) + if err != nil { + if !test.hasError { + t.Errorf("test: %v had error: %v", test.description, err) + } + } else if !bytes.Equal(n.Bytes(), test.expected) { + t.Errorf("%v, bytes not equal\n%x\n%x", test.description, n.Bytes(), test.expected) + } + } +} + +func TestFromBytes(t *testing.T) { + t.Parallel() + + validRaw, _ := hex.DecodeString("f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + validExpected, _ := hex.DecodeString("f1b462c84a0c51dad44293951f0b084a8871b3700ac1b9fc7a53a20bc0ba0fed40e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + invalidHash, _ := hex.DecodeString("182e0ca0d2fb1da76da6caf36a9d0d2838655632e85891216dc8b545d8f1410940e4350b03d8b0c9e340321210b259d9a20b19632929b4a219254a4269c11f820c75168c6a91d309f4b134a7d715a5ac408991e1cf9415995053cf8a4e185dae22a06617ac51ebf7d232bc49e567f90be4db815c2b88ca0d9a4ef7a5119c0e592c88dfb96706e6510fb8a657c0f70f6695ea310d24786e6d980e9b33cf2665342b965b2391f6bb982c4c5f6058b9cba58038d32452e07cdee9420a8bd7f514e1") + + testTable := []struct { + rawBytes []byte + expected []byte + hasError bool + description string + }{ + { + rawBytes: validRaw, + expected: validExpected, + hasError: false, + description: "valid raw bytes", + }, + { + rawBytes: make([]byte, 0), + expected: nil, + hasError: true, + description: "empty bytes", + }, + { + rawBytes: make([]byte, NeedleLength-1), + expected: nil, + hasError: true, + description: "too few bytes, one less than expected", + }, + { + rawBytes: make([]byte, 0), + expected: nil, + hasError: true, + description: "too few bytes, no bytes", + }, + { + rawBytes: make([]byte, NeedleLength+1), + expected: nil, + hasError: true, + description: "too many bytes", + }, + { + rawBytes: invalidHash, + expected: nil, + hasError: true, + description: "invalid hash", + }, + } + for _, test := range testTable { + n, err := FromBytes(test.rawBytes) + if err != nil { + if !test.hasError { + t.Errorf("test: %v had error: %v", test.description, err) + } + } else if !bytes.Equal(n.Bytes(), test.expected) { + t.Errorf("%v, bytes not equal\n%x\n%x", test.description, n.Bytes(), test.expected) + } + } +} diff --git a/examples/gno.land/p/n2p5/loci/gno.mod b/examples/gno.land/p/n2p5/loci/gno.mod new file mode 100644 index 00000000000..ec30d72d752 --- /dev/null +++ b/examples/gno.land/p/n2p5/loci/gno.mod @@ -0,0 +1 @@ +module gno.land/p/n2p5/loci diff --git a/examples/gno.land/p/n2p5/loci/loci.gno b/examples/gno.land/p/n2p5/loci/loci.gno new file mode 100644 index 00000000000..7bd5c29c3af --- /dev/null +++ b/examples/gno.land/p/n2p5/loci/loci.gno @@ -0,0 +1,44 @@ +// loci is a single purpose datastore keyed by the caller's address. It has two +// functions: Set and Get. loci is plural for locus, which is a central or core +// place where something is found or from which it originates. In this case, +// it's a simple key-value store where an address (the key) can store exactly +// one value (in the form of a byte slice). Only the caller can set the value +// for their address, but anyone can retrieve the value for any address. +package loci + +import ( + "std" + + "gno.land/p/demo/avl" +) + +// LociStore is a simple key-value store that uses +// an AVL tree to store the data. +type LociStore struct { + internal *avl.Tree +} + +// New creates a reference to a new LociStore. +func New() *LociStore { + return &LociStore{ + internal: avl.NewTree(), + } +} + +// Set stores a byte slice in the AVL tree using the `std.PrevRealm().Addr()` +// string as the key. +func (s *LociStore) Set(value []byte) { + key := string(std.PrevRealm().Addr()) + s.internal.Set(key, value) +} + +// Get retrieves a byte slice from the AVL tree using the provided address. +// The return values are the byte slice value and a boolean indicating +// whether the value exists. +func (s *LociStore) Get(addr std.Address) []byte { + value, exists := s.internal.Get(string(addr)) + if !exists { + return nil + } + return value.([]byte) +} diff --git a/examples/gno.land/p/n2p5/loci/loci_test.gno b/examples/gno.land/p/n2p5/loci/loci_test.gno new file mode 100644 index 00000000000..bb216a8539e --- /dev/null +++ b/examples/gno.land/p/n2p5/loci/loci_test.gno @@ -0,0 +1,84 @@ +package loci + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" +) + +func TestLociStore(t *testing.T) { + t.Parallel() + + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u1") + + t.Run("TestSet", func(t *testing.T) { + t.Parallel() + store := New() + u1 := testutils.TestAddress("u1") + + m1 := []byte("hello") + m2 := []byte("world") + std.TestSetOrigCaller(u1) + + // Ensure that the value is nil before setting it. + r1 := store.Get(u1) + if r1 != nil { + t.Errorf("expected value to be nil, got '%s'", r1) + } + store.Set(m1) + // Ensure that the value is correct after setting it. + r2 := store.Get(u1) + if string(r2) != "hello" { + t.Errorf("expected value to be 'hello', got '%s'", r2) + } + store.Set(m2) + // Ensure that the value is correct after overwriting it. + r3 := store.Get(u1) + if string(r3) != "world" { + t.Errorf("expected value to be 'world', got '%s'", r3) + } + }) + t.Run("TestGet", func(t *testing.T) { + t.Parallel() + store := New() + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u2") + u3 := testutils.TestAddress("u3") + u4 := testutils.TestAddress("u4") + + m1 := []byte("hello") + m2 := []byte("world") + m3 := []byte("goodbye") + + std.TestSetOrigCaller(u1) + store.Set(m1) + std.TestSetOrigCaller(u2) + store.Set(m2) + std.TestSetOrigCaller(u3) + store.Set(m3) + + // Ensure that the value is correct after setting it. + r0 := store.Get(u4) + if r0 != nil { + t.Errorf("expected value to be nil, got '%s'", r0) + } + // Ensure that the value is correct after setting it. + r1 := store.Get(u1) + if string(r1) != "hello" { + t.Errorf("expected value to be 'hello', got '%s'", r1) + } + // Ensure that the value is correct after setting it. + r2 := store.Get(u2) + if string(r2) != "world" { + t.Errorf("expected value to be 'world', got '%s'", r2) + } + // Ensure that the value is correct after setting it. + r3 := store.Get(u3) + if string(r3) != "goodbye" { + t.Errorf("expected value to be 'goodbye', got '%s'", r3) + } + }) + +} diff --git a/examples/gno.land/p/n2p5/mgroup/gno.mod b/examples/gno.land/p/n2p5/mgroup/gno.mod new file mode 100644 index 00000000000..132913d9c3d --- /dev/null +++ b/examples/gno.land/p/n2p5/mgroup/gno.mod @@ -0,0 +1 @@ +module gno.land/p/n2p5/mgroup diff --git a/examples/gno.land/p/n2p5/mgroup/mgroup.gno b/examples/gno.land/p/n2p5/mgroup/mgroup.gno new file mode 100644 index 00000000000..566d625a003 --- /dev/null +++ b/examples/gno.land/p/n2p5/mgroup/mgroup.gno @@ -0,0 +1,184 @@ +// Package mgroup is a simple managed group managing ownership and membership +// for authorization in gno realms. The ManagedGroup struct is used to manage +// the owner, backup owners, and members of a group. The owner is the primary +// owner of the group and can add and remove backup owners and members. Backup +// owners can claim ownership of the group. This is meant to provide backup +// accounts for the owner in case the owner account is lost or compromised. +// Members are used to authorize actions across realms. +package mgroup + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" +) + +var ( + ErrCannotRemoveOwner = errors.New("mgroup: cannot remove owner") + ErrNotBackupOwner = errors.New("mgroup: not a backup owner") + ErrNotMember = errors.New("mgroup: not a member") + ErrInvalidAddress = errors.New("mgroup: address is invalid") +) + +type ManagedGroup struct { + owner *ownable.Ownable + backupOwners *avl.Tree + members *avl.Tree +} + +// New creates a new ManagedGroup with the owner set to the provided address. +// The owner is automatically added as a backup owner and member of the group. +func New(ownerAddress std.Address) *ManagedGroup { + g := &ManagedGroup{ + owner: ownable.NewWithAddress(ownerAddress), + backupOwners: avl.NewTree(), + members: avl.NewTree(), + } + g.AddBackupOwner(ownerAddress) + g.AddMember(ownerAddress) + return g +} + +// AddBackupOwner adds a backup owner to the group by std.Address. +// If the caller is not the owner, an error is returned. +func (g *ManagedGroup) AddBackupOwner(addr std.Address) error { + if !g.owner.CallerIsOwner() { + return ownable.ErrUnauthorized + } + if !addr.IsValid() { + return ErrInvalidAddress + } + g.backupOwners.Set(addr.String(), struct{}{}) + return nil +} + +// RemoveBackupOwner removes a backup owner from the group by std.Address. +// The owner cannot be removed. If the caller is not the owner, an error is returned. +func (g *ManagedGroup) RemoveBackupOwner(addr std.Address) error { + if !g.owner.CallerIsOwner() { + return ownable.ErrUnauthorized + } + if !addr.IsValid() { + return ErrInvalidAddress + } + if addr == g.Owner() { + return ErrCannotRemoveOwner + } + g.backupOwners.Remove(addr.String()) + return nil +} + +// ClaimOwnership allows a backup owner to claim ownership of the group. +// If the caller is not a backup owner, an error is returned. +// The caller is automatically added as a member of the group. +func (g *ManagedGroup) ClaimOwnership() error { + caller := std.PrevRealm().Addr() + // already owner, skip + if caller == g.Owner() { + return nil + } + if !g.IsBackupOwner(caller) { + return ErrNotMember + } + g.owner = ownable.NewWithAddress(caller) + g.AddMember(caller) + return nil +} + +// AddMember adds a member to the group by std.Address. +// If the caller is not the owner, an error is returned. +func (g *ManagedGroup) AddMember(addr std.Address) error { + if !g.owner.CallerIsOwner() { + return ownable.ErrUnauthorized + } + if !addr.IsValid() { + return ErrInvalidAddress + } + g.members.Set(addr.String(), struct{}{}) + return nil +} + +// RemoveMember removes a member from the group by std.Address. +// The owner cannot be removed. If the caller is not the owner, +// an error is returned. +func (g *ManagedGroup) RemoveMember(addr std.Address) error { + if !g.owner.CallerIsOwner() { + return ownable.ErrUnauthorized + } + if !addr.IsValid() { + return ErrInvalidAddress + } + if addr == g.Owner() { + return ErrCannotRemoveOwner + } + g.members.Remove(addr.String()) + return nil +} + +// MemberCount returns the number of members in the group. +func (g *ManagedGroup) MemberCount() int { + return g.members.Size() +} + +// BackupOwnerCount returns the number of backup owners in the group. +func (g *ManagedGroup) BackupOwnerCount() int { + return g.backupOwners.Size() +} + +// IsMember checks if an address is a member of the group. +func (g *ManagedGroup) IsMember(addr std.Address) bool { + return g.members.Has(addr.String()) +} + +// IsBackupOwner checks if an address is a backup owner in the group. +func (g *ManagedGroup) IsBackupOwner(addr std.Address) bool { + return g.backupOwners.Has(addr.String()) +} + +// Owner returns the owner of the group. +func (g *ManagedGroup) Owner() std.Address { + return g.owner.Owner() +} + +// BackupOwners returns a slice of all backup owners in the group, using the underlying +// avl.Tree to iterate over the backup owners. If you have a large group, you may +// want to use BackupOwnersWithOffset to iterate over backup owners in chunks. +func (g *ManagedGroup) BackupOwners() []string { + return g.BackupOwnersWithOffset(0, g.BackupOwnerCount()) +} + +// Members returns a slice of all members in the group, using the underlying +// avl.Tree to iterate over the members. If you have a large group, you may +// want to use MembersWithOffset to iterate over members in chunks. +func (g *ManagedGroup) Members() []string { + return g.MembersWithOffset(0, g.MemberCount()) +} + +// BackupOwnersWithOffset returns a slice of backup owners in the group, using the underlying +// avl.Tree to iterate over the backup owners. The offset and count parameters allow you +// to iterate over backup owners in chunks to support patterns such as pagination. +func (g *ManagedGroup) BackupOwnersWithOffset(offset, count int) []string { + return sliceWithOffset(g.backupOwners, offset, count) +} + +// MembersWithOffset returns a slice of members in the group, using the underlying +// avl.Tree to iterate over the members. The offset and count parameters allow you +// to iterate over members in chunks to support patterns such as pagination. +func (g *ManagedGroup) MembersWithOffset(offset, count int) []string { + return sliceWithOffset(g.members, offset, count) +} + +// sliceWithOffset is a helper function to iterate over an avl.Tree with an offset and count. +func sliceWithOffset(t *avl.Tree, offset, count int) []string { + var result []string + t.IterateByOffset(offset, count, func(k string, _ interface{}) bool { + if k == "" { + return true + } + result = append(result, k) + return false + }) + return result +} diff --git a/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno b/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno new file mode 100644 index 00000000000..cd02db98683 --- /dev/null +++ b/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno @@ -0,0 +1,420 @@ +package mgroup + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/testutils" +) + +func TestManagedGroup(t *testing.T) { + t.Parallel() + + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u2") + u3 := testutils.TestAddress("u3") + + t.Run("AddBackupOwner", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + err := g.AddBackupOwner(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.AddBackupOwner(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ErrNotBackupOwner.Error(), err.Error()) + } + } + // ensure invalid address is caught + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.AddBackupOwner(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + }) + t.Run("RemoveBackupOwner", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + g.AddBackupOwner(u2) + err := g.RemoveBackupOwner(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // running this twice should not error. + { + std.TestSetOrigCaller(u1) + err := g.RemoveBackupOwner(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.RemoveBackupOwner(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ErrNotBackupOwner.Error(), err.Error()) + } + } + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.RemoveBackupOwner(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + { + std.TestSetOrigCaller(u1) + err := g.RemoveBackupOwner(u1) + if err != ErrCannotRemoveOwner { + t.Errorf("expected %v, got %v", ErrCannotRemoveOwner.Error(), err.Error()) + } + } + }) + t.Run("ClaimOwnership", func(t *testing.T) { + t.Parallel() + g := New(u1) + g.AddBackupOwner(u2) + // happy path + { + std.TestSetOrigCaller(u2) + err := g.ClaimOwnership() + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + if g.Owner() != u2 { + t.Errorf("expected %v, got %v", u2, g.Owner()) + } + if !g.IsMember(u2) { + t.Errorf("expected %v to be a member", u2) + } + } + // running this twice should not error. + { + std.TestSetOrigCaller(u2) + err := g.ClaimOwnership() + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u3) + err := g.ClaimOwnership() + if err != ErrNotMember { + t.Errorf("expected %v, got %v", ErrNotMember.Error(), err.Error()) + } + } + }) + t.Run("AddMember", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + err := g.AddMember(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + if !g.IsMember(u2) { + t.Errorf("expected %v to be a member", u2) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.AddMember(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ownable.ErrUnauthorized.Error(), err.Error()) + } + } + // ensure invalid address is caught + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.AddMember(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + }) + t.Run("RemoveMember", func(t *testing.T) { + t.Parallel() + g := New(u1) + // happy path + { + std.TestSetOrigCaller(u1) + g.AddMember(u2) + err := g.RemoveMember(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + if g.IsMember(u2) { + t.Errorf("expected %v to not be a member", u2) + } + } + // running this twice should not error. + { + std.TestSetOrigCaller(u1) + err := g.RemoveMember(u2) + if err != nil { + t.Errorf("expected nil, got %v", err.Error()) + } + } + // ensure checking for authorized caller + { + std.TestSetOrigCaller(u2) + err := g.RemoveMember(u3) + if err != ownable.ErrUnauthorized { + t.Errorf("expected %v, got %v", ownable.ErrUnauthorized.Error(), err.Error()) + } + } + // ensure invalid address is caught + { + std.TestSetOrigCaller(u1) + var badAddr std.Address + err := g.RemoveMember(badAddr) + if err != ErrInvalidAddress { + t.Errorf("expected %v, got %v", ErrInvalidAddress.Error(), err.Error()) + } + } + // ensure owner cannot be removed + { + std.TestSetOrigCaller(u1) + err := g.RemoveMember(u1) + if err != ErrCannotRemoveOwner { + t.Errorf("expected %v, got %v", ErrCannotRemoveOwner.Error(), err.Error()) + } + } + }) + t.Run("MemberCount", func(t *testing.T) { + t.Parallel() + g := New(u1) + if g.MemberCount() != 1 { + t.Errorf("expected 0, got %v", g.MemberCount()) + } + g.AddMember(u2) + if g.MemberCount() != 2 { + t.Errorf("expected 1, got %v", g.MemberCount()) + } + g.AddMember(u3) + if g.MemberCount() != 3 { + t.Errorf("expected 2, got %v", g.MemberCount()) + } + g.RemoveMember(u2) + if g.MemberCount() != 2 { + t.Errorf("expected 1, got %v", g.MemberCount()) + } + }) + t.Run("BackupOwnerCount", func(t *testing.T) { + t.Parallel() + g := New(u1) + if g.BackupOwnerCount() != 1 { + t.Errorf("expected 0, got %v", g.BackupOwnerCount()) + } + g.AddBackupOwner(u2) + if g.BackupOwnerCount() != 2 { + t.Errorf("expected 1, got %v", g.BackupOwnerCount()) + } + g.AddBackupOwner(u3) + if g.BackupOwnerCount() != 3 { + t.Errorf("expected 2, got %v", g.BackupOwnerCount()) + } + g.RemoveBackupOwner(u2) + if g.BackupOwnerCount() != 2 { + t.Errorf("expected 1, got %v", g.BackupOwnerCount()) + } + }) + t.Run("IsMember", func(t *testing.T) { + t.Parallel() + g := New(u1) + if !g.IsMember(u1) { + t.Errorf("expected %v to be a member", u1) + } + if g.IsMember(u2) { + t.Errorf("expected %v to not be a member", u2) + } + g.AddMember(u2) + if !g.IsMember(u2) { + t.Errorf("expected %v to be a member", u2) + } + }) + t.Run("IsBackupOwner", func(t *testing.T) { + t.Parallel() + g := New(u1) + if !g.IsBackupOwner(u1) { + t.Errorf("expected %v to be a backup owner", u1) + } + if g.IsBackupOwner(u2) { + t.Errorf("expected %v to not be a backup owner", u2) + } + g.AddBackupOwner(u2) + if !g.IsBackupOwner(u2) { + t.Errorf("expected %v to be a backup owner", u2) + } + }) + t.Run("Owner", func(t *testing.T) { + t.Parallel() + g := New(u1) + if g.Owner() != u1 { + t.Errorf("expected %v, got %v", u1, g.Owner()) + } + g.AddBackupOwner(u2) + if g.Owner() != u1 { + t.Errorf("expected %v, got %v", u1, g.Owner()) + } + std.TestSetOrigCaller(u2) + g.ClaimOwnership() + if g.Owner() != u2 { + t.Errorf("expected %v, got %v", u2, g.Owner()) + } + }) + t.Run("BackupOwners", func(t *testing.T) { + t.Parallel() + std.TestSetOrigCaller(u1) + g := New(u1) + g.AddBackupOwner(u2) + g.AddBackupOwner(u3) + owners := g.BackupOwners() + if len(owners) != 3 { + t.Errorf("expected 2, got %v", len(owners)) + } + if owners[0] != u1.String() { + t.Errorf("expected %v, got %v", u2, owners[0]) + } + if owners[1] != u3.String() { + t.Errorf("expected %v, got %v", u3, owners[1]) + } + if owners[2] != u2.String() { + t.Errorf("expected %v, got %v", u3, owners[1]) + } + }) + t.Run("Members", func(t *testing.T) { + t.Parallel() + std.TestSetOrigCaller(u1) + g := New(u1) + g.AddMember(u2) + g.AddMember(u3) + members := g.Members() + if len(members) != 3 { + t.Errorf("expected 2, got %v", len(members)) + } + if members[0] != u1.String() { + t.Errorf("expected %v, got %v", u2, members[0]) + } + if members[1] != u3.String() { + t.Errorf("expected %v, got %v", u3, members[1]) + } + if members[2] != u2.String() { + t.Errorf("expected %v, got %v", u3, members[1]) + } + }) +} + +func TestSliceWithOffset(t *testing.T) { + t.Parallel() + testTable := []struct { + name string + slice []string + offset int + count int + expected []string + expectedCount int + }{ + { + name: "empty", + slice: []string{}, + offset: 0, + count: 0, + expected: []string{}, + expectedCount: 0, + }, + { + name: "single", + slice: []string{"a"}, + offset: 0, + count: 1, + expected: []string{"a"}, + expectedCount: 1, + }, + { + name: "single offset", + slice: []string{"a"}, + offset: 1, + count: 1, + expected: []string{}, + expectedCount: 0, + }, + { + name: "multiple", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 0, + count: 10, + expected: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + expectedCount: 10, + }, + { + name: "multiple offset", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 5, + count: 5, + expected: []string{"f", "g", "h", "i", "j"}, + expectedCount: 5, + }, + { + name: "multiple offset end", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 10, + count: 5, + expected: []string{}, + expectedCount: 0, + }, + { + name: "multiple offset past end", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 11, + count: 5, + expected: []string{}, + expectedCount: 0, + }, + { + name: "multiple offset count past end", + slice: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}, + offset: 5, + count: 20, + expected: []string{"f", "g", "h", "i", "j"}, + expectedCount: 5, + }, + } + for _, test := range testTable { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + tree := avl.NewTree() + for _, s := range test.slice { + tree.Set(s, struct{}{}) + } + slice := sliceWithOffset(tree, test.offset, test.count) + if len(slice) != test.expectedCount { + t.Errorf("expected %v, got %v", test.expectedCount, len(slice)) + } + }) + } +} diff --git a/examples/gno.land/p/nt/poa/gno.mod b/examples/gno.land/p/nt/poa/gno.mod index 5c1b75eb05a..965eeb56aed 100644 --- a/examples/gno.land/p/nt/poa/gno.mod +++ b/examples/gno.land/p/nt/poa/gno.mod @@ -1,10 +1 @@ module gno.land/p/nt/poa - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/sys/validators v0.0.0-latest -) diff --git a/examples/gno.land/p/wyhaines/rand/isaac/README.md b/examples/gno.land/p/wyhaines/rand/isaac/README.md new file mode 100644 index 00000000000..05f4a94425f --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/README.md @@ -0,0 +1,86 @@ +# package isaac // import "gno.land/p/demo/math/rand/isaac" + +This is a port of the ISAAC cryptographically secure PRNG, +originally based on the reference implementation found at +https://burtleburtle.net/bob/rand/isaacafa.html + +ISAAC has excellent statistical properties, with long cycle times, and +uniformly distributed, unbiased, and unpredictable number generation. It can +not be distinguished from real random data, and in three decades of scrutiny, +no practical attacks have been found. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the 32-bit ISAAC PRNG algorithm. This +algorithm provides very strong statistical performance, and is cryptographically +secure, while still being substantially faster than the default PCG +implementation in `math/rand`. Note that this package does implement a `Uint64()` +function in order to generate a 64 bit number out of two 32 bit numbers. Doing this +makes the generator only slightly faster than PCG, however, + +Note that the approach to seeing with ISAAC is very important for best results, +and seeding with ISAAC is not as simple as seeding with a single uint64 value. +The ISAAC algorithm requires a 256-element seed. If used for cryptographic +purposes, this will likely require entropy generated off-chain for actual +cryptographically secure seeding. For other purposes, however, one can utilize +the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to +generate any missing seeds if fewer than 256 are provided. + + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.58s +ISAAC: 1000000 Uint64 generated in 13.23s (uint64) +ISAAC: 1000000 Uint32 generated in 6.43s (uint32) +Ratio: x1.18 times faster than PCG (uint64) +Ratio: x2.42 times faster than PCG (uint32) +``` + +Use it directly: + +``` +prng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest + // will be generated using the xorshiftr128plus PRNG. +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = isaac.New() +prng := rand.New(source) +``` + +# TYPES + +` +type ISAAC struct { + // Has unexported fields. +} +` + +`func New(seeds ...uint32) *ISAAC` + ISAAC requires a large, 256-element seed. This implementation will leverage + the entropy package combined with the the xorshiftr128plus PRNG to generate + any missing seeds of fewer than the required number of arguments are + provided. + +`func (isaac *ISAAC) MarshalBinary() ([]byte, error)` + MarshalBinary() returns a byte array that encodes the state of the PRNG. + This can later be used with UnmarshalBinary() to restore the state of the + PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (isaac *ISAAC) Seed(seed [256]uint32)` + +`func (isaac *ISAAC) Uint32() uint32` + +`func (isaac *ISAAC) Uint64() uint64` + +`func (isaac *ISAAC) UnmarshalBinary(data []byte) error` + UnmarshalBinary() restores the state of the PRNG from a byte array + that was created with MarshalBinary(). UnmarshalBinary implements the + encoding.BinaryUnmarshaler interface. + diff --git a/examples/gno.land/p/wyhaines/rand/isaac/gno.mod b/examples/gno.land/p/wyhaines/rand/isaac/gno.mod new file mode 100644 index 00000000000..538f52e6e7e --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/gno.mod @@ -0,0 +1 @@ +module gno.land/p/wyhaines/rand/isaac diff --git a/examples/gno.land/p/wyhaines/rand/isaac/isaac.gno b/examples/gno.land/p/wyhaines/rand/isaac/isaac.gno new file mode 100644 index 00000000000..4508dd5d5af --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/isaac.gno @@ -0,0 +1,435 @@ +// This is a port of the ISAAC cryptographically secure PRNG, originally based on the reference +// implementation found at https://burtleburtle.net/bob/rand/isaacafa.html +// +// ISAAC has excellent statistical properties, with long cycle times, and uniformly distributed, +// unbiased, and unpredictable number generation. It can not be distinguished from real random +// data, and in three decades of scrutiny, no practical attacks have been found. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementation, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the 32-bit ISAAC PRNG algorithm. This +// algorithm provides very strong statistical performance, and is cryptographically +// secure, while still being substantially faster than the default PCG +// implementation in `math/rand`. Note that this package does implement a `Uint64()` +// function in order to generate a 64 bit number out of two 32 bit numbers. Doing this +// makes the generator only slightly faster than PCG, however, +// +// Note that the approach to seeing with ISAAC is very important for best results, and seeding with +// ISAAC is not as simple as seeding with a single uint64 value. The ISAAC algorithm requires a +// 256-element seed. If used for cryptographic purposes, this will likely require entropy generated +// off-chain for actual cryptographically secure seeding. For other purposes, however, one can +// utilize the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to generate +// any missing seeds if fewer than 256 are provided. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.58s +// ISAAC: 1000000 Uint64 generated in 13.23s +// ISAAC: 1000000 Uint32 generated in 6.43s +// Ratio: x1.18 times faster than PCG (uint64) +// Ratio: x2.42 times faster than PCG (uint32) +// +// Use it directly: +// +// prng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest +// // will be generated using the xorshiftr128plus PRNG. +// +// Or use it as a drop-in replacement for the default PRNG in Rand: +// +// source = isaac.New() +// prng := rand.New(source) +package isaac + +import ( + "errors" + "math" + "math/rand" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" + "gno.land/p/wyhaines/rand/xorshiftr128plus" +) + +type ISAAC struct { + randrsl [256]uint32 + randcnt uint32 + mm [256]uint32 + aa, bb, cc uint32 + seed [256]uint32 +} + +// ISAAC requires a large, 256-element seed. This implementation will leverage the entropy +// package combined with the the xorshiftr128plus PRNG to generate any missing seeds of +// fewer than the required number of arguments are provided. +func New(seeds ...uint32) *ISAAC { + isaac := &ISAAC{} + seed := [256]uint32{} + + index := 0 + for index = 0; index < len(seeds); index++ { + seed[index] = seeds[index] + } + + if index < 4 { + e := entropy.New() + for ; index < 4; index++ { + seed[index] = e.Value() + } + } + + // Use up to the first four seeds as seeding inputs for xorshiftr128+, in order to + // use it to provide any remaining missing seeds. + prng := xorshiftr128plus.New( + (uint64(seed[0])<<32)|uint64(seed[1]), + (uint64(seed[2])<<32)|uint64(seed[3]), + ) + for ; index < 256; index += 2 { + val := prng.Uint64() + seed[index] = uint32(val & 0xffffffff) + if index+1 < 256 { + seed[index+1] = uint32(val >> 32) + } + } + isaac.Seed(seed) + return isaac +} + +func (isaac *ISAAC) Seed(seed [256]uint32) { + isaac.randrsl = seed + isaac.seed = seed + isaac.randinit(true) +} + +// beUint32() decodes a uint32 from a set of four bytes, assuming big endian encoding. +// binary.bigEndian.Uint32, copied to avoid dependency +func beUint32(b []byte) uint32 { + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 +} + +// bePutUint32() encodes a uint64 into a buffer of eight bytes. +// binary.bigEndian.PutUint32, copied to avoid dependency +func bePutUint32(b []byte, v uint32) { + _ = b[3] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 24) + b[1] = byte(v >> 16) + b[2] = byte(v >> 8) + b[3] = byte(v) +} + +// A label to identify the marshalled data. +var marshalISAACLabel = []byte("isaac:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (isaac *ISAAC) MarshalBinary() ([]byte, error) { + b := make([]byte, 3094) // 6 + 1024 + 1024 + 1024 + 4 + 4 + 4 + 4 == 3090 + copy(b, marshalISAACLabel) + for i := 0; i < 256; i++ { + bePutUint32(b[6+i*4:], isaac.seed[i]) + } + for i := 256; i < 512; i++ { + bePutUint32(b[6+i*4:], isaac.randrsl[i-256]) + } + for i := 512; i < 768; i++ { + bePutUint32(b[6+i*4:], isaac.mm[i-512]) + } + bePutUint32(b[3078:], isaac.aa) + bePutUint32(b[3082:], isaac.bb) + bePutUint32(b[3086:], isaac.cc) + bePutUint32(b[3090:], isaac.randcnt) + + return b, nil +} + +// errUnmarshalISAAC is returned when unmarshalling fails. +var errUnmarshalISAAC = errors.New("invalid ISAAC encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (isaac *ISAAC) UnmarshalBinary(data []byte) error { + if len(data) != 3094 || string(data[:6]) != string(marshalISAACLabel) { + return errUnmarshalISAAC + } + for i := 0; i < 256; i++ { + isaac.seed[i] = beUint32(data[6+i*4:]) + } + for i := 256; i < 512; i++ { + isaac.randrsl[i-256] = beUint32(data[6+i*4:]) + } + for i := 512; i < 768; i++ { + isaac.mm[i-512] = beUint32(data[6+i*4:]) + } + isaac.aa = beUint32(data[3078:]) + isaac.bb = beUint32(data[3082:]) + isaac.cc = beUint32(data[3086:]) + isaac.randcnt = beUint32(data[3090:]) + return nil +} + +func (isaac *ISAAC) randinit(flag bool) { + isaac.aa = 0 + isaac.bb = 0 + isaac.cc = 0 + + var a, b, c, d, e, f, g, h uint32 = 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9, 0x9e3779b9 + + for i := 0; i < 4; i++ { + a ^= b << 11 + d += a + b += c + b ^= c >> 2 + e += b + c += d + c ^= d << 8 + f += c + d += e + d ^= e >> 16 + g += d + e += f + e ^= f << 10 + h += e + f += g + f ^= g >> 4 + a += f + g += h + g ^= h << 8 + b += g + h += a + h ^= a >> 9 + c += h + a += b + } + + for i := 0; i < 256; i += 8 { + if flag { + a += isaac.randrsl[i] + b += isaac.randrsl[i+1] + c += isaac.randrsl[i+2] + d += isaac.randrsl[i+3] + e += isaac.randrsl[i+4] + f += isaac.randrsl[i+5] + g += isaac.randrsl[i+6] + h += isaac.randrsl[i+7] + } + + a ^= b << 11 + d += a + b += c + b ^= c >> 2 + e += b + c += d + c ^= d << 8 + f += c + d += e + d ^= e >> 16 + g += d + e += f + e ^= f << 10 + h += e + f += g + f ^= g >> 4 + a += f + g += h + g ^= h << 8 + b += g + h += a + h ^= a >> 9 + c += h + a += b + + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + + if flag { + for i := 0; i < 256; i += 8 { + a += isaac.mm[i] + b += isaac.mm[i+1] + c += isaac.mm[i+2] + d += isaac.mm[i+3] + e += isaac.mm[i+4] + f += isaac.mm[i+5] + g += isaac.mm[i+6] + h += isaac.mm[i+7] + + a ^= b << 11 + d += a + b += c + b ^= c >> 2 + e += b + c += d + c ^= d << 8 + f += c + d += e + d ^= e >> 16 + g += d + e += f + e ^= f << 10 + h += e + f += g + f ^= g >> 4 + a += f + g += h + g ^= h << 8 + b += g + h += a + h ^= a >> 9 + c += h + a += b + + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + } + + isaac.isaac() + isaac.randcnt = uint32(256) +} + +func (isaac *ISAAC) isaac() { + isaac.cc++ + isaac.bb += isaac.cc + + for i := 0; i < 256; i++ { + x := isaac.mm[i] + switch i % 4 { + case 0: + isaac.aa ^= isaac.aa << 13 + case 1: + isaac.aa ^= isaac.aa >> 6 + case 2: + isaac.aa ^= isaac.aa << 2 + case 3: + isaac.aa ^= isaac.aa >> 16 + } + isaac.aa += isaac.mm[(i+128)&0xff] + + y := isaac.mm[(x>>2)&0xff] + isaac.aa + isaac.bb + isaac.mm[i] = y + isaac.bb = isaac.mm[(y>>10)&0xff] + x + isaac.randrsl[i] = isaac.bb + } +} + +// Returns a random uint32. +func (isaac *ISAAC) Uint32() uint32 { + if isaac.randcnt == uint32(0) { + isaac.isaac() + isaac.randcnt = uint32(256) + } + isaac.randcnt-- + return isaac.randrsl[isaac.randcnt] +} + +// Returns a random uint64 by combining two uint32s. +func (isaac *ISAAC) Uint64() uint64 { + return uint64(isaac.Uint32()) | (uint64(isaac.Uint32()) << 32) +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkISAAC()' xorshift64star.gno +func benchmarkISAAC(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New() + + for i := 0; i < iterations; i++ { + _ = isaac.Uint64() + } + ufmt.Println(ufmt.Sprintf("ISAAC: generate %d uint64\n", iterations)) +} + +// The averageISAAC() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the ISAAC PRNG. +func averageISAAC(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New(987654321, 123456789, 999999999, 111111111) + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := isaac.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("ISAAC average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("ISAAC standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("ISAAC theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} + +func averagePCG(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := rand.NewPCG(987654321, 123456789) + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := isaac.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("PCG average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("PCG standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("PCG theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno b/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno new file mode 100644 index 00000000000..b08621e271c --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac/isaac_test.gno @@ -0,0 +1,165 @@ +package isaac + +import ( + "math/rand" + "testing" +) + +type OpenISAAC struct { + Randrsl [256]uint32 + Randcnt uint32 + Mm [256]uint32 + Aa, Bb, Cc uint32 + Seed [256]uint32 +} + +func TestISAACSeeding(t *testing.T) { + isaac := New() +} + +func TestISAACRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + 0.17828173023837635, + 0.7327795780287832, + 0.4850369074875177, + 0.9474842397428482, + 0.6747135561813891, + 0.7522507082868403, + 0.041115261836534356, + 0.7405243709084567, + 0.672863376128768, + 0.11866211399980553, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestISAACUint64(t *testing.T) { + isaac := New() + + expected := []uint64{ + 5986068031949215749, + 10437354066128700566, + 13478007513323023970, + 8969511410255984224, + 3869229557962857982, + 1762449743873204415, + 5292356290662282456, + 7893982194485405616, + 4296136494566588699, + 12414349056998262772, + } + + for i, exp := range expected { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func dupState(i *ISAAC) *OpenISAAC { + state := &OpenISAAC{} + state.Seed = i.seed + state.Randrsl = i.randrsl + state.Mm = i.mm + state.Aa = i.aa + state.Bb = i.bb + state.Cc = i.cc + state.Randcnt = i.randcnt + + return state +} + +func TestISAACMarshalUnmarshal(t *testing.T) { + isaac := New() + + expected1 := []uint64{ + 5986068031949215749, + 10437354066128700566, + 13478007513323023970, + 8969511410255984224, + 3869229557962857982, + } + + expected2 := []uint64{ + 1762449743873204415, + 5292356290662282456, + 7893982194485405616, + 4296136494566588699, + 12414349056998262772, + } + + for i, exp := range expected1 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := isaac.MarshalBinary() + + t.Logf("State: [%v]\n", dupState(isaac)) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := dupState(isaac) + + if err != nil { + t.Errorf("ISAAC.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + isaac.Uint64() + + for i, exp := range expected2 { + val := isaac.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%v]\n", dupState(isaac)) + + // Now restore the state of the PRNG + err = isaac.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%v]\n", dupState(isaac)) + + if state_before.Seed != dupState(isaac).Seed { + t.Errorf("Seed mismatch") + } + if state_before.Randrsl != dupState(isaac).Randrsl { + t.Errorf("Randrsl mismatch") + } + if state_before.Mm != dupState(isaac).Mm { + t.Errorf("Mm mismatch") + } + if state_before.Aa != dupState(isaac).Aa { + t.Errorf("Aa mismatch") + } + if state_before.Bb != dupState(isaac).Bb { + t.Errorf("Bb mismatch") + } + if state_before.Cc != dupState(isaac).Cc { + t.Errorf("Cc mismatch") + } + if state_before.Randcnt != dupState(isaac).Randcnt { + t.Errorf("Randcnt mismatch") + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/README.md b/examples/gno.land/p/wyhaines/rand/isaac64/README.md new file mode 100644 index 00000000000..813b062a5cd --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/README.md @@ -0,0 +1,97 @@ +# package isaac64 // import "gno.land/p/demo/math/rand/isaac64" + +This is a port of the 64-bit version of the ISAAC cryptographically +secure PRNG, originally based on the reference implementation found at +https://burtleburtle.net/bob/rand/isaacafa.html + +ISAAC has excellent statistical properties, with long cycle times, and +uniformly distributed, unbiased, and unpredictable number generation. It can +not be distinguished from real random data, and in three decades of scrutiny, +no practical attacks have been found. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the 64-bit ISAAC PRNG algorithm. This +algorithm provides very strong statistical performance, and is cryptographically +secure, while still being substantially faster than the default PCG +implementation in `math/rand`. + +Note that the approach to seeing with ISAAC is very important for best results, +and seeding with ISAAC is not as simple as seeding with a single uint64 value. +The ISAAC algorithm requires a 256-element seed. If used for cryptographic +purposes, this will likely require entropy generated off-chain for actual +cryptographically secure seeding. For other purposes, however, one can utilize +the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to +generate any missing seeds if fewer than 256 are provided. + + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.58s +ISAAC: 1000000 Uint64 generated in 8.95s +ISAAC: 1000000 Uint32 generated in 7.66s +Ratio: x1.74 times faster than PCG (uint64) +Ratio: x2.03 times faster than PCG (uint32) +``` + +Use it directly: + + +``` +prng = isaac.New() // pass 0 to 256 uint64 seeds; if fewer than 256 are provided, the rest + // will be generated using the xorshiftr128plus PRNG. +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = isaac64.New() +prng := rand.New(source) +``` + +## CONSTANTS + + +``` +const ( + RANDSIZL = 8 + RANDSIZ = 1 << RANDSIZL // 256 +) +``` + +## TYPES + + +``` +type ISAAC struct { + // Has unexported fields. +} +``` + +`func New(seeds ...uint64) *ISAAC` +ISAAC requires a large, 256-element seed. This implementation will leverage +the entropy package combined with the xorshiftr128plus PRNG to generate any +missing seeds if fewer than the required number of arguments are provided. + +`func (isaac *ISAAC) MarshalBinary() ([]byte, error)` +MarshalBinary() returns a byte array that encodes the state of the PRNG. +This can later be used with UnmarshalBinary() to restore the state of the +PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (isaac *ISAAC) Seed(seed [256]uint64)` +Reinitialize the generator with a new seed. A seed must be composed of 256 uint64. + +`func (isaac *ISAAC) Uint32() uint32` +Return a 32 bit random integer, composed of the high 32 bits of the generated 32 bit result. + +`func (isaac *ISAAC) Uint64() uint64` +Return a 64 bit random integer. + +`func (isaac *ISAAC) UnmarshalBinary(data []byte) error` +UnmarshalBinary() restores the state of the PRNG from a byte array +that was created with MarshalBinary(). UnmarshalBinary implements the +encoding.BinaryUnmarshaler interface. diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod b/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod new file mode 100644 index 00000000000..79772dfe8d8 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/gno.mod @@ -0,0 +1 @@ +module gno.land/p/wyhaines/rand/isaac64 diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno new file mode 100644 index 00000000000..6f2d95150fc --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64.gno @@ -0,0 +1,429 @@ +// This is a port of the 64-bit version of the ISAAC cryptographically secure PRNG, originally +// based on the reference implementation found at https://burtleburtle.net/bob/rand/isaacafa.html +// +// ISAAC has excellent statistical properties, with long cycle times, and uniformly distributed, +// unbiased, and unpredictable number generation. It can not be distinguished from real random +// data, and in three decades of scrutiny, no practical attacks have been found. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the 64-bit ISAAC PRNG algorithm. This algorithm +// provides very strong statistical performance, and is cryptographically secure, while still +// being substantially faster than the default PCG implementation in `math/rand`. +// +// Note that the approach to seeing with ISAAC is very important for best results, and seeding with +// ISAAC is not as simple as seeding with a single uint64 value. The ISAAC algorithm requires a +// 256-element seed. If used for cryptographic purposes, this will likely require entropy generated +// off-chain for actual cryptographically secure seeding. For other purposes, however, one can +// utilize the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to generate +// any missing seeds if fewer than 256 are provided. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.58s +// ISAAC: 1000000 Uint64 generated in 8.95s +// ISAAC: 1000000 Uint32 generated in 7.66s +// Ratio: x1.74 times faster than PCG (uint64) +// Ratio: x2.03 times faster than PCG (uint32) +// +// Use it directly: +// +// prng = isaac.New() // pass 0 to 256 uint64 seeds; if fewer than 256 are provided, the rest +// // will be generated using the xorshiftr128plus PRNG. +// +// Or use it as a drop-in replacement for the default PRNT in Rand: +// +// source = isaac64.New() +// prng := rand.New(source) +package isaac64 + +import ( + "errors" + "math" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" + "gno.land/p/wyhaines/rand/xorshiftr128plus" +) + +const ( + RANDSIZL = 8 + RANDSIZ = 1 << RANDSIZL // 256 +) + +type ISAAC struct { + randrsl [256]uint64 + randcnt uint64 + mm [256]uint64 + aa, bb, cc uint64 + seed [256]uint64 +} + +// ISAAC requires a large, 256-element seed. This implementation will leverage the entropy +// package combined with the xorshiftr128plus PRNG to generate any missing seeds if fewer than +// the required number of arguments are provided. +func New(seeds ...uint64) *ISAAC { + isaac := &ISAAC{} + seed := [256]uint64{} + + index := 0 + for index = 0; index < len(seeds) && index < 256; index++ { + seed[index] = seeds[index] + } + + if index < 2 { + e := entropy.New() + for ; index < 2; index++ { + seed[index] = e.Value64() + } + } + + // Use the first two seeds as seeding inputs for xorshiftr128plus, in order to + // use it to provide any remaining missing seeds. + prng := xorshiftr128plus.New( + seed[0], + seed[1], + ) + for ; index < 256; index++ { + seed[index] = prng.Uint64() + } + isaac.Seed(seed) + return isaac +} + +// Reinitialize the generator with a new seed. A seed must be composed of 256 uint64. +func (isaac *ISAAC) Seed(seed [256]uint64) { + isaac.randrsl = seed + isaac.seed = seed + isaac.randinit(true) +} + +// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding. +func beUint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// bePutUint64() encodes a uint64 into a buffer of eight bytes. +func bePutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +// A label to identify the marshalled data. +var marshalISAACLabel = []byte("isaac:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (isaac *ISAAC) MarshalBinary() ([]byte, error) { + b := make([]byte, 6+2048*3+8*3+8) // 6 + 2048*3 + 8*3 + 8 == 6182 + copy(b, marshalISAACLabel) + offset := 6 + for i := 0; i < 256; i++ { + bePutUint64(b[offset:], isaac.seed[i]) + offset += 8 + } + for i := 0; i < 256; i++ { + bePutUint64(b[offset:], isaac.randrsl[i]) + offset += 8 + } + for i := 0; i < 256; i++ { + bePutUint64(b[offset:], isaac.mm[i]) + offset += 8 + } + bePutUint64(b[offset:], isaac.aa) + offset += 8 + bePutUint64(b[offset:], isaac.bb) + offset += 8 + bePutUint64(b[offset:], isaac.cc) + offset += 8 + bePutUint64(b[offset:], isaac.randcnt) + return b, nil +} + +// errUnmarshalISAAC is returned when unmarshalling fails. +var errUnmarshalISAAC = errors.New("invalid ISAAC encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (isaac *ISAAC) UnmarshalBinary(data []byte) error { + if len(data) != 6182 || string(data[:6]) != string(marshalISAACLabel) { + return errUnmarshalISAAC + } + offset := 6 + for i := 0; i < 256; i++ { + isaac.seed[i] = beUint64(data[offset:]) + offset += 8 + } + for i := 0; i < 256; i++ { + isaac.randrsl[i] = beUint64(data[offset:]) + offset += 8 + } + for i := 0; i < 256; i++ { + isaac.mm[i] = beUint64(data[offset:]) + offset += 8 + } + isaac.aa = beUint64(data[offset:]) + offset += 8 + isaac.bb = beUint64(data[offset:]) + offset += 8 + isaac.cc = beUint64(data[offset:]) + offset += 8 + isaac.randcnt = beUint64(data[offset:]) + return nil +} + +func (isaac *ISAAC) randinit(flag bool) { + var a, b, c, d, e, f, g, h uint64 + isaac.aa = 0 + isaac.bb = 0 + isaac.cc = 0 + + a = 0x9e3779b97f4a7c13 + b = 0x9e3779b97f4a7c13 + c = 0x9e3779b97f4a7c13 + d = 0x9e3779b97f4a7c13 + e = 0x9e3779b97f4a7c13 + f = 0x9e3779b97f4a7c13 + g = 0x9e3779b97f4a7c13 + h = 0x9e3779b97f4a7c13 + + // scramble it + for i := 0; i < 4; i++ { + mix(&a, &b, &c, &d, &e, &f, &g, &h) + } + + // fill in mm[] with messy stuff + for i := 0; i < RANDSIZ; i += 8 { + if flag { + a += isaac.randrsl[i] + b += isaac.randrsl[i+1] + c += isaac.randrsl[i+2] + d += isaac.randrsl[i+3] + e += isaac.randrsl[i+4] + f += isaac.randrsl[i+5] + g += isaac.randrsl[i+6] + h += isaac.randrsl[i+7] + } + mix(&a, &b, &c, &d, &e, &f, &g, &h) + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + + if flag { + // do a second pass to make all of the seed affect all of mm + for i := 0; i < RANDSIZ; i += 8 { + a += isaac.mm[i] + b += isaac.mm[i+1] + c += isaac.mm[i+2] + d += isaac.mm[i+3] + e += isaac.mm[i+4] + f += isaac.mm[i+5] + g += isaac.mm[i+6] + h += isaac.mm[i+7] + mix(&a, &b, &c, &d, &e, &f, &g, &h) + isaac.mm[i] = a + isaac.mm[i+1] = b + isaac.mm[i+2] = c + isaac.mm[i+3] = d + isaac.mm[i+4] = e + isaac.mm[i+5] = f + isaac.mm[i+6] = g + isaac.mm[i+7] = h + } + } + + isaac.isaac() + isaac.randcnt = RANDSIZ +} + +func mix(a, b, c, d, e, f, g, h *uint64) { + *a -= *e + *f ^= *h >> 9 + *h += *a + + *b -= *f + *g ^= *a << 9 + *a += *b + + *c -= *g + *h ^= *b >> 23 + *b += *c + + *d -= *h + *a ^= *c << 15 + *c += *d + + *e -= *a + *b ^= *d >> 14 + *d += *e + + *f -= *b + *c ^= *e << 20 + *e += *f + + *g -= *c + *d ^= *f >> 17 + *f += *g + + *h -= *d + *e ^= *g << 14 + *g += *h +} + +func ind(mm []uint64, x uint64) uint64 { + return mm[(x>>3)&(RANDSIZ-1)] +} + +func (isaac *ISAAC) isaac() { + var a, b, x, y uint64 + a = isaac.aa + b = isaac.bb + isaac.cc + 1 + isaac.cc++ + + m := isaac.mm[:] + r := isaac.randrsl[:] + + var i, m2Index int + + // First half + for i = 0; i < RANDSIZ/2; i++ { + m2Index = i + RANDSIZ/2 + switch i % 4 { + case 0: + a = ^(a ^ (a << 21)) + m[m2Index] + case 1: + a = (a ^ (a >> 5)) + m[m2Index] + case 2: + a = (a ^ (a << 12)) + m[m2Index] + case 3: + a = (a ^ (a >> 33)) + m[m2Index] + } + x = m[i] + y = ind(m, x) + a + b + m[i] = y + b = ind(m, y>>RANDSIZL) + x + r[i] = b + } + + // Second half + for i = RANDSIZ / 2; i < RANDSIZ; i++ { + m2Index = i - RANDSIZ/2 + switch i % 4 { + case 0: + a = ^(a ^ (a << 21)) + m[m2Index] + case 1: + a = (a ^ (a >> 5)) + m[m2Index] + case 2: + a = (a ^ (a << 12)) + m[m2Index] + case 3: + a = (a ^ (a >> 33)) + m[m2Index] + } + x = m[i] + y = ind(m, x) + a + b + m[i] = y + b = ind(m, y>>RANDSIZL) + x + r[i] = b + } + + isaac.bb = b + isaac.aa = a +} + +// Return a 64 bit random integer. +func (isaac *ISAAC) Uint64() uint64 { + if isaac.randcnt == 0 { + isaac.isaac() + isaac.randcnt = RANDSIZ + } + isaac.randcnt-- + return isaac.randrsl[isaac.randcnt] +} + +var gencycle int = 0 +var bufferFor32 uint64 = uint64(0) + +// Return a 32 bit random integer, composed of the high 32 bits of the generated 32 bit result. +func (isaac *ISAAC) Uint32() uint32 { + if gencycle == 0 { + bufferFor32 = isaac.Uint64() + gencycle = 1 + return uint32(bufferFor32 >> 32) + } + + gencycle = 0 + return uint32(bufferFor32 & 0xffffffff) +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkISAAC()' isaac64.gno +func benchmarkISAAC(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New() + + for i := 0; i < iterations; i++ { + _ = isaac.Uint64() + } + ufmt.Println(ufmt.Sprintf("ISAAC: generated %d uint64\n", iterations)) +} + +// The averageISAAC() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the ISAAC PRNG. +func averageISAAC(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + isaac := New(987654321987654321, 123456789987654321, 1, 997755331886644220) + + var average float64 = 0 + var squares []uint64 = make([]uint64, iterations) + for i := 0; i < iterations; i++ { + n := isaac.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("ISAAC average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("ISAAC standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("ISAAC theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno new file mode 100644 index 00000000000..239e7f818fb --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/isaac64/isaac64_test.gno @@ -0,0 +1,165 @@ +package isaac64 + +import ( + "math/rand" + "testing" +) + +type OpenISAAC struct { + Randrsl [256]uint64 + Randcnt uint64 + Mm [256]uint64 + Aa, Bb, Cc uint64 + Seed [256]uint64 +} + +func TestISAACSeeding(t *testing.T) { + isaac := New() +} + +func TestISAACRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + 0.9273376778618531, + 0.327620245173309, + 0.49315436150113456, + 0.9222536383598948, + 0.2999297342641162, + 0.4050531597269049, + 0.5321357451089953, + 0.19478000239059667, + 0.5156043950865713, + 0.9233494881511063, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestISAACUint64(t *testing.T) { + isaac := New() + + expected := []uint64{ + 6781932227698873623, + 14800945299485332986, + 4114322996297394168, + 5328012296808356526, + 12789214124608876433, + 17611101631239575547, + 6877490613942924608, + 15954522518901325556, + 14180160756719376887, + 4977949063252893357, + } + + for i, exp := range expected { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func dupState(i *ISAAC) *OpenISAAC { + state := &OpenISAAC{} + state.Seed = i.seed + state.Randrsl = i.randrsl + state.Mm = i.mm + state.Aa = i.aa + state.Bb = i.bb + state.Cc = i.cc + state.Randcnt = i.randcnt + + return state +} + +func TestISAACMarshalUnmarshal(t *testing.T) { + isaac := New() + + expected1 := []uint64{ + 6781932227698873623, + 14800945299485332986, + 4114322996297394168, + 5328012296808356526, + 12789214124608876433, + } + + expected2 := []uint64{ + 17611101631239575547, + 6877490613942924608, + 15954522518901325556, + 14180160756719376887, + 4977949063252893357, + } + + for i, exp := range expected1 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := isaac.MarshalBinary() + + t.Logf("State: [%v]\n", dupState(isaac)) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := dupState(isaac) + + if err != nil { + t.Errorf("ISAAC.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + isaac.Uint64() + + for i, exp := range expected2 { + val := isaac.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%v]\n", dupState(isaac)) + + // Now restore the state of the PRNG + err = isaac.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%v]\n", dupState(isaac)) + + if state_before.Seed != dupState(isaac).Seed { + t.Errorf("Seed mismatch") + } + if state_before.Randrsl != dupState(isaac).Randrsl { + t.Errorf("Randrsl mismatch") + } + if state_before.Mm != dupState(isaac).Mm { + t.Errorf("Mm mismatch") + } + if state_before.Aa != dupState(isaac).Aa { + t.Errorf("Aa mismatch") + } + if state_before.Bb != dupState(isaac).Bb { + t.Errorf("Bb mismatch") + } + if state_before.Cc != dupState(isaac).Cc { + t.Errorf("Cc mismatch") + } + if state_before.Randcnt != dupState(isaac).Randcnt { + t.Errorf("Randcnt mismatch") + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := isaac.Uint64() + if exp != val { + t.Errorf("ISAAC.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD b/examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD new file mode 100644 index 00000000000..00ed4412db0 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/README.MD @@ -0,0 +1,69 @@ +# package xorshift64star // import "gno.land/p/demo/math/rand/xorshift64star" + +Xorshift64* is a very fast psuedo-random number generation algorithm with strong +statistical properties. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the Xorshift64* PRNG algorithm. +This algorithm provides strong statistical performance with most seeds (just +don't seed it with zero), and the performance of this implementation in Gno is +more than four times faster than the default PCG implementation in `math/rand`. + + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.58s +Xorshift64*: 1000000 Uint64 generated in 3.77s +Ratio: x4.11 times faster than PCG +``` + +Use it directly: + +``` +prng = xorshift64star.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = xorshift64star.New() +prng := rand.New(source) +``` + +## TYPES + +``` +type Xorshift64Star struct { + // Has unexported fields. +} +``` + +Xorshift64Star is a PRNG that implements the Xorshift64* algorithm. + +`func New(seed ...uint64) *Xorshift64Star` + New() creates a new instance of the PRNG with a given seed, which should + be a uint64. If no seed is provided, the PRNG will be seeded via the + gno.land/p/demo/entropy package. + +`func (xs *Xorshift64Star) MarshalBinary() ([]byte, error)` + MarshalBinary() returns a byte array that encodes the state of the PRNG. + This can later be used with UnmarshalBinary() to restore the state of the + PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (xs *Xorshift64Star) Seed(seed ...uint64)` + Seed() implements the rand.Source interface. It provides a way to set the + seed for the PRNG. + +`func (xs *Xorshift64Star) Uint64() uint64` + Uint64() generates the next random uint64 value. + +`func (xs *Xorshift64Star) UnmarshalBinary(data []byte) error` + UnmarshalBinary() restores the state of the PRNG from a byte array + that was created with MarshalBinary(). UnmarshalBinary implements the + encoding.BinaryUnmarshaler interface. + diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod b/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod new file mode 100644 index 00000000000..7918a7e7d2d --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/gno.mod @@ -0,0 +1 @@ +module gno.land/p/wyhaines/rand/xorshift64star diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno new file mode 100644 index 00000000000..4934fe3a878 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star.gno @@ -0,0 +1,172 @@ +// Xorshift64* is a very fast psuedo-random number generation algorithm with strong +// statistical properties. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the Xorshift64* PRNG algorithm. This algorithm provides +// strong statistical performance with most seeds (just don't seed it with zero), and the performance +// of this implementation in Gno is more than four times faster than the default PCG implementation in +// `math/rand`. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.58s +// Xorshift64*: 1000000 Uint64 generated in 3.77s +// Ratio: x4.11 times faster than PCG +// +// Use it directly: +// +// prng = xorshift64star.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +// +// Or use it as a drop-in replacement for the default PRNT in Rand: +// +// source = xorshift64star.New() +// prng := rand.New(source) +package xorshift64star + +import ( + "errors" + "math" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" +) + +// Xorshift64Star is a PRNG that implements the Xorshift64* algorithm. +type Xorshift64Star struct { + seed uint64 +} + +// New() creates a new instance of the PRNG with a given seed, which +// should be a uint64. If no seed is provided, the PRNG will be seeded via the +// gno.land/p/demo/entropy package. +func New(seed ...uint64) *Xorshift64Star { + xs := &Xorshift64Star{} + xs.Seed(seed...) + return xs +} + +// Seed() implements the rand.Source interface. It provides a way to set the seed for the PRNG. +func (xs *Xorshift64Star) Seed(seed ...uint64) { + if len(seed) == 0 { + e := entropy.New() + xs.seed = e.Value64() + } else { + xs.seed = seed[0] + } +} + +// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding. +// binary.bigEndian.Uint64, copied to avoid dependency +func beUint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// bePutUint64() encodes a uint64 into a buffer of eight bytes. +// binary.bigEndian.PutUint64, copied to avoid dependency +func bePutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +// A label to identify the marshalled data. +var marshalXorshift64StarLabel = []byte("xorshift64*:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (xs *Xorshift64Star) MarshalBinary() ([]byte, error) { + b := make([]byte, 20) + copy(b, marshalXorshift64StarLabel) + bePutUint64(b[12:], xs.seed) + return b, nil +} + +// errUnmarshalXorshift64Star is returned when unmarshalling fails. +var errUnmarshalXorshift64Star = errors.New("invalid Xorshift64* encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (xs *Xorshift64Star) UnmarshalBinary(data []byte) error { + if len(data) != 20 || string(data[:12]) != string(marshalXorshift64StarLabel) { + return errUnmarshalXorshift64Star + } + xs.seed = beUint64(data[12:]) + return nil +} + +// Uint64() generates the next random uint64 value. +func (xs *Xorshift64Star) Uint64() uint64 { + xs.seed ^= xs.seed >> 12 + xs.seed ^= xs.seed << 25 + xs.seed ^= xs.seed >> 27 + xs.seed *= 2685821657736338717 + return xs.seed // Operations naturally wrap around in uint64 +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkXorshift64Star()' xorshift64star.gno +func benchmarkXorshift64Star(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs64s := New() + + for i := 0; i < iterations; i++ { + _ = xs64s.Uint64() + } + ufmt.Println(ufmt.Sprintf("Xorshift64*: generate %d uint64\n", iterations)) +} + +// The averageXorshift64Star() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the Xorshift64* PRNG. +func averageXorshift64Star(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs64s := New() + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := xs64s.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("Xorshift64* average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("Xorshift64* standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("Xorshift64* theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno new file mode 100644 index 00000000000..8a73bd9718d --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshift64star/xorshift64star_test.gno @@ -0,0 +1,134 @@ +package xorshift64star + +import ( + "math/rand" + "testing" +) + +func TestXorshift64StarSeeding(t *testing.T) { + xs64s := New() + value1 := xs64s.Uint64() + + xs64s = New(987654321) + value2 := xs64s.Uint64() + + if value1 != 5083824587905981259 || value2 != 18211065302896784785 || value1 == value2 { + t.Errorf("Expected 5083824587905981259 to be != to 18211065302896784785; got: %d == %d", value1, value2) + } +} + +func TestXorshift64StarRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + .8344002228310946, + 0.01777174153236205, + 0.23521769507865276, + 0.5387610198576143, + 0.631539862225968, + 0.9369068148346704, + 0.6387002315083188, + 0.5047507613688854, + 0.5208486273732391, + 0.25023746271541747, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestXorshift64StarUint64(t *testing.T) { + xs64s := New() + + expected := []uint64{ + 5083824587905981259, + 4607286371009545754, + 2070557085263023674, + 14094662988579565368, + 2910745910478213381, + 18037409026311016155, + 17169624916429864153, + 10459214929523155306, + 11840179828060641081, + 1198750959721587199, + } + + for i, exp := range expected { + val := xs64s.Uint64() + if exp != val { + t.Errorf("Xorshift64Star.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func TestXorshift64StarMarshalUnmarshal(t *testing.T) { + xs64s := New() + + expected1 := []uint64{ + 5083824587905981259, + 4607286371009545754, + 2070557085263023674, + 14094662988579565368, + 2910745910478213381, + } + + expected2 := []uint64{ + 18037409026311016155, + 17169624916429864153, + 10459214929523155306, + 11840179828060641081, + 1198750959721587199, + } + + for i, exp := range expected1 { + val := xs64s.Uint64() + if exp != val { + t.Errorf("Xorshift64Star.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := xs64s.MarshalBinary() + + t.Logf("Original State: [%x]\n", xs64s.seed) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := xs64s.seed + + if err != nil { + t.Errorf("Xorshift64Star.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + xs64s.Uint64() + + for i, exp := range expected2 { + val := xs64s.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%x]\n", xs64s.seed) + + // Now restore the state of the PRNG + err = xs64s.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%x]\n", xs64s.seed) + + if state_before != xs64s.seed { + t.Errorf("States before and after marshal/unmarshal are not equal; go %x and %x", state_before, xs64s.seed) + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := xs64s.Uint64() + if exp != val { + t.Errorf("Xorshift64Star.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD new file mode 100644 index 00000000000..444d1e1cdd9 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/README.MD @@ -0,0 +1,60 @@ +# package xorshiftr128plus // import "gno.land/p/demo/math/rand/xorshiftr128plus" + +Xorshiftr128+ is a very fast psuedo-random number generation algorithm with +strong statistical properties. + +The default random number algorithm in gno was ported from Go's v2 rand +implementatoon, which defaults to the PCG algorithm. This algorithm is +commonly used in language PRNG implementations because it has modest seeding +requirements, and generates statistically strong randomness. + +This package provides an implementation of the Xorshiftr128+ PRNG algorithm. +This algorithm provides strong statistical performance with most seeds (just +don't seed it with zeros), and the performance of this implementation in Gno is +more than four times faster than the default PCG implementation in `math/rand`. + +``` +Benchmark +--------- +PCG: 1000000 Uint64 generated in 15.48s +Xorshiftr128+: 1000000 Uint64 generated in 3.22s +Ratio: x4.81 times faster than PCG +``` + +Use it directly: + +``` +prng = xorshiftr128plus.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +``` + +Or use it as a drop-in replacement for the default PRNT in Rand: + +``` +source = xorshiftr128plus.New() +prng := rand.New(source) +``` + +## TYPES + +``` +type Xorshiftr128Plus struct { + // Has unexported fields. +} +``` + +`func New(seeds ...uint64) *Xorshiftr128Plus` + +`func (xs *Xorshiftr128Plus) MarshalBinary() ([]byte, error)` + MarshalBinary() returns a byte array that encodes the state of the PRNG. + This can later be used with UnmarshalBinary() to restore the state of the + PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface. + +`func (x *Xorshiftr128Plus) Seed(s1, s2 uint64)` + +`func (x *Xorshiftr128Plus) Uint64() uint64` + +`func (xs *Xorshiftr128Plus) UnmarshalBinary(data []byte) error` + UnmarshalBinary() restores the state of the PRNG from a byte array + that was created with MarshalBinary(). UnmarshalBinary implements the + encoding.BinaryUnmarshaler interface. + diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod new file mode 100644 index 00000000000..9f3be9ea8df --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/gno.mod @@ -0,0 +1 @@ +module gno.land/p/wyhaines/rand/xorshiftr128plus diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno new file mode 100644 index 00000000000..d950ab5108a --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus.gno @@ -0,0 +1,186 @@ +// Xorshiftr128+ is a very fast psuedo-random number generation algorithm with strong +// statistical properties. +// +// The default random number algorithm in gno was ported from Go's v2 rand implementatoon, which +// defaults to the PCG algorithm. This algorithm is commonly used in language PRNG implementations +// because it has modest seeding requirements, and generates statistically strong randomness. +// +// This package provides an implementation of the Xorshiftr128+ PRNG algorithm. This algorithm provides +// strong statistical performance with most seeds (just don't seed it with zeros), and the performance +// of this implementation in Gno is more than four times faster than the default PCG implementation in +// `math/rand`. +// +// Benchmark +// --------- +// PCG: 1000000 Uint64 generated in 15.48s +// Xorshiftr128+: 1000000 Uint64 generated in 3.22s +// Ratio: x4.81 times faster than PCG +// +// Use it directly: +// +// prng = xorshiftr128plus.New() // pass a uint64 to seed it or pass nothing to seed it with entropy +// +// Or use it as a drop-in replacement for the default PRNT in Rand: +// +// source = xorshiftr128plus.New() +// prng := rand.New(source) +package xorshiftr128plus + +import ( + "errors" + "math" + + "gno.land/p/demo/entropy" + "gno.land/p/demo/ufmt" +) + +type Xorshiftr128Plus struct { + seed [2]uint64 // Seeds +} + +func New(seeds ...uint64) *Xorshiftr128Plus { + var s1, s2 uint64 + seed_length := len(seeds) + if seed_length < 2 { + e := entropy.New() + if seed_length == 0 { + s1 = e.Value64() + s2 = e.Value64() + } else { + s1 = seeds[0] + s2 = e.Value64() + } + } else { + s1 = seeds[0] + s2 = seeds[1] + } + + prng := &Xorshiftr128Plus{} + prng.Seed(s1, s2) + return prng +} + +func (x *Xorshiftr128Plus) Seed(s1, s2 uint64) { + if s1 == 0 && s2 == 0 { + panic("Seeds must not both be zero") + } + x.seed[0] = s1 + x.seed[1] = s2 +} + +// beUint64() decodes a uint64 from a set of eight bytes, assuming big endian encoding. +// binary.bigEndian.Uint64, copied to avoid dependency +func beUint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// bePutUint64() encodes a uint64 into a buffer of eight bytes. +// binary.bigEndian.PutUint64, copied to avoid dependency +func bePutUint64(b []byte, v uint64) { + _ = b[7] // early bounds check to guarantee safety of writes below + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +// A label to identify the marshalled data. +var marshalXorshiftr128PlusLabel = []byte("xorshiftr128+:") + +// MarshalBinary() returns a byte array that encodes the state of the PRNG. This can later be used +// with UnmarshalBinary() to restore the state of the PRNG. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (xs *Xorshiftr128Plus) MarshalBinary() ([]byte, error) { + b := make([]byte, 30) + copy(b, marshalXorshiftr128PlusLabel) + bePutUint64(b[14:], xs.seed[0]) + bePutUint64(b[22:], xs.seed[1]) + return b, nil +} + +// errUnmarshalXorshiftr128Plus is returned when unmarshalling fails. +var errUnmarshalXorshiftr128Plus = errors.New("invalid Xorshiftr128Plus encoding") + +// UnmarshalBinary() restores the state of the PRNG from a byte array that was created with MarshalBinary(). +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (xs *Xorshiftr128Plus) UnmarshalBinary(data []byte) error { + if len(data) != 30 || string(data[:14]) != string(marshalXorshiftr128PlusLabel) { + return errUnmarshalXorshiftr128Plus + } + xs.seed[0] = beUint64(data[14:]) + xs.seed[1] = beUint64(data[22:]) + return nil +} + +func (x *Xorshiftr128Plus) Uint64() uint64 { + x0 := x.seed[0] + x1 := x.seed[1] + x.seed[0] = x1 + x0 ^= x0 << 23 + x0 ^= x0 >> 17 + x0 ^= x1 + x.seed[1] = x0 + x1 + return x.seed[1] +} + +// Until there is better benchmarking support in gno, you can test the performance of this PRNG with this function. +// This isn't perfect, since it will include the startup time of gno in the results, but this will give you a timing +// for generating a million random uint64 numbers on any unix based system: +// +// `time gno run -expr 'benchmarkXorshiftr128Plus()' xorshiftr128plus.gno +func benchmarkXorshiftr128Plus(_iterations ...int) { + iterations := 1000000 + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs128p := New() + + for i := 0; i < iterations; i++ { + _ = xs128p.Uint64() + } + ufmt.Println(ufmt.Sprintf("Xorshiftr128Plus: generate %d uint64\n", iterations)) +} + +// The averageXorshiftr128Plus() function is a simple benchmarking helper to demonstrate +// the most basic statistical property of the Xorshiftr128+ PRNG. +func averageXorshiftr128Plus(_iterations ...int) { + target := uint64(500000) + iterations := 1000000 + var squares [1000000]uint64 + + ufmt.Println( + ufmt.Sprintf( + "Averaging %d random numbers. The average should be very close to %d.\n", + iterations, + target)) + + if len(_iterations) > 0 { + iterations = _iterations[0] + } + xs128p := New() + + var average float64 = 0 + for i := 0; i < iterations; i++ { + n := xs128p.Uint64()%(target*2) + 1 + average += (float64(n) - average) / float64(i+1) + squares[i] = n + } + + sum_of_squares := uint64(0) + // transform numbers into their squares of the distance from the average + for i := 0; i < iterations; i++ { + difference := average - float64(squares[i]) + square := uint64(difference * difference) + sum_of_squares += square + } + + ufmt.Println(ufmt.Sprintf("Xorshiftr128+ average of %d uint64: %f\n", iterations, average)) + ufmt.Println(ufmt.Sprintf("Xorshiftr128+ standard deviation : %f\n", math.Sqrt(float64(sum_of_squares)/float64(iterations)))) + ufmt.Println(ufmt.Sprintf("Xorshiftr128+ theoretical perfect deviation: %f\n", (float64(target*2)-1)/math.Sqrt(12))) +} diff --git a/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno new file mode 100644 index 00000000000..c5d86edd073 --- /dev/null +++ b/examples/gno.land/p/wyhaines/rand/xorshiftr128plus/xorshiftr128plus_test.gno @@ -0,0 +1,142 @@ +package xorshiftr128plus + +import ( + "math/rand" + "testing" +) + +func TestXorshift64StarSeeding(t *testing.T) { + xs128p := New() + value1 := xs128p.Uint64() + + xs128p = New(987654321) + value2 := xs128p.Uint64() + + xs128p = New(987654321, 9876543210) + value3 := xs128p.Uint64() + + if value1 != 13970141264473760763 || + value2 != 17031892808144362974 || + value3 != 8285073084540510 || + value1 == value2 || + value2 == value3 || + value1 == value3 { + t.Errorf("Expected three different values: 13970141264473760763, 17031892808144362974, and 8285073084540510\n got: %d, %d, %d", value1, value2, value3) + } +} + +func TestXorshiftr128PlusRand(t *testing.T) { + source := New(987654321) + rng := rand.New(source) + + // Expected outputs for the first 5 random floats with the given seed + expected := []float64{ + 0.9199548549485674, + 0.0027491282372705816, + 0.31493362274701164, + 0.3531250819119609, + 0.09957852858060356, + 0.731941362705936, + 0.3476937688876708, + 0.1444018086140385, + 0.9106467321832331, + 0.8024870151488901, + } + + for i, exp := range expected { + val := rng.Float64() + if exp != val { + t.Errorf("Rand.Float64() at iteration %d: got %g, expected %g", i, val, exp) + } + } +} + +func TestXorshiftr128PlusUint64(t *testing.T) { + xs128p := New(987654321, 9876543210) + + expected := []uint64{ + 8285073084540510, + 97010855169053386, + 11353359435625603792, + 10289232744262291728, + 14019961444418950453, + 15829492476941720545, + 2764732928842099222, + 6871047144273883379, + 16142204260470661970, + 11803223757041229095, + } + + for i, exp := range expected { + val := xs128p.Uint64() + if exp != val { + t.Errorf("Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } +} + +func TestXorshiftr128PlusMarshalUnmarshal(t *testing.T) { + xs128p := New(987654321, 9876543210) + + expected1 := []uint64{ + 8285073084540510, + 97010855169053386, + 11353359435625603792, + 10289232744262291728, + 14019961444418950453, + } + + expected2 := []uint64{ + 15829492476941720545, + 2764732928842099222, + 6871047144273883379, + 16142204260470661970, + 11803223757041229095, + } + + for i, exp := range expected1 { + val := xs128p.Uint64() + if exp != val { + t.Errorf("Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d", i, val, exp) + } + } + + marshalled, err := xs128p.MarshalBinary() + + t.Logf("Original State: [%x]\n", xs128p.seed) + t.Logf("Marshalled State: [%x] -- %v\n", marshalled, err) + state_before := xs128p.seed + + if err != nil { + t.Errorf("Xorshiftr128Plus.MarshalBinary() error: %v", err) + } + + // Advance state by one number; then check the next 5. The expectation is that they _will_ fail. + xs128p.Uint64() + + for i, exp := range expected2 { + val := xs128p.Uint64() + if exp == val { + t.Errorf(" Iteration %d matched %d; which is from iteration %d; something strange is happening.", (i + 6), val, (i + 5)) + } + } + + t.Logf("State before unmarshall: [%x]\n", xs128p.seed) + + // Now restore the state of the PRNG + err = xs128p.UnmarshalBinary(marshalled) + + t.Logf("State after unmarshall: [%x]\n", xs128p.seed) + + if state_before != xs128p.seed { + t.Errorf("States before and after marshal/unmarshal are not equal; go %x and %x", state_before, xs128p.seed) + } + + // Now we should be back on track for the last 5 numbers + for i, exp := range expected2 { + val := xs128p.Uint64() + if exp != val { + t.Errorf("Xorshiftr128Plus.Uint64() at iteration %d: got %d, expected %d", (i + 5), val, exp) + } + } +} diff --git a/examples/gno.land/r/README.md b/examples/gno.land/r/README.md new file mode 100644 index 00000000000..b12a996d781 --- /dev/null +++ b/examples/gno.land/r/README.md @@ -0,0 +1,10 @@ +# `r/` + +This directory primarily contains realms. It further branches out into namespaces: +- `demo` - realms meant to demonstrate Gno functionality +- `docs` - realms meant to teach about specific packages and concepts +- `gnoland` - official gno.land realms +- `gov` - governance realms +- `sys` - system realms +- `x` - experimental realms +- `*` - can include personal namespaces, such as `manfred`, `leon`, etc. \ No newline at end of file diff --git a/examples/gno.land/r/demo/art/gnoface/gno.mod b/examples/gno.land/r/demo/art/gnoface/gno.mod index 072c98f3bd6..9465af6216a 100644 --- a/examples/gno.land/r/demo/art/gnoface/gno.mod +++ b/examples/gno.land/r/demo/art/gnoface/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/art/gnoface - -require ( - gno.land/p/demo/entropy v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/art/millipede/gno.mod b/examples/gno.land/r/demo/art/millipede/gno.mod index 7cd604206fa..3e5177efdcd 100644 --- a/examples/gno.land/r/demo/art/millipede/gno.mod +++ b/examples/gno.land/r/demo/art/millipede/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/art/millipede - -require ( - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/banktest/z_0_filetest.gno b/examples/gno.land/r/demo/banktest/z_0_filetest.gno index 4ea76bbe17a..5a8c8d70a48 100644 --- a/examples/gno.land/r/demo/banktest/z_0_filetest.gno +++ b/examples/gno.land/r/demo/banktest/z_0_filetest.gno @@ -1,6 +1,11 @@ +// Empty line between the directives is important for them to be parsed +// independently. :facepalm: + +// PKGPATH: gno.land/r/demo/bank1 + // SEND: 100000000ugnot -package main +package bank1 import ( "std" @@ -11,7 +16,7 @@ import ( func main() { // set up main address and banktest addr. banktestAddr := std.DerivePkgAddr("gno.land/r/demo/banktest") - mainaddr := std.DerivePkgAddr("main") + mainaddr := std.DerivePkgAddr("gno.land/r/demo/bank1") std.TestSetOrigCaller(mainaddr) std.TestSetOrigPkgAddr(banktestAddr) @@ -37,12 +42,12 @@ func main() { } // Output: -// main before: 300000000ugnot +// main before: 100000000ugnot // Deposit(): returned! -// main after: 250000000ugnot +// main after: 50000000ugnot // ## recent activity // -// * g17rgsdnfxzza0sdfsdma37sdwxagsz378833ca4 100000000ugnot sent, 50000000ugnot returned, at 2009-02-13 11:31pm UTC +// * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 50000000ugnot returned, at 2009-02-13 11:31pm UTC // // ## total deposits // 50000000ugnot diff --git a/examples/gno.land/r/demo/banktest/z_1_filetest.gno b/examples/gno.land/r/demo/banktest/z_1_filetest.gno index 8f9f7647036..39682d26330 100644 --- a/examples/gno.land/r/demo/banktest/z_1_filetest.gno +++ b/examples/gno.land/r/demo/banktest/z_1_filetest.gno @@ -1,4 +1,9 @@ -package main +// Empty line between the directives is important for them to be parsed +// independently. :facepalm: + +// PKGPATH: gno.land/r/demo/bank1 + +package bank1 import ( "std" diff --git a/examples/gno.land/r/demo/banktest/z_2_filetest.gno b/examples/gno.land/r/demo/banktest/z_2_filetest.gno index a0280e0d75b..e839f60354a 100644 --- a/examples/gno.land/r/demo/banktest/z_2_filetest.gno +++ b/examples/gno.land/r/demo/banktest/z_2_filetest.gno @@ -1,4 +1,9 @@ -package main +// Empty line between the directives is important for them to be parsed +// independently. :facepalm: + +// PKGPATH: gno.land/r/demo/bank1 + +package bank1 import ( "std" @@ -10,7 +15,7 @@ func main() { banktestAddr := std.DerivePkgAddr("gno.land/r/demo/banktest") // print main balance before. - mainaddr := std.DerivePkgAddr("main") + mainaddr := std.DerivePkgAddr("gno.land/r/demo/bank1") std.TestSetOrigCaller(mainaddr) banker := std.GetBanker(std.BankerTypeReadonly) @@ -34,12 +39,12 @@ func main() { } // Output: -// main before: 200000000ugnot +// main before: // Deposit(): returned! -// main after: 255000000ugnot +// main after: 55000000ugnot // ## recent activity // -// * g17rgsdnfxzza0sdfsdma37sdwxagsz378833ca4 100000000ugnot sent, 55000000ugnot returned, at 2009-02-13 11:31pm UTC +// * g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk 100000000ugnot sent, 55000000ugnot returned, at 2009-02-13 11:31pm UTC // // ## total deposits // 45000000ugnot diff --git a/examples/gno.land/r/demo/banktest/z_3_filetest.gno b/examples/gno.land/r/demo/banktest/z_3_filetest.gno index ca8717dfcc9..7b6758c3e4f 100644 --- a/examples/gno.land/r/demo/banktest/z_3_filetest.gno +++ b/examples/gno.land/r/demo/banktest/z_3_filetest.gno @@ -1,4 +1,9 @@ -package main +// Empty line between the directives is important for them to be parsed +// independently. :facepalm: + +// PKGPATH: gno.land/r/demo/bank1 + +package bank1 import ( "std" @@ -7,7 +12,7 @@ import ( func main() { banktestAddr := std.DerivePkgAddr("gno.land/r/demo/banktest") - mainaddr := std.DerivePkgAddr("main") + mainaddr := std.DerivePkgAddr("gno.land/r/demo/bank1") std.TestSetOrigCaller(mainaddr) banker := std.GetBanker(std.BankerTypeRealmSend) @@ -17,4 +22,4 @@ func main() { } // Error: -// can only send coins from realm that created banker "g17rgsdnfxzza0sdfsdma37sdwxagsz378833ca4", not "g1dv3435088tlrgggf745kaud0ptrkc9v42k8llz" +// can only send coins from realm that created banker "g1tnpdmvrmtgql8fmxgsq9rwtst5hsxahk3f05dk", not "g1dv3435088tlrgggf745kaud0ptrkc9v42k8llz" diff --git a/examples/gno.land/r/demo/bar20/bar20.gno b/examples/gno.land/r/demo/bar20/bar20.gno index 1d6ecd3d378..52f1baa7408 100644 --- a/examples/gno.land/r/demo/bar20/bar20.gno +++ b/examples/gno.land/r/demo/bar20/bar20.gno @@ -9,21 +9,21 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ufmt" + "gno.land/r/demo/grc20reg" ) var ( - banker *grc20.Banker // private banker. - Token grc20.Token // public safe-object. + Token, adm = grc20.NewToken("Bar", "BAR", 4) + UserTeller = Token.CallerTeller() ) func init() { - banker = grc20.NewBanker("Bar", "BAR", 4) - Token = banker.Token() + grc20reg.Register(Token.Getter(), "") } func Faucet() string { caller := std.PrevRealm().Addr() - if err := banker.Mint(caller, 1_000_000); err != nil { + if err := adm.Mint(caller, 1_000_000); err != nil { return "error: " + err.Error() } return "OK" @@ -35,7 +35,7 @@ func Render(path string) string { switch { case path == "": - return banker.RenderHome() // XXX: should be Token.RenderHome() + return Token.RenderHome() case c == 2 && parts[0] == "balance": owner := std.Address(parts[1]) balance := Token.BalanceOf(owner) diff --git a/examples/gno.land/r/demo/bar20/bar20_test.gno b/examples/gno.land/r/demo/bar20/bar20_test.gno index 20349258c1b..0561d13c865 100644 --- a/examples/gno.land/r/demo/bar20/bar20_test.gno +++ b/examples/gno.land/r/demo/bar20/bar20_test.gno @@ -13,7 +13,7 @@ func TestPackage(t *testing.T) { std.TestSetRealm(std.NewUserRealm(alice)) std.TestSetOrigCaller(alice) // XXX: should not need this - urequire.Equal(t, Token.BalanceOf(alice), uint64(0)) + urequire.Equal(t, UserTeller.BalanceOf(alice), uint64(0)) urequire.Equal(t, Faucet(), "OK") - urequire.Equal(t, Token.BalanceOf(alice), uint64(1_000_000)) + urequire.Equal(t, UserTeller.BalanceOf(alice), uint64(1_000_000)) } diff --git a/examples/gno.land/r/demo/bar20/gno.mod b/examples/gno.land/r/demo/bar20/gno.mod index 2ec82d7be0b..e8ede1ea44f 100644 --- a/examples/gno.land/r/demo/bar20/gno.mod +++ b/examples/gno.land/r/demo/bar20/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/bar20 - -require ( - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/boards/README.md b/examples/gno.land/r/demo/boards/README.md index a9b68ec9c92..174e1c242fc 100644 --- a/examples/gno.land/r/demo/boards/README.md +++ b/examples/gno.land/r/demo/boards/README.md @@ -8,8 +8,8 @@ name ["gno.land/r/demo/boards"](https://gno.land/r/demo/boards/) ## Build `gnokey`, create your account, and interact with Gno. NOTE: Where you see `-remote localhost:26657` here, that flag can be replaced -with `-remote test3.gno.land:26657` if you have $GNOT on the testnet. -(To use the testnet, also replace `-chainid dev` with `-chainid test3` .) +with `-remote gno.land:26657` if you have $GNOT on the testnet. +(To use the testnet, also replace `-chainid dev` with `-chainid portal-loop` .) ### Build `gnokey` (and other tools). @@ -58,7 +58,8 @@ your `ACCOUNT_ADDR` and `KEYNAME` Instead of editing `gno.land/genesis/genesis_balances.txt`, a more general solution (with more steps) is to run a local "faucet" and use the web browser to add $GNOT. (This can be done at any time.) -See this page: https://github.com/gnolang/gno/blob/master/gno.land/cmd/gnofaucet/README.md +See this page: https://github.com/gnolang/gno/blob/master/contribs/gnofaucet/README.md + ### Start the `gnoland` node. @@ -84,7 +85,7 @@ The `USERNAME` for posting can different than your `KEYNAME`. It is internally l ./build/gnokey maketx call -pkgpath "gno.land/r/demo/users" -func "Register" -args "" -args "USERNAME" -args "Profile description" -gas-fee "10000000ugnot" -gas-wanted "2000000" -send "200000000ugnot" -broadcast -chainid dev -remote 127.0.0.1:26657 KEYNAME ``` -Interactive documentation: https://test3.gno.land/r/demo/users?help&__func=Register +Interactive documentation: https://gno.land/r/demo/users$help&func=Register ### Create a board with a smart contract call. @@ -92,7 +93,7 @@ Interactive documentation: https://test3.gno.land/r/demo/users?help&__func=Regis ./build/gnokey maketx call -pkgpath "gno.land/r/demo/boards" -func "CreateBoard" -args "BOARDNAME" -gas-fee "1000000ugnot" -gas-wanted "10000000" -broadcast -chainid dev -remote localhost:26657 KEYNAME ``` -Interactive documentation: https://test3.gno.land/r/demo/boards?help&__func=CreateBoard +Interactive documentation: https://gno.land/r/demo/boards$help&func=CreateBoard Next, query for the permanent board ID by querying (you need this to create a new post): @@ -108,7 +109,7 @@ NOTE: If a board was created successfully, your SEQUENCE_NUMBER would have incre ./build/gnokey maketx call -pkgpath "gno.land/r/demo/boards" -func "CreateThread" -args BOARD_ID -args "Hello gno.land" -args "Text of the post" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote localhost:26657 KEYNAME ``` -Interactive documentation: https://test3.gno.land/r/demo/boards?help&__func=CreateThread +Interactive documentation: https://gno.land/r/demo/boards$help&func=CreateThread ### Create a comment to a post. @@ -116,7 +117,7 @@ Interactive documentation: https://test3.gno.land/r/demo/boards?help&__func=Crea ./build/gnokey maketx call -pkgpath "gno.land/r/demo/boards" -func "CreateReply" -args BOARD_ID -args "1" -args "1" -args "Nice to meet you too." -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid dev -remote localhost:26657 KEYNAME ``` -Interactive documentation: https://test3.gno.land/r/demo/boards?help&__func=CreateReply +Interactive documentation: https://gno.land/r/demo/boards$help&func=CreateReply ```bash ./build/gnokey query "vm/qrender" -data "gno.land/r/demo/boards:BOARDNAME/1" -remote localhost:26657 diff --git a/examples/gno.land/r/demo/boards/board.gno b/examples/gno.land/r/demo/boards/board.gno index a9cf56c2a91..9b9fb730c68 100644 --- a/examples/gno.land/r/demo/boards/board.gno +++ b/examples/gno.land/r/demo/boards/board.gno @@ -6,6 +6,7 @@ import ( "time" "gno.land/p/demo/avl" + "gno.land/p/moul/txlink" ) //---------------------------------------- @@ -134,7 +135,5 @@ func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string } func (board *Board) GetPostFormURL() string { - return "/r/demo/boards?help&__func=CreateThread" + - "&bid=" + board.id.String() + - "&body.type=textarea" + return txlink.Call("CreateThread", "bid", board.id.String()) } diff --git a/examples/gno.land/r/demo/boards/gno.mod b/examples/gno.land/r/demo/boards/gno.mod index 434ad019883..dffb96740fc 100644 --- a/examples/gno.land/r/demo/boards/gno.mod +++ b/examples/gno.land/r/demo/boards/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/boards - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/boards/post.gno b/examples/gno.land/r/demo/boards/post.gno index f35cf23628c..c6e23cd59d0 100644 --- a/examples/gno.land/r/demo/boards/post.gno +++ b/examples/gno.land/r/demo/boards/post.gno @@ -6,6 +6,7 @@ import ( "time" "gno.land/p/demo/avl" + "gno.land/p/moul/txlink" ) //---------------------------------------- @@ -155,27 +156,26 @@ func (post *Post) GetURL() string { } func (post *Post) GetReplyFormURL() string { - return "/r/demo/boards?help&__func=CreateReply" + - "&bid=" + post.board.id.String() + - "&threadid=" + post.threadID.String() + - "&postid=" + post.id.String() + - "&body.type=textarea" + return txlink.Call("CreateReply", + "bid", post.board.id.String(), + "threadid", post.threadID.String(), + "postid", post.id.String(), + ) } func (post *Post) GetRepostFormURL() string { - return "/r/demo/boards?help&__func=CreateRepost" + - "&bid=" + post.board.id.String() + - "&postid=" + post.id.String() + - "&title.type=textarea" + - "&body.type=textarea" + - "&dstBoardID.type=textarea" + return txlink.Call("CreateRepost", + "bid", post.board.id.String(), + "postid", post.id.String(), + ) } func (post *Post) GetDeleteFormURL() string { - return "/r/demo/boards?help&__func=DeletePost" + - "&bid=" + post.board.id.String() + - "&threadid=" + post.threadID.String() + - "&postid=" + post.id.String() + return txlink.Call("DeletePost", + "bid", post.board.id.String(), + "threadid", post.threadID.String(), + "postid", post.id.String(), + ) } func (post *Post) RenderSummary() string { diff --git a/examples/gno.land/r/demo/boards/z_0_filetest.gno b/examples/gno.land/r/demo/boards/z_0_filetest.gno index e20964d50b7..a649895cb01 100644 --- a/examples/gno.land/r/demo/boards/z_0_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_filetest.gno @@ -24,16 +24,18 @@ func main() { } // Output: -// \[[post](/r/demo/boards?help&__func=CreateThread&bid=1&body.type=textarea)] +// \[[post](/r/demo/boards$help&func=CreateThread&bid=1)] // // ---------------------------------------- // ## [First Post (title)](/r/demo/boards:test_board/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) // // ---------------------------------------- // ## [Second Post (title)](/r/demo/boards:test_board/2) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] (1 replies) (0 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/2) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] (1 replies) (0 reposts) +// +// diff --git a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno index 8555af0b576..7dd460500d6 100644 --- a/examples/gno.land/r/demo/boards/z_10_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_c_filetest.gno @@ -35,14 +35,15 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // > First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=2&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] // // ---------------------------------------------------- // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// diff --git a/examples/gno.land/r/demo/boards/z_10_filetest.gno b/examples/gno.land/r/demo/boards/z_10_filetest.gno index 548b5865f65..8a6d11c79cf 100644 --- a/examples/gno.land/r/demo/boards/z_10_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_filetest.gno @@ -33,7 +33,7 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // ---------------------------------------------------- // thread does not exist with id: 1 diff --git a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno index c114e769ab1..f64b4c84bba 100644 --- a/examples/gno.land/r/demo/boards/z_11_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_d_filetest.gno @@ -35,18 +35,19 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // > First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=2&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] // // ---------------------------------------------------- // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // > Edited: First reply of the First post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=2&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// diff --git a/examples/gno.land/r/demo/boards/z_11_filetest.gno b/examples/gno.land/r/demo/boards/z_11_filetest.gno index 4cbdeeca4c3..3f56293b3bd 100644 --- a/examples/gno.land/r/demo/boards/z_11_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_filetest.gno @@ -33,10 +33,11 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // ---------------------------------------------------- // # Edited: First Post in (title) // // Edited: Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] +// diff --git a/examples/gno.land/r/demo/boards/z_12_filetest.gno b/examples/gno.land/r/demo/boards/z_12_filetest.gno index 4ea75b27753..ac4adf6ee7b 100644 --- a/examples/gno.land/r/demo/boards/z_12_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_filetest.gno @@ -30,11 +30,13 @@ func main() { // Output: // 1 -// \[[post](/r/demo/boards?help&__func=CreateThread&bid=2&body.type=textarea)] +// \[[post](/r/demo/boards$help&func=CreateThread&bid=2)] // // ---------------------------------------- // Repost: Check this out // ## [First Post (title)](/r/demo/boards:test_board1/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (1 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board1/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (1 reposts) +// +// diff --git a/examples/gno.land/r/demo/boards/z_1_filetest.gno b/examples/gno.land/r/demo/boards/z_1_filetest.gno index ba0a277e2f1..4d46c81b83d 100644 --- a/examples/gno.land/r/demo/boards/z_1_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_1_filetest.gno @@ -26,3 +26,4 @@ func main() { // // * [/r/demo/boards:test_board_1](/r/demo/boards:test_board_1) // * [/r/demo/boards:test_board_2](/r/demo/boards:test_board_2) +// diff --git a/examples/gno.land/r/demo/boards/z_2_filetest.gno b/examples/gno.land/r/demo/boards/z_2_filetest.gno index f0d53204e38..31b39644b24 100644 --- a/examples/gno.land/r/demo/boards/z_2_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_2_filetest.gno @@ -32,7 +32,8 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=2&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// diff --git a/examples/gno.land/r/demo/boards/z_3_filetest.gno b/examples/gno.land/r/demo/boards/z_3_filetest.gno index 021ae10b825..0b2a2df2f91 100644 --- a/examples/gno.land/r/demo/boards/z_3_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_3_filetest.gno @@ -34,7 +34,8 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=2&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] +// diff --git a/examples/gno.land/r/demo/boards/z_4_filetest.gno b/examples/gno.land/r/demo/boards/z_4_filetest.gno index f0620c28c9d..b781e94e4db 100644 --- a/examples/gno.land/r/demo/boards/z_4_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_4_filetest.gno @@ -37,13 +37,14 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=2&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] // // > Second reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=4&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// // Realm: // switchrealm["gno.land/r/demo/users"] @@ -884,6 +885,25 @@ func main() { // "RefCount": "1" // } // } +// u[f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84]={ +// "ObjectInfo": { +// "ID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:84", +// "IsEscaped": true, +// "ModTime": "127", +// "RefCount": "6" +// }, +// "Value": { +// "T": { +// "@type": "/gno.RefType", +// "ID": "gno.land/r/demo/boards.Board" +// }, +// "V": { +// "@type": "/gno.RefValue", +// "Hash": "a88a9b837af217656ee27084309f7cd02cd94cb3", +// "ObjectID": "f6dbf411da22e67d74cd7ddba6a76cd7e14a4822:85" +// } +// } +// } // switchrealm["gno.land/r/demo/boards"] // switchrealm["gno.land/r/demo/users"] // switchrealm["gno.land/r/demo/users"] diff --git a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno index 176b1d89015..723e6a10204 100644 --- a/examples/gno.land/r/demo/boards/z_5_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_c_filetest.gno @@ -33,7 +33,8 @@ func main() { // # First Post (title) // // Body of the first post. (body) -// \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] +// \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] // // > Reply of the first post -// > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=2&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=2)] +// > \- [g1w3jhxapjta047h6lta047h6lta047h6laqcyu4](/r/demo/users:g1w3jhxapjta047h6lta047h6lta047h6laqcyu4), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=2)] +// diff --git a/examples/gno.land/r/demo/boards/z_5_filetest.gno b/examples/gno.land/r/demo/boards/z_5_filetest.gno index c326d961c91..712af483891 100644 --- a/examples/gno.land/r/demo/boards/z_5_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_filetest.gno @@ -33,11 +33,12 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=2&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] // // > Second reply of the second post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=4&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// diff --git a/examples/gno.land/r/demo/boards/z_6_filetest.gno b/examples/gno.land/r/demo/boards/z_6_filetest.gno index b7de2d08bf9..ec40cf5f8e9 100644 --- a/examples/gno.land/r/demo/boards/z_6_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_6_filetest.gno @@ -35,15 +35,16 @@ func main() { // # Second Post (title) // // Body of the second post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=2&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=2&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=2)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=2)] // // > Reply of the second post -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] // > // > > First reply of the first reply // > > -// > > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=5&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=5)] +// > > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] // // > Second reply of the second post // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=4&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=4)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/4) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=4)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=4)] +// diff --git a/examples/gno.land/r/demo/boards/z_7_filetest.gno b/examples/gno.land/r/demo/boards/z_7_filetest.gno index f1d41aa1723..353b84f6d87 100644 --- a/examples/gno.land/r/demo/boards/z_7_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_7_filetest.gno @@ -22,10 +22,12 @@ func main() { } // Output: -// \[[post](/r/demo/boards?help&__func=CreateThread&bid=1&body.type=textarea)] +// \[[post](/r/demo/boards$help&func=CreateThread&bid=1)] // // ---------------------------------------- // ## [First Post (title)](/r/demo/boards:test_board/1) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm UTC](/r/demo/boards:test_board/1) \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=1&postid=1)] (0 replies) (0 reposts) +// +// diff --git a/examples/gno.land/r/demo/boards/z_8_filetest.gno b/examples/gno.land/r/demo/boards/z_8_filetest.gno index 18ad64083f4..4896dfcfccf 100644 --- a/examples/gno.land/r/demo/boards/z_8_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_8_filetest.gno @@ -35,10 +35,11 @@ func main() { // _[see thread](/r/demo/boards:test_board/2)_ // // Reply of the second post -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=3&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=3)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/3) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=3)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=3)] // // _[see all 1 replies](/r/demo/boards:test_board/2/3)_ // // > First reply of the first reply // > -// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=2&postid=5&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=2&postid=5)] +// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/2/5) \[[reply](/r/demo/boards$help&func=CreateReply&bid=1&threadid=2&postid=5)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&threadid=2&postid=5)] +// diff --git a/examples/gno.land/r/demo/boards/z_9_filetest.gno b/examples/gno.land/r/demo/boards/z_9_filetest.gno index 10a1444fd35..ca37e306bda 100644 --- a/examples/gno.land/r/demo/boards/z_9_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_filetest.gno @@ -34,4 +34,5 @@ func main() { // # First Post in (title) // // Body of the first post. (body) -// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=2&threadid=1&postid=1&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=2&threadid=1&postid=1)] +// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:second_board/1/1) \[[reply](/r/demo/boards$help&func=CreateReply&bid=2&threadid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=2&threadid=1&postid=1)] +// diff --git a/examples/gno.land/r/demo/btree_dao/btree_dao.gno b/examples/gno.land/r/demo/btree_dao/btree_dao.gno new file mode 100644 index 00000000000..c90742eb29b --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/btree_dao.gno @@ -0,0 +1,209 @@ +package btree_dao + +import ( + "errors" + "std" + "strings" + "time" + + "gno.land/p/demo/btree" + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" +) + +// RegistrationDetails holds the details of a user's registration in the BTree DAO. +// It stores the user's address, registration time, their B-Tree if they planted one, +// and their NFT ID. +type RegistrationDetails struct { + Address std.Address + RegTime time.Time + UserBTree *btree.BTree + NFTID string +} + +// Less implements the btree.Record interface for RegistrationDetails. +// It compares two RegistrationDetails based on their registration time. +// Returns true if the current registration time is before the other registration time. +func (rd *RegistrationDetails) Less(than btree.Record) bool { + other := than.(*RegistrationDetails) + return rd.RegTime.Before(other.RegTime) +} + +var ( + dao = grc721.NewBasicNFT("BTree DAO", "BTDAO") + tokenID = 0 + members = btree.New() +) + +// PlantTree allows a user to plant their B-Tree in the DAO forest. +// It mints an NFT to the user and registers their tree in the DAO. +// Returns an error if the tree is already planted, empty, or if NFT minting fails. +func PlantTree(userBTree *btree.BTree) error { + return plantImpl(userBTree, "") +} + +// PlantSeed allows a user to register as a seed in the DAO with a message. +// It mints an NFT to the user and registers them as a seed member. +// Returns an error if the message is empty or if NFT minting fails. +func PlantSeed(message string) error { + return plantImpl(nil, message) +} + +// plantImpl is the internal implementation that handles both tree planting and seed registration. +// For tree planting (userBTree != nil), it verifies the tree isn't already planted and isn't empty. +// For seed planting (userBTree == nil), it verifies the seed message isn't empty. +// In both cases, it mints an NFT to the user and adds their registration details to the members tree. +// Returns an error if any validation fails or if NFT minting fails. +func plantImpl(userBTree *btree.BTree, seedMessage string) error { + // Get the caller's address + userAddress := std.GetOrigCaller() + + var nftID string + var regDetails *RegistrationDetails + + if userBTree != nil { + // Handle tree planting + var treeExists bool + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == userBTree { + treeExists = true + return false + } + return true + }) + if treeExists { + return errors.New("tree is already planted in the forest") + } + + if userBTree.Len() == 0 { + return errors.New("cannot plant an empty tree") + } + + nftID = ufmt.Sprintf("%d", tokenID) + regDetails = &RegistrationDetails{ + Address: userAddress, + RegTime: time.Now(), + UserBTree: userBTree, + NFTID: nftID, + } + } else { + // Handle seed planting + if seedMessage == "" { + return errors.New("seed message cannot be empty") + } + nftID = "seed_" + ufmt.Sprintf("%d", tokenID) + regDetails = &RegistrationDetails{ + Address: userAddress, + RegTime: time.Now(), + UserBTree: nil, + NFTID: nftID, + } + } + + // Mint an NFT to the user + err := dao.Mint(userAddress, grc721.TokenID(nftID)) + if err != nil { + return err + } + + members.Insert(regDetails) + tokenID++ + return nil +} + +// Render generates a Markdown representation of the DAO members. +// It displays: +// - Total number of NFTs minted +// - Total number of members +// - Size of the biggest planted tree +// - The first 3 members (OGs) +// - The latest 10 members +// Each member entry includes their address and owned NFTs (🌳 for trees, 🌱 for seeds). +// The path parameter is currently unused. +// Returns a formatted Markdown string. +func Render(path string) string { + var latestMembers []string + var ogMembers []string + + // Get total size and first member + totalSize := members.Len() + biggestTree := 0 + if maxMember := members.Max(); maxMember != nil { + if userBTree := maxMember.(*RegistrationDetails).UserBTree; userBTree != nil { + biggestTree = userBTree.Len() + } + } + + // Collect the latest 10 members + members.Descend(func(record btree.Record) bool { + if len(latestMembers) < 10 { + regDetails := record.(*RegistrationDetails) + addr := regDetails.Address + nftList := "" + balance, err := dao.BalanceOf(addr) + if err == nil && balance > 0 { + nftList = " (NFTs: " + for i := uint64(0); i < balance; i++ { + if i > 0 { + nftList += ", " + } + if regDetails.UserBTree == nil { + nftList += "🌱#" + regDetails.NFTID + } else { + nftList += "🌳#" + regDetails.NFTID + } + } + nftList += ")" + } + latestMembers = append(latestMembers, string(addr)+nftList) + return true + } + return false + }) + + // Collect the first 3 members (OGs) + members.Ascend(func(record btree.Record) bool { + if len(ogMembers) < 3 { + regDetails := record.(*RegistrationDetails) + addr := regDetails.Address + nftList := "" + balance, err := dao.BalanceOf(addr) + if err == nil && balance > 0 { + nftList = " (NFTs: " + for i := uint64(0); i < balance; i++ { + if i > 0 { + nftList += ", " + } + if regDetails.UserBTree == nil { + nftList += "🌱#" + regDetails.NFTID + } else { + nftList += "🌳#" + regDetails.NFTID + } + } + nftList += ")" + } + ogMembers = append(ogMembers, string(addr)+nftList) + return true + } + return false + }) + + var sb strings.Builder + + sb.WriteString(md.H1("B-Tree DAO Members")) + sb.WriteString(md.H2("Total NFTs Minted")) + sb.WriteString(ufmt.Sprintf("Total NFTs minted: %d\n\n", dao.TokenCount())) + sb.WriteString(md.H2("Member Stats")) + sb.WriteString(ufmt.Sprintf("Total members: %d\n", totalSize)) + if biggestTree > 0 { + sb.WriteString(ufmt.Sprintf("Biggest tree size: %d\n", biggestTree)) + } + sb.WriteString(md.H2("OG Members")) + sb.WriteString(md.BulletList(ogMembers)) + sb.WriteString(md.H2("Latest Members")) + sb.WriteString(md.BulletList(latestMembers)) + + return sb.String() +} diff --git a/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno b/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno new file mode 100644 index 00000000000..0514f52f7b4 --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno @@ -0,0 +1,97 @@ +package btree_dao + +import ( + "std" + "strings" + "testing" + "time" + + "gno.land/p/demo/btree" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func setupTest() { + std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y")) + members = btree.New() +} + +type TestElement struct { + value int +} + +func (te *TestElement) Less(than btree.Record) bool { + return te.value < than.(*TestElement).value +} + +func TestPlantTree(t *testing.T) { + setupTest() + + tree := btree.New() + elements := []int{30, 10, 50, 20, 40} + for _, val := range elements { + tree.Insert(&TestElement{value: val}) + } + + err := PlantTree(tree) + urequire.NoError(t, err) + + found := false + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == tree { + found = true + return false + } + return true + }) + uassert.True(t, found) + + err = PlantTree(tree) + uassert.Error(t, err) + + emptyTree := btree.New() + err = PlantTree(emptyTree) + uassert.Error(t, err) +} + +func TestPlantSeed(t *testing.T) { + setupTest() + + err := PlantSeed("Hello DAO!") + urequire.NoError(t, err) + + found := false + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == nil { + found = true + uassert.NotEmpty(t, regDetails.NFTID) + uassert.True(t, strings.Contains(regDetails.NFTID, "seed_")) + return false + } + return true + }) + uassert.True(t, found) + + err = PlantSeed("") + uassert.Error(t, err) +} + +func TestRegistrationDetailsOrdering(t *testing.T) { + setupTest() + + rd1 := &RegistrationDetails{ + Address: std.Address("test1"), + RegTime: time.Now(), + NFTID: "0", + } + rd2 := &RegistrationDetails{ + Address: std.Address("test2"), + RegTime: time.Now().Add(time.Hour), + NFTID: "1", + } + + uassert.True(t, rd1.Less(rd2)) + uassert.False(t, rd2.Less(rd1)) +} diff --git a/examples/gno.land/r/demo/btree_dao/gno.mod b/examples/gno.land/r/demo/btree_dao/gno.mod new file mode 100644 index 00000000000..01b99acc300 --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/btree_dao diff --git a/examples/gno.land/r/demo/counter/counter.gno b/examples/gno.land/r/demo/counter/counter.gno new file mode 100644 index 00000000000..43943e114dc --- /dev/null +++ b/examples/gno.land/r/demo/counter/counter.gno @@ -0,0 +1,14 @@ +package counter + +import "strconv" + +var counter int + +func Increment() int { + counter++ + return counter +} + +func Render(_ string) string { + return strconv.Itoa(counter) +} diff --git a/examples/gno.land/r/demo/counter/counter_test.gno b/examples/gno.land/r/demo/counter/counter_test.gno new file mode 100644 index 00000000000..352889f7e59 --- /dev/null +++ b/examples/gno.land/r/demo/counter/counter_test.gno @@ -0,0 +1,22 @@ +package counter + +import "testing" + +func TestIncrement(t *testing.T) { + counter = 0 + val := Increment() + if val != 1 { + t.Fatalf("result from Increment(): %d != 1", val) + } + if counter != val { + t.Fatalf("counter (%d) != val (%d)", counter, val) + } +} + +func TestRender(t *testing.T) { + counter = 1337 + res := Render("") + if res != "1337" { + t.Fatalf("render result %q != %q", res, "1337") + } +} diff --git a/examples/gno.land/r/demo/counter/gno.mod b/examples/gno.land/r/demo/counter/gno.mod new file mode 100644 index 00000000000..332d4e6da6a --- /dev/null +++ b/examples/gno.land/r/demo/counter/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/counter diff --git a/examples/gno.land/r/demo/daoweb/daoweb.gno b/examples/gno.land/r/demo/daoweb/daoweb.gno new file mode 100644 index 00000000000..d753a1ed32a --- /dev/null +++ b/examples/gno.land/r/demo/daoweb/daoweb.gno @@ -0,0 +1,116 @@ +package daoweb + +import ( + "std" + + "gno.land/p/demo/dao" + "gno.land/p/demo/json" + "gno.land/r/gov/dao/bridge" +) + +// Proposals returns the paginated GovDAO proposals +func Proposals(offset, count uint64) string { + var ( + propStore = bridge.GovDAO().GetPropStore() + size = propStore.Size() + ) + + // Get the props + props := propStore.Proposals(offset, count) + + resp := ProposalsResponse{ + Proposals: make([]Proposal, 0, count), + Total: uint64(size), + } + + for _, p := range props { + prop := Proposal{ + Author: p.Author(), + Description: p.Description(), + Status: p.Status(), + Stats: p.Stats(), + IsExpired: p.IsExpired(), + } + + resp.Proposals = append(resp.Proposals, prop) + } + + // Encode the response into JSON + encodedProps, err := json.Marshal(encodeProposalsResponse(resp)) + if err != nil { + panic(err) + } + + return string(encodedProps) +} + +// ProposalByID fetches the proposal using the given ID +func ProposalByID(id uint64) string { + propStore := bridge.GovDAO().GetPropStore() + + p, err := propStore.ProposalByID(id) + if err != nil { + panic(err) + } + + // Encode the response into JSON + prop := Proposal{ + Author: p.Author(), + Description: p.Description(), + Status: p.Status(), + Stats: p.Stats(), + IsExpired: p.IsExpired(), + } + + encodedProp, err := json.Marshal(encodeProposal(prop)) + if err != nil { + panic(err) + } + + return string(encodedProp) +} + +// encodeProposal encodes a proposal into a json node +func encodeProposal(p Proposal) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "author": json.StringNode("author", p.Author.String()), + "description": json.StringNode("description", p.Description), + "status": json.StringNode("status", p.Status.String()), + "stats": json.ObjectNode("stats", map[string]*json.Node{ + "yay_votes": json.NumberNode("yay_votes", float64(p.Stats.YayVotes)), + "nay_votes": json.NumberNode("nay_votes", float64(p.Stats.NayVotes)), + "abstain_votes": json.NumberNode("abstain_votes", float64(p.Stats.AbstainVotes)), + "total_voting_power": json.NumberNode("total_voting_power", float64(p.Stats.TotalVotingPower)), + }), + "is_expired": json.BoolNode("is_expired", p.IsExpired), + }) +} + +// encodeProposalsResponse encodes a proposal response into a JSON node +func encodeProposalsResponse(props ProposalsResponse) *json.Node { + proposals := make([]*json.Node, 0, len(props.Proposals)) + + for _, p := range props.Proposals { + proposals = append(proposals, encodeProposal(p)) + } + + return json.ObjectNode("", map[string]*json.Node{ + "proposals": json.ArrayNode("proposals", proposals), + "total": json.NumberNode("total", float64(props.Total)), + }) +} + +// ProposalsResponse is a paginated proposal response +type ProposalsResponse struct { + Proposals []Proposal `json:"proposals"` + Total uint64 `json:"total"` +} + +// Proposal is a single GovDAO proposal +type Proposal struct { + Author std.Address `json:"author"` + Description string `json:"description"` + Status dao.ProposalStatus `json:"status"` + Stats dao.Stats `json:"stats"` + IsExpired bool `json:"is_expired"` +} diff --git a/examples/gno.land/r/demo/daoweb/gno.mod b/examples/gno.land/r/demo/daoweb/gno.mod new file mode 100644 index 00000000000..74ae149cdb6 --- /dev/null +++ b/examples/gno.land/r/demo/daoweb/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/daoweb diff --git a/examples/gno.land/r/demo/disperse/gno.mod b/examples/gno.land/r/demo/disperse/gno.mod index 0ba9c88810a..06e81884dfa 100644 --- a/examples/gno.land/r/demo/disperse/gno.mod +++ b/examples/gno.land/r/demo/disperse/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/disperse - -require gno.land/r/demo/grc20factory v0.0.0-latest diff --git a/examples/gno.land/r/demo/disperse/z_0_filetest.gno b/examples/gno.land/r/demo/disperse/z_0_filetest.gno index 62a34cfdf26..ca1e9ea0ce8 100644 --- a/examples/gno.land/r/demo/disperse/z_0_filetest.gno +++ b/examples/gno.land/r/demo/disperse/z_0_filetest.gno @@ -1,3 +1,5 @@ +// PKGPATH: gno.land/r/demo/main + // SEND: 200ugnot package main @@ -10,7 +12,7 @@ import ( func main() { disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") - mainaddr := std.DerivePkgAddr("main") + mainaddr := std.DerivePkgAddr("gno.land/r/demo/main") std.TestSetOrigPkgAddr(disperseAddr) std.TestSetOrigCaller(mainaddr) @@ -28,5 +30,5 @@ func main() { } // Output: -// main before: 200000200ugnot -// main after: 200000000ugnot +// main before: 200ugnot +// main after: diff --git a/examples/gno.land/r/demo/disperse/z_1_filetest.gno b/examples/gno.land/r/demo/disperse/z_1_filetest.gno index 1e042d320f6..4c27c50749f 100644 --- a/examples/gno.land/r/demo/disperse/z_1_filetest.gno +++ b/examples/gno.land/r/demo/disperse/z_1_filetest.gno @@ -1,3 +1,5 @@ +// PKGPATH: gno.land/r/demo/main + // SEND: 300ugnot package main @@ -10,7 +12,7 @@ import ( func main() { disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") - mainaddr := std.DerivePkgAddr("main") + mainaddr := std.DerivePkgAddr("gno.land/r/demo/main") std.TestSetOrigPkgAddr(disperseAddr) std.TestSetOrigCaller(mainaddr) @@ -28,5 +30,5 @@ func main() { } // Output: -// main before: 200000300ugnot -// main after: 200000100ugnot +// main before: 300ugnot +// main after: 100ugnot diff --git a/examples/gno.land/r/demo/disperse/z_2_filetest.gno b/examples/gno.land/r/demo/disperse/z_2_filetest.gno index 163bb2fc1ab..79e8d81e2b1 100644 --- a/examples/gno.land/r/demo/disperse/z_2_filetest.gno +++ b/examples/gno.land/r/demo/disperse/z_2_filetest.gno @@ -1,3 +1,5 @@ +// PKGPATH: gno.land/r/demo/main + // SEND: 300ugnot package main @@ -10,7 +12,7 @@ import ( func main() { disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") - mainaddr := std.DerivePkgAddr("main") + mainaddr := std.DerivePkgAddr("gno.land/r/demo/main") std.TestSetOrigPkgAddr(disperseAddr) std.TestSetOrigCaller(mainaddr) diff --git a/examples/gno.land/r/demo/disperse/z_3_filetest.gno b/examples/gno.land/r/demo/disperse/z_3_filetest.gno index eabed52fb38..7cb7ffbe71d 100644 --- a/examples/gno.land/r/demo/disperse/z_3_filetest.gno +++ b/examples/gno.land/r/demo/disperse/z_3_filetest.gno @@ -1,3 +1,5 @@ +// PKGPATH: gno.land/r/demo/main + // SEND: 300ugnot package main @@ -11,7 +13,7 @@ import ( func main() { disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") - mainaddr := std.DerivePkgAddr("main") + mainaddr := std.DerivePkgAddr("gno.land/r/demo/main") beneficiary1 := std.Address("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0") beneficiary2 := std.Address("g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c") diff --git a/examples/gno.land/r/demo/disperse/z_4_filetest.gno b/examples/gno.land/r/demo/disperse/z_4_filetest.gno index ebf4bed4473..4dafb780e83 100644 --- a/examples/gno.land/r/demo/disperse/z_4_filetest.gno +++ b/examples/gno.land/r/demo/disperse/z_4_filetest.gno @@ -1,3 +1,5 @@ +// PKGPATH: gno.land/r/demo/main + // SEND: 300ugnot package main @@ -11,7 +13,7 @@ import ( func main() { disperseAddr := std.DerivePkgAddr("gno.land/r/demo/disperse") - mainaddr := std.DerivePkgAddr("main") + mainaddr := std.DerivePkgAddr("gno.land/r/demo/main") beneficiary1 := std.Address("g1dmt3sa5ucvecxuhf3j6ne5r0e3z4x7h6c03xc0") beneficiary2 := std.Address("g1akeqsvhucjt8gf5yupyzjxsjd29wv8fayng37c") diff --git a/examples/gno.land/r/demo/echo/gno.mod b/examples/gno.land/r/demo/echo/gno.mod index 4ca5ccab6e0..f07d78943d1 100644 --- a/examples/gno.land/r/demo/echo/gno.mod +++ b/examples/gno.land/r/demo/echo/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/echo - -require gno.land/p/demo/urequire v0.0.0-latest diff --git a/examples/gno.land/r/demo/emit/emit.gno b/examples/gno.land/r/demo/emit/emit.gno new file mode 100644 index 00000000000..a3de8f764a5 --- /dev/null +++ b/examples/gno.land/r/demo/emit/emit.gno @@ -0,0 +1,12 @@ +// Package emit demonstrates how to use the std.Emit() function +// to emit Gno events that can be used to track data changes off-chain. +// std.Emit is variadic; apart from the event name, it can take in any number of key-value pairs to emit. +package emit + +import ( + "std" +) + +func Emit(value string) { + std.Emit("EventName", "key", value) +} diff --git a/examples/gno.land/r/demo/emit/gno.mod b/examples/gno.land/r/demo/emit/gno.mod new file mode 100644 index 00000000000..cf9c2b6b98e --- /dev/null +++ b/examples/gno.land/r/demo/emit/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/emit diff --git a/examples/gno.land/r/demo/emit/z1_filetest.gno b/examples/gno.land/r/demo/emit/z1_filetest.gno new file mode 100644 index 00000000000..7dcdbf8e0a3 --- /dev/null +++ b/examples/gno.land/r/demo/emit/z1_filetest.gno @@ -0,0 +1,34 @@ +package main + +import "gno.land/r/demo/emit" + +func main() { + emit.Emit("foo") + emit.Emit("bar") +} + +// Events: +// [ +// { +// "type": "EventName", +// "attrs": [ +// { +// "key": "key", +// "value": "foo" +// } +// ], +// "pkg_path": "gno.land/r/demo/emit", +// "func": "Emit" +// }, +// { +// "type": "EventName", +// "attrs": [ +// { +// "key": "key", +// "value": "bar" +// } +// ], +// "pkg_path": "gno.land/r/demo/emit", +// "func": "Emit" +// } +// ] diff --git a/examples/gno.land/r/demo/event/event.gno b/examples/gno.land/r/demo/event/event.gno deleted file mode 100644 index 9e5de540734..00000000000 --- a/examples/gno.land/r/demo/event/event.gno +++ /dev/null @@ -1,9 +0,0 @@ -package event - -import ( - "std" -) - -func Emit(value string) { - std.Emit("TAG", "key", value) -} diff --git a/examples/gno.land/r/demo/event/gno.mod b/examples/gno.land/r/demo/event/gno.mod deleted file mode 100644 index 64987d43d79..00000000000 --- a/examples/gno.land/r/demo/event/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/demo/event diff --git a/examples/gno.land/r/demo/foo1155/gno.mod b/examples/gno.land/r/demo/foo1155/gno.mod index 0a405c5b4a2..eae12bcd1e3 100644 --- a/examples/gno.land/r/demo/foo1155/gno.mod +++ b/examples/gno.land/r/demo/foo1155/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/foo1155 - -require ( - gno.land/p/demo/grc/grc1155 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/foo20/foo20.gno b/examples/gno.land/r/demo/foo20/foo20.gno index 9d4e5d40193..6522fbdc90e 100644 --- a/examples/gno.land/r/demo/foo20/foo20.gno +++ b/examples/gno.land/r/demo/foo20/foo20.gno @@ -1,5 +1,5 @@ -// foo20 is a GRC20 token contract where all the GRC20 methods are proxified -// with top-level functions. see also gno.land/r/demo/bar20. +// foo20 is a GRC20 token contract where all the grc20.Teller methods are +// proxified with top-level functions. see also gno.land/r/demo/bar20. package foo20 import ( @@ -10,49 +10,50 @@ import ( "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" pusers "gno.land/p/demo/users" + "gno.land/r/demo/grc20reg" "gno.land/r/demo/users" ) var ( - banker *grc20.Banker - admin *ownable.Ownable - token grc20.Token + Token, privateLedger = grc20.NewToken("Foo", "FOO", 4) + UserTeller = Token.CallerTeller() + Ownable = ownable.NewWithAddress("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @manfred ) func init() { - admin = ownable.NewWithAddress("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @manfred - banker = grc20.NewBanker("Foo", "FOO", 4) - banker.Mint(admin.Owner(), 1000000*10000) // @administrator (1M) - token = banker.Token() + privateLedger.Mint(Ownable.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M) + grc20reg.Register(Token.Getter(), "") } -func TotalSupply() uint64 { return token.TotalSupply() } +func TotalSupply() uint64 { + return UserTeller.TotalSupply() +} func BalanceOf(owner pusers.AddressOrName) uint64 { ownerAddr := users.Resolve(owner) - return token.BalanceOf(ownerAddr) + return UserTeller.BalanceOf(ownerAddr) } func Allowance(owner, spender pusers.AddressOrName) uint64 { ownerAddr := users.Resolve(owner) spenderAddr := users.Resolve(spender) - return token.Allowance(ownerAddr, spenderAddr) + return UserTeller.Allowance(ownerAddr, spenderAddr) } func Transfer(to pusers.AddressOrName, amount uint64) { toAddr := users.Resolve(to) - checkErr(token.Transfer(toAddr, amount)) + checkErr(UserTeller.Transfer(toAddr, amount)) } func Approve(spender pusers.AddressOrName, amount uint64) { spenderAddr := users.Resolve(spender) - checkErr(token.Approve(spenderAddr, amount)) + checkErr(UserTeller.Approve(spenderAddr, amount)) } func TransferFrom(from, to pusers.AddressOrName, amount uint64) { fromAddr := users.Resolve(from) toAddr := users.Resolve(to) - checkErr(token.TransferFrom(fromAddr, toAddr, amount)) + checkErr(UserTeller.TransferFrom(fromAddr, toAddr, amount)) } // Faucet is distributing foo20 tokens without restriction (unsafe). @@ -60,19 +61,19 @@ func TransferFrom(from, to pusers.AddressOrName, amount uint64) { func Faucet() { caller := std.PrevRealm().Addr() amount := uint64(1_000 * 10_000) // 1k - checkErr(banker.Mint(caller, amount)) + checkErr(privateLedger.Mint(caller, amount)) } func Mint(to pusers.AddressOrName, amount uint64) { - admin.AssertCallerIsOwner() + Ownable.AssertCallerIsOwner() toAddr := users.Resolve(to) - checkErr(banker.Mint(toAddr, amount)) + checkErr(privateLedger.Mint(toAddr, amount)) } func Burn(from pusers.AddressOrName, amount uint64) { - admin.AssertCallerIsOwner() + Ownable.AssertCallerIsOwner() fromAddr := users.Resolve(from) - checkErr(banker.Burn(fromAddr, amount)) + checkErr(privateLedger.Burn(fromAddr, amount)) } func Render(path string) string { @@ -81,11 +82,11 @@ func Render(path string) string { switch { case path == "": - return banker.RenderHome() + return Token.RenderHome() case c == 2 && parts[0] == "balance": owner := pusers.AddressOrName(parts[1]) ownerAddr := users.Resolve(owner) - balance := banker.BalanceOf(ownerAddr) + balance := UserTeller.BalanceOf(ownerAddr) return ufmt.Sprintf("%d\n", balance) default: return "404\n" diff --git a/examples/gno.land/r/demo/foo20/foo20_test.gno b/examples/gno.land/r/demo/foo20/foo20_test.gno index 77c99d0525e..b9e80fbb476 100644 --- a/examples/gno.land/r/demo/foo20/foo20_test.gno +++ b/examples/gno.land/r/demo/foo20/foo20_test.gno @@ -12,7 +12,7 @@ import ( func TestReadOnlyPublicMethods(t *testing.T) { var ( - admin = pusers.AddressOrName("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + admin = pusers.AddressOrName("g1manfred47kzduec920z88wfr64ylksmdcedlf5") alice = pusers.AddressOrName(testutils.TestAddress("alice")) bob = pusers.AddressOrName(testutils.TestAddress("bob")) ) @@ -60,7 +60,7 @@ func TestReadOnlyPublicMethods(t *testing.T) { func TestErrConditions(t *testing.T) { var ( - admin = pusers.AddressOrName("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + admin = pusers.AddressOrName("g1manfred47kzduec920z88wfr64ylksmdcedlf5") alice = pusers.AddressOrName(testutils.TestAddress("alice")) empty = pusers.AddressOrName("") ) @@ -71,10 +71,18 @@ func TestErrConditions(t *testing.T) { fn func() } - std.TestSetOrigCaller(users.Resolve(admin)) + privateLedger.Mint(std.Address(admin), 10000) { tests := []test{ - {"Transfer(admin, 1)", "cannot send transfer to self", func() { Transfer(admin, 1) }}, + {"Transfer(admin, 1)", "cannot send transfer to self", func() { + // XXX: should replace with: Transfer(admin, 1) + // but there is currently a limitation in manipulating the frame stack and simulate + // calling this package from an outside point of view. + adminAddr := std.Address(admin) + if err := privateLedger.Transfer(adminAddr, adminAddr, 1); err != nil { + panic(err) + } + }}, {"Approve(empty, 1))", "invalid address", func() { Approve(empty, 1) }}, } for _, tc := range tests { diff --git a/examples/gno.land/r/demo/foo20/gno.mod b/examples/gno.land/r/demo/foo20/gno.mod index 4035f9b1200..79dea556e78 100644 --- a/examples/gno.land/r/demo/foo20/gno.mod +++ b/examples/gno.land/r/demo/foo20/gno.mod @@ -1,11 +1 @@ module gno.land/r/demo/foo20 - -require ( - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/foo721/gno.mod b/examples/gno.land/r/demo/foo721/gno.mod index e013677379d..4779f2fc467 100644 --- a/examples/gno.land/r/demo/foo721/gno.mod +++ b/examples/gno.land/r/demo/foo721/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/foo721 - -require ( - gno.land/p/demo/grc/grc721 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno new file mode 100644 index 00000000000..4dbbd6c7682 --- /dev/null +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -0,0 +1,309 @@ +package dice_roller + +import ( + "errors" + "math/rand" + "sort" + "std" + "strconv" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/entropy" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/users" +) + +type ( + // game represents a Dice Roller game between two players + game struct { + player1, player2 std.Address + roll1, roll2 int + } + + // player holds the information about each player including their stats + player struct { + addr std.Address + wins, losses, draws, points int + } + + // leaderBoard is a slice of players, used to sort players by rank + leaderBoard []player +) + +const ( + // Constants to represent game result outcomes + ongoing = iota + win + draw + loss +) + +var ( + games avl.Tree // AVL tree for storing game states + gameId seqid.ID // Sequence ID for games + + players avl.Tree // AVL tree for storing player data + + seed = uint64(entropy.New().Seed()) + r = rand.New(rand.NewPCG(seed, 0xdeadbeef)) +) + +// rollDice generates a random dice roll between 1 and 6 +func rollDice() int { + return r.IntN(6) + 1 +} + +// NewGame initializes a new game with the provided opponent's address +func NewGame(addr std.Address) int { + if !addr.IsValid() { + panic("invalid opponent's address") + } + + games.Set(gameId.Next().String(), &game{ + player1: std.PrevRealm().Addr(), + player2: addr, + }) + + return int(gameId) +} + +// Play allows a player to roll the dice and updates the game state accordingly +func Play(idx int) int { + g, err := getGame(idx) + if err != nil { + panic(err) + } + + roll := rollDice() // Random the player's dice roll + + // Play the game and update the player's roll + if err := g.play(std.PrevRealm().Addr(), roll); err != nil { + panic(err) + } + + // If both players have rolled, update the results and leaderboard + if g.isFinished() { + // If the player is playing against themselves, no points are awarded + if g.player1 == g.player2 { + return roll + } + + player1 := getPlayer(g.player1) + player2 := getPlayer(g.player2) + + if g.roll1 > g.roll2 { + player1.updateStats(win) + player2.updateStats(loss) + } else if g.roll2 > g.roll1 { + player2.updateStats(win) + player1.updateStats(loss) + } else { + player1.updateStats(draw) + player2.updateStats(draw) + } + } + + return roll +} + +// play processes a player's roll and updates their score +func (g *game) play(player std.Address, roll int) error { + if player != g.player1 && player != g.player2 { + return errors.New("invalid player") + } + + if g.isFinished() { + return errors.New("game over") + } + + if player == g.player1 && g.roll1 == 0 { + g.roll1 = roll + return nil + } + + if player == g.player2 && g.roll2 == 0 { + g.roll2 = roll + return nil + } + + return errors.New("already played") +} + +// isFinished checks if the game has ended +func (g *game) isFinished() bool { + return g.roll1 != 0 && g.roll2 != 0 +} + +// checkResult returns the game status as a formatted string +func (g *game) status() string { + if !g.isFinished() { + return resultIcon(ongoing) + " Game still in progress" + } + + if g.roll1 > g.roll2 { + return resultIcon(win) + " Player1 Wins !" + } else if g.roll2 > g.roll1 { + return resultIcon(win) + " Player2 Wins !" + } else { + return resultIcon(draw) + " It's a Draw !" + } +} + +// Render provides a summary of the current state of games and leader board +func Render(path string) string { + var sb strings.Builder + + sb.WriteString(`# 🎲 **Dice Roller Game** + +Welcome to Dice Roller! Challenge your friends to a simple yet exciting dice rolling game. Roll the dice and see who gets the highest score ! + +--- + +## **How to Play**: +1. **Create a game**: Challenge an opponent using [NewGame](./dice_roller$help&func=NewGame) +2. **Roll the dice**: Play your turn by rolling a dice using [Play](./dice_roller$help&func=Play) + +--- + +## **Scoring Rules**: +- **Win** 🏆: +3 points +- **Draw** 🤝: +1 point each +- **Lose** ❌: No points +- **Playing against yourself**: No points or stats changes for you + +--- + +## **Recent Games**: +Below are the results from the most recent games. Up to 10 recent games are displayed + +| Game | Player 1 | 🎲 Roll 1 | Player 2 | 🎲 Roll 2 | 🏆 Winner | +|------|----------|-----------|----------|-----------|-----------| +`) + + maxGames := 10 + for n := int(gameId); n > 0 && int(gameId)-n < maxGames; n-- { + g, err := getGame(n) + if err != nil { + continue + } + + sb.WriteString(strconv.Itoa(n) + " | " + + "" + shortName(g.player1) + "" + " | " + diceIcon(g.roll1) + " | " + + "" + shortName(g.player2) + "" + " | " + diceIcon(g.roll2) + " | " + + g.status() + "\n") + } + + sb.WriteString(` +--- + +## **Leaderboard**: +The top players are ranked by performance. Games played against oneself are not counted in the leaderboard + +| Rank | Player | Wins | Losses | Draws | Points | +|------|-----------------------|------|--------|-------|--------| +`) + + for i, player := range getLeaderBoard() { + sb.WriteString(ufmt.Sprintf("| %s | **%s** | %d | %d | %d | %d |\n", + rankIcon(i+1), + shortName(player.addr), + player.wins, + player.losses, + player.draws, + player.points, + )) + } + + sb.WriteString("\n---\n**Good luck and have fun !** 🎉") + return sb.String() +} + +// shortName returns a shortened name for the given address +func shortName(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user != nil { + return user.Name + } + if len(addr) < 10 { + return string(addr) + } + return string(addr)[:10] + "..." +} + +// getGame retrieves the game state by its ID +func getGame(idx int) (*game, error) { + v, ok := games.Get(seqid.ID(idx).String()) + if !ok { + return nil, errors.New("game not found") + } + return v.(*game), nil +} + +// updateResult updates the player's stats and points based on the game outcome +func (p *player) updateStats(result int) { + switch result { + case win: + p.wins++ + p.points += 3 + case loss: + p.losses++ + case draw: + p.draws++ + p.points++ + } +} + +// getPlayer retrieves a player or initializes a new one if they don't exist +func getPlayer(addr std.Address) *player { + v, ok := players.Get(addr.String()) + if !ok { + player := &player{ + addr: addr, + } + players.Set(addr.String(), player) + return player + } + + return v.(*player) +} + +// getLeaderBoard generates a leaderboard sorted by points +func getLeaderBoard() leaderBoard { + board := leaderBoard{} + players.Iterate("", "", func(key string, value interface{}) bool { + player := value.(*player) + board = append(board, *player) + return false + }) + + sort.Sort(board) + + return board +} + +// Methods for sorting the leaderboard +func (r leaderBoard) Len() int { + return len(r) +} + +func (r leaderBoard) Less(i, j int) bool { + if r[i].points != r[j].points { + return r[i].points > r[j].points + } + + if r[i].wins != r[j].wins { + return r[i].wins > r[j].wins + } + + if r[i].draws != r[j].draws { + return r[i].draws > r[j].draws + } + + return false +} + +func (r leaderBoard) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno new file mode 100644 index 00000000000..2f6770a366f --- /dev/null +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno @@ -0,0 +1,139 @@ +package dice_roller + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" +) + +var ( + player1 = testutils.TestAddress("alice") + player2 = testutils.TestAddress("bob") + unknownPlayer = testutils.TestAddress("unknown") +) + +// resetGameState resets the game state for testing +func resetGameState() { + games = avl.Tree{} + gameId = seqid.ID(0) + players = avl.Tree{} +} + +// TestNewGame tests the initialization of a new game +func TestNewGame(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2) + + // Verify that the game has been correctly initialized + g, err := getGame(gameID) + urequire.NoError(t, err) + urequire.Equal(t, player1.String(), g.player1.String()) + urequire.Equal(t, player2.String(), g.player2.String()) + urequire.Equal(t, 0, g.roll1) + urequire.Equal(t, 0, g.roll2) +} + +// TestPlay tests the dice rolling functionality for both players +func TestPlay(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2) + + g, err := getGame(gameID) + urequire.NoError(t, err) + + // Simulate rolling dice for player 1 + roll1 := Play(gameID) + + // Verify player 1's roll + urequire.NotEqual(t, 0, g.roll1) + urequire.Equal(t, g.roll1, roll1) + urequire.Equal(t, 0, g.roll2) // Player 2 hasn't rolled yet + + // Simulate rolling dice for player 2 + std.TestSetOrigCaller(player2) + roll2 := Play(gameID) + + // Verify player 2's roll + urequire.NotEqual(t, 0, g.roll2) + urequire.Equal(t, g.roll1, roll1) + urequire.Equal(t, g.roll2, roll2) +} + +// TestPlayAgainstSelf tests the scenario where a player plays against themselves +func TestPlayAgainstSelf(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player1) + + // Simulate rolling dice twice by the same player + roll1 := Play(gameID) + roll2 := Play(gameID) + + g, err := getGame(gameID) + urequire.NoError(t, err) + urequire.Equal(t, g.roll1, roll1) + urequire.Equal(t, g.roll2, roll2) +} + +// TestPlayInvalidPlayer tests the scenario where an invalid player tries to play +func TestPlayInvalidPlayer(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player1) + + // Attempt to play as an invalid player + std.TestSetOrigCaller(unknownPlayer) + urequire.PanicsWithMessage(t, "invalid player", func() { + Play(gameID) + }) +} + +// TestPlayAlreadyPlayed tests the scenario where a player tries to play again after already playing +func TestPlayAlreadyPlayed(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2) + + // Player 1 rolls + Play(gameID) + + // Player 1 tries to roll again + urequire.PanicsWithMessage(t, "already played", func() { + Play(gameID) + }) +} + +// TestPlayBeyondGameEnd tests that playing after both players have finished their rolls fails +func TestPlayBeyondGameEnd(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2) + + // Play for both players + std.TestSetOrigCaller(player1) + Play(gameID) + std.TestSetOrigCaller(player2) + Play(gameID) + + // Check if the game is over + g, err := getGame(gameID) + urequire.NoError(t, err) + + // Attempt to play more should fail + std.TestSetOrigCaller(player1) + urequire.PanicsWithMessage(t, "game over", func() { + Play(gameID) + }) +} diff --git a/examples/gno.land/r/demo/games/dice_roller/gno.mod b/examples/gno.land/r/demo/games/dice_roller/gno.mod new file mode 100644 index 00000000000..3aae9cbe791 --- /dev/null +++ b/examples/gno.land/r/demo/games/dice_roller/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/games/dice_roller diff --git a/examples/gno.land/r/demo/games/dice_roller/icon.gno b/examples/gno.land/r/demo/games/dice_roller/icon.gno new file mode 100644 index 00000000000..3417253e7b1 --- /dev/null +++ b/examples/gno.land/r/demo/games/dice_roller/icon.gno @@ -0,0 +1,55 @@ +package dice_roller + +import ( + "strconv" +) + +// diceIcon returns an icon of the dice roll +func diceIcon(roll int) string { + switch roll { + case 1: + return "🎲1" + case 2: + return "🎲2" + case 3: + return "🎲3" + case 4: + return "🎲4" + case 5: + return "🎲5" + case 6: + return "🎲6" + default: + return "❓" + } +} + +// resultIcon returns the icon representing the result of a game +func resultIcon(result int) string { + switch result { + case ongoing: + return "🔄" + case win: + return "🏆" + case loss: + return "❌" + case draw: + return "🤝" + default: + return "❓" + } +} + +// rankIcon returns the icon for a player's rank +func rankIcon(rank int) string { + switch rank { + case 1: + return "🥇" + case 2: + return "🥈" + case 3: + return "🥉" + default: + return strconv.Itoa(rank) + } +} diff --git a/examples/gno.land/r/demo/games/shifumi/gno.mod b/examples/gno.land/r/demo/games/shifumi/gno.mod new file mode 100644 index 00000000000..e6a428090a9 --- /dev/null +++ b/examples/gno.land/r/demo/games/shifumi/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/games/shifumi diff --git a/examples/gno.land/r/demo/games/shifumi/shifumi.gno b/examples/gno.land/r/demo/games/shifumi/shifumi.gno new file mode 100644 index 00000000000..3de09196da1 --- /dev/null +++ b/examples/gno.land/r/demo/games/shifumi/shifumi.gno @@ -0,0 +1,120 @@ +package shifumi + +import ( + "errors" + "std" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" + + "gno.land/r/demo/users" +) + +const ( + empty = iota + rock + paper + scissors + last +) + +type game struct { + player1, player2 std.Address // shifumi is a 2 players game + move1, move2 int // can be empty, rock, paper, or scissors +} + +var games avl.Tree +var id seqid.ID + +func (g *game) play(player std.Address, move int) error { + if !(move > empty && move < last) { + return errors.New("invalid move") + } + if player != g.player1 && player != g.player2 { + return errors.New("invalid player") + } + if player == g.player1 && g.move1 == empty { + g.move1 = move + return nil + } + if player == g.player2 && g.move2 == empty { + g.move2 = move + return nil + } + return errors.New("already played") +} + +func (g *game) winner() int { + if g.move1 == empty || g.move2 == empty { + return -1 + } + if g.move1 == g.move2 { + return 0 + } + if g.move1 == rock && g.move2 == scissors || + g.move1 == paper && g.move2 == rock || + g.move1 == scissors && g.move2 == paper { + return 1 + } + return 2 +} + +// NewGame creates a new game where player1 is the caller and player2 the argument. +// A new game index is returned. +func NewGame(player std.Address) int { + games.Set(id.Next().String(), &game{player1: std.PrevRealm().Addr(), player2: player}) + return int(id) +} + +// Play executes a move for the game at index idx, where move can be: +// 1 (rock), 2 (paper), 3 (scissors). +func Play(idx, move int) { + v, ok := games.Get(seqid.ID(idx).String()) + if !ok { + panic("game not found") + } + if err := v.(*game).play(std.PrevRealm().Addr(), move); err != nil { + panic(err) + } +} + +func Render(path string) string { + mov1 := []string{"", " 🤜 ", " 🫱 ", " 👉 "} + mov2 := []string{"", " 🤛 ", " 🫲 ", " 👈 "} + win := []string{"pending", "draw", "player1", "player2"} + + output := `# 👊 ✋ ✌️ Shifumi +Actions: +* [NewGame](shifumi$help&func=NewGame) opponentAddress +* [Play](shifumi$help&func=Play) gameIndex move (1=rock, 2=paper, 3=scissors) + + game | player1 | | player2 | | win + --- | --- | --- | --- | --- | --- +` + // Output the 100 most recent games. + maxGames := 100 + for n := int(id); n > 0 && int(id)-n < maxGames; n-- { + v, ok := games.Get(seqid.ID(n).String()) + if !ok { + continue + } + g := v.(*game) + output += strconv.Itoa(n) + " | " + + shortName(g.player1) + " | " + mov1[g.move1] + " | " + + shortName(g.player2) + " | " + mov2[g.move2] + " | " + + win[g.winner()+1] + "\n" + } + return output +} + +func shortName(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user != nil { + return user.Name + } + if len(addr) < 10 { + return string(addr) + } + return string(addr)[:10] + "..." +} diff --git a/examples/gno.land/r/demo/grc20factory/gno.mod b/examples/gno.land/r/demo/grc20factory/gno.mod index 8d0fbd0c46b..f89ee5872a5 100644 --- a/examples/gno.land/r/demo/grc20factory/gno.mod +++ b/examples/gno.land/r/demo/grc20factory/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/grc20factory - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory.gno b/examples/gno.land/r/demo/grc20factory/grc20factory.gno index f37a9370a9e..aa91084ab32 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory.gno @@ -8,10 +8,18 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" + "gno.land/r/demo/grc20reg" ) var instances avl.Tree // symbol -> instance +type instance struct { + token *grc20.Token + ledger *grc20.PrivateLedger + admin *ownable.Ownable + faucet uint64 // per-request amount. disabled if 0. +} + func New(name, symbol string, decimals uint, initialMint, faucet uint64) { caller := std.PrevRealm().Addr() NewWithAdmin(name, symbol, decimals, initialMint, faucet, caller) @@ -23,56 +31,68 @@ func NewWithAdmin(name, symbol string, decimals uint, initialMint, faucet uint64 panic("token already exists") } - banker := grc20.NewBanker(name, symbol, decimals) + token, ledger := grc20.NewToken(name, symbol, decimals) if initialMint > 0 { - banker.Mint(admin, initialMint) + ledger.Mint(admin, initialMint) } inst := instance{ - banker: banker, + token: token, + ledger: ledger, admin: ownable.NewWithAddress(admin), faucet: faucet, } - instances.Set(symbol, &inst) + grc20reg.Register(token.Getter(), symbol) } -type instance struct { - banker *grc20.Banker - admin *ownable.Ownable - faucet uint64 // per-request amount. disabled if 0. +func (inst instance) Token() *grc20.Token { + return inst.token +} + +func (inst instance) CallerTeller() grc20.Teller { + return inst.token.CallerTeller() } -func (inst instance) Token() grc20.Token { return inst.banker.Token() } +func Bank(symbol string) *grc20.Token { + inst := mustGetInstance(symbol) + return inst.token +} func TotalSupply(symbol string) uint64 { inst := mustGetInstance(symbol) - return inst.Token().TotalSupply() + return inst.token.ReadonlyTeller().TotalSupply() } func BalanceOf(symbol string, owner std.Address) uint64 { inst := mustGetInstance(symbol) - return inst.Token().BalanceOf(owner) + return inst.token.ReadonlyTeller().BalanceOf(owner) } func Allowance(symbol string, owner, spender std.Address) uint64 { inst := mustGetInstance(symbol) - return inst.Token().Allowance(owner, spender) + return inst.token.ReadonlyTeller().Allowance(owner, spender) } func Transfer(symbol string, to std.Address, amount uint64) { inst := mustGetInstance(symbol) - checkErr(inst.Token().Transfer(to, amount)) + caller := std.PrevRealm().Addr() + teller := inst.ledger.ImpersonateTeller(caller) + checkErr(teller.Transfer(to, amount)) } func Approve(symbol string, spender std.Address, amount uint64) { inst := mustGetInstance(symbol) - checkErr(inst.Token().Approve(spender, amount)) + caller := std.PrevRealm().Addr() + teller := inst.ledger.ImpersonateTeller(caller) + checkErr(teller.Approve(spender, amount)) } func TransferFrom(symbol string, from, to std.Address, amount uint64) { inst := mustGetInstance(symbol) - checkErr(inst.Token().TransferFrom(from, to, amount)) + caller := std.PrevRealm().Addr() + teller := inst.ledger.ImpersonateTeller(caller) + checkErr(teller.TransferFrom(from, to, amount)) } // faucet. @@ -84,19 +104,30 @@ func Faucet(symbol string) { // FIXME: add limits? // FIXME: add payment in gnot? caller := std.PrevRealm().Addr() - checkErr(inst.banker.Mint(caller, inst.faucet)) + checkErr(inst.ledger.Mint(caller, inst.faucet)) } func Mint(symbol string, to std.Address, amount uint64) { inst := mustGetInstance(symbol) inst.admin.AssertCallerIsOwner() - checkErr(inst.banker.Mint(to, amount)) + checkErr(inst.ledger.Mint(to, amount)) } func Burn(symbol string, from std.Address, amount uint64) { inst := mustGetInstance(symbol) inst.admin.AssertCallerIsOwner() - checkErr(inst.banker.Burn(from, amount)) + checkErr(inst.ledger.Burn(from, amount)) +} + +// instance admin functionality +func DropInstanceOwnership(symbol string) { + inst := mustGetInstance(symbol) + checkErr(inst.admin.DropOwnership()) +} + +func TransferInstanceOwnership(symbol string, newOwner std.Address) { + inst := mustGetInstance(symbol) + checkErr(inst.admin.TransferOwnership(newOwner)) } func Render(path string) string { @@ -109,12 +140,12 @@ func Render(path string) string { case c == 1: symbol := parts[0] inst := mustGetInstance(symbol) - return inst.banker.RenderHome() + return inst.token.RenderHome() case c == 3 && parts[1] == "balance": symbol := parts[0] inst := mustGetInstance(symbol) owner := std.Address(parts[2]) - balance := inst.Token().BalanceOf(owner) + balance := inst.token.CallerTeller().BalanceOf(owner) return ufmt.Sprintf("%d", balance) default: return "404\n" @@ -131,6 +162,6 @@ func mustGetInstance(symbol string) *instance { func checkErr(err error) { if err != nil { - panic(err) + panic(err.Error()) } } diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno b/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno index 5dfb6a760cc..46fc07fabf2 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory_test.gno @@ -4,16 +4,16 @@ import ( "std" "testing" + "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" ) func TestReadOnlyPublicMethods(t *testing.T) { - admin := std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") - manfred := std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") - unknown := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // valid but never used. - NewWithAdmin("Foo", "FOO", 4, 10_000*1_000_000, 0, admin) - NewWithAdmin("Bar", "BAR", 4, 10_000*1_000, 0, admin) - mustGetInstance("FOO").banker.Mint(manfred, 100_000_000) + std.TestSetOrigPkgAddr("gno.land/r/demo/grc20factory") + admin := testutils.TestAddress("admin") + bob := testutils.TestAddress("bob") + carl := testutils.TestAddress("carl") type test struct { name string @@ -21,36 +21,52 @@ func TestReadOnlyPublicMethods(t *testing.T) { fn func() uint64 } - // check balances #1. - { + checkBalances := func(step string, totSup, balAdm, balBob, allowAdmBob, balCarl uint64) { tests := []test{ - {"TotalSupply", 10_100_000_000, func() uint64 { return TotalSupply("FOO") }}, - {"BalanceOf(admin)", 10_000_000_000, func() uint64 { return BalanceOf("FOO", admin) }}, - {"BalanceOf(manfred)", 100_000_000, func() uint64 { return BalanceOf("FOO", manfred) }}, - {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance("FOO", admin, manfred) }}, - {"BalanceOf(unknown)", 0, func() uint64 { return BalanceOf("FOO", unknown) }}, + {"TotalSupply", totSup, func() uint64 { return TotalSupply("FOO") }}, + {"BalanceOf(admin)", balAdm, func() uint64 { return BalanceOf("FOO", admin) }}, + {"BalanceOf(bob)", balBob, func() uint64 { return BalanceOf("FOO", bob) }}, + {"Allowance(admin, bob)", allowAdmBob, func() uint64 { return Allowance("FOO", admin, bob) }}, + {"BalanceOf(carl)", balCarl, func() uint64 { return BalanceOf("FOO", carl) }}, } for _, tc := range tests { - uassert.Equal(t, tc.balance, tc.fn(), "balance does not match") + reason := ufmt.Sprintf("%s.%s - %s", step, tc.name, "balances do not match") + uassert.Equal(t, tc.balance, tc.fn(), reason) } } - return - // unknown uses the faucet. - std.TestSetOrigCaller(unknown) + // admin creates FOO and BAR. + std.TestSetOrigCaller(admin) + std.TestSetRealm(std.NewUserRealm(admin)) + NewWithAdmin("Foo", "FOO", 3, 1_111_111_000, 5_555, admin) + NewWithAdmin("Bar", "BAR", 3, 2_222_000, 6_666, admin) + checkBalances("step1", 1_111_111_000, 1_111_111_000, 0, 0, 0) + + // admin mints to bob. + mustGetInstance("FOO").ledger.Mint(bob, 333_333_000) + checkBalances("step2", 1_444_444_000, 1_111_111_000, 333_333_000, 0, 0) + + // carl uses the faucet. + std.TestSetOrigCaller(carl) + std.TestSetRealm(std.NewUserRealm(carl)) Faucet("FOO") + checkBalances("step3", 1_444_449_555, 1_111_111_000, 333_333_000, 0, 5_555) - // check balances #2. - { - tests := []test{ - {"TotalSupply", 10_110_000_000, func() uint64 { return TotalSupply("FOO") }}, - {"BalanceOf(admin)", 10_000_000_000, func() uint64 { return BalanceOf("FOO", admin) }}, - {"BalanceOf(manfred)", 100_000_000, func() uint64 { return BalanceOf("FOO", manfred) }}, - {"Allowance(admin, manfred)", 0, func() uint64 { return Allowance("FOO", admin, manfred) }}, - {"BalanceOf(unknown)", 10_000_000, func() uint64 { return BalanceOf("FOO", unknown) }}, - } - for _, tc := range tests { - uassert.Equal(t, tc.balance, tc.fn(), "balance does not match") - } - } + // admin gives to bob some allowance. + std.TestSetOrigCaller(admin) + std.TestSetRealm(std.NewUserRealm(admin)) + Approve("FOO", bob, 1_000_000) + checkBalances("step4", 1_444_449_555, 1_111_111_000, 333_333_000, 1_000_000, 5_555) + + // bob uses a part of the allowance. + std.TestSetOrigCaller(bob) + std.TestSetRealm(std.NewUserRealm(bob)) + TransferFrom("FOO", admin, carl, 400_000) + checkBalances("step5", 1_444_449_555, 1_110_711_000, 333_333_000, 600_000, 405_555) + + // bob uses a part of the allowance. + std.TestSetOrigCaller(bob) + std.TestSetRealm(std.NewUserRealm(bob)) + TransferFrom("FOO", admin, carl, 600_000) + checkBalances("step6", 1_444_449_555, 1_110_111_000, 333_333_000, 0, 1_005_555) } diff --git a/examples/gno.land/r/demo/grc20reg/gno.mod b/examples/gno.land/r/demo/grc20reg/gno.mod new file mode 100644 index 00000000000..c5065c60064 --- /dev/null +++ b/examples/gno.land/r/demo/grc20reg/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/grc20reg diff --git a/examples/gno.land/r/demo/grc20reg/grc20reg.gno b/examples/gno.land/r/demo/grc20reg/grc20reg.gno new file mode 100644 index 00000000000..ff46ec94860 --- /dev/null +++ b/examples/gno.land/r/demo/grc20reg/grc20reg.gno @@ -0,0 +1,76 @@ +package grc20reg + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/fqname" + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/ufmt" +) + +var registry = avl.NewTree() // rlmPath[.slug] -> TokenGetter (slug is optional) + +func Register(tokenGetter grc20.TokenGetter, slug string) { + rlmPath := std.PrevRealm().PkgPath() + key := fqname.Construct(rlmPath, slug) + registry.Set(key, tokenGetter) + std.Emit( + registerEvent, + "pkgpath", rlmPath, + "slug", slug, + ) +} + +func Get(key string) grc20.TokenGetter { + tokenGetter, ok := registry.Get(key) + if !ok { + return nil + } + return tokenGetter.(grc20.TokenGetter) +} + +func MustGet(key string) grc20.TokenGetter { + tokenGetter := Get(key) + if tokenGetter == nil { + panic("unknown token: " + key) + } + return tokenGetter +} + +func Render(path string) string { + switch { + case path == "": // home + // TODO: add pagination + s := "" + count := 0 + registry.Iterate("", "", func(key string, tokenI interface{}) bool { + count++ + tokenGetter := tokenI.(grc20.TokenGetter) + token := tokenGetter() + rlmPath, slug := fqname.Parse(key) + rlmLink := fqname.RenderLink(rlmPath, slug) + infoLink := "/r/demo/grc20reg:" + key + s += ufmt.Sprintf("- **%s** - %s - [info](%s)\n", token.GetName(), rlmLink, infoLink) + return false + }) + if count == 0 { + return "No registered token." + } + return s + default: // specific token + key := path + tokenGetter := MustGet(key) + token := tokenGetter() + rlmPath, slug := fqname.Parse(key) + rlmLink := fqname.RenderLink(rlmPath, slug) + s := ufmt.Sprintf("# %s\n", token.GetName()) + s += ufmt.Sprintf("- symbol: **%s**\n", token.GetSymbol()) + s += ufmt.Sprintf("- realm: %s\n", rlmLink) + s += ufmt.Sprintf("- decimals: %d\n", token.GetDecimals()) + s += ufmt.Sprintf("- total supply: %d\n", token.TotalSupply()) + return s + } +} + +const registerEvent = "register" diff --git a/examples/gno.land/r/demo/grc20reg/grc20reg_test.gno b/examples/gno.land/r/demo/grc20reg/grc20reg_test.gno new file mode 100644 index 00000000000..c93365ff7a1 --- /dev/null +++ b/examples/gno.land/r/demo/grc20reg/grc20reg_test.gno @@ -0,0 +1,59 @@ +package grc20reg + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/urequire" +) + +func TestRegistry(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/foo")) + realmAddr := std.CurrentRealm().PkgPath() + token, ledger := grc20.NewToken("TestToken", "TST", 4) + ledger.Mint(std.CurrentRealm().Addr(), 1234567) + tokenGetter := func() *grc20.Token { return token } + // register + Register(tokenGetter, "") + regTokenGetter := Get(realmAddr) + regToken := regTokenGetter() + urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil + urequire.Equal(t, regToken.GetSymbol(), "TST") + + expected := `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo) - [info](/r/demo/grc20reg:gno.land/r/demo/foo) +` + got := Render("") + urequire.True(t, strings.Contains(got, expected)) + // 404 + invalidToken := Get("0xdeadbeef") + urequire.True(t, invalidToken == nil) + + // register with a slug + Register(tokenGetter, "mySlug") + regTokenGetter = Get(realmAddr + ".mySlug") + regToken = regTokenGetter() + urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil + urequire.Equal(t, regToken.GetSymbol(), "TST") + + // override + Register(tokenGetter, "") + regTokenGetter = Get(realmAddr + "") + regToken = regTokenGetter() + urequire.True(t, regToken != nil, "expected to find a token") // fixme: use urequire.NotNil + urequire.Equal(t, regToken.GetSymbol(), "TST") + + got = Render("") + urequire.True(t, strings.Contains(got, `- **TestToken** - [gno.land/r/demo/foo](/r/demo/foo) - [info](/r/demo/grc20reg:gno.land/r/demo/foo)`)) + urequire.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)`)) + + expected = `# TestToken +- symbol: **TST** +- realm: [gno.land/r/demo/foo](/r/demo/foo).mySlug +- decimals: 4 +- total supply: 1234567 +` + got = Render("gno.land/r/demo/foo.mySlug") + urequire.Equal(t, expected, got) +} diff --git a/examples/gno.land/r/demo/groups/gno.mod b/examples/gno.land/r/demo/groups/gno.mod index fc6756e13e2..6f715471ced 100644 --- a/examples/gno.land/r/demo/groups/gno.mod +++ b/examples/gno.land/r/demo/groups/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/groups - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/groups/z_0_c_filetest.gno b/examples/gno.land/r/demo/groups/z_0_c_filetest.gno index cf5902928db..60600e38b78 100644 --- a/examples/gno.land/r/demo/groups/z_0_c_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_0_c_filetest.gno @@ -22,3 +22,4 @@ func main() { // List of all Groups: // // * [test_group](/r/demo/groups:test_group) +// diff --git a/examples/gno.land/r/demo/groups/z_1_a_filetest.gno b/examples/gno.land/r/demo/groups/z_1_a_filetest.gno index aeff9ab7774..71da1b966ec 100644 --- a/examples/gno.land/r/demo/groups/z_1_a_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_1_a_filetest.gno @@ -13,7 +13,7 @@ import ( var gid groups.GroupID -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main @@ -76,3 +76,5 @@ func main() { // Group Members: // // [0000000000, g1vahx7atnv4erxh6lta047h6lta047h6ll85gpy, 32, i am from UAE, 2009-02-13 23:31:30 +0000 UTC m=+1234567890.000000001], +// +// diff --git a/examples/gno.land/r/demo/groups/z_2_a_filetest.gno b/examples/gno.land/r/demo/groups/z_2_a_filetest.gno index d1cc53d612f..0c482e1b52f 100644 --- a/examples/gno.land/r/demo/groups/z_2_a_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_a_filetest.gno @@ -13,7 +13,7 @@ import ( var gid groups.GroupID -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main @@ -76,3 +76,5 @@ func main() { // Group Last MemberID: 0000000001 // // Group Members: +// +// diff --git a/examples/gno.land/r/demo/groups/z_2_e_filetest.gno b/examples/gno.land/r/demo/groups/z_2_e_filetest.gno index cbfff97c7a7..ff38acf45a4 100644 --- a/examples/gno.land/r/demo/groups/z_2_e_filetest.gno +++ b/examples/gno.land/r/demo/groups/z_2_e_filetest.gno @@ -21,3 +21,5 @@ func main() { // Output: // 1 // List of all Groups: +// +// diff --git a/examples/gno.land/r/demo/keystore/gno.mod b/examples/gno.land/r/demo/keystore/gno.mod index 49b0f3494a4..cd07d24adf6 100644 --- a/examples/gno.land/r/demo/keystore/gno.mod +++ b/examples/gno.land/r/demo/keystore/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/keystore - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/keystore/keystore_test.gno b/examples/gno.land/r/demo/keystore/keystore_test.gno index ffd8e60936f..9b5fafa2f95 100644 --- a/examples/gno.land/r/demo/keystore/keystore_test.gno +++ b/examples/gno.land/r/demo/keystore/keystore_test.gno @@ -11,7 +11,7 @@ import ( ) func TestRender(t *testing.T) { - const ( + var ( author1 std.Address = testutils.TestAddress("author1") author2 std.Address = testutils.TestAddress("author2") ) diff --git a/examples/gno.land/r/demo/math_eval/gno.mod b/examples/gno.land/r/demo/math_eval/gno.mod index 0e3fcfe6e9b..c797becfa7d 100644 --- a/examples/gno.land/r/demo/math_eval/gno.mod +++ b/examples/gno.land/r/demo/math_eval/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/math_eval - -require ( - gno.land/p/demo/math_eval/int32 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/memeland/gno.mod b/examples/gno.land/r/demo/memeland/gno.mod index 5c73379519b..0ccb353659f 100644 --- a/examples/gno.land/r/demo/memeland/gno.mod +++ b/examples/gno.land/r/demo/memeland/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/memeland - -require gno.land/p/demo/memeland v0.0.0-latest diff --git a/examples/gno.land/r/demo/microblog/gno.mod b/examples/gno.land/r/demo/microblog/gno.mod index 26349e481d4..a622200b76d 100644 --- a/examples/gno.land/r/demo/microblog/gno.mod +++ b/examples/gno.land/r/demo/microblog/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/microblog - -require ( - gno.land/p/demo/microblog v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/microblog/microblog_test.gno b/examples/gno.land/r/demo/microblog/microblog_test.gno index a3c8f04ee7f..9ad98d3cbfe 100644 --- a/examples/gno.land/r/demo/microblog/microblog_test.gno +++ b/examples/gno.land/r/demo/microblog/microblog_test.gno @@ -10,7 +10,7 @@ import ( ) func TestMicroblog(t *testing.T) { - const ( + var ( author1 std.Address = testutils.TestAddress("author1") author2 std.Address = testutils.TestAddress("author2") ) diff --git a/examples/gno.land/r/demo/mirror/doc.gno b/examples/gno.land/r/demo/mirror/doc.gno new file mode 100644 index 00000000000..40fdbd5bc26 --- /dev/null +++ b/examples/gno.land/r/demo/mirror/doc.gno @@ -0,0 +1,3 @@ +// Package mirror demonstrates that users can pass realm functions +// as arguments to other realms. +package mirror diff --git a/examples/gno.land/r/demo/mirror/gno.mod b/examples/gno.land/r/demo/mirror/gno.mod new file mode 100644 index 00000000000..cb53585644a --- /dev/null +++ b/examples/gno.land/r/demo/mirror/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/mirror diff --git a/examples/gno.land/r/demo/mirror/mirror.gno b/examples/gno.land/r/demo/mirror/mirror.gno new file mode 100644 index 00000000000..770fddc4fda --- /dev/null +++ b/examples/gno.land/r/demo/mirror/mirror.gno @@ -0,0 +1,33 @@ +package mirror + +import ( + "gno.land/p/demo/avl" +) + +var store avl.Tree + +func Register(pkgpath string, rndr func(string) string) { + if store.Has(pkgpath) { + return + } + + if rndr == nil { + return + } + + store.Set(pkgpath, rndr) +} + +func Render(path string) string { + if raw, ok := store.Get(path); ok { + return raw.(func(string) string)("") + } + + if store.Size() == 0 { + return "None are fair." + } + + return "Mirror, mirror on the wall, which realm's the fairest of them all?" +} + +// Credits to @jeronimoalbi diff --git a/examples/gno.land/r/demo/nft/gno.mod b/examples/gno.land/r/demo/nft/gno.mod index 89e0055be51..ad760d186ab 100644 --- a/examples/gno.land/r/demo/nft/gno.mod +++ b/examples/gno.land/r/demo/nft/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/nft - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/grc/grc721 v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/profile/gno.mod b/examples/gno.land/r/demo/profile/gno.mod index e7feac5d680..3e875672a99 100644 --- a/examples/gno.land/r/demo/profile/gno.mod +++ b/examples/gno.land/r/demo/profile/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/profile - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/mux v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/profile/profile.gno b/examples/gno.land/r/demo/profile/profile.gno index cc7d80e016d..1318e19eaf3 100644 --- a/examples/gno.land/r/demo/profile/profile.gno +++ b/examples/gno.land/r/demo/profile/profile.gno @@ -1,11 +1,11 @@ package profile import ( - "errors" "std" "gno.land/p/demo/avl" "gno.land/p/demo/mux" + "gno.land/p/demo/ufmt" ) var ( @@ -13,6 +13,7 @@ var ( router = mux.NewRouter() ) +// Standard fields const ( DisplayName = "DisplayName" Homepage = "Homepage" @@ -25,13 +26,28 @@ const ( InvalidField = "InvalidField" ) +// Events +const ( + ProfileFieldCreated = "ProfileFieldCreated" + ProfileFieldUpdated = "ProfileFieldUpdated" +) + +// Field types used when emitting event +const FieldType = "FieldType" + +const ( + BoolField = "BoolField" + StringField = "StringField" + IntField = "IntField" +) + func init() { router.HandleFunc("", homeHandler) router.HandleFunc("u/{addr}", profileHandler) router.HandleFunc("f/{addr}/{field}", fieldHandler) } -// list of supported string fields +// List of supported string fields var stringFields = map[string]bool{ DisplayName: true, Homepage: true, @@ -41,54 +57,61 @@ var stringFields = map[string]bool{ GravatarEmail: true, } -// list of support int fields +// List of support int fields var intFields = map[string]bool{ Age: true, } -// list of support bool fields +// List of support bool fields var boolFields = map[string]bool{ AvailableForHiring: true, } // Setters -func SetStringField(field, value string) error { +func SetStringField(field, value string) bool { addr := std.PrevRealm().Addr() - if _, ok := stringFields[field]; !ok { - return errors.New("invalid string field") + key := addr.String() + ":" + field + updated := fields.Set(key, value) + + event := ProfileFieldCreated + if updated { + event = ProfileFieldUpdated } - key := addr.String() + ":" + field - fields.Set(key, value) + std.Emit(event, FieldType, StringField, field, value) - return nil + return updated } -func SetIntField(field string, value int) error { +func SetIntField(field string, value int) bool { addr := std.PrevRealm().Addr() + key := addr.String() + ":" + field + updated := fields.Set(key, value) - if _, ok := intFields[field]; !ok { - return errors.New("invalid int field") + event := ProfileFieldCreated + if updated { + event = ProfileFieldUpdated } - key := addr.String() + ":" + field - fields.Set(key, value) + std.Emit(event, FieldType, IntField, field, string(value)) - return nil + return updated } -func SetBoolField(field string, value bool) error { +func SetBoolField(field string, value bool) bool { addr := std.PrevRealm().Addr() + key := addr.String() + ":" + field + updated := fields.Set(key, value) - if _, ok := boolFields[field]; !ok { - return errors.New("invalid bool field") + event := ProfileFieldCreated + if updated { + event = ProfileFieldUpdated } - key := addr.String() + ":" + field - fields.Set(key, value) + std.Emit(event, FieldType, BoolField, field, ufmt.Sprintf("%t", value)) - return nil + return updated } // Getters diff --git a/examples/gno.land/r/demo/profile/profile_test.gno b/examples/gno.land/r/demo/profile/profile_test.gno index 987632a594d..3947897289e 100644 --- a/examples/gno.land/r/demo/profile/profile_test.gno +++ b/examples/gno.land/r/demo/profile/profile_test.gno @@ -27,11 +27,15 @@ func TestStringFields(t *testing.T) { name := GetStringField(alice, DisplayName, "anon") uassert.Equal(t, "anon", name) - // Set - err := SetStringField(DisplayName, "Alice foo") - uassert.NoError(t, err) - err = SetStringField(Homepage, "https://example.com") - uassert.NoError(t, err) + // Set new key + updated := SetStringField(DisplayName, "Alice foo") + uassert.Equal(t, updated, false) + updated = SetStringField(Homepage, "https://example.com") + uassert.Equal(t, updated, false) + + // Update the key + updated = SetStringField(DisplayName, "Alice foo") + uassert.Equal(t, updated, true) // Get after setting name = GetStringField(alice, DisplayName, "anon") @@ -50,9 +54,13 @@ func TestIntFields(t *testing.T) { age := GetIntField(bob, Age, 25) uassert.Equal(t, 25, age) - // Set - err := SetIntField(Age, 30) - uassert.NoError(t, err) + // Set new key + updated := SetIntField(Age, 30) + uassert.Equal(t, updated, false) + + // Update the key + updated = SetIntField(Age, 30) + uassert.Equal(t, updated, true) // Get after setting age = GetIntField(bob, Age, 25) @@ -67,45 +75,28 @@ func TestBoolFields(t *testing.T) { uassert.Equal(t, false, hiring) // Set - err := SetBoolField(AvailableForHiring, true) - uassert.NoError(t, err) + updated := SetBoolField(AvailableForHiring, true) + uassert.Equal(t, updated, false) + + // Update the key + updated = SetBoolField(AvailableForHiring, true) + uassert.Equal(t, updated, true) // Get after setting hiring = GetBoolField(charlie, AvailableForHiring, false) uassert.Equal(t, true, hiring) } -func TestInvalidStringField(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(dave)) - - err := SetStringField(InvalidField, "test") - uassert.Error(t, err) -} - -func TestInvalidIntField(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(eve)) - - err := SetIntField(InvalidField, 123) - uassert.Error(t, err) -} - -func TestInvalidBoolField(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(frank)) - - err := SetBoolField(InvalidField, true) - uassert.Error(t, err) -} - func TestMultipleProfiles(t *testing.T) { // Set profile for user1 std.TestSetRealm(std.NewUserRealm(user1)) - err := SetStringField(DisplayName, "User One") - uassert.NoError(t, err) + updated := SetStringField(DisplayName, "User One") + uassert.Equal(t, updated, false) // Set profile for user2 std.TestSetRealm(std.NewUserRealm(user2)) - err = SetStringField(DisplayName, "User Two") - uassert.NoError(t, err) + updated = SetStringField(DisplayName, "User Two") + uassert.Equal(t, updated, false) // Get profiles std.TestSetRealm(std.NewUserRealm(user1)) // Switch back to user1 @@ -116,3 +107,36 @@ func TestMultipleProfiles(t *testing.T) { uassert.Equal(t, "User One", name1) uassert.Equal(t, "User Two", name2) } + +func TestArbitraryStringField(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(user1)) + + // Set arbitrary string field + updated := SetStringField("MyEmail", "my@email.com") + uassert.Equal(t, updated, false) + + val := GetStringField(user1, "MyEmail", "") + uassert.Equal(t, val, "my@email.com") +} + +func TestArbitraryIntField(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(user1)) + + // Set arbitrary int field + updated := SetIntField("MyIncome", 100_000) + uassert.Equal(t, updated, false) + + val := GetIntField(user1, "MyIncome", 0) + uassert.Equal(t, val, 100_000) +} + +func TestArbitraryBoolField(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(user1)) + + // Set arbitrary int field + updated := SetBoolField("IsWinner", true) + uassert.Equal(t, updated, false) + + val := GetBoolField(user1, "IsWinner", false) + uassert.Equal(t, val, true) +} diff --git a/examples/gno.land/r/demo/profile/render.gno b/examples/gno.land/r/demo/profile/render.gno index 79d1078a997..223839851dd 100644 --- a/examples/gno.land/r/demo/profile/render.gno +++ b/examples/gno.land/r/demo/profile/render.gno @@ -11,9 +11,9 @@ import ( const ( BaseURL = "/r/demo/profile" - SetStringFieldURL = BaseURL + "?help&__func=SetStringField&field=%s" - SetIntFieldURL = BaseURL + "?help&__func=SetIntField&field=%s" - SetBoolFieldURL = BaseURL + "?help&__func=SetBoolField&field=%s" + SetStringFieldURL = BaseURL + "$help&func=SetStringField&field=%s" + SetIntFieldURL = BaseURL + "$help&func=SetIntField&field=%s" + SetBoolFieldURL = BaseURL + "$help&func=SetBoolField&field=%s" ViewAllFieldsURL = BaseURL + ":u/%s" ViewFieldURL = BaseURL + ":f/%s/%s" ) diff --git a/examples/gno.land/r/demo/releases_example/gno.mod b/examples/gno.land/r/demo/releases_example/gno.mod index 22f640fe797..0dc5d6561dc 100644 --- a/examples/gno.land/r/demo/releases_example/gno.mod +++ b/examples/gno.land/r/demo/releases_example/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/releases_example - -require gno.land/p/demo/releases v0.0.0-latest diff --git a/examples/gno.land/r/demo/releases_example/releases0_filetest.gno b/examples/gno.land/r/demo/releases_example/releases0_filetest.gno index 193f9bdbc90..ca599a54892 100644 --- a/examples/gno.land/r/demo/releases_example/releases0_filetest.gno +++ b/examples/gno.land/r/demo/releases_example/releases0_filetest.gno @@ -49,3 +49,4 @@ func main() { // // * various improvements // * new shiny logo +// diff --git a/examples/gno.land/r/demo/tamagotchi/gno.mod b/examples/gno.land/r/demo/tamagotchi/gno.mod index b7a2deea2c2..bccf4841666 100644 --- a/examples/gno.land/r/demo/tamagotchi/gno.mod +++ b/examples/gno.land/r/demo/tamagotchi/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/tamagotchi - -require ( - gno.land/p/demo/tamagotchi v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/tamagotchi/realm.gno b/examples/gno.land/r/demo/tamagotchi/realm.gno index f8f62c9fc7a..f6d648180ed 100644 --- a/examples/gno.land/r/demo/tamagotchi/realm.gno +++ b/examples/gno.land/r/demo/tamagotchi/realm.gno @@ -43,10 +43,10 @@ func Heal() string { func Render(path string) string { tama := t.Markdown() links := `Actions: -* [Feed](/r/demo/tamagotchi?help&__func=Feed) -* [Play](/r/demo/tamagotchi?help&__func=Play) -* [Heal](/r/demo/tamagotchi?help&__func=Heal) -* [Reset](/r/demo/tamagotchi?help&__func=Reset) +* [Feed](/r/demo/tamagotchi$help&func=Feed) +* [Play](/r/demo/tamagotchi$help&func=Play) +* [Heal](/r/demo/tamagotchi$help&func=Heal) +* [Reset](/r/demo/tamagotchi$help&func=Reset) ` return tama + "\n\n" + links diff --git a/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno b/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno index e494ec5cbc8..4072c0b30d7 100644 --- a/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno +++ b/examples/gno.land/r/demo/tamagotchi/z0_filetest.gno @@ -19,7 +19,8 @@ func main() { // * sleepy: 0 // // Actions: -// * [Feed](/r/demo/tamagotchi?help&__func=Feed) -// * [Play](/r/demo/tamagotchi?help&__func=Play) -// * [Heal](/r/demo/tamagotchi?help&__func=Heal) -// * [Reset](/r/demo/tamagotchi?help&__func=Reset) +// * [Feed](/r/demo/tamagotchi$help&func=Feed) +// * [Play](/r/demo/tamagotchi$help&func=Play) +// * [Heal](/r/demo/tamagotchi$help&func=Heal) +// * [Reset](/r/demo/tamagotchi$help&func=Reset) +// diff --git a/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno b/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno index 97273f642de..1cc5a3f8e18 100644 --- a/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno +++ b/examples/gno.land/r/demo/tests/crossrealm/crossrealm.gno @@ -27,3 +27,31 @@ func Make1() *p_crossrealm.Container { B: local, } } + +type Fooer interface{ Foo() } + +var fooer Fooer + +func SetFooer(f Fooer) Fooer { + fooer = f + return fooer +} + +func GetFooer() Fooer { return fooer } + +func CallFooerFoo() { fooer.Foo() } + +type FooerGetter func() Fooer + +var fooerGetter FooerGetter + +func SetFooerGetter(fg FooerGetter) FooerGetter { + fooerGetter = fg + return fg +} + +func GetFooerGetter() FooerGetter { + return fooerGetter +} + +func CallFooerGetterFoo() { fooerGetter().Foo() } diff --git a/examples/gno.land/r/demo/tests/crossrealm/gno.mod b/examples/gno.land/r/demo/tests/crossrealm/gno.mod index 71a89ec2ec5..2f7f217d288 100644 --- a/examples/gno.land/r/demo/tests/crossrealm/gno.mod +++ b/examples/gno.land/r/demo/tests/crossrealm/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/tests/crossrealm - -require ( - gno.land/p/demo/tests/p_crossrealm v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno b/examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno new file mode 100644 index 00000000000..d412b6ee6b1 --- /dev/null +++ b/examples/gno.land/r/demo/tests/crossrealm_b/crossrealm.gno @@ -0,0 +1,25 @@ +package crossrealm_b + +import ( + "std" + + "gno.land/r/demo/tests/crossrealm" +) + +type fooer struct { + s string +} + +func (f *fooer) SetS(newVal string) { + f.s = newVal +} + +func (f *fooer) Foo() { + println("hello " + f.s + " cur=" + std.CurrentRealm().PkgPath() + " prev=" + std.PrevRealm().PkgPath()) +} + +var ( + Fooer = &fooer{s: "A"} + FooerGetter = func() crossrealm.Fooer { return Fooer } + FooerGetterBuilder = func() crossrealm.FooerGetter { return func() crossrealm.Fooer { return Fooer } } +) diff --git a/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod b/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod new file mode 100644 index 00000000000..236010c21b3 --- /dev/null +++ b/examples/gno.land/r/demo/tests/crossrealm_b/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/tests/crossrealm_b diff --git a/examples/gno.land/r/demo/tests/gno.mod b/examples/gno.land/r/demo/tests/gno.mod index c51571e7d04..f04aa5cf7bd 100644 --- a/examples/gno.land/r/demo/tests/gno.mod +++ b/examples/gno.land/r/demo/tests/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/tests - -require ( - gno.land/p/demo/nestedpkg v0.0.0-latest - gno.land/r/demo/tests/subtests v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/tests/test20/gno.mod b/examples/gno.land/r/demo/tests/test20/gno.mod new file mode 100644 index 00000000000..7a71668d2df --- /dev/null +++ b/examples/gno.land/r/demo/tests/test20/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/tests/test20 diff --git a/examples/gno.land/r/demo/tests/test20/test20.gno b/examples/gno.land/r/demo/tests/test20/test20.gno new file mode 100644 index 00000000000..9c4df58d1c4 --- /dev/null +++ b/examples/gno.land/r/demo/tests/test20/test20.gno @@ -0,0 +1,20 @@ +// Package test20 implements a deliberately insecure ERC20 token for testing purposes. +// The Test20 token allows anyone to mint any amount of tokens to any address, making +// it unsuitable for production use. The primary goal of this package is to facilitate +// testing and experimentation without any security measures or restrictions. +// +// WARNING: This token is highly insecure and should not be used in any +// production environment. It is intended solely for testing and +// educational purposes. +package test20 + +import ( + "gno.land/p/demo/grc/grc20" + "gno.land/r/demo/grc20reg" +) + +var Token, PrivateLedger = grc20.NewToken("Test20", "TST", 4) + +func init() { + grc20reg.Register(Token.Getter(), "") +} diff --git a/examples/gno.land/r/demo/tests/tests.gno b/examples/gno.land/r/demo/tests/tests.gno index 421ac6528c9..e7fde94ea08 100644 --- a/examples/gno.land/r/demo/tests/tests.gno +++ b/examples/gno.land/r/demo/tests/tests.gno @@ -50,6 +50,8 @@ type TestRealmObject struct { Field string } +var TestRealmObjectValue TestRealmObject + func ModifyTestRealmObject(t *TestRealmObject) { t.Field += "_modified" } diff --git a/examples/gno.land/r/demo/tests/z2_filetest.gno b/examples/gno.land/r/demo/tests/z2_filetest.gno new file mode 100644 index 00000000000..147d2c12c6c --- /dev/null +++ b/examples/gno.land/r/demo/tests/z2_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/tests" +) + +// When a single realm in the frames, PrevRealm returns the user +// When 2 or more realms in the frames, PrevRealm returns the second to last +func main() { + var ( + eoa = testutils.TestAddress("someone") + rTestsAddr = std.DerivePkgAddr("gno.land/r/demo/tests") + ) + std.TestSetOrigCaller(eoa) + println("tests.GetPrevRealm().Addr(): ", tests.GetPrevRealm().Addr()) + println("tests.GetRSubtestsPrevRealm().Addr(): ", tests.GetRSubtestsPrevRealm().Addr()) +} + +// Output: +// tests.GetPrevRealm().Addr(): g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk +// tests.GetRSubtestsPrevRealm().Addr(): g1gz4ycmx0s6ln2wdrsh4e00l9fsel2wskqa3snq diff --git a/examples/gno.land/r/demo/tests/z3_filetest.gno b/examples/gno.land/r/demo/tests/z3_filetest.gno new file mode 100644 index 00000000000..5430e7f7151 --- /dev/null +++ b/examples/gno.land/r/demo/tests/z3_filetest.gno @@ -0,0 +1,28 @@ +// PKGPATH: gno.land/r/demo/test_test +package test_test + +import ( + "std" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/tests" +) + +func main() { + var ( + eoa = testutils.TestAddress("someone") + rTestsAddr = std.DerivePkgAddr("gno.land/r/demo/tests") + ) + std.TestSetOrigCaller(eoa) + // Contrarily to z2_filetest.gno we EXPECT GetPrevRealms != eoa (#1704) + if addr := tests.GetPrevRealm().Addr(); addr != eoa { + println("want tests.GetPrevRealm().Addr ==", eoa, "got", addr) + } + // When 2 or more realms in the frames, it is also different + if addr := tests.GetRSubtestsPrevRealm().Addr(); addr != rTestsAddr { + println("want GetRSubtestsPrevRealm().Addr ==", rTestsAddr, "got", addr) + } +} + +// Output: +// want tests.GetPrevRealm().Addr == g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk got g1xufrdvnfk6zc9r0nqa23ld3tt2r5gkyvw76q63 diff --git a/examples/gno.land/r/demo/tests_foo/gno.mod b/examples/gno.land/r/demo/tests_foo/gno.mod index 226271ae4b0..e5a00113181 100644 --- a/examples/gno.land/r/demo/tests_foo/gno.mod +++ b/examples/gno.land/r/demo/tests_foo/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/tests_foo - -require gno.land/r/demo/tests v0.0.0-latest diff --git a/examples/gno.land/r/demo/todolist/gno.mod b/examples/gno.land/r/demo/todolist/gno.mod index 36909859a6f..acd336f1724 100644 --- a/examples/gno.land/r/demo/todolist/gno.mod +++ b/examples/gno.land/r/demo/todolist/gno.mod @@ -1,9 +1 @@ module gno.land/r/demo/todolist - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/todolist v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/types/gno.mod b/examples/gno.land/r/demo/types/gno.mod index 0e86e5d5676..c24f7ddbc93 100644 --- a/examples/gno.land/r/demo/types/gno.mod +++ b/examples/gno.land/r/demo/types/gno.mod @@ -1,3 +1 @@ module gno.land/r/demo/types - -require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/demo/ui/gno.mod b/examples/gno.land/r/demo/ui/gno.mod index 0ef5d9dd40e..591b0b93190 100644 --- a/examples/gno.land/r/demo/ui/gno.mod +++ b/examples/gno.land/r/demo/ui/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/ui - -require ( - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ui v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/userbook/gno.mod b/examples/gno.land/r/demo/userbook/gno.mod index 213586d12ee..bb709a39ed7 100644 --- a/examples/gno.land/r/demo/userbook/gno.mod +++ b/examples/gno.land/r/demo/userbook/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/userbook - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/mux v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/userbook/render.gno b/examples/gno.land/r/demo/userbook/render.gno new file mode 100644 index 00000000000..94f7567cbf4 --- /dev/null +++ b/examples/gno.land/r/demo/userbook/render.gno @@ -0,0 +1,40 @@ +// Package userbook demonstrates a small userbook system working with gnoweb +package userbook + +import ( + "strconv" + + "gno.land/r/demo/users" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +const usersLink = "/r/demo/users" + +func Render(path string) string { + p := pager.NewPager(signupsTree, 20, true) + page := p.MustGetPageByPath(path) + + out := "# Welcome to UserBook!\n\n" + + out += ufmt.Sprintf("## [Click here to sign up!](%s)\n\n", txlink.Call("SignUp")) + out += "---\n\n" + + for _, item := range page.Items { + signup := item.Value.(*Signup) + user := signup.address.String() + + if data := users.GetUserByAddress(signup.address); data != nil { + user = ufmt.Sprintf("[%s](%s:%s)", data.Name, usersLink, data.Name) + } + + out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", signup.ordinal, user, signup.timestamp.Format("January 2 2006, 03:04:04 PM")) + } + + out += "---\n\n" + out += "**Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "**\n\n" + out += page.Picker() + return out +} diff --git a/examples/gno.land/r/demo/userbook/userbook.gno b/examples/gno.land/r/demo/userbook/userbook.gno index c49bd90fa42..03027f064b0 100644 --- a/examples/gno.land/r/demo/userbook/userbook.gno +++ b/examples/gno.land/r/demo/userbook/userbook.gno @@ -1,158 +1,54 @@ -// This realm demonstrates a small userbook system working with gnoweb +// Package userbook demonstrates a small userbook system working with gnoweb package userbook import ( "std" - "strconv" + "time" "gno.land/p/demo/avl" - "gno.land/p/demo/mux" + "gno.land/p/demo/seqid" "gno.land/p/demo/ufmt" ) type Signup struct { - account string - height int64 + address std.Address + ordinal int + timestamp time.Time } -// signups - keep a slice of signed up addresses efficient pagination -var signups []Signup - -// tracker - keep track of who signed up var ( - tracker *avl.Tree - router *mux.Router + signupsTree = avl.NewTree() + tracker = avl.NewTree() + idCounter seqid.ID ) -const ( - defaultPageSize = 20 - pathArgument = "number" - subPath = "page/{" + pathArgument + "}" - signUpEvent = "SignUp" -) +const signUpEvent = "SignUp" func init() { - // Set up tracker tree - tracker = avl.NewTree() - - // Set up route handling - router = mux.NewRouter() - router.HandleFunc("", renderHelper) - router.HandleFunc(subPath, renderHelper) - - // Sign up the deployer - SignUp() + SignUp() // Sign up the deployer } func SignUp() string { // Get transaction caller - caller := std.PrevRealm().Addr().String() - height := std.GetHeight() + caller := std.PrevRealm().Addr() // Check if the user is already signed up - if _, exists := tracker.Get(caller); exists { - panic(caller + " is already signed up!") + if _, exists := tracker.Get(caller.String()); exists { + panic(caller.String() + " is already signed up!") } + now := time.Now() + // Sign up the user - tracker.Set(caller, struct{}{}) - signup := Signup{ + signupsTree.Set(idCounter.Next().String(), &Signup{ caller, - height, - } - - signups = append(signups, signup) - std.Emit(signUpEvent, "SignedUpAccount", signup.account) - - return ufmt.Sprintf("%s added to userbook up at block #%d!", signup.account, signup.height) -} - -func GetSignupsInRange(page, pageSize int) ([]Signup, int) { - if page < 1 { - panic("page number cannot be less than 1") - } - - if pageSize < 1 || pageSize > 50 { - panic("page size must be from 1 to 50") - } - - // Pagination - // Calculate indexes - startIndex := (page - 1) * pageSize - endIndex := startIndex + pageSize - - // If page does not contain any users - if startIndex >= len(signups) { - return nil, -1 - } - - // If page contains fewer users than the page size - if endIndex > len(signups) { - endIndex = len(signups) - } + signupsTree.Size(), + now, + }) - return signups[startIndex:endIndex], endIndex -} - -func renderHelper(res *mux.ResponseWriter, req *mux.Request) { - totalSignups := len(signups) - res.Write("# Welcome to UserBook!\n\n") - - // Get URL parameter - page, err := strconv.Atoi(req.GetVar("number")) - if err != nil { - page = 1 // render first page on bad input - } - - // Fetch paginated signups - fetchedSignups, endIndex := GetSignupsInRange(page, defaultPageSize) - // Handle empty page case - if len(fetchedSignups) == 0 { - res.Write("No users on this page!\n\n") - res.Write("---\n\n") - res.Write("[Back to Page #1](/r/demo/userbook:page/1)\n\n") - return - } - - // Write page title - res.Write(ufmt.Sprintf("## UserBook - Page #%d:\n\n", page)) - - // Write signups - pageStartIndex := defaultPageSize * (page - 1) - for i, signup := range fetchedSignups { - out := ufmt.Sprintf("#### User #%d - %s - signed up at Block #%d\n", pageStartIndex+i, signup.account, signup.height) - res.Write(out) - } + tracker.Set(caller.String(), struct{}{}) - res.Write("---\n\n") - - // Write UserBook info - latestSignupIndex := totalSignups - 1 - res.Write(ufmt.Sprintf("#### Total users: %d\n", totalSignups)) - res.Write(ufmt.Sprintf("#### Latest signup: User #%d at Block #%d\n", latestSignupIndex, signups[latestSignupIndex].height)) - - res.Write("---\n\n") - - // Write page number - res.Write(ufmt.Sprintf("You're viewing page #%d", page)) - - // Write navigation buttons - var prevPage string - var nextPage string - // If we are on any page that is not the first page - if page > 1 { - prevPage = ufmt.Sprintf(" - [Previous page](/r/demo/userbook:page/%d)", page-1) - } - - // If there are more pages after the current one - if endIndex < totalSignups { - nextPage = ufmt.Sprintf(" - [Next page](/r/demo/userbook:page/%d)\n\n", page+1) - } - - res.Write(prevPage) - res.Write(nextPage) -} + std.Emit(signUpEvent, "account", caller.String()) -func Render(path string) string { - return router.Render(path) + return ufmt.Sprintf("%s added to userbook! Timestamp: %s", caller.String(), now.Format(time.RFC822Z)) } diff --git a/examples/gno.land/r/demo/userbook/userbook_test.gno b/examples/gno.land/r/demo/userbook/userbook_test.gno deleted file mode 100644 index 8d10d381e08..00000000000 --- a/examples/gno.land/r/demo/userbook/userbook_test.gno +++ /dev/null @@ -1,79 +0,0 @@ -package userbook - -import ( - "std" - "strings" - "testing" - - "gno.land/p/demo/testutils" - "gno.land/p/demo/ufmt" -) - -func TestRender(t *testing.T) { - // Sign up 20 users + deployer - for i := 0; i < 20; i++ { - addrName := ufmt.Sprintf("test%d", i) - caller := testutils.TestAddress(addrName) - std.TestSetOrigCaller(caller) - SignUp() - } - - testCases := []struct { - name string - nextPage bool - prevPage bool - path string - expectedNumberOfUsers int - }{ - { - name: "1st page render", - nextPage: true, - prevPage: false, - path: "page/1", - expectedNumberOfUsers: 20, - }, - { - name: "2nd page render", - nextPage: false, - prevPage: true, - path: "page/2", - expectedNumberOfUsers: 1, - }, - { - name: "Invalid path render", - nextPage: true, - prevPage: false, - path: "page/invalidtext", - expectedNumberOfUsers: 20, - }, - { - name: "Empty Page", - nextPage: false, - prevPage: false, - path: "page/1000", - expectedNumberOfUsers: 0, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := Render(tc.path) - numUsers := countUsers(got) - - if tc.prevPage && !strings.Contains(got, "Previous page") { - t.Fatalf("expected to find Previous page, didn't find it") - } - if tc.nextPage && !strings.Contains(got, "Next page") { - t.Fatalf("expected to find Next page, didn't find it") - } - - if tc.expectedNumberOfUsers != numUsers { - t.Fatalf("expected %d, got %d users", tc.expectedNumberOfUsers, numUsers) - } - }) - } -} - -func countUsers(input string) int { - return strings.Count(input, "#### User #") -} diff --git a/examples/gno.land/r/demo/users/gno.mod b/examples/gno.land/r/demo/users/gno.mod index a2ee2ea86ba..4d7fd15d1cd 100644 --- a/examples/gno.land/r/demo/users/gno.mod +++ b/examples/gno.land/r/demo/users/gno.mod @@ -1,6 +1 @@ module gno.land/r/demo/users - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/users/preregister.gno b/examples/gno.land/r/demo/users/preregister.gno index a6377c54938..e87bb478d4e 100644 --- a/examples/gno.land/r/demo/users/preregister.gno +++ b/examples/gno.land/r/demo/users/preregister.gno @@ -26,6 +26,9 @@ var preRegisteredUsers = []struct { {"nt", "g15ge0ae9077eh40erwrn2eq0xw6wupwqthpv34l"}, // -> @r_nt {"sys", "g1r929wt2qplfawe4lvqv9zuwfdcz4vxdun7qh8l"}, // -> @r_sys {"x", "g164sdpew3c2t3rvxj3kmfv7c7ujlvcw2punzzuz"}, // -> @r_x + + // test1 user + {"test1", "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"}, // -> @test1 } func init() { diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index 9b8e93b579b..451afc7bf96 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -7,6 +7,8 @@ import ( "strings" "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/avlhelpers" "gno.land/p/demo/users" ) @@ -14,7 +16,7 @@ import ( // State var ( - admin std.Address = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @moul + admin std.Address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul restricted avl.Tree // Name -> true - restricted name name2User avl.Tree // Name -> *users.User @@ -255,6 +257,12 @@ func GetUserByAddressOrName(input users.AddressOrName) *users.User { return GetUserByAddress(std.Address(input)) } +// Get a list of user names starting from the given prefix. Limit the +// number of results to maxResults. (This can be used for a name search tool.) +func ListUsersByPrefix(prefix string, maxResults int) []string { + return avlhelpers.ListByteStringKeysByPrefix(&name2User, prefix, maxResults) +} + func Resolve(input users.AddressOrName) std.Address { name, isName := input.GetName() if !isName { @@ -294,9 +302,10 @@ var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`) //---------------------------------------- // Render main page -func Render(path string) string { +func Render(fullPath string) string { + path, _ := splitPathAndQuery(fullPath) if path == "" { - return renderHome() + return renderHome(fullPath) } else if len(path) >= 38 { // 39? 40? if path[:2] != "g1" { return "invalid address " + path @@ -316,12 +325,26 @@ func Render(path string) string { } } -func renderHome() string { +func renderHome(path string) string { doc := "" - name2User.Iterate("", "", func(key string, value interface{}) bool { - user := value.(*users.User) + + page := pager.NewPager(&name2User, 50, false).MustGetPageByPath(path) + + for _, item := range page.Items { + user := item.Value.(*users.User) doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n" - return false - }) + } + doc += "\n" + doc += page.Picker() return doc } + +func splitPathAndQuery(fullPath string) (string, string) { + parts := strings.SplitN(fullPath, "?", 2) + path := parts[0] + queryString := "" + if len(parts) > 1 { + queryString = "?" + parts[1] + } + return path, queryString +} diff --git a/examples/gno.land/r/demo/users/users_test.gno b/examples/gno.land/r/demo/users/users_test.gno new file mode 100644 index 00000000000..864793dc514 --- /dev/null +++ b/examples/gno.land/r/demo/users/users_test.gno @@ -0,0 +1,13 @@ +package users + +import ( + "testing" + + "gno.land/p/demo/uassert" +) + +func TestPreRegisteredTest1(t *testing.T) { + names := ListUsersByPrefix("test1", 1) + uassert.Equal(t, len(names), 1) + uassert.Equal(t, names[0], "test1") +} diff --git a/examples/gno.land/r/demo/users/z_10_filetest.gno b/examples/gno.land/r/demo/users/z_10_filetest.gno index 078058c0703..afeecffcc42 100644 --- a/examples/gno.land/r/demo/users/z_10_filetest.gno +++ b/examples/gno.land/r/demo/users/z_10_filetest.gno @@ -8,7 +8,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func init() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_11_filetest.gno b/examples/gno.land/r/demo/users/z_11_filetest.gno index 603d63f371d..27c7e9813da 100644 --- a/examples/gno.land/r/demo/users/z_11_filetest.gno +++ b/examples/gno.land/r/demo/users/z_11_filetest.gno @@ -8,7 +8,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_11b_filetest.gno b/examples/gno.land/r/demo/users/z_11b_filetest.gno index 5e661e8f8c1..be508963911 100644 --- a/examples/gno.land/r/demo/users/z_11b_filetest.gno +++ b/examples/gno.land/r/demo/users/z_11b_filetest.gno @@ -8,7 +8,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_12_filetest.gno b/examples/gno.land/r/demo/users/z_12_filetest.gno new file mode 100644 index 00000000000..0fb7d27bd34 --- /dev/null +++ b/examples/gno.land/r/demo/users/z_12_filetest.gno @@ -0,0 +1,49 @@ +package main + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/users" +) + +func main() { + users.Register("", "alicia", "my profile") + + { + // Normal usage + names := users.ListUsersByPrefix("a", 1) + println("# names: " + strconv.Itoa(len(names))) + println("name: " + names[0]) + } + + { + // Empty prefix: match all + names := users.ListUsersByPrefix("", 1) + println("# names: " + strconv.Itoa(len(names))) + println("name: " + names[0]) + } + + { + // The prefix is before "alicia" + names := users.ListUsersByPrefix("alich", 1) + println("# names: " + strconv.Itoa(len(names))) + } + + { + // The prefix is after the last name + names := users.ListUsersByPrefix("y", 10) + println("# names: " + strconv.Itoa(len(names))) + } + + // More tests are in p/demo/avlhelpers +} + +// Output: +// # names: 1 +// name: alicia +// # names: 1 +// name: alicia +// # names: 0 +// # names: 0 diff --git a/examples/gno.land/r/demo/users/z_13_filetest.gno b/examples/gno.land/r/demo/users/z_13_filetest.gno new file mode 100644 index 00000000000..6ef312dc41c --- /dev/null +++ b/examples/gno.land/r/demo/users/z_13_filetest.gno @@ -0,0 +1,22 @@ +package main + +// SEND: 200000000ugnot + +import ( + "strconv" + + "gno.land/r/demo/users" +) + +func main() { + { + // Verify pre-registered test1 user + names := users.ListUsersByPrefix("test1", 1) + println("# names: " + strconv.Itoa(len(names))) + println("name: " + names[0]) + } +} + +// Output: +// # names: 1 +// name: test1 diff --git a/examples/gno.land/r/demo/users/z_2_filetest.gno b/examples/gno.land/r/demo/users/z_2_filetest.gno index 84b62a7e483..c1b92790f8b 100644 --- a/examples/gno.land/r/demo/users/z_2_filetest.gno +++ b/examples/gno.land/r/demo/users/z_2_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_3_filetest.gno b/examples/gno.land/r/demo/users/z_3_filetest.gno index ce34c6bba66..5402235e03d 100644 --- a/examples/gno.land/r/demo/users/z_3_filetest.gno +++ b/examples/gno.land/r/demo/users/z_3_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_4_filetest.gno b/examples/gno.land/r/demo/users/z_4_filetest.gno index 1a46d915c96..613fadf9625 100644 --- a/examples/gno.land/r/demo/users/z_4_filetest.gno +++ b/examples/gno.land/r/demo/users/z_4_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_5_filetest.gno b/examples/gno.land/r/demo/users/z_5_filetest.gno index 4ab68ec0e0b..6465cc9c378 100644 --- a/examples/gno.land/r/demo/users/z_5_filetest.gno +++ b/examples/gno.land/r/demo/users/z_5_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main @@ -28,6 +28,8 @@ func main() { users.Register(caller, "satoshi", "my other profile") println(users.Render("")) println("========================================") + println(users.Render("?page=2")) + println("========================================") println(users.Render("gnouser")) println("========================================") println(users.Render("satoshi")) @@ -36,7 +38,7 @@ func main() { } // Output: -// * [archives](/r/demo/users:archives) +// * [archives](/r/demo/users:archives) // * [demo](/r/demo/users:demo) // * [gno](/r/demo/users:gno) // * [gnoland](/r/demo/users:gnoland) @@ -46,8 +48,13 @@ func main() { // * [nt](/r/demo/users:nt) // * [satoshi](/r/demo/users:satoshi) // * [sys](/r/demo/users:sys) +// * [test1](/r/demo/users:test1) // * [x](/r/demo/users:x) // +// +// ======================================== +// +// // ======================================== // ## user gnouser // diff --git a/examples/gno.land/r/demo/users/z_6_filetest.gno b/examples/gno.land/r/demo/users/z_6_filetest.gno index 85305fff1ad..919088088a2 100644 --- a/examples/gno.land/r/demo/users/z_6_filetest.gno +++ b/examples/gno.land/r/demo/users/z_6_filetest.gno @@ -6,7 +6,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() diff --git a/examples/gno.land/r/demo/users/z_7_filetest.gno b/examples/gno.land/r/demo/users/z_7_filetest.gno index 3332ab49af4..1d3c9e3a917 100644 --- a/examples/gno.land/r/demo/users/z_7_filetest.gno +++ b/examples/gno.land/r/demo/users/z_7_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_7b_filetest.gno b/examples/gno.land/r/demo/users/z_7b_filetest.gno index 60a397abe79..09c15bb135d 100644 --- a/examples/gno.land/r/demo/users/z_7b_filetest.gno +++ b/examples/gno.land/r/demo/users/z_7b_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_8_filetest.gno b/examples/gno.land/r/demo/users/z_8_filetest.gno index 1eaa017b7d2..78fada74a71 100644 --- a/examples/gno.land/r/demo/users/z_8_filetest.gno +++ b/examples/gno.land/r/demo/users/z_8_filetest.gno @@ -9,7 +9,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/users/z_9_filetest.gno b/examples/gno.land/r/demo/users/z_9_filetest.gno index 2bd9bf555dc..c73c685aebd 100644 --- a/examples/gno.land/r/demo/users/z_9_filetest.gno +++ b/examples/gno.land/r/demo/users/z_9_filetest.gno @@ -7,7 +7,7 @@ import ( "gno.land/r/demo/users" ) -const admin = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") +const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { caller := std.GetOrigCaller() // main diff --git a/examples/gno.land/r/demo/wugnot/gno.mod b/examples/gno.land/r/demo/wugnot/gno.mod index f076e90e068..12b6baa7ae2 100644 --- a/examples/gno.land/r/demo/wugnot/gno.mod +++ b/examples/gno.land/r/demo/wugnot/gno.mod @@ -1,8 +1 @@ module gno.land/r/demo/wugnot - -require ( - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/wugnot/wugnot.gno b/examples/gno.land/r/demo/wugnot/wugnot.gno index e1028530c8c..b72f5161e7d 100644 --- a/examples/gno.land/r/demo/wugnot/wugnot.gno +++ b/examples/gno.land/r/demo/wugnot/wugnot.gno @@ -7,26 +7,29 @@ import ( "gno.land/p/demo/grc/grc20" "gno.land/p/demo/ufmt" pusers "gno.land/p/demo/users" + "gno.land/r/demo/grc20reg" "gno.land/r/demo/users" ) -var ( - banker *grc20.Banker = grc20.NewBanker("wrapped GNOT", "wugnot", 0) - Token = banker.Token() -) +var Token, adm = grc20.NewToken("wrapped GNOT", "wugnot", 0) const ( ugnotMinDeposit uint64 = 1000 wugnotMinDeposit uint64 = 1 ) +func init() { + grc20reg.Register(Token.Getter(), "") +} + func Deposit() { caller := std.PrevRealm().Addr() sent := std.GetOrigSend() amount := sent.AmountOf("ugnot") require(uint64(amount) >= ugnotMinDeposit, ufmt.Sprintf("Deposit below minimum: %d/%d ugnot.", amount, ugnotMinDeposit)) - checkErr(banker.Mint(caller, uint64(amount))) + + checkErr(adm.Mint(caller, uint64(amount))) } func Withdraw(amount uint64) { @@ -41,7 +44,7 @@ func Withdraw(amount uint64) { stdBanker := std.GetBanker(std.BankerTypeRealmSend) send := std.Coins{{"ugnot", int64(amount)}} stdBanker.SendCoins(pkgaddr, caller, send) - checkErr(banker.Burn(caller, amount)) + checkErr(adm.Burn(caller, amount)) } func Render(path string) string { @@ -50,7 +53,7 @@ func Render(path string) string { switch { case path == "": - return banker.RenderHome() + return Token.RenderHome() case c == 2 && parts[0] == "balance": owner := std.Address(parts[1]) balance := Token.BalanceOf(owner) @@ -75,18 +78,21 @@ func Allowance(owner, spender pusers.AddressOrName) uint64 { func Transfer(to pusers.AddressOrName, amount uint64) { toAddr := users.Resolve(to) - checkErr(Token.Transfer(toAddr, amount)) + userTeller := Token.CallerTeller() + checkErr(userTeller.Transfer(toAddr, amount)) } func Approve(spender pusers.AddressOrName, amount uint64) { spenderAddr := users.Resolve(spender) - checkErr(Token.Approve(spenderAddr, amount)) + userTeller := Token.CallerTeller() + checkErr(userTeller.Approve(spenderAddr, amount)) } func TransferFrom(from, to pusers.AddressOrName, amount uint64) { fromAddr := users.Resolve(from) toAddr := users.Resolve(to) - checkErr(Token.TransferFrom(fromAddr, toAddr, amount)) + userTeller := Token.CallerTeller() + checkErr(userTeller.TransferFrom(fromAddr, toAddr, amount)) } func require(condition bool, msg string) { diff --git a/examples/gno.land/r/demo/wugnot/z0_filetest.gno b/examples/gno.land/r/demo/wugnot/z0_filetest.gno index bef65c03b68..264bc8f19aa 100644 --- a/examples/gno.land/r/demo/wugnot/z0_filetest.gno +++ b/examples/gno.land/r/demo/wugnot/z0_filetest.gno @@ -55,17 +55,17 @@ func printBalances() { // Output: // ----------- -// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=0 | ugnot=200000000 | +// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=0 | ugnot=0 | // | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 | // | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 | // ----------- // ----------- -// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=123400 | ugnot=200000000 | +// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=123400 | ugnot=0 | // | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=100000001 | // | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 | // ----------- // ----------- -// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=119158 | ugnot=200004242 | +// | wugnot_test | addr=g19rmydykafrqyyegc8uuaxxpzqwzcnxraj2dev9 | wugnot=119158 | ugnot=4242 | // | wugnot | addr=g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 | wugnot=0 | ugnot=99995759 | // | addr1 | addr=g1w3jhxap3ta047h6lta047h6lta047h6l4mfnm7 | wugnot=0 | ugnot=100000001 | // ----------- diff --git a/examples/gno.land/r/docs/adder/adder.gno b/examples/gno.land/r/docs/adder/adder.gno new file mode 100644 index 00000000000..33e971c7c0b --- /dev/null +++ b/examples/gno.land/r/docs/adder/adder.gno @@ -0,0 +1,42 @@ +package adder + +import ( + "strconv" + "time" + + "gno.land/p/moul/txlink" +) + +// Global variables to store the current number and last update timestamp +var ( + number int + lastUpdate time.Time +) + +// Add function to update the number and timestamp +func Add(n int) { + number += n + lastUpdate = time.Now() +} + +// Render displays the current number value, last update timestamp, and a link to call Add with 42 +func Render(path string) string { + // Display the current number and formatted last update time + result := "# Add Example\n\n" + result += "Current Number: " + strconv.Itoa(number) + "\n\n" + result += "Last Updated: " + formatTimestamp(lastUpdate) + "\n\n" + + // Generate a transaction link to call Add with 42 as the default parameter + txLink := txlink.Call("Add", "n", "42") + result += "[Increase Number](" + txLink + ")\n" + + return result +} + +// Helper function to format the timestamp for readability +func formatTimestamp(timestamp time.Time) string { + if timestamp.IsZero() { + return "Never" + } + return timestamp.Format("2006-01-02 15:04:05") +} diff --git a/examples/gno.land/r/docs/adder/adder_test.gno b/examples/gno.land/r/docs/adder/adder_test.gno new file mode 100644 index 00000000000..327908ab2d3 --- /dev/null +++ b/examples/gno.land/r/docs/adder/adder_test.gno @@ -0,0 +1,44 @@ +package adder + +import ( + "testing" +) + +func TestRenderAndAdd(t *testing.T) { + // Initial Render output + output := Render("") + expected := `# Add Example + +Current Number: 0 + +Last Updated: Never + +[Increase Number](/r/docs/adder$help&func=Add&n=42) +` + if output != expected { + t.Errorf("Initial Render failed, got:\n%s", output) + } + + // Call Add with a value of 10 + Add(10) + + // Call Add again with a value of -5 + Add(-5) + + // Render after two Add calls + finalOutput := Render("") + + // Initial Render output + output = Render("") + expected = `# Add Example + +Current Number: 5 + +Last Updated: 2009-02-13 23:31:30 + +[Increase Number](/r/docs/adder$help&func=Add&n=42) +` + if output != expected { + t.Errorf("Final Render failed, got:\n%s\nexpected:\n%s", output, finalOutput) + } +} diff --git a/examples/gno.land/r/docs/adder/gno.mod b/examples/gno.land/r/docs/adder/gno.mod new file mode 100644 index 00000000000..f4958c6494d --- /dev/null +++ b/examples/gno.land/r/docs/adder/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/adder diff --git a/examples/gno.land/r/docs/avl_pager/avl_pager.gno b/examples/gno.land/r/docs/avl_pager/avl_pager.gno new file mode 100644 index 00000000000..af8a6a10b48 --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager/avl_pager.gno @@ -0,0 +1,40 @@ +package avl_pager + +import ( + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" +) + +// Tree instance for 100 items +var tree *avl.Tree + +// Initialize a tree with 100 items. +func init() { + tree = avl.NewTree() + for i := 1; i <= 100; i++ { + key := "Item" + strconv.Itoa(i) + tree.Set(key, "Value of "+key) + } +} + +// Render paginated content based on the given URL path. +// URL format: `...?page=&size=` (default is page 1 and size 10). +func Render(path string) string { + p := pager.NewPager(tree, 10, false) // Default page size is 10 + page := p.MustGetPageByPath(path) + + // Header and pagination info + result := "# Paginated Items\n" + result += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n" + result += page.Picker() + "\n\n" + + // Display items on the current page + for _, item := range page.Items { + result += "- " + item.Key + ": " + item.Value.(string) + "\n" + } + + result += "\n" + page.Picker() // Repeat page picker for ease of navigation + return result +} diff --git a/examples/gno.land/r/docs/avl_pager/avl_pager_test.gno b/examples/gno.land/r/docs/avl_pager/avl_pager_test.gno new file mode 100644 index 00000000000..1ffc9a0c3ba --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager/avl_pager_test.gno @@ -0,0 +1,55 @@ +package avl_pager + +import ( + "testing" +) + +func TestRender(t *testing.T) { + // Test default Render output (first page) + output := Render("") + expected := `# Paginated Items +Page 1 of 10 + +**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10) + +- Item1: Value of Item1 +- Item10: Value of Item10 +- Item100: Value of Item100 +- Item11: Value of Item11 +- Item12: Value of Item12 +- Item13: Value of Item13 +- Item14: Value of Item14 +- Item15: Value of Item15 +- Item16: Value of Item16 +- Item17: Value of Item17 + +**1** | [2](?page=2) | [3](?page=3) | … | [10](?page=10)` + if output != expected { + t.Errorf("Render(\"\") failed, got:\n%s", output) + } +} + +func TestRender_page2(t *testing.T) { + // Test Render output for a custom page (page 2) + output := Render("?page=2&size=10") + expected := `# Paginated Items +Page 2 of 10 + +[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10) + +- Item18: Value of Item18 +- Item19: Value of Item19 +- Item2: Value of Item2 +- Item20: Value of Item20 +- Item21: Value of Item21 +- Item22: Value of Item22 +- Item23: Value of Item23 +- Item24: Value of Item24 +- Item25: Value of Item25 +- Item26: Value of Item26 + +[1](?page=1) | **2** | [3](?page=3) | [4](?page=4) | … | [10](?page=10)` + if output != expected { + t.Errorf("Render(\"\") failed, got:\n%s", output) + } +} diff --git a/examples/gno.land/r/docs/avl_pager/gno.mod b/examples/gno.land/r/docs/avl_pager/gno.mod new file mode 100644 index 00000000000..bc7214f7bc1 --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/avl_pager diff --git a/examples/gno.land/r/docs/buttons/buttons.gno b/examples/gno.land/r/docs/buttons/buttons.gno new file mode 100644 index 00000000000..cb050b1bc38 --- /dev/null +++ b/examples/gno.land/r/docs/buttons/buttons.gno @@ -0,0 +1,44 @@ +package buttons + +import ( + "std" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +var ( + motd = "The Initial Message\n\n" + lastCaller std.Address +) + +func UpdateMOTD(newmotd string) { + motd = newmotd + lastCaller = std.PrevRealm().Addr() +} + +func Render(path string) string { + if path == "motd" { + out := "# Message of the Day:\n\n" + out += "---\n\n" + out += "# " + motd + "\n\n" + out += "---\n\n" + link := txlink.Call("UpdateMOTD", "newmotd", "Message!") // "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message!" + out += ufmt.Sprintf("Click **[here](%s)** to update the Message of The Day!\n\n", link) + out += "[Go back to home page](/r/docs/buttons)\n\n" + out += "Last updated by " + lastCaller.String() + + return out + } + + out := `# Buttons + +Users can create simple hyperlink buttons to view specific realm pages and +do specific realm actions, such as calling a specific function with some arguments. + +The foundation for this functionality are markdown links; for example, you can +click... +` + "\n## [here](/r/docs/buttons:motd)\n" + `...to view this realm's message of the day.` + + return out +} diff --git a/examples/gno.land/r/docs/buttons/buttons_test.gno b/examples/gno.land/r/docs/buttons/buttons_test.gno new file mode 100644 index 00000000000..2903fa1a858 --- /dev/null +++ b/examples/gno.land/r/docs/buttons/buttons_test.gno @@ -0,0 +1,14 @@ +package buttons + +import ( + "strings" + "testing" +) + +func TestRenderMotdLink(t *testing.T) { + res := Render("motd") + const wantLink = "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message!" + if !strings.Contains(res, wantLink) { + t.Fatalf("%s\ndoes not contain correct help page link: %s", res, wantLink) + } +} diff --git a/examples/gno.land/r/docs/buttons/gno.mod b/examples/gno.land/r/docs/buttons/gno.mod new file mode 100644 index 00000000000..43cc2d773da --- /dev/null +++ b/examples/gno.land/r/docs/buttons/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/buttons diff --git a/examples/gno.land/r/docs/docs.gno b/examples/gno.land/r/docs/docs.gno new file mode 100644 index 00000000000..28bac4171b5 --- /dev/null +++ b/examples/gno.land/r/docs/docs.gno @@ -0,0 +1,24 @@ +package docs + +func Render(_ string) string { + return `# Gno Examples Documentation + +Welcome to the Gno examples documentation index. +Explore various examples to learn more about Gno functionality and usage. + +## Examples + +- [Hello World](/r/docs/hello) - A simple introductory example. +- [Adder](/r/docs/adder) - An interactive example to update a number with transactions. +- [Source](/r/docs/source) - View realm source code. +- [Buttons](/r/docs/buttons) - Add buttons to your realm's render. +- [AVL Pager](/r/docs/avl_pager) - Paginate through AVL tree items. +- [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. +- ... + + +## Other resources + +- [Official documentation](https://github.com/gnolang/gno/tree/master/docs) +` +} diff --git a/examples/gno.land/r/docs/docs_test.gno b/examples/gno.land/r/docs/docs_test.gno new file mode 100644 index 00000000000..aa25332f91b --- /dev/null +++ b/examples/gno.land/r/docs/docs_test.gno @@ -0,0 +1,22 @@ +package docs + +import ( + "strings" + "testing" +) + +func TestRenderHome(t *testing.T) { + output := Render("") + + // Check for the presence of key sections + if !contains(output, "# Gno Examples Documentation") { + t.Errorf("Render output is missing the title.") + } + if !contains(output, "Official documentation") { + t.Errorf("Render output is missing the official documentation link.") + } +} + +func contains(s, substr string) bool { + return strings.Index(s, substr) >= 0 +} diff --git a/examples/gno.land/r/docs/gno.mod b/examples/gno.land/r/docs/gno.mod new file mode 100644 index 00000000000..227ceb91124 --- /dev/null +++ b/examples/gno.land/r/docs/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs diff --git a/examples/gno.land/r/docs/hello/gno.mod b/examples/gno.land/r/docs/hello/gno.mod new file mode 100644 index 00000000000..25ddf30051f --- /dev/null +++ b/examples/gno.land/r/docs/hello/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/hello diff --git a/examples/gno.land/r/docs/hello/hello.gno b/examples/gno.land/r/docs/hello/hello.gno new file mode 100644 index 00000000000..e881c155cdd --- /dev/null +++ b/examples/gno.land/r/docs/hello/hello.gno @@ -0,0 +1,11 @@ +// Package hello_world demonstrates basic usage of Render(). +// Try adding `:World` at the end of the URL, like `.../hello:World`. +package hello + +// Render outputs a greeting. It customizes the message based on the provided path. +func Render(path string) string { + if path == "" { + return "# Hello, 世界!" + } + return "# Hello, " + path + "!" +} diff --git a/examples/gno.land/r/docs/hello/hello_test.gno b/examples/gno.land/r/docs/hello/hello_test.gno new file mode 100644 index 00000000000..8159fb1341c --- /dev/null +++ b/examples/gno.land/r/docs/hello/hello_test.gno @@ -0,0 +1,19 @@ +package hello + +import ( + "testing" +) + +func TestHello(t *testing.T) { + expected := "# Hello, 世界!" + got := Render("") + if got != expected { + t.Fatalf("Expected %s, got %s", expected, got) + } + + got = Render("world") + expected = "# Hello, world!" + if got != expected { + t.Fatalf("Expected %s, got %s", expected, got) + } +} diff --git a/examples/gno.land/r/docs/img_embed/gno.mod b/examples/gno.land/r/docs/img_embed/gno.mod new file mode 100644 index 00000000000..784914baef5 --- /dev/null +++ b/examples/gno.land/r/docs/img_embed/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/img_embed diff --git a/examples/gno.land/r/docs/img_embed/img_embed.gno b/examples/gno.land/r/docs/img_embed/img_embed.gno new file mode 100644 index 00000000000..b65512d1968 --- /dev/null +++ b/examples/gno.land/r/docs/img_embed/img_embed.gno @@ -0,0 +1,10 @@ +package image_embed + +// Render displays a title and an embedded image from Imgur +func Render(path string) string { + return `# Image Embed Example + +Here’s an example of embedding an image in a Gno realm: + +![Example Image](https://i.imgur.com/So4rBPB.jpeg)` +} diff --git a/examples/gno.land/r/docs/source/gno.mod b/examples/gno.land/r/docs/source/gno.mod new file mode 100644 index 00000000000..a2b5ad313c0 --- /dev/null +++ b/examples/gno.land/r/docs/source/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/source diff --git a/examples/gno.land/r/docs/source/source.gno b/examples/gno.land/r/docs/source/source.gno new file mode 100644 index 00000000000..45db3c98f06 --- /dev/null +++ b/examples/gno.land/r/docs/source/source.gno @@ -0,0 +1,17 @@ +package source + +// Welcome to the source code of this realm! + +func Render(_ string) string { + return `# Viewing source code +gno.land makes it easy to view the source code of any pure +package or realm, by using ABCI queries. + +gno.land's web frontend, ` + "`gnoweb`, " + ` makes this easy by +providing a intuitive UI that fetches the source of the +realm, that you can inspect anywhere by simply clicking +on the [source] button. + +Check it out in the top right corner! +` +} diff --git a/examples/gno.land/r/gnoland/blog/admin.gno b/examples/gno.land/r/gnoland/blog/admin.gno index 08b0911cf24..87d465449f3 100644 --- a/examples/gno.land/r/gnoland/blog/admin.gno +++ b/examples/gno.land/r/gnoland/blog/admin.gno @@ -5,8 +5,8 @@ import ( "strings" "gno.land/p/demo/avl" - "gno.land/p/demo/context" - "gno.land/p/gov/proposal" + "gno.land/p/demo/dao" + "gno.land/r/gov/dao/bridge" ) var ( @@ -18,7 +18,7 @@ var ( func init() { // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. - adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" + adminAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul } func AdminSetAdminAddr(addr std.Address) { @@ -41,10 +41,14 @@ func AdminRemoveModerator(addr std.Address) { moderatorList.Set(addr.String(), false) // FIXME: delete instead? } -func DaoAddPost(ctx context.Context, slug, title, body, publicationDate, authors, tags string) { - proposal.AssertContextApprovedByGovDAO(ctx) - caller := std.DerivePkgAddr("gno.land/r/gov/dao") - addPost(caller, slug, title, body, publicationDate, authors, tags) +func NewPostExecutor(slug, title, body, publicationDate, authors, tags string) dao.Executor { + callback := func() error { + addPost(std.PrevRealm().Addr(), slug, title, body, publicationDate, authors, tags) + + return nil + } + + return bridge.GovDAO().NewGovDAOExecutor(callback) } func ModAddPost(slug, title, body, publicationDate, authors, tags string) { diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index 17c17e0cfa6..b510867c485 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -1,8 +1 @@ module gno.land/r/gnoland/blog - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/blog v0.0.0-latest - gno.land/p/demo/context v0.0.0-latest - gno.land/p/gov/proposal v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/blog/gnoblog.gno b/examples/gno.land/r/gnoland/blog/gnoblog.gno index 1cdc95fe9a8..d2a163543e5 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog.gno @@ -7,7 +7,7 @@ import ( ) var b = &blog.Blog{ - Title: "Gnoland's Blog", + Title: "gno.land's blog", Prefix: "/r/gnoland/blog:", } diff --git a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno index 15688ca4bc7..b4658db4fb5 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno @@ -7,7 +7,7 @@ import ( ) func TestPackage(t *testing.T) { - std.TestSetOrigCaller(std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq")) + std.TestSetOrigCaller(std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5")) author := std.GetOrigCaller() @@ -15,7 +15,7 @@ func TestPackage(t *testing.T) { { got := Render("") expected := ` -# Gnoland's Blog +# gno.land's blog No posts. ` @@ -28,7 +28,7 @@ No posts. ModAddPost("slug2", "title2", "body2", "2022-05-20T13:17:23Z", "moul", "tag1,tag3") got := Render("") expected := ` - # Gnoland's Blog + # gno.land's blog
@@ -59,7 +59,7 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) Written by moul on 20 May 2022 -Published by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog +Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to gno.land's blog ---
Comment section @@ -74,12 +74,12 @@ Published by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog // list by tags. { got := Render("t/invalid") - expected := "# [Gnoland's Blog](/r/gnoland/blog:) / t / invalid\n\nNo posts." + expected := "# [gno.land's blog](/r/gnoland/blog:) / t / invalid\n\nNo posts." assertMDEquals(t, got, expected) got = Render("t/tag2") expected = ` -# [Gnoland's Blog](/r/gnoland/blog:) / t / tag2 +# [gno.land's blog](/r/gnoland/blog:) / t / tag2
@@ -110,20 +110,20 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) Written by moul on 20 May 2022 -Published by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog +Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to gno.land's blog ---
Comment section
comment4 -
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+
by g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC
---
comment2 -
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+
by g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC
--- @@ -152,20 +152,20 @@ Tags: [#tag1](/r/gnoland/blog:t/tag1) [#tag4](/r/gnoland/blog:t/tag4) Written by manfred on 20 May 2022 -Published by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq to Gnoland's Blog +Published by g1manfred47kzduec920z88wfr64ylksmdcedlf5 to gno.land's blog ---
Comment section
comment4 -
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+
by g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC
---
comment2 -
by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 13 Feb 09 23:31 UTC
+
by g1manfred47kzduec920z88wfr64ylksmdcedlf5 on 13 Feb 09 23:31 UTC
--- diff --git a/examples/gno.land/r/gnoland/events/administration.gno b/examples/gno.land/r/gnoland/events/administration.gno deleted file mode 100644 index 02914adee69..00000000000 --- a/examples/gno.land/r/gnoland/events/administration.gno +++ /dev/null @@ -1,26 +0,0 @@ -package events - -import ( - "std" - - "gno.land/p/demo/ownable/exts/authorizable" -) - -var ( - su = std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") // @leohhhn - auth = authorizable.NewAuthorizableWithAddress(su) -) - -// GetOwner gets the owner of the events realm -func GetOwner() std.Address { - return auth.Owner() -} - -// AddModerator adds a moderator to the events realm -func AddModerator(mod std.Address) { - auth.AssertCallerIsOwner() - - if err := auth.AddToAuthList(mod); err != nil { - panic(err) - } -} diff --git a/examples/gno.land/r/gnoland/events/events.gno b/examples/gno.land/r/gnoland/events/events.gno index 0984edf75a9..d72638ceaaf 100644 --- a/examples/gno.land/r/gnoland/events/events.gno +++ b/examples/gno.land/r/gnoland/events/events.gno @@ -9,6 +9,7 @@ import ( "strings" "time" + "gno.land/p/demo/ownable/exts/authorizable" "gno.land/p/demo/seqid" "gno.land/p/demo/ufmt" ) @@ -28,6 +29,9 @@ type ( ) var ( + su = std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") // @leohhhn + Auth = authorizable.NewAuthorizableWithAddress(su) + events = make(eventsSlice, 0) // sorted idCounter seqid.ID ) @@ -42,7 +46,7 @@ const ( // AddEvent adds auth new event // Start time & end time need to be specified in RFC3339, ie 2024-08-08T12:00:00+02:00 func AddEvent(name, description, link, location, startTime, endTime string) (string, error) { - auth.AssertOnAuthList() + Auth.AssertOnAuthList() if strings.TrimSpace(name) == "" { return "", ErrEmptyName @@ -73,8 +77,7 @@ func AddEvent(name, description, link, location, startTime, endTime string) (str sort.Sort(events) std.Emit(EventAdded, - "id", - e.id, + "id", e.id, ) return id, nil @@ -82,7 +85,7 @@ func AddEvent(name, description, link, location, startTime, endTime string) (str // DeleteEvent deletes an event with auth given ID func DeleteEvent(id string) { - auth.AssertOnAuthList() + Auth.AssertOnAuthList() e, idx, err := GetEventByID(id) if err != nil { @@ -92,8 +95,7 @@ func DeleteEvent(id string) { events = append(events[:idx], events[idx+1:]...) std.Emit(EventDeleted, - "id", - e.id, + "id", e.id, ) } @@ -101,7 +103,7 @@ func DeleteEvent(id string) { // It only updates values corresponding to non-empty arguments sent with the call // Note: if you need to update the start time or end time, you need to provide both every time func EditEvent(id string, name, description, link, location, startTime, endTime string) { - auth.AssertOnAuthList() + Auth.AssertOnAuthList() e, _, err := GetEventByID(id) if err != nil { @@ -142,8 +144,7 @@ func EditEvent(id string, name, description, link, location, startTime, endTime } std.Emit(EventEdited, - "id", - e.id, + "id", e.id, ) } diff --git a/examples/gno.land/r/gnoland/events/events_test.gno b/examples/gno.land/r/gnoland/events/events_test.gno index 357857352d8..1d79b754ee4 100644 --- a/examples/gno.land/r/gnoland/events/events_test.gno +++ b/examples/gno.land/r/gnoland/events/events_test.gno @@ -85,7 +85,8 @@ func TestAddEventErrors(t *testing.T) { } func TestDeleteEvent(t *testing.T) { - events = nil // remove elements from previous tests - see issue #1982 + std.TestSetOrigCaller(su) + std.TestSetRealm(suRealm) e1Start := parsedTimeNow.Add(time.Hour * 24 * 5) e1End := e1Start.Add(time.Hour * 4) @@ -107,7 +108,8 @@ func TestDeleteEvent(t *testing.T) { } func TestEditEvent(t *testing.T) { - events = nil // remove elements from previous tests - see issue #1982 + std.TestSetOrigCaller(su) + std.TestSetRealm(suRealm) e1Start := parsedTimeNow.Add(time.Hour * 24 * 5) e1End := e1Start.Add(time.Hour * 4) @@ -136,7 +138,8 @@ func TestEditEvent(t *testing.T) { } func TestInvalidEdit(t *testing.T) { - events = nil // remove elements from previous tests - see issue #1982 + std.TestSetOrigCaller(su) + std.TestSetRealm(suRealm) uassert.PanicsWithMessage(t, ErrNoSuchID.Error(), func() { EditEvent("123123", "", "", "", "", "", "") @@ -162,9 +165,11 @@ func TestParseTimes(t *testing.T) { } func TestRenderEventWidget(t *testing.T) { - events = nil // remove elements from previous tests - see issue #1982 + std.TestSetOrigCaller(su) + std.TestSetRealm(suRealm) // No events yet + events = nil out, err := RenderEventWidget(1) uassert.NoError(t, err) uassert.Equal(t, out, "No events.") diff --git a/examples/gno.land/r/gnoland/events/gno.mod b/examples/gno.land/r/gnoland/events/gno.mod index bd3e4652b04..50aa3d8fc27 100644 --- a/examples/gno.land/r/gnoland/events/gno.mod +++ b/examples/gno.land/r/gnoland/events/gno.mod @@ -1,9 +1 @@ module gno.land/r/gnoland/events - -require ( - gno.land/p/demo/ownable/exts/authorizable v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/events/render.gno b/examples/gno.land/r/gnoland/events/render.gno new file mode 100644 index 00000000000..89f9a69cc8a --- /dev/null +++ b/examples/gno.land/r/gnoland/events/render.gno @@ -0,0 +1,145 @@ +package events + +import ( + "bytes" + "time" + + "gno.land/p/demo/ufmt" +) + +const ( + MaxWidgetSize = 5 +) + +// RenderEventWidget shows up to eventsToRender of the latest events to a caller +func RenderEventWidget(eventsToRender int) (string, error) { + numOfEvents := len(events) + if numOfEvents == 0 { + return "No events.", nil + } + + if eventsToRender > MaxWidgetSize { + return "", ErrMaxWidgetSize + } + + if eventsToRender < 1 { + return "", ErrMinWidgetSize + } + + if eventsToRender > numOfEvents { + eventsToRender = numOfEvents + } + + output := "" + + for _, event := range events[:eventsToRender] { + output += ufmt.Sprintf("- [%s](%s)\n", event.name, event.link) + } + + return output, nil +} + +// renderHome renders the home page of the events realm +func renderHome(admin bool) string { + output := "# gno.land events\n\n" + + if len(events) == 0 { + output += "No upcoming or past events." + return output + } + + output += "Below is a list of all gno.land events, including in progress, upcoming, and past ones.\n\n" + output += "---\n\n" + + var ( + inProgress = "" + upcoming = "" + past = "" + now = time.Now() + ) + + for _, e := range events { + if now.Before(e.startTime) { + upcoming += e.Render(admin) + } else if now.After(e.endTime) { + past += e.Render(admin) + } else { + inProgress += e.Render(admin) + } + } + + if upcoming != "" { + // Add upcoming events + output += "## Upcoming events\n\n" + output += "
" + + output += upcoming + + output += "
\n\n" + output += "---\n\n" + } + + if inProgress != "" { + output += "## Currently in progress\n\n" + output += "
" + + output += inProgress + + output += "
\n\n" + output += "---\n\n" + } + + if past != "" { + // Add past events + output += "## Past events\n\n" + output += "
" + + output += past + + output += "
\n\n" + } + + return output +} + +// Render returns the markdown representation of a single event instance +func (e Event) Render(admin bool) string { + var buf bytes.Buffer + + buf.WriteString("
\n\n") + buf.WriteString(ufmt.Sprintf("### %s\n\n", e.name)) + buf.WriteString(ufmt.Sprintf("%s\n\n", e.description)) + buf.WriteString(ufmt.Sprintf("**Location:** %s\n\n", e.location)) + + _, offset := e.startTime.Zone() // offset is in seconds + hoursOffset := offset / (60 * 60) + sign := "" + if offset >= 0 { + sign = "+" + } + + buf.WriteString(ufmt.Sprintf("**Starts:** %s UTC%s%d\n\n", e.startTime.Format("02 Jan 2006, 03:04 PM"), sign, hoursOffset)) + buf.WriteString(ufmt.Sprintf("**Ends:** %s UTC%s%d\n\n", e.endTime.Format("02 Jan 2006, 03:04 PM"), sign, hoursOffset)) + + if admin { + buf.WriteString(ufmt.Sprintf("[EDIT](/r/gnoland/events$help&func=EditEvent&id=%s)\n\n", e.id)) + buf.WriteString(ufmt.Sprintf("[DELETE](/r/gnoland/events$help&func=DeleteEvent&id=%s)\n\n", e.id)) + } + + if e.link != "" { + buf.WriteString(ufmt.Sprintf("[See more](%s)\n\n", e.link)) + } + + buf.WriteString("
") + + return buf.String() +} + +// Render is the main rendering entry point +func Render(path string) string { + if path == "admin" { + return renderHome(true) + } + + return renderHome(false) +} diff --git a/examples/gno.land/r/gnoland/events/rendering.gno b/examples/gno.land/r/gnoland/events/rendering.gno deleted file mode 100644 index d98879c68f6..00000000000 --- a/examples/gno.land/r/gnoland/events/rendering.gno +++ /dev/null @@ -1,145 +0,0 @@ -package events - -import ( - "bytes" - "time" - - "gno.land/p/demo/ufmt" -) - -const ( - MaxWidgetSize = 5 -) - -// RenderEventWidget shows up to eventsToRender of the latest events to a caller -func RenderEventWidget(eventsToRender int) (string, error) { - numOfEvents := len(events) - if numOfEvents == 0 { - return "No events.", nil - } - - if eventsToRender > MaxWidgetSize { - return "", ErrMaxWidgetSize - } - - if eventsToRender < 1 { - return "", ErrMinWidgetSize - } - - if eventsToRender > numOfEvents { - eventsToRender = numOfEvents - } - - output := "" - - for _, event := range events[:eventsToRender] { - output += ufmt.Sprintf("- [%s](%s)\n", event.name, event.link) - } - - return output, nil -} - -// renderHome renders the home page of the events realm -func renderHome(admin bool) string { - output := "# gno.land events\n\n" - - if len(events) == 0 { - output += "No upcoming or past events." - return output - } - - output += "Below is a list of all gno.land events, including in progress, upcoming, and past ones.\n\n" - output += "---\n\n" - - var ( - inProgress = "" - upcoming = "" - past = "" - now = time.Now() - ) - - for _, e := range events { - if now.Before(e.startTime) { - upcoming += e.Render(admin) - } else if now.After(e.endTime) { - past += e.Render(admin) - } else { - inProgress += e.Render(admin) - } - } - - if upcoming != "" { - // Add upcoming events - output += "## Upcoming events\n\n" - output += "
" - - output += upcoming - - output += "
\n\n" - output += "---\n\n" - } - - if inProgress != "" { - output += "## Currently in progress\n\n" - output += "
" - - output += inProgress - - output += "
\n\n" - output += "---\n\n" - } - - if past != "" { - // Add past events - output += "## Past events\n\n" - output += "
" - - output += past - - output += "
\n\n" - } - - return output -} - -// Render returns the markdown representation of a single event instance -func (e Event) Render(admin bool) string { - var buf bytes.Buffer - - buf.WriteString("
\n\n") - buf.WriteString(ufmt.Sprintf("### %s\n\n", e.name)) - buf.WriteString(ufmt.Sprintf("%s\n\n", e.description)) - buf.WriteString(ufmt.Sprintf("**Location:** %s\n\n", e.location)) - - _, offset := e.startTime.Zone() // offset is in seconds - hoursOffset := offset / (60 * 60) - sign := "" - if offset >= 0 { - sign = "+" - } - - buf.WriteString(ufmt.Sprintf("**Starts:** %s UTC%s%d\n\n", e.startTime.Format("02 Jan 2006, 03:04 PM"), sign, hoursOffset)) - buf.WriteString(ufmt.Sprintf("**Ends:** %s UTC%s%d\n\n", e.endTime.Format("02 Jan 2006, 03:04 PM"), sign, hoursOffset)) - - if admin { - buf.WriteString(ufmt.Sprintf("[EDIT](/r/gnoland/events?help&__func=EditEvent&id=%s)\n\n", e.id)) - buf.WriteString(ufmt.Sprintf("[DELETE](/r/gnoland/events?help&__func=DeleteEvent&id=%s)\n\n", e.id)) - } - - if e.link != "" { - buf.WriteString(ufmt.Sprintf("[See more](%s)\n\n", e.link)) - } - - buf.WriteString("
") - - return buf.String() -} - -// Render is the main rendering entry point -func Render(path string) string { - if path == "admin" { - return renderHome(true) - } - - return renderHome(false) -} diff --git a/examples/gno.land/r/gnoland/faucet/faucet_test.gno b/examples/gno.land/r/gnoland/faucet/faucet_test.gno index 1f492adb2dc..cecbb2ebcd6 100644 --- a/examples/gno.land/r/gnoland/faucet/faucet_test.gno +++ b/examples/gno.land/r/gnoland/faucet/faucet_test.gno @@ -28,7 +28,7 @@ func TestPackage(t *testing.T) { ) // deposit 1000gnot to faucet contract std.TestIssueCoins(faucetaddr, std.Coins{{"ugnot", 1000000000}}) - assertBalance(t, faucetaddr, 1200000000) + assertBalance(t, faucetaddr, 1000000000) // by default, balance is empty, and as a user I cannot call Transfer, or Admin commands. @@ -43,7 +43,7 @@ func TestPackage(t *testing.T) { // as an admin, add the controller to contract and deposit more 2000gnot to contract std.TestSetOrigCaller(adminaddr) assertNoErr(t, faucet.AdminAddController(controlleraddr1)) - assertBalance(t, faucetaddr, 1200000000) + assertBalance(t, faucetaddr, 1000000000) // now, send some tokens as controller. std.TestSetOrigCaller(controlleraddr1) @@ -51,7 +51,7 @@ func TestPackage(t *testing.T) { assertBalance(t, test1addr, 1000000) assertNoErr(t, faucet.Transfer(test1addr, 1000000)) assertBalance(t, test1addr, 2000000) - assertBalance(t, faucetaddr, 1198000000) + assertBalance(t, faucetaddr, 998000000) // remove controller // as an admin, remove controller diff --git a/examples/gno.land/r/gnoland/faucet/gno.mod b/examples/gno.land/r/gnoland/faucet/gno.mod index 693b0e795cf..6193d111e4f 100644 --- a/examples/gno.land/r/gnoland/faucet/gno.mod +++ b/examples/gno.land/r/gnoland/faucet/gno.mod @@ -1,7 +1 @@ module gno.land/r/gnoland/faucet - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/faucet/z0_filetest.gno b/examples/gno.land/r/gnoland/faucet/z0_filetest.gno index bcc75897c85..7e729bdd358 100644 --- a/examples/gno.land/r/gnoland/faucet/z0_filetest.gno +++ b/examples/gno.land/r/gnoland/faucet/z0_filetest.gno @@ -33,3 +33,5 @@ func main() { // // // Per request limit: 350000000ugnot +// +// diff --git a/examples/gno.land/r/gnoland/faucet/z1_filetest.gno b/examples/gno.land/r/gnoland/faucet/z1_filetest.gno index 6afb14b024b..c6fd6298488 100644 --- a/examples/gno.land/r/gnoland/faucet/z1_filetest.gno +++ b/examples/gno.land/r/gnoland/faucet/z1_filetest.gno @@ -33,3 +33,5 @@ func main() { // // // Per request limit: 350000000ugnot +// +// diff --git a/examples/gno.land/r/gnoland/faucet/z2_filetest.gno b/examples/gno.land/r/gnoland/faucet/z2_filetest.gno index 054e5329476..d0616b3afcd 100644 --- a/examples/gno.land/r/gnoland/faucet/z2_filetest.gno +++ b/examples/gno.land/r/gnoland/faucet/z2_filetest.gno @@ -48,3 +48,5 @@ func main() { // g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v // // Per request limit: 350000000ugnot +// +// diff --git a/examples/gno.land/r/gnoland/faucet/z3_filetest.gno b/examples/gno.land/r/gnoland/faucet/z3_filetest.gno index 4a48ca390e2..0da06593710 100644 --- a/examples/gno.land/r/gnoland/faucet/z3_filetest.gno +++ b/examples/gno.land/r/gnoland/faucet/z3_filetest.gno @@ -60,3 +60,5 @@ func main() { // g1vdhkuarjdakxcetjx9047h6lta047h6lsdacav g1vdhkuarjdakxcetjxf047h6lta047h6lnrev3v // // Per request limit: 350000000ugnot +// +// diff --git a/examples/gno.land/r/gnoland/ghverify/contract.gno b/examples/gno.land/r/gnoland/ghverify/contract.gno index b40c9ef1448..3b8f7fcbbe1 100644 --- a/examples/gno.land/r/gnoland/ghverify/contract.gno +++ b/examples/gno.land/r/gnoland/ghverify/contract.gno @@ -83,6 +83,11 @@ func RequestVerification(githubHandle string) { ); err != nil { panic(err) } + std.Emit( + "verification_requested", + "from", gnoAddress, + "handle", githubHandle, + ) } // GnorkleEntrypoint is the entrypoint to the gnorkle oracle handler. @@ -139,7 +144,7 @@ func Render(_ string) string { result += `"` + handle + `": "` + address.(string) + `"` appendComma = true - return true + return false }) return result + "}" diff --git a/examples/gno.land/r/gnoland/ghverify/contract_test.gno b/examples/gno.land/r/gnoland/ghverify/contract_test.gno index d9c399942ae..5c0be0afcb1 100644 --- a/examples/gno.land/r/gnoland/ghverify/contract_test.gno +++ b/examples/gno.land/r/gnoland/ghverify/contract_test.gno @@ -9,7 +9,8 @@ import ( func TestVerificationLifecycle(t *testing.T) { defaultAddress := std.GetOrigCaller() - userAddress := std.Address(testutils.TestAddress("user")) + user1Address := std.Address(testutils.TestAddress("user 1")) + user2Address := std.Address(testutils.TestAddress("user 2")) // Verify request returns no feeds. result := GnorkleEntrypoint("request") @@ -18,7 +19,7 @@ func TestVerificationLifecycle(t *testing.T) { } // Make a verification request with the created user. - std.TestSetOrigCaller(userAddress) + std.TestSetOrigCaller(user1Address) RequestVerification("deelawn") // A subsequent request from the same address should panic because there is @@ -42,26 +43,32 @@ func TestVerificationLifecycle(t *testing.T) { t.Fatalf("expected empty request result, got %s", result) } + // Make a verification request with the created user. + std.TestSetOrigCaller(user2Address) + RequestVerification("omarsy") + // Set the caller back to the whitelisted user and verify that the feed data // returned matches what should have been created by the `RequestVerification` // invocation. std.TestSetOrigCaller(defaultAddress) result = GnorkleEntrypoint("request") - expResult := `[{"id":"` + string(userAddress) + `","type":"0","value_type":"string","tasks":[{"gno_address":"` + - string(userAddress) + `","github_handle":"deelawn"}]}]` + expResult := `[{"id":"` + string(user1Address) + `","type":"0","value_type":"string","tasks":[{"gno_address":"` + + string(user1Address) + `","github_handle":"deelawn"}]},` + + `{"id":"` + string(user2Address) + `","type":"0","value_type":"string","tasks":[{"gno_address":"` + + string(user2Address) + `","github_handle":"omarsy"}]}]` if result != expResult { t.Fatalf("expected request result %s, got %s", expResult, result) } // Try to trigger feed ingestion from the non-authorized user. - std.TestSetOrigCaller(userAddress) + std.TestSetOrigCaller(user1Address) func() { defer func() { if r := recover(); r != nil { errMsg = r.(error).Error() } }() - GnorkleEntrypoint("ingest," + string(userAddress) + ",OK") + GnorkleEntrypoint("ingest," + string(user1Address) + ",OK") }() if errMsg != "caller not whitelisted" { t.Fatalf("expected caller not whitelisted, got %s", errMsg) @@ -69,15 +76,15 @@ func TestVerificationLifecycle(t *testing.T) { // Set the caller back to the whitelisted user and transfer contract ownership. std.TestSetOrigCaller(defaultAddress) - SetOwner(userAddress) + SetOwner(defaultAddress) // Now trigger the feed ingestion from the user and new owner and only whitelisted address. - std.TestSetOrigCaller(userAddress) - GnorkleEntrypoint("ingest," + string(userAddress) + ",OK") + GnorkleEntrypoint("ingest," + string(user1Address) + ",OK") + GnorkleEntrypoint("ingest," + string(user2Address) + ",OK") // Verify the ingestion autocommitted the value and triggered the post handler. data := Render("") - expResult = `{"deelawn": "` + string(userAddress) + `"}` + expResult = `{"deelawn": "` + string(user1Address) + `","omarsy": "` + string(user2Address) + `"}` if data != expResult { t.Fatalf("expected render data %s, got %s", expResult, data) } @@ -89,10 +96,10 @@ func TestVerificationLifecycle(t *testing.T) { } // Check that the accessor functions are working as expected. - if handle := GetHandleByAddress(string(userAddress)); handle != "deelawn" { + if handle := GetHandleByAddress(string(user1Address)); handle != "deelawn" { t.Fatalf("expected deelawn, got %s", handle) } - if address := GetAddressByHandle("deelawn"); address != string(userAddress) { - t.Fatalf("expected %s, got %s", string(userAddress), address) + if address := GetAddressByHandle("deelawn"); address != string(user1Address) { + t.Fatalf("expected %s, got %s", string(user1Address), address) } } diff --git a/examples/gno.land/r/gnoland/ghverify/gno.mod b/examples/gno.land/r/gnoland/ghverify/gno.mod index 386bd9293d2..8ffdec663f7 100644 --- a/examples/gno.land/r/gnoland/ghverify/gno.mod +++ b/examples/gno.land/r/gnoland/ghverify/gno.mod @@ -1,9 +1 @@ module gno.land/r/gnoland/ghverify - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/gnorkle/feeds/static v0.0.0-latest - gno.land/p/demo/gnorkle/gnorkle v0.0.0-latest - gno.land/p/demo/gnorkle/message v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod index c208ad421c9..09eb0eb19e1 100644 --- a/examples/gno.land/r/gnoland/home/gno.mod +++ b/examples/gno.land/r/gnoland/home/gno.mod @@ -1,9 +1 @@ module gno.land/r/gnoland/home - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/ui v0.0.0-latest - gno.land/r/gnoland/blog v0.0.0-latest - gno.land/r/gnoland/events v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno index 921492d81b4..2d1aad8a1a0 100644 --- a/examples/gno.land/r/gnoland/home/home.gno +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -8,6 +8,7 @@ import ( "gno.land/p/demo/ui" blog "gno.land/r/gnoland/blog" events "gno.land/r/gnoland/events" + "gno.land/r/leon/hof" ) // XXX: p/demo/ui API is crappy, we need to make it more idiomatic @@ -16,7 +17,7 @@ import ( var ( override string - admin = ownable.NewWithAddress("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @manfred by default + admin = ownable.NewWithAddress("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @moul ) func Render(_ string) string { @@ -37,7 +38,7 @@ func Render(_ string) string { ui.Columns{3, []ui.Element{ lastBlogposts(4), upcomingEvents(), - lastContributions(4), + latestHOFItems(5), }}, ) @@ -69,14 +70,14 @@ func Render(_ string) string { func lastBlogposts(limit int) ui.Element { posts := blog.RenderLastPostsWidget(limit) return ui.Element{ - ui.H3("[Latest Blogposts](/r/gnoland/blog)"), + ui.H2("[Latest Blogposts](/r/gnoland/blog)"), ui.Text(posts), } } func lastContributions(limit int) ui.Element { return ui.Element{ - ui.H3("Latest Contributions"), + ui.H2("Latest Contributions"), // TODO: import r/gh to ui.Link{Text: "View latest contributions", URL: "https://github.com/gnolang/gno/pulls"}, } @@ -85,14 +86,23 @@ func lastContributions(limit int) ui.Element { func upcomingEvents() ui.Element { out, _ := events.RenderEventWidget(events.MaxWidgetSize) return ui.Element{ - ui.H3("[Latest Events](/r/gnoland/events)"), + ui.H2("[Latest Events](/r/gnoland/events)"), ui.Text(out), } } +func latestHOFItems(num int) ui.Element { + submissions := hof.RenderExhibWidget(num) + + return ui.Element{ + ui.H2("[Hall of Fame](/r/leon/hof)"), + ui.Text(submissions), + } +} + func introSection() ui.Element { return ui.Element{ - ui.H3("We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts."), + ui.Text("**We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts.**"), ui.Paragraph("With transparent and timeless code, gno.land is the next generation of smart contract platforms, serving as the “GitHub” of the ecosystem, with realms built using fully transparent, auditable code that anyone can inspect and reuse."), ui.Paragraph("Intuitive and easy to use, gno.land lowers the barrier to web3 and makes censorship-resistant platforms accessible to everyone. If you want to help lay the foundations of a fairer and freer world, join us today."), } @@ -125,7 +135,7 @@ func worxDAO() ui.Element { ## Contributors ``*/ return ui.Element{ - ui.H3("Contributions (WorxDAO & GoR)"), + ui.H2("Contributions (WorxDAO & GoR)"), // TODO: GoR dashboard + WorxDAO topics ui.Text(`coming soon`), } @@ -144,28 +154,28 @@ func quoteOfTheBlock() ui.Element { qotb := quotes[idx] return ui.Element{ - ui.H3(ufmt.Sprintf("Quote of the ~Day~ Block#%d", height)), + ui.H2(ufmt.Sprintf("Quote of the ~Day~ Block#%d", height)), ui.Quote(qotb), } } func socialLinks() ui.Element { return ui.Element{ - ui.H3("Socials"), + ui.H2("Socials"), ui.BulletList{ // XXX: improve UI to support a nice GO api for such links ui.Text("Check out our [community projects](https://github.com/gnolang/awesome-gno)"), - ui.Text("![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn)"), - ui.Text("![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland)"), - ui.Text("![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland)"), - ui.Text("![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland)"), + ui.Text("[Discord](https://discord.gg/S8nKUqwkPn)"), + ui.Text("[Twitter](https://twitter.com/_gnoland)"), + ui.Text("[Youtube](https://www.youtube.com/@_gnoland)"), + ui.Text("[Telegram](https://t.me/gnoland)"), }, } } func playgroundSection() ui.Element { return ui.Element{ - ui.H3("[Gno Playground](https://play.gno.land)"), + ui.H2("[Gno Playground](https://play.gno.land)"), ui.Paragraph(`Gno Playground is a web application designed for building, running, testing, and interacting with your Gno code, enhancing your understanding of the Gno language. With Gno Playground, you can share your code, execute tests, deploy your realms and packages to gno.land, and explore a multitude of other features.`), @@ -176,12 +186,12 @@ execute tests, deploy your realms and packages to gno.land, and explore a multit func packageStaffPicks() ui.Element { // XXX: make it modifiable from a DAO return ui.Element{ - ui.H3("Explore New Packages and Realms"), + ui.H2("Explore New Packages and Realms"), ui.Columns{ 3, []ui.Element{ { - ui.H4("[r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland)"), + ui.H3("[r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland)"), ui.BulletList{ ui.Link{URL: "r/gnoland/blog"}, ui.Link{URL: "r/gnoland/dao"}, @@ -189,14 +199,14 @@ func packageStaffPicks() ui.Element { ui.Link{URL: "r/gnoland/home"}, ui.Link{URL: "r/gnoland/pages"}, }, - ui.H4("[r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)"), + ui.H3("[r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys)"), ui.BulletList{ ui.Link{URL: "r/sys/names"}, ui.Link{URL: "r/sys/rewards"}, - ui.Link{URL: "r/sys/validators"}, + ui.Link{URL: "/r/sys/validators/v2"}, }, }, { - ui.H4("[r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)"), + ui.H3("[r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo)"), ui.BulletList{ ui.Link{URL: "r/demo/boards"}, ui.Link{URL: "r/demo/users"}, @@ -212,7 +222,7 @@ func packageStaffPicks() ui.Element { ui.Text("..."), }, }, { - ui.H4("[p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo)"), + ui.H3("[p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo)"), ui.BulletList{ ui.Link{URL: "p/demo/avl"}, ui.Link{URL: "p/demo/blog"}, @@ -237,7 +247,7 @@ func discoverLinks() ui.Element { ui.Text(`
-### Learn about gno.land +## Learn about gno.land - [About](/about) - [GitHub](https://github.com/gnolang) @@ -246,13 +256,13 @@ func discoverLinks() ui.Element { - Tokenomics (soon) - [Partners, Fund, Grants](/partners) - [Explore the Ecosystem](/ecosystem) -- [Careers](https://jobs.lever.co/allinbits?department=Gno.land) +- [Careers](https://jobs.ashbyhq.com/allinbits)
-### Build with Gno +## Build with Gno - [Write Gno in the browser](https://play.gno.land) - [Read about the Gno Language](/gnolang) @@ -264,15 +274,13 @@ func discoverLinks() ui.Element {
-### Explore the universe +## Explore the universe - [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples) - [Gnoscan](https://gnoscan.io) - [Portal Loop](https://docs.gno.land/concepts/portal-loop) -- [Testnet 4](https://test4.gno.land/) (Launched July 2024!) -- [Testnet 3](https://test3.gno.land/) (archive) -- [Testnet 2](https://test2.gno.land/) (archive) -- Testnet Faucet Hub (soon) +- [Testnet 4](https://test4.gno.land/) +- [Faucet Hub](https://faucet.gno.land)
`), diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno index b70b22c80af..5b5ff5740c3 100644 --- a/examples/gno.land/r/gnoland/home/home_filetest.gno +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -11,8 +11,7 @@ func main() { // // # Welcome to gno.land // -// ### We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts. -// +// **We’re building gno.land, set to become the leading open-source smart contract platform, using Gno, an interpreted and fully deterministic variation of the Go programming language for succinct and composable smart contracts.** // // With transparent and timeless code, gno.land is the next generation of smart contract platforms, serving as the “GitHub” of the ecosystem, with realms built using fully transparent, auditable code that anyone can inspect and reuse. // @@ -24,7 +23,7 @@ func main() { //
//
// -// ### Learn about gno.land +// ## Learn about gno.land // // - [About](/about) // - [GitHub](https://github.com/gnolang) @@ -33,13 +32,13 @@ func main() { // - Tokenomics (soon) // - [Partners, Fund, Grants](/partners) // - [Explore the Ecosystem](/ecosystem) -// - [Careers](https://jobs.lever.co/allinbits?department=Gno.land) +// - [Careers](https://jobs.ashbyhq.com/allinbits) // //
// //
// -// ### Build with Gno +// ## Build with Gno // // - [Write Gno in the browser](https://play.gno.land) // - [Read about the Gno Language](/gnolang) @@ -51,15 +50,13 @@ func main() { //
//
// -// ### Explore the universe +// ## Explore the universe // // - [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples) // - [Gnoscan](https://gnoscan.io) // - [Portal Loop](https://docs.gno.land/concepts/portal-loop) -// - [Testnet 4](https://test4.gno.land/) (Launched July 2024!) -// - [Testnet 3](https://test3.gno.land/) (archive) -// - [Testnet 2](https://test2.gno.land/) (archive) -// - Testnet Faucet Hub (soon) +// - [Testnet 4](https://test4.gno.land/) +// - [Faucet Hub](https://faucet.gno.land) // //
//
@@ -68,28 +65,28 @@ func main() { //
//
// -// ### [Latest Blogposts](/r/gnoland/blog) +// ## [Latest Blogposts](/r/gnoland/blog) // // No posts. //
//
// -// ### [Latest Events](/r/gnoland/events) +// ## [Latest Events](/r/gnoland/events) // // No events. //
//
// -// ### Latest Contributions +// ## [Hall of Fame](/r/leon/hof) +// // -// [View latest contributions](https://github.com/gnolang/gno/pulls) //
//
// // // --- // -// ### [Gno Playground](https://play.gno.land) +// ## [Gno Playground](https://play.gno.land) // // // Gno Playground is a web application designed for building, running, testing, and interacting @@ -102,12 +99,12 @@ func main() { // // --- // -// ### Explore New Packages and Realms +// ## Explore New Packages and Realms // //
//
// -// #### [r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland) +// ### [r/gnoland](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/gnoland) // // - [r/gnoland/blog](r/gnoland/blog) // - [r/gnoland/dao](r/gnoland/dao) @@ -115,16 +112,16 @@ func main() { // - [r/gnoland/home](r/gnoland/home) // - [r/gnoland/pages](r/gnoland/pages) // -// #### [r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys) +// ### [r/sys](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/sys) // // - [r/sys/names](r/sys/names) // - [r/sys/rewards](r/sys/rewards) -// - [r/sys/validators](r/sys/validators) +// - [/r/sys/validators/v2](/r/sys/validators/v2) // //
//
// -// #### [r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo) +// ### [r/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo) // // - [r/demo/boards](r/demo/boards) // - [r/demo/users](r/demo/users) @@ -142,7 +139,7 @@ func main() { //
//
// -// #### [p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo) +// ### [p/demo](https://github.com/gnolang/gno/tree/master/examples/gno.land/p/demo) // // - [p/demo/avl](p/demo/avl) // - [p/demo/blog](p/demo/blog) @@ -162,7 +159,7 @@ func main() { // // --- // -// ### Contributions (WorxDAO & GoR) +// ## Contributions (WorxDAO & GoR) // // coming soon // @@ -172,18 +169,18 @@ func main() { //
//
// -// ### Socials +// ## Socials // // - Check out our [community projects](https://github.com/gnolang/awesome-gno) -// - ![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn) -// - ![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland) -// - ![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland) -// - ![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland) +// - [Discord](https://discord.gg/S8nKUqwkPn) +// - [Twitter](https://twitter.com/_gnoland) +// - [Youtube](https://www.youtube.com/@_gnoland) +// - [Telegram](https://t.me/gnoland) // //
//
// -// ### Quote of the ~Day~ Block#123 +// ## Quote of the ~Day~ Block#123 // // > Now, you Gno. // diff --git a/examples/gno.land/r/gnoland/home/overide_filetest.gno b/examples/gno.land/r/gnoland/home/overide_filetest.gno index 4f21b90a3c2..be7e33501d6 100644 --- a/examples/gno.land/r/gnoland/home/overide_filetest.gno +++ b/examples/gno.land/r/gnoland/home/overide_filetest.gno @@ -8,7 +8,7 @@ import ( ) func main() { - std.TestSetOrigCaller("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") + std.TestSetOrigCaller("g1manfred47kzduec920z88wfr64ylksmdcedlf5") home.AdminSetOverride("Hello World!") println(home.Render("")) home.AdminTransferOwnership(testutils.TestAddress("newAdmin")) diff --git a/examples/gno.land/r/gnoland/monit/gno.mod b/examples/gno.land/r/gnoland/monit/gno.mod index e67fdaa7d71..6086a3fa21f 100644 --- a/examples/gno.land/r/gnoland/monit/gno.mod +++ b/examples/gno.land/r/gnoland/monit/gno.mod @@ -1,8 +1 @@ module gno.land/r/gnoland/monit - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/watchdog v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/monit/monit.gno b/examples/gno.land/r/gnoland/monit/monit.gno index 8747ea582b3..be94fbdd2bb 100644 --- a/examples/gno.land/r/gnoland/monit/monit.gno +++ b/examples/gno.land/r/gnoland/monit/monit.gno @@ -20,7 +20,7 @@ var ( lastUpdate time.Time lastCaller std.Address wd = watchdog.Watchdog{Duration: 5 * time.Minute} - owner = ownable.New() // TODO: replace with -> ownable.NewWithAddress... + Ownable = ownable.New() // TODO: replace with -> ownable.NewWithAddress... watchdogDuration = 5 * time.Minute ) @@ -37,9 +37,8 @@ func Incr() int { // Reset resets the realm state. // This function can only be called by the admin. func Reset() { - if owner.CallerIsOwner() != nil { // TODO: replace with owner.AssertCallerIsOwner - panic("unauthorized") - } + Ownable.AssertCallerIsOwner() + counter = 0 lastCaller = std.PrevRealm().Addr() lastUpdate = time.Now() @@ -53,7 +52,3 @@ func Render(_ string) string { counter, lastUpdate, lastCaller, status, ) } - -// TransferOwnership transfers ownership to a new owner. This is a proxy to -// ownable.Ownable.TransferOwnership. -func TransferOwnership(newOwner std.Address) { owner.TransferOwnership(newOwner) } diff --git a/examples/gno.land/r/gnoland/pages/admin.gno b/examples/gno.land/r/gnoland/pages/admin.gno index ab447e8f604..71050f4ef57 100644 --- a/examples/gno.land/r/gnoland/pages/admin.gno +++ b/examples/gno.land/r/gnoland/pages/admin.gno @@ -15,7 +15,7 @@ var ( func init() { // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. - adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" + adminAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul } func AdminSetAdminAddr(addr std.Address) { diff --git a/examples/gno.land/r/gnoland/pages/gno.mod b/examples/gno.land/r/gnoland/pages/gno.mod index 31e9ad2c85b..e041fd948bc 100644 --- a/examples/gno.land/r/gnoland/pages/gno.mod +++ b/examples/gno.land/r/gnoland/pages/gno.mod @@ -1,6 +1 @@ module gno.land/r/gnoland/pages - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/blog v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/pages/page_about.gno b/examples/gno.land/r/gnoland/pages/page_about.gno index 6b1f5a6c556..99a879b4ba3 100644 --- a/examples/gno.land/r/gnoland/pages/page_about.gno +++ b/examples/gno.land/r/gnoland/pages/page_about.gno @@ -2,28 +2,28 @@ package gnopages func init() { path := "about" - title := "Gno.land Is A Platform To Write Smart Contracts In Gno" + title := "gno.land Is A Platform To Write Smart Contracts In Gno" // 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." body := ` -Gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go +gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go programming language. On gno.land, smart contracts can be uploaded on-chain only by publishing their full source code, -making it trivial to verify the contract or fork it into an improved version. With a system to publish reusable code -libraries on-chain, gno.land serves as the “GitHub” of the ecosystem, with realms built using fully transparent, +making it trivial to verify the contract or fork it into an improved version. With a system to publish reusable code +libraries on-chain, gno.land serves as the “GitHub” of the ecosystem, with realms built using fully transparent, auditable code that anyone can inspect and reuse. -Gno.land addresses many pressing issues in the blockchain space, starting with the ease of use and intuitiveness of -smart contract platforms. Developers can write smart contracts without having to learn a new language that’s exclusive +gno.land addresses many pressing issues in the blockchain space, starting with the ease of use and intuitiveness of +smart contract platforms. Developers can write smart contracts without having to learn a new language that’s exclusive to a single ecosystem or limited by design. Go developers can easily port their existing web apps to gno.land or build new ones from scratch, making web3 vastly more accessible. -Secured by Proof of Contribution (PoC), a DAO-managed Proof-of-Authority consensus mechanism, gno.land prioritizes -fairness and merit, rewarding the people most active on the platform. PoC restructures the financial incentives that -often corrupt blockchain projects, opting instead to reward contributors for their work based on expertise, commitment, and -alignment. +Secured by Proof of Contribution (PoC), a DAO-managed Proof-of-Authority consensus mechanism, gno.land prioritizes +fairness and merit, rewarding the people most active on the platform. PoC restructures the financial incentives that +often corrupt blockchain projects, opting instead to reward contributors for their work based on expertise, commitment, and +alignment. One of our inspirations for gno.land is the gospels, which built a system of moral code that lasted thousands of years. -By observing a minimal production implementation, gno.land’s design will endure over time and serve as a reference for -future generations with censorship-resistant tools that improve their understanding of the world. +By observing a minimal production implementation, gno.land’s design will endure over time and serve as a reference for +future generations with censorship-resistant tools that improve their understanding of the world. ` _ = b.NewPost("", path, title, body, "2022-05-20T13:17:22Z", nil, nil) } diff --git a/examples/gno.land/r/gnoland/pages/page_contribute.gno b/examples/gno.land/r/gnoland/pages/page_contribute.gno new file mode 100644 index 00000000000..0855dc327cd --- /dev/null +++ b/examples/gno.land/r/gnoland/pages/page_contribute.gno @@ -0,0 +1,106 @@ +package gnopages + +func init() { + path := "contribute" + title := "Contributor Ecosystem: Call for Contributions" + body := ` + +gno.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. + +gno.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. + +As 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. + +## Where to get started + +If 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. + +A 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. + +## Gno Bounties + +Additionally, you can look out to help on specific issues labeled as bounties. All contributions will then concur to form your profile for Game of Realms. + +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). + +Recommendations on participating in the gno.land Bounty Program: + +- 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. +- Once you have spent time on the code related to the bounty, we recommend submitting a 'draft' PR as soon as possible. + - 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. + - Make sure to reference the bounty issue on the PR description you're writing. + - After submitting the 'draft' PR, continue working until you are ready to mark the PR as "ready for review". + - 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. +- Ask for clarification early if an element on the requirements or implementation design is unclear. + - 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. + - 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. + - 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. + +You 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. + +Don'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: + +- 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). +- 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. + - 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. +- 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. + +Participants 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). + +### Bounty sizes + +Each 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. + +In 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. + +The 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. + + +t-shirt size | expected compensation +-------------|----------------------- +[XS] | $ 500 +[S] | $ 1000 +[M] | $ 2000 +[L] | $ 4000 +[XL] | $ 8000 +_[XXL]_ \* | $ 16000 +_[3XL]_ \* | $ 32000 + +[XS]: https://github.com/gnolang/gno/labels/bounty%2FXS +[S]: https://github.com/gnolang/gno/labels/bounty%2FS +[M]: https://github.com/gnolang/gno/labels/bounty%2FM +[L]: https://github.com/gnolang/gno/labels/bounty%2FL +[XL]: https://github.com/gnolang/gno/labels/bounty%2FXL +[XXL]: https://github.com/gnolang/gno/labels/bounty%2FXXL +[3XL]: https://github.com/gnolang/gno/labels/bounty%2F3XL + +\*: XXL and 3XL bounties are exceptional. Almost no issues will have these sizes; most will be broken down into smaller bounties. + +## gno.land Grants + +The 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. + +For more details on gno.land grants, suggested topics, and how to apply, visit our grants [repository](https://github.com/gnolang/grants). + +## Join Game of Realms + +Game 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). + +These 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. + +The 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. + +You 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. + +There are a variety of ways to make your contributions count: + +- Core code contributions +- Realm and pure package development +- Validator tooling +- Developer tooling +- Tutorials and documentation + +To 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.` + + _ = b.NewPost("", path, title, body, "2024-09-05T00:00:00Z", nil, nil) +} diff --git a/examples/gno.land/r/gnoland/pages/page_ecosystem.gno b/examples/gno.land/r/gnoland/pages/page_ecosystem.gno index e1a540c98a5..514ea7b2a98 100644 --- a/examples/gno.land/r/gnoland/pages/page_ecosystem.gno +++ b/examples/gno.land/r/gnoland/pages/page_ecosystem.gno @@ -3,42 +3,56 @@ package gnopages func init() { var ( path = "ecosystem" - title = "Discover Gno.land Ecosystem Projects & Initiatives" + title = "Discover gno.land Ecosystem Projects & Initiatives" // XXX: description = "Dive further into the gno.land ecosystem and discover the core infrastructure, projects, smart contracts, and tooling we’re building." body = ` ### [Gno Playground](https://play.gno.land) -Gno Playground is a simple web interface that lets you write, test, and experiment with your Gno code to improve your +Gno Playground is a simple web interface that lets you write, test, and experiment with your Gno code to improve your understanding of the Gno language. You can share your code, run unit tests, deploy your realms and packages, and execute -functions in your code using the repo. +functions in your code using the repo. Visit the playground at [play.gno.land](https://play.gno.land)! +### [Gno Studio Connect](https://gno.studio/connect) + +Gno Studio Connect provides seamless access to realms, making it simple to explore, interact, and engage +with gno.land’s smart contracts through function calls. Connect focuses on function calls, enabling users to interact +with any realm’s exposed function(s) on gno.land. + +See your realm interactions in [Gno Studio Connect](https://gno.studio/connect) + ### [Gnoscan](https://gnoscan.io) Developed by the Onbloc team, Gnoscan is gno.land’s blockchain explorer. Anyone can use Gnoscan to easily find -information that resides on the gno.land blockchain, such as wallet addresses, TX hashes, blocks, and contracts. +information that resides on the gno.land blockchain, such as wallet addresses, TX hashes, blocks, and contracts. Gnoscan makes our on-chain data easy to read and intuitive to discover. Explore the gno.land blockchain at [gnoscan.io](https://gnoscan.io)! ### Adena -Adena is a user-friendly non-custodial wallet for gno.land. Open-source and developed by Onbloc, Adena allows gnomes to +Adena is a user-friendly non-custodial wallet for gno.land. Open-source and developed by Onbloc, Adena allows gnomes to interact easily with the chain. With an emphasis on UX, Adena is built to handle millions of realms and tokens with a -high-quality interface, support for NFTs and custom tokens, and seamless integration. +high-quality interface, support for NFTs and custom tokens, and seamless integration. Install Adena via the [official website](https://www.adena.app/) ### Gnoswap -Gnoswap is currently under development and led by the Onbloc team. Gnoswap will be the first DEX on gno.land and is an +Gnoswap is currently under development and led by the Onbloc team. Gnoswap will be the first DEX on gno.land and is an automated market maker (AMM) protocol written in Gno that allows for permissionless token exchanges on the platform. ### Flippando Flippando is a simple on-chain memory game, ported from Solidity to Gno, which starts with an empty matrix to flip tiles -on to see what’s underneath. If the tiles match, they remain uncovered; if not, they are briefly shown, and the player +on to see what’s underneath. If the tiles match, they remain uncovered; if not, they are briefly shown, and the player must memorize their colors until the entire matrix is uncovered. The end result can be minted as an NFT, which can later -be assembled into bigger, more complex NFTs, creating a digital “painting” with the uncovered tiles. +be assembled into bigger, more complex NFTs, creating a digital “painting” with the uncovered tiles. Play the game at [Flippando](https://gno.flippando.xyz/flip) + +### Gno Native Kit + +[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. + + ` ) _ = b.NewPost("", path, title, body, "2022-05-20T13:17:23Z", nil, nil) diff --git a/examples/gno.land/r/gnoland/pages/page_gnolang.gno b/examples/gno.land/r/gnoland/pages/page_gnolang.gno index 13fc4072b1a..ac7bd9025b0 100644 --- a/examples/gno.land/r/gnoland/pages/page_gnolang.gno +++ b/examples/gno.land/r/gnoland/pages/page_gnolang.gno @@ -3,7 +3,7 @@ package gnopages func init() { var ( path = "gnolang" - title = "About the Gno, the Language for Gno.land" + title = "About the Gno, the Language for gno.land" // TODO fix broken images body = ` diff --git a/examples/gno.land/r/gnoland/pages/page_gor.gno b/examples/gno.land/r/gnoland/pages/page_gor.gno deleted file mode 100644 index d46e9cb0ccc..00000000000 --- a/examples/gno.land/r/gnoland/pages/page_gor.gno +++ /dev/null @@ -1,221 +0,0 @@ -package gnopages - -func init() { - path := "gor" - title := "Game of Realms - A Contest For The Best Contributors" - // XXX: description := "Game of Realms is the first high-stakes competition held in two phases to find the best contributors to the gno.land platform with a 133,700 ATOM prize pool." - body := ` - -
- -### Game of Realms - -The first high-stakes contest will see participants compete for tiered membership to co-own the gno.land blockchain. A series of complex technical and non-technical tasks will challenge contributors to create innovative patterns that push the chain to new limits. Start building the foundation for tomorrow through key smart contracts and other contributions that change our understanding of the world. - -
- -The competition is currently in phase one – for advanced developers only. - -Once the necessary tools to start phase two are ready, we’ll open up the competition to newer devs and non-technical contributors. - -If you want to stack ATOM rewards and play a key role in the success of gno.land and web3, read more about Game of Realms or open a [PR](https://github.com/gnolang/gno/) today. - -
- -
-
-
- -## Phase I. (ongoing) - -- - -- - -- - -
-
- -## Phase II. (Locked) - -
-
-
- -
- -
- -## Evaluation DAO - -This complex challenge seeks your skills in DAO development and implementation and is one of the most important challenges of phase one. The Evaluation DAO will ensure that contributions in Game of Realms and the gno.land platform are fairly rewarded. - -
- - - - - - - -
- -Game of Realms participants and core contributors are still in discussions, proposing additional ideas, and seeing how the proposal for the Evaluation DAO evolves over time. - -
- - - -
- -See [GitHub issue 519](https://github.com/gnolang/gno/issues/519) for the most up-to-date discussion so far on how voting should work for the DAO, what the responsibilities are, how to join, etc. - -
- - - - - - - - - - - - - - - - - -
-
- -
- -## Tutorials - -To progress to phase two of the competition, we need high-quality tutorials, guides, and documentation from phase one participants. Help to create materials that will onboard more contributors to gno.land. - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
- -## Governance Module - -Can you define and implement a governance contract suite that rivals existing ones, such as the Cosmos Hub? Show us how! We’re looking for the fairest and most efficient governance solution possible. - -
- - - - - - - -
- -Game of Realms participants and core contributors have made significant progress teaming up to complete this challenge but discussions and additional ideas are still ongoing. - -
- - - - - - - - - - - - - - - - - - - - - -
-
-
-
- -## Register Now - - -
-
- -
-
- - -
- -
- - -
- - -
-
-
- -` - _ = b.NewPost("", path, title, body, "2022-05-20T13:17:26Z", nil, nil) -} diff --git a/examples/gno.land/r/gnoland/pages/page_testnets.gno b/examples/gno.land/r/gnoland/pages/page_testnets.gno index 05f29a8e0f4..0811cd68e6d 100644 --- a/examples/gno.land/r/gnoland/pages/page_testnets.gno +++ b/examples/gno.land/r/gnoland/pages/page_testnets.gno @@ -2,14 +2,11 @@ package gnopages func init() { path := "testnets" - title := "Gno.land Testnet List" + title := "gno.land Testnet List" body := ` -- [Portal Loop](https://docs.gno.land/concepts/portal-loop) - a rolling testnet +- [Portal Loop](https://docs.gno.land/concepts/portal-loop) - a rolling testnet - [staging.gno.land](https://staging.gno.land) - wiped every commit to monorepo master -- test4.gno.land (upcoming) -- _[test3.gno.land](https://test3.gno.land) (latest)_ -- _[test2.gno.land](https://test2.gno.land) (archive)_ -- _[test1.gno.land](https://test1.gno.land) (archive)_ +- _[test4.gno.land](https://test4.gno.land) (latest)_ For a list of RPC endpoints, see the [reference documentation](https://docs.gno.land/reference/rpc-endpoints). diff --git a/examples/gno.land/r/gnoland/pages/page_tokenomics.gno b/examples/gno.land/r/gnoland/pages/page_tokenomics.gno index f51364c36e6..3070e58cc6f 100644 --- a/examples/gno.land/r/gnoland/pages/page_tokenomics.gno +++ b/examples/gno.land/r/gnoland/pages/page_tokenomics.gno @@ -3,7 +3,7 @@ package gnopages func init() { var ( path = "tokenomics" - title = "Gno.land Tokenomics" + title = "gno.land Tokenomics" // XXX: description = """ body = `Lorem Ipsum` ) diff --git a/examples/gno.land/r/gnoland/pages/pages_test.gno b/examples/gno.land/r/gnoland/pages/pages_test.gno index c7972686bb3..16984a1c7ff 100644 --- a/examples/gno.land/r/gnoland/pages/pages_test.gno +++ b/examples/gno.land/r/gnoland/pages/pages_test.gno @@ -11,7 +11,7 @@ func TestHome(t *testing.T) { expectedSubtrings := []string{ "/r/gnoland/pages:p/tokenomics", "/r/gnoland/pages:p/start", - "/r/gnoland/pages:p/gor", + "/r/gnoland/pages:p/contribute", "/r/gnoland/pages:p/about", "/r/gnoland/pages:p/gnolang", } @@ -30,8 +30,8 @@ func TestAbout(t *testing.T) { printedOnce := false got := Render("p/about") expectedSubtrings := []string{ - "Gno.land Is A Platform To Write Smart Contracts In Gno", - "Gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\nprogramming language.", + "gno.land Is A Platform To Write Smart Contracts In Gno", + "gno.land is a next-generation smart contract platform using Gno, an interpreted version of the general-purpose Go\nprogramming language.", } for _, substring := range expectedSubtrings { if !strings.Contains(got, substring) { diff --git a/examples/gno.land/r/gnoland/valopers/gno.mod b/examples/gno.land/r/gnoland/valopers/gno.mod deleted file mode 100644 index 2d24fb27952..00000000000 --- a/examples/gno.land/r/gnoland/valopers/gno.mod +++ /dev/null @@ -1,11 +0,0 @@ -module gno.land/r/gnoland/valopers - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/sys/validators v0.0.0-latest - gno.land/r/gov/dao v0.0.0-latest - gno.land/r/sys/validators v0.0.0-latest -) diff --git a/examples/gno.land/r/gnoland/valopers/v2/gno.mod b/examples/gno.land/r/gnoland/valopers/v2/gno.mod new file mode 100644 index 00000000000..064fe6d811e --- /dev/null +++ b/examples/gno.land/r/gnoland/valopers/v2/gno.mod @@ -0,0 +1 @@ +module gno.land/r/gnoland/valopers/v2 diff --git a/examples/gno.land/r/gnoland/valopers/init.gno b/examples/gno.land/r/gnoland/valopers/v2/init.gno similarity index 100% rename from examples/gno.land/r/gnoland/valopers/init.gno rename to examples/gno.land/r/gnoland/valopers/v2/init.gno diff --git a/examples/gno.land/r/gnoland/valopers/valopers.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno similarity index 90% rename from examples/gno.land/r/gnoland/valopers/valopers.gno rename to examples/gno.land/r/gnoland/valopers/v2/valopers.gno index 74cec941e0d..d88ea4b872f 100644 --- a/examples/gno.land/r/gnoland/valopers/valopers.gno +++ b/examples/gno.land/r/gnoland/valopers/v2/valopers.gno @@ -6,10 +6,11 @@ import ( "std" "gno.land/p/demo/avl" + "gno.land/p/demo/dao" "gno.land/p/demo/ufmt" pVals "gno.land/p/sys/validators" - govdao "gno.land/r/gov/dao" - "gno.land/r/sys/validators" + "gno.land/r/gov/dao/bridge" + validators "gno.land/r/sys/validators/v2" ) const ( @@ -25,6 +26,7 @@ var valopers *avl.Tree // Address -> Valoper // Valoper represents a validator operator profile type Valoper struct { Name string // the display name of the valoper + Moniker string // the moniker of the valoper Description string // the description of the valoper Address std.Address // The bech32 gno address of the validator @@ -101,7 +103,7 @@ func Render(_ string) string { // Render renders a single valoper with their information func (v Valoper) Render() string { - output := ufmt.Sprintf("## %s\n", v.Name) + output := ufmt.Sprintf("## %s (%s)\n", v.Name, v.Moniker) output += ufmt.Sprintf("%s\n\n", v.Description) output += ufmt.Sprintf("- Address: %s\n", v.Address.String()) output += ufmt.Sprintf("- PubKey: %s\n", v.PubKey) @@ -168,14 +170,19 @@ func GovDAOProposal(address std.Address) { // Create the executor executor := validators.NewPropExecutor(changesFn) - // Craft the proposal comment - comment := ufmt.Sprintf( - "Proposal to add valoper %s (Address: %s; PubKey: %s) to the valset", + // Craft the proposal description + description := ufmt.Sprintf( + "Add valoper %s (Address: %s; PubKey: %s) to the valset", valoper.Name, valoper.Address.String(), valoper.PubKey, ) + prop := dao.ProposalRequest{ + Description: description, + Executor: executor, + } + // Create the govdao proposal - govdao.Propose(comment, executor) + bridge.GovDAO().Propose(prop) } diff --git a/examples/gno.land/r/gnoland/valopers/valopers_test.gno b/examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno similarity index 97% rename from examples/gno.land/r/gnoland/valopers/valopers_test.gno rename to examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno index 89544c46ee5..b5940738769 100644 --- a/examples/gno.land/r/gnoland/valopers/valopers_test.gno +++ b/examples/gno.land/r/gnoland/valopers/v2/valopers_test.gno @@ -38,6 +38,7 @@ func TestValopers_Register(t *testing.T) { v := Valoper{ Address: testutils.TestAddress("valoper"), Name: "new valoper", + Moniker: "val-1", PubKey: "pub key", } @@ -50,6 +51,7 @@ func TestValopers_Register(t *testing.T) { uassert.Equal(t, v.Address, valoper.Address) uassert.Equal(t, v.Name, valoper.Name) + uassert.Equal(t, v.Moniker, valoper.Moniker) uassert.Equal(t, v.PubKey, valoper.PubKey) }) }) diff --git a/examples/gno.land/r/gov/dao/bridge/bridge.gno b/examples/gno.land/r/gov/dao/bridge/bridge.gno new file mode 100644 index 00000000000..ba47978f33f --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/bridge.gno @@ -0,0 +1,39 @@ +package bridge + +import ( + "std" + + "gno.land/p/demo/ownable" +) + +const initialOwner = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @moul + +var b *Bridge + +// Bridge is the active GovDAO +// implementation bridge +type Bridge struct { + *ownable.Ownable + + dao DAO +} + +// init constructs the initial GovDAO implementation +func init() { + b = &Bridge{ + Ownable: ownable.NewWithAddress(initialOwner), + dao: &govdaoV2{}, + } +} + +// SetDAO sets the currently active GovDAO implementation +func SetDAO(dao DAO) { + b.AssertCallerIsOwner() + + b.dao = dao +} + +// GovDAO returns the current GovDAO implementation +func GovDAO() DAO { + return b.dao +} diff --git a/examples/gno.land/r/gov/dao/bridge/bridge_test.gno b/examples/gno.land/r/gov/dao/bridge/bridge_test.gno new file mode 100644 index 00000000000..38b5d4be257 --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/bridge_test.gno @@ -0,0 +1,64 @@ +package bridge + +import ( + "testing" + + "std" + + "gno.land/p/demo/dao" + "gno.land/p/demo/ownable" + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestBridge_DAO(t *testing.T) { + var ( + proposalID = uint64(10) + mockDAO = &mockDAO{ + proposeFn: func(_ dao.ProposalRequest) uint64 { + return proposalID + }, + } + ) + + b.dao = mockDAO + + uassert.Equal(t, proposalID, GovDAO().Propose(dao.ProposalRequest{})) +} + +func TestBridge_SetDAO(t *testing.T) { + t.Run("invalid owner", func(t *testing.T) { + // Attempt to set a new DAO implementation + uassert.PanicsWithMessage(t, ownable.ErrUnauthorized.Error(), func() { + SetDAO(&mockDAO{}) + }) + }) + + t.Run("valid owner", func(t *testing.T) { + var ( + addr = testutils.TestAddress("owner") + + proposalID = uint64(10) + mockDAO = &mockDAO{ + proposeFn: func(_ dao.ProposalRequest) uint64 { + return proposalID + }, + } + ) + + std.TestSetOrigCaller(addr) + + b.Ownable = ownable.NewWithAddress(addr) + + urequire.NotPanics(t, func() { + SetDAO(mockDAO) + }) + + uassert.Equal( + t, + mockDAO.Propose(dao.ProposalRequest{}), + GovDAO().Propose(dao.ProposalRequest{}), + ) + }) +} diff --git a/examples/gno.land/r/gov/dao/bridge/doc.gno b/examples/gno.land/r/gov/dao/bridge/doc.gno new file mode 100644 index 00000000000..f812b3c0787 --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/doc.gno @@ -0,0 +1,4 @@ +// Package bridge represents a GovDAO implementation wrapper, used by other Realms and Packages to +// always fetch the most active GovDAO implementation, instead of directly referencing it, and having to +// update it each time the GovDAO implementation changes +package bridge diff --git a/examples/gno.land/r/gov/dao/bridge/gno.mod b/examples/gno.land/r/gov/dao/bridge/gno.mod new file mode 100644 index 00000000000..9f472eaa464 --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/gno.mod @@ -0,0 +1 @@ +module gno.land/r/gov/dao/bridge diff --git a/examples/gno.land/r/gov/dao/bridge/mock_test.gno b/examples/gno.land/r/gov/dao/bridge/mock_test.gno new file mode 100644 index 00000000000..05ac430b4c4 --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/mock_test.gno @@ -0,0 +1,68 @@ +package bridge + +import ( + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" +) + +type ( + proposeDelegate func(dao.ProposalRequest) uint64 + voteOnProposalDelegate func(uint64, dao.VoteOption) + executeProposalDelegate func(uint64) + getPropStoreDelegate func() dao.PropStore + getMembStoreDelegate func() membstore.MemberStore + newGovDAOExecutorDelegate func(func() error) dao.Executor +) + +type mockDAO struct { + proposeFn proposeDelegate + voteOnProposalFn voteOnProposalDelegate + executeProposalFn executeProposalDelegate + getPropStoreFn getPropStoreDelegate + getMembStoreFn getMembStoreDelegate + newGovDAOExecutorFn newGovDAOExecutorDelegate +} + +func (m *mockDAO) Propose(request dao.ProposalRequest) uint64 { + if m.proposeFn != nil { + return m.proposeFn(request) + } + + return 0 +} + +func (m *mockDAO) VoteOnProposal(id uint64, option dao.VoteOption) { + if m.voteOnProposalFn != nil { + m.voteOnProposalFn(id, option) + } +} + +func (m *mockDAO) ExecuteProposal(id uint64) { + if m.executeProposalFn != nil { + m.executeProposalFn(id) + } +} + +func (m *mockDAO) GetPropStore() dao.PropStore { + if m.getPropStoreFn != nil { + return m.getPropStoreFn() + } + + return nil +} + +func (m *mockDAO) GetMembStore() membstore.MemberStore { + if m.getMembStoreFn != nil { + return m.getMembStoreFn() + } + + return nil +} + +func (m *mockDAO) NewGovDAOExecutor(cb func() error) dao.Executor { + if m.newGovDAOExecutorFn != nil { + return m.newGovDAOExecutorFn(cb) + } + + return nil +} diff --git a/examples/gno.land/r/gov/dao/bridge/types.gno b/examples/gno.land/r/gov/dao/bridge/types.gno new file mode 100644 index 00000000000..27ea8fb62d4 --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/types.gno @@ -0,0 +1,17 @@ +package bridge + +import ( + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" +) + +// DAO abstracts the commonly used DAO interface +type DAO interface { + Propose(dao.ProposalRequest) uint64 + VoteOnProposal(uint64, dao.VoteOption) + ExecuteProposal(uint64) + GetPropStore() dao.PropStore + GetMembStore() membstore.MemberStore + + NewGovDAOExecutor(func() error) dao.Executor +} diff --git a/examples/gno.land/r/gov/dao/bridge/v2.gno b/examples/gno.land/r/gov/dao/bridge/v2.gno new file mode 100644 index 00000000000..216419cf31d --- /dev/null +++ b/examples/gno.land/r/gov/dao/bridge/v2.gno @@ -0,0 +1,42 @@ +package bridge + +import ( + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" + govdao "gno.land/r/gov/dao/v2" +) + +// govdaoV2 is a wrapper for interacting with the /r/gov/dao/v2 Realm +type govdaoV2 struct{} + +func (g *govdaoV2) Propose(request dao.ProposalRequest) uint64 { + return govdao.Propose(request) +} + +func (g *govdaoV2) VoteOnProposal(id uint64, option dao.VoteOption) { + govdao.VoteOnProposal(id, option) +} + +func (g *govdaoV2) ExecuteProposal(id uint64) { + govdao.ExecuteProposal(id) +} + +func (g *govdaoV2) GetPropStore() dao.PropStore { + return govdao.GetPropStore() +} + +func (g *govdaoV2) GetMembStore() membstore.MemberStore { + return govdao.GetMembStore() +} + +func (g *govdaoV2) NewGovDAOExecutor(cb func() error) dao.Executor { + return govdao.NewGovDAOExecutor(cb) +} + +func (g *govdaoV2) NewMemberPropExecutor(cb func() []membstore.Member) dao.Executor { + return govdao.NewMemberPropExecutor(cb) +} + +func (g *govdaoV2) NewMembStoreImplExecutor(cb func() membstore.MemberStore) dao.Executor { + return govdao.NewMembStoreImplExecutor(cb) +} diff --git a/examples/gno.land/r/gov/dao/dao.gno b/examples/gno.land/r/gov/dao/dao.gno deleted file mode 100644 index 632935dafed..00000000000 --- a/examples/gno.land/r/gov/dao/dao.gno +++ /dev/null @@ -1,207 +0,0 @@ -package govdao - -import ( - "std" - "strconv" - - "gno.land/p/demo/ufmt" - pproposal "gno.land/p/gov/proposal" -) - -var ( - proposals = make([]*proposal, 0) - members = make([]std.Address, 0) // XXX: these should be pointers to avoid data duplication. Not possible due to VM bugs -) - -const ( - msgMissingExecutor = "missing proposal executor" - msgPropExecuted = "prop already executed" - msgPropExpired = "prop is expired" - msgPropInactive = "prop is not active anymore" - msgPropActive = "prop is still active" - msgPropNotAccepted = "prop is not accepted" - - msgCallerNotAMember = "caller is not member of govdao" - msgProposalNotFound = "proposal not found" -) - -type proposal struct { - author std.Address - comment string - executor pproposal.Executor - voter Voter - executed bool - voters []std.Address // XXX: these should be pointers to avoid data duplication. Not possible due to VM bugs. -} - -func (p proposal) Status() Status { - if p.executor.IsExpired() { - return Expired - } - - if p.executor.IsDone() { - return Succeeded - } - - if !p.voter.IsFinished(members) { - return Active - } - - if p.voter.IsAccepted(members) { - return Accepted - } - - return NotAccepted -} - -// Propose is designed to be called by another contract or with -// `maketx run`, not by a `maketx call`. -func Propose(comment string, executor pproposal.Executor) int { - // XXX: require payment? - if executor == nil { - panic(msgMissingExecutor) - } - caller := std.GetOrigCaller() // XXX: CHANGE THIS WHEN MSGRUN PERSIST CODE ESCAPING THE main() SCOPE! IT IS UNSAFE! - AssertIsMember(caller) - - prop := &proposal{ - comment: comment, - executor: executor, - author: caller, - voter: NewPercentageVoter(66), // at least 2/3 must say yes - } - - proposals = append(proposals, prop) - - return len(proposals) - 1 -} - -func VoteOnProposal(idx int, option string) { - assertProposalExists(idx) - caller := std.GetOrigCaller() // XXX: CHANGE THIS WHEN MSGRUN PERSIST CODE ESCAPING THE main() SCOPE! IT IS UNSAFE! - AssertIsMember(caller) - - prop := getProposal(idx) - - if prop.executed { - panic(msgPropExecuted) - } - - if prop.executor.IsExpired() { - panic(msgPropExpired) - } - - if prop.voter.IsFinished(members) { - panic(msgPropInactive) - } - - prop.voter.Vote(members, caller, option) -} - -func ExecuteProposal(idx int) { - assertProposalExists(idx) - prop := getProposal(idx) - - if prop.executed { - panic(msgPropExecuted) - } - - if prop.executor.IsExpired() { - panic(msgPropExpired) - } - - if !prop.voter.IsFinished(members) { - panic(msgPropActive) - } - - if !prop.voter.IsAccepted(members) { - panic(msgPropNotAccepted) - } - - prop.executor.Execute() - prop.voters = members - prop.executed = true -} - -func IsMember(addr std.Address) bool { - if len(members) == 0 { // special case for initial execution - return true - } - - for _, v := range members { - if v == addr { - return true - } - } - - return false -} - -func AssertIsMember(addr std.Address) { - if !IsMember(addr) { - panic(msgCallerNotAMember) - } -} - -func Render(path string) string { - if path == "" { - if len(proposals) == 0 { - return "No proposals found :(" // corner case - } - - output := "" - for idx, prop := range proposals { - output += ufmt.Sprintf("- [%d](/r/gov/dao:%d) - %s (**%s**)(by %s)\n", idx, idx, prop.comment, string(prop.Status()), prop.author) - } - - return output - } - - // else display the proposal - idx, err := strconv.Atoi(path) - if err != nil { - return "404" - } - - if !proposalExists(idx) { - return "404" - } - prop := getProposal(idx) - - vs := members - if prop.executed { - vs = prop.voters - } - - output := "" - output += ufmt.Sprintf("# Prop #%d", idx) - output += "\n\n" - output += prop.comment - output += "\n\n" - output += ufmt.Sprintf("Status: %s", string(prop.Status())) - output += "\n\n" - output += ufmt.Sprintf("Voting status: %s", prop.voter.Status(vs)) - output += "\n\n" - output += ufmt.Sprintf("Author: %s", string(prop.author)) - output += "\n\n" - - return output -} - -func getProposal(idx int) *proposal { - if idx > len(proposals)-1 { - panic(msgProposalNotFound) - } - - return proposals[idx] -} - -func proposalExists(idx int) bool { - return idx >= 0 && idx <= len(proposals) -} - -func assertProposalExists(idx int) { - if !proposalExists(idx) { - panic("invalid proposal id") - } -} diff --git a/examples/gno.land/r/gov/dao/dao_test.gno b/examples/gno.land/r/gov/dao/dao_test.gno deleted file mode 100644 index 96eaba7f5e9..00000000000 --- a/examples/gno.land/r/gov/dao/dao_test.gno +++ /dev/null @@ -1,192 +0,0 @@ -package govdao - -import ( - "std" - "testing" - - "gno.land/p/demo/testutils" - "gno.land/p/demo/urequire" - pproposal "gno.land/p/gov/proposal" -) - -func TestPackage(t *testing.T) { - u1 := testutils.TestAddress("u1") - u2 := testutils.TestAddress("u2") - u3 := testutils.TestAddress("u3") - - members = append(members, u1) - members = append(members, u2) - members = append(members, u3) - - nu1 := testutils.TestAddress("random1") - - out := Render("") - - expected := "No proposals found :(" - urequire.Equal(t, expected, out) - - var called bool - ex := pproposal.NewExecutor(func() error { - called = true - return nil - }) - - std.TestSetOrigCaller(u1) - pid := Propose("dummy proposal", ex) - - // try to vote not being a member - std.TestSetOrigCaller(nu1) - - urequire.PanicsWithMessage(t, msgCallerNotAMember, func() { - VoteOnProposal(pid, "YES") - }) - - // try to vote several times - std.TestSetOrigCaller(u1) - urequire.NotPanics(t, func() { - VoteOnProposal(pid, "YES") - }) - urequire.PanicsWithMessage(t, msgAlreadyVoted, func() { - VoteOnProposal(pid, "YES") - }) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: active - -Voting status: YES: 1, NO: 0, percent: 33, members: 3 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - std.TestSetOrigCaller(u2) - urequire.PanicsWithMessage(t, msgWrongVotingValue, func() { - VoteOnProposal(pid, "INCORRECT") - }) - urequire.NotPanics(t, func() { - VoteOnProposal(pid, "NO") - }) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: active - -Voting status: YES: 1, NO: 1, percent: 33, members: 3 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - std.TestSetOrigCaller(u3) - urequire.NotPanics(t, func() { - VoteOnProposal(pid, "YES") - }) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: accepted - -Voting status: YES: 2, NO: 1, percent: 66, members: 3 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - // Add a new member, so non-executed proposals will change the voting status - u4 := testutils.TestAddress("u4") - members = append(members, u4) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: active - -Voting status: YES: 2, NO: 1, percent: 50, members: 4 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - std.TestSetOrigCaller(u4) - urequire.NotPanics(t, func() { - VoteOnProposal(pid, "YES") - }) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: accepted - -Voting status: YES: 3, NO: 1, percent: 75, members: 4 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - ExecuteProposal(pid) - urequire.True(t, called) - - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: succeeded - -Voting status: YES: 3, NO: 1, percent: 75, members: 4 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - - // Add a new member and try to vote an already executed proposal - u5 := testutils.TestAddress("u5") - members = append(members, u5) - std.TestSetOrigCaller(u5) - urequire.PanicsWithMessage(t, msgPropExecuted, func() { - ExecuteProposal(pid) - }) - - // even if we added a new member the executed proposal is showing correctly the members that voted on it - out = Render("0") - expected = `# Prop #0 - -dummy proposal - -Status: succeeded - -Voting status: YES: 3, NO: 1, percent: 75, members: 4 - -Author: g1w5c47h6lta047h6lta047h6lta047h6ly5kscr - -` - - urequire.Equal(t, expected, out) - -} diff --git a/examples/gno.land/r/gov/dao/gno.mod b/examples/gno.land/r/gov/dao/gno.mod deleted file mode 100644 index f3c0bae990e..00000000000 --- a/examples/gno.land/r/gov/dao/gno.mod +++ /dev/null @@ -1,8 +0,0 @@ -module gno.land/r/gov/dao - -require ( - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/urequire v0.0.0-latest - gno.land/p/gov/proposal v0.0.0-latest -) diff --git a/examples/gno.land/r/gov/dao/memberset.gno b/examples/gno.land/r/gov/dao/memberset.gno deleted file mode 100644 index 3abd52ae99d..00000000000 --- a/examples/gno.land/r/gov/dao/memberset.gno +++ /dev/null @@ -1,40 +0,0 @@ -package govdao - -import ( - "std" - - pproposal "gno.land/p/gov/proposal" -) - -const daoPkgPath = "gno.land/r/gov/dao" - -const ( - errNoChangesProposed = "no set changes proposed" - errNotGovDAO = "caller not govdao executor" -) - -func NewPropExecutor(changesFn func() []std.Address) pproposal.Executor { - if changesFn == nil { - panic(errNoChangesProposed) - } - - callback := func() error { - // Make sure the GovDAO executor runs the valset changes - assertGovDAOCaller() - - for _, addr := range changesFn() { - members = append(members, addr) - } - - return nil - } - - return pproposal.NewExecutor(callback) -} - -// assertGovDAOCaller verifies the caller is the GovDAO executor -func assertGovDAOCaller() { - if std.CurrentRealm().PkgPath() != daoPkgPath { - panic(errNotGovDAO) - } -} diff --git a/examples/gno.land/r/gov/dao/prop1_filetest.gno b/examples/gno.land/r/gov/dao/prop1_filetest.gno deleted file mode 100644 index 49a200fd561..00000000000 --- a/examples/gno.land/r/gov/dao/prop1_filetest.gno +++ /dev/null @@ -1,131 +0,0 @@ -// Please note that this package is intended for demonstration purposes only. -// You could execute this code (the init part) by running a `maketx run` command -// or by uploading a similar package to a personal namespace. -// -// For the specific case of validators, a `r/gnoland/valopers` will be used to -// organize the lifecycle of validators (register, etc), and this more complex -// contract will be responsible to generate proposals. -package main - -import ( - "std" - - pVals "gno.land/p/sys/validators" - govdao "gno.land/r/gov/dao" - "gno.land/r/sys/validators" -) - -const daoPkgPath = "gno.land/r/gov/dao" - -func init() { - membersFn := func() []std.Address { - return []std.Address{ - std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), - } - } - - mExec := govdao.NewPropExecutor(membersFn) - - comment := "adding someone to vote" - id := govdao.Propose(comment, mExec) - govdao.ExecuteProposal(id) - - changesFn := func() []pVals.Validator { - return []pVals.Validator{ - { - Address: std.Address("g12345678"), - PubKey: "pubkey", - VotingPower: 10, // add a new validator - }, - { - Address: std.Address("g000000000"), - PubKey: "pubkey", - VotingPower: 10, // add a new validator - }, - { - Address: std.Address("g000000000"), - PubKey: "pubkey", - VotingPower: 0, // remove an existing validator - }, - } - } - - // Wraps changesFn to emit a certified event only if executed from a - // complete governance proposal process. - executor := validators.NewPropExecutor(changesFn) - - // Create a proposal. - // XXX: payment - comment = "manual valset changes proposal example" - govdao.Propose(comment, executor) -} - -func main() { - println("--") - println(govdao.Render("")) - println("--") - println(govdao.Render("1")) - println("--") - govdao.VoteOnProposal(1, "YES") - println("--") - println(govdao.Render("1")) - println("--") - println(validators.Render("")) - println("--") - govdao.ExecuteProposal(1) - println("--") - println(govdao.Render("1")) - println("--") - println(validators.Render("")) -} - -// Output: -// -- -// - [0](/r/gov/dao:0) - adding someone to vote (**succeeded**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) -// - [1](/r/gov/dao:1) - manual valset changes proposal example (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) -// -// -- -// # Prop #1 -// -// manual valset changes proposal example -// -// Status: active -// -// Voting status: YES: 0, NO: 0, percent: 0, members: 1 -// -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// -// -// -- -// -- -// # Prop #1 -// -// manual valset changes proposal example -// -// Status: accepted -// -// Voting status: YES: 1, NO: 0, percent: 100, members: 1 -// -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// -// -// -- -// No valset changes to apply. -// -- -// -- -// # Prop #1 -// -// manual valset changes proposal example -// -// Status: succeeded -// -// Voting status: YES: 1, NO: 0, percent: 100, members: 1 -// -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// -// -// -- -// Valset changes: -// - #123: g12345678 (10) -// - #123: g000000000 (10) -// - #123: g000000000 (0) diff --git a/examples/gno.land/r/gov/dao/prop2_filetest.gno b/examples/gno.land/r/gov/dao/prop2_filetest.gno deleted file mode 100644 index 047709cc45f..00000000000 --- a/examples/gno.land/r/gov/dao/prop2_filetest.gno +++ /dev/null @@ -1,120 +0,0 @@ -package main - -import ( - "std" - "time" - - "gno.land/p/demo/context" - "gno.land/p/gov/proposal" - gnoblog "gno.land/r/gnoland/blog" - govdao "gno.land/r/gov/dao" -) - -func init() { - membersFn := func() []std.Address { - return []std.Address{ - std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), - } - } - - mExec := govdao.NewPropExecutor(membersFn) - - comment := "adding someone to vote" - - id := govdao.Propose(comment, mExec) - - govdao.ExecuteProposal(id) - - executor := proposal.NewCtxExecutor(func(ctx context.Context) error { - gnoblog.DaoAddPost( - ctx, - "hello-from-govdao", // slug - "Hello from GovDAO!", // title - "This post was published by a GovDAO proposal.", // body - time.Now().Format(time.RFC3339), // publidation date - "moul", // authors - "govdao,example", // tags - ) - return nil - }) - - // Create a proposal. - // XXX: payment - comment = "post a new blogpost about govdao" - govdao.Propose(comment, executor) -} - -func main() { - println("--") - println(govdao.Render("")) - println("--") - println(govdao.Render("1")) - println("--") - govdao.VoteOnProposal(1, "YES") - println("--") - println(govdao.Render("1")) - println("--") - println(gnoblog.Render("")) - println("--") - govdao.ExecuteProposal(1) - println("--") - println(govdao.Render("1")) - println("--") - println(gnoblog.Render("")) -} - -// Output: -// -- -// - [0](/r/gov/dao:0) - adding someone to vote (**succeeded**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) -// - [1](/r/gov/dao:1) - post a new blogpost about govdao (**active**)(by g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm) -// -// -- -// # Prop #1 -// -// post a new blogpost about govdao -// -// Status: active -// -// Voting status: YES: 0, NO: 0, percent: 0, members: 1 -// -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// -// -// -- -// -- -// # Prop #1 -// -// post a new blogpost about govdao -// -// Status: accepted -// -// Voting status: YES: 1, NO: 0, percent: 100, members: 1 -// -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// -// -// -- -// # Gnoland's Blog -// -// No posts. -// -- -// -- -// # Prop #1 -// -// post a new blogpost about govdao -// -// Status: succeeded -// -// Voting status: YES: 1, NO: 0, percent: 100, members: 1 -// -// Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm -// -// -// -- -// # Gnoland's Blog -// -//
-// -// ### [Hello from GovDAO!](/r/gnoland/blog:p/hello-from-govdao) -// 13 Feb 2009 -//
diff --git a/examples/gno.land/r/gov/dao/types.gno b/examples/gno.land/r/gov/dao/types.gno deleted file mode 100644 index 123fc489075..00000000000 --- a/examples/gno.land/r/gov/dao/types.gno +++ /dev/null @@ -1,32 +0,0 @@ -package govdao - -import ( - "std" -) - -// Status enum. -type Status string - -var ( - Accepted Status = "accepted" - Active Status = "active" - NotAccepted Status = "not accepted" - Expired Status = "expired" - Succeeded Status = "succeeded" -) - -// Voter defines the needed methods for a voting system -type Voter interface { - - // IsAccepted indicates if the voting process had been accepted - IsAccepted(voters []std.Address) bool - - // IsFinished indicates if the voting process is finished - IsFinished(voters []std.Address) bool - - // Vote adds a new vote to the voting system - Vote(voters []std.Address, caller std.Address, flag string) - - // Status returns a human friendly string describing how the voting process is going - Status(voters []std.Address) string -} diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno new file mode 100644 index 00000000000..5ee8e63236a --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -0,0 +1,67 @@ +package govdao + +import ( + "std" + + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" + "gno.land/p/demo/simpledao" +) + +var ( + d *simpledao.SimpleDAO // the current active DAO implementation + members membstore.MemberStore // the member store +) + +const daoPkgPath = "gno.land/r/gov/dao/v2" + +func init() { + // Example initial member set (just test addresses) + set := []membstore.Member{ + { + Address: std.Address("g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm"), + VotingPower: 10, + }, + } + + // Set the member store + members = membstore.NewMembStore(membstore.WithInitialMembers(set), membstore.WithDAOPkgPath(daoPkgPath)) + + // Set the DAO implementation + d = simpledao.New(members) +} + +// Propose is designed to be called by another contract or with +// `maketx run`, not by a `maketx call`. +func Propose(request dao.ProposalRequest) uint64 { + idx, err := d.Propose(request) + if err != nil { + panic(err) + } + + return idx +} + +// VoteOnProposal casts a vote for the given proposal +func VoteOnProposal(id uint64, option dao.VoteOption) { + if err := d.VoteOnProposal(id, option); err != nil { + panic(err) + } +} + +// ExecuteProposal executes the proposal +func ExecuteProposal(id uint64) { + if err := d.ExecuteProposal(id); err != nil { + panic(err) + } +} + +// GetPropStore returns the active proposal store +func GetPropStore() dao.PropStore { + return d +} + +// GetMembStore returns the active member store +func GetMembStore() membstore.MemberStore { + return members +} diff --git a/examples/gno.land/r/gov/dao/v2/gno.mod b/examples/gno.land/r/gov/dao/v2/gno.mod new file mode 100644 index 00000000000..4daf8c600a1 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/gno.mod @@ -0,0 +1 @@ +module gno.land/r/gov/dao/v2 diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno new file mode 100644 index 00000000000..30d8a403f6e --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -0,0 +1,92 @@ +package govdao + +import ( + "errors" + "std" + + "gno.land/p/demo/combinederr" + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" + "gno.land/p/gov/executor" +) + +var errNoChangesProposed = errors.New("no set changes proposed") + +// NewGovDAOExecutor creates the govdao wrapped callback executor +func NewGovDAOExecutor(cb func() error) dao.Executor { + if cb == nil { + panic(errNoChangesProposed) + } + + return executor.NewCallbackExecutor( + cb, + std.CurrentRealm().PkgPath(), + ) +} + +// NewMemberPropExecutor returns the GOVDAO member change executor +func NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor { + if changesFn == nil { + panic(errNoChangesProposed) + } + + callback := func() error { + errs := &combinederr.CombinedError{} + cbMembers := changesFn() + + for _, member := range cbMembers { + switch { + case !members.IsMember(member.Address): + // Addition request + err := members.AddMember(member) + + errs.Add(err) + case member.VotingPower == 0: + // Remove request + err := members.UpdateMember(member.Address, membstore.Member{ + Address: member.Address, + VotingPower: 0, // 0 indicated removal + }) + + errs.Add(err) + default: + // Update request + err := members.UpdateMember(member.Address, member) + + errs.Add(err) + } + } + + // Check if there were any execution errors + if errs.Size() == 0 { + return nil + } + + return errs + } + + return NewGovDAOExecutor(callback) +} + +func NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor { + if changeFn == nil { + panic(errNoChangesProposed) + } + + callback := func() error { + setMembStoreImpl(changeFn()) + + return nil + } + + return NewGovDAOExecutor(callback) +} + +// setMembStoreImpl sets a new dao.MembStore implementation +func setMembStoreImpl(impl membstore.MemberStore) { + if impl == nil { + panic("invalid member store") + } + + members = impl +} diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno new file mode 100644 index 00000000000..7d8975e1fe8 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -0,0 +1,259 @@ +// Please note that this package is intended for demonstration purposes only. +// You could execute this code (the init part) by running a `maketx run` command +// or by uploading a similar package to a personal namespace. +// +// For the specific case of validators, a `r/gnoland/valopers` will be used to +// organize the lifecycle of validators (register, etc), and this more complex +// contract will be responsible to generate proposals. +package main + +import ( + "std" + + "gno.land/p/demo/dao" + pVals "gno.land/p/sys/validators" + govdao "gno.land/r/gov/dao/v2" + validators "gno.land/r/sys/validators/v2" +) + +func init() { + changesFn := func() []pVals.Validator { + return []pVals.Validator{ + { + Address: std.Address("g12345678"), + PubKey: "pubkey", + VotingPower: 10, // add a new validator + }, + { + Address: std.Address("g000000000"), + PubKey: "pubkey", + VotingPower: 10, // add a new validator + }, + { + Address: std.Address("g000000000"), + PubKey: "pubkey", + VotingPower: 0, // remove an existing validator + }, + } + } + + // Wraps changesFn to emit a certified event only if executed from a + // complete governance proposal process. + executor := validators.NewPropExecutor(changesFn) + + // Create a proposal + title := "Valset change" + description := "manual valset changes proposal example" + + prop := dao.ProposalRequest{ + Title: title, + Description: description, + Executor: executor, + } + + govdao.Propose(prop) +} + +func main() { + println("--") + println(govdao.Render("")) + println("--") + println(govdao.Render("0")) + println("--") + govdao.VoteOnProposal(0, dao.YesVote) + println("--") + println(govdao.Render("0")) + println("--") + println(validators.Render("")) + println("--") + govdao.ExecuteProposal(0) + println("--") + println(govdao.Render("0")) + println("--") + println(validators.Render("")) +} + +// Output: +// -- +// # GovDAO Proposals +// +// ## [Prop #0 - Valset change](/r/gov/dao/v2:0) +// +// **Status: ACTIVE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// +// -- +// # Proposal #0 - Valset change +// +// ## Description +// +// manual valset changes proposal example +// +// ## Proposal information +// +// **Status: ACTIVE** +// +// **Voting stats:** +// - YES 0 (0%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 10 (100%) +// +// +// **Threshold met: FALSE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)] +// +// +// -- +// -- +// # Proposal #0 - Valset change +// +// ## Description +// +// manual valset changes proposal example +// +// ## Proposal information +// +// **Status: ACCEPTED** +// +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. +// +// +// -- +// No valset changes to apply. +// -- +// -- +// # Proposal #0 - Valset change +// +// ## Description +// +// manual valset changes proposal example +// +// ## Proposal information +// +// **Status: EXECUTION SUCCESSFUL** +// +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. +// +// +// -- +// Valset changes: +// - #123: g12345678 (10) +// - #123: g000000000 (10) +// - #123: g000000000 (0) +// + +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "ValidatorAdded", +// "attrs": [], +// "pkg_path": "gno.land/r/sys/validators/v2", +// "func": "addValidator" +// }, +// { +// "type": "ValidatorAdded", +// "attrs": [], +// "pkg_path": "gno.land/r/sys/validators/v2", +// "func": "addValidator" +// }, +// { +// "type": "ValidatorRemoved", +// "attrs": [], +// "pkg_path": "gno.land/r/sys/validators/v2", +// "func": "removeValidator" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno new file mode 100644 index 00000000000..84a64bc4ee2 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno @@ -0,0 +1,222 @@ +package main + +import ( + "time" + + "gno.land/p/demo/dao" + gnoblog "gno.land/r/gnoland/blog" + govdao "gno.land/r/gov/dao/v2" +) + +func init() { + ex := gnoblog.NewPostExecutor( + "hello-from-govdao", // slug + "Hello from GovDAO!", // title + "This post was published by a GovDAO proposal.", // body + time.Now().Format(time.RFC3339), // publication date + "moul", // authors + "govdao,example", // tags + ) + + // Create a proposal + title := "govdao blog post title" + description := "post a new blogpost about govdao" + + prop := dao.ProposalRequest{ + Title: title, + Description: description, + Executor: ex, + } + + govdao.Propose(prop) +} + +func main() { + println("--") + println(govdao.Render("")) + println("--") + println(govdao.Render("0")) + println("--") + govdao.VoteOnProposal(0, "YES") + println("--") + println(govdao.Render("0")) + println("--") + println(gnoblog.Render("")) + println("--") + govdao.ExecuteProposal(0) + println("--") + println(govdao.Render("0")) + println("--") + println(gnoblog.Render("")) +} + +// Output: +// -- +// # GovDAO Proposals +// +// ## [Prop #0 - govdao blog post title](/r/gov/dao/v2:0) +// +// **Status: ACTIVE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// +// -- +// # Proposal #0 - govdao blog post title +// +// ## Description +// +// post a new blogpost about govdao +// +// ## Proposal information +// +// **Status: ACTIVE** +// +// **Voting stats:** +// - YES 0 (0%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 10 (100%) +// +// +// **Threshold met: FALSE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)] +// +// +// -- +// -- +// # Proposal #0 - govdao blog post title +// +// ## Description +// +// post a new blogpost about govdao +// +// ## Proposal information +// +// **Status: ACCEPTED** +// +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. +// +// +// -- +// # gno.land's blog +// +// No posts. +// -- +// -- +// # Proposal #0 - govdao blog post title +// +// ## Description +// +// post a new blogpost about govdao +// +// ## Proposal information +// +// **Status: EXECUTION SUCCESSFUL** +// +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. +// +// +// -- +// # gno.land's blog +// +//
+// +// ### [Hello from GovDAO!](/r/gnoland/blog:p/hello-from-govdao) +// 13 Feb 2009 +//
+ +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno new file mode 100644 index 00000000000..068f520e7e2 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno @@ -0,0 +1,247 @@ +package main + +import ( + "std" + + "gno.land/p/demo/dao" + "gno.land/p/demo/membstore" + "gno.land/r/gov/dao/bridge" + govdao "gno.land/r/gov/dao/v2" +) + +func init() { + memberFn := func() []membstore.Member { + return []membstore.Member{ + { + Address: std.Address("g123"), + VotingPower: 10, + }, + { + Address: std.Address("g456"), + VotingPower: 10, + }, + { + Address: std.Address("g789"), + VotingPower: 10, + }, + } + } + + // Create a proposal + title := "new govdao member addition" + description := "add new members to the govdao" + + prop := dao.ProposalRequest{ + Title: title, + Description: description, + Executor: govdao.NewMemberPropExecutor(memberFn), + } + + bridge.GovDAO().Propose(prop) +} + +func main() { + println("--") + println(govdao.GetMembStore().Size()) + println("--") + println(govdao.Render("")) + println("--") + println(govdao.Render("0")) + println("--") + govdao.VoteOnProposal(0, "YES") + println("--") + println(govdao.Render("0")) + println("--") + println(govdao.Render("")) + println("--") + govdao.ExecuteProposal(0) + println("--") + println(govdao.Render("0")) + println("--") + println(govdao.Render("")) + println("--") + println(govdao.GetMembStore().Size()) +} + +// Output: +// -- +// 1 +// -- +// # GovDAO Proposals +// +// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0) +// +// **Status: ACTIVE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// +// -- +// # Proposal #0 - new govdao member addition +// +// ## Description +// +// add new members to the govdao +// +// ## Proposal information +// +// **Status: ACTIVE** +// +// **Voting stats:** +// - YES 0 (0%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 10 (100%) +// +// +// **Threshold met: FALSE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)] +// +// +// -- +// -- +// # Proposal #0 - new govdao member addition +// +// ## Description +// +// add new members to the govdao +// +// ## Proposal information +// +// **Status: ACCEPTED** +// +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. +// +// +// -- +// # GovDAO Proposals +// +// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0) +// +// **Status: ACCEPTED** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// +// -- +// -- +// # Proposal #0 - new govdao member addition +// +// ## Description +// +// add new members to the govdao +// +// ## Proposal information +// +// **Status: EXECUTION SUCCESSFUL** +// +// **Voting stats:** +// - YES 10 (25%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 30 (75%) +// +// +// **Threshold met: FALSE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. +// +// +// -- +// # GovDAO Proposals +// +// ## [Prop #0 - new govdao member addition](/r/gov/dao/v2:0) +// +// **Status: EXECUTION SUCCESSFUL** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// +// -- +// 4 + +// Events: +// [ +// { +// "type": "ProposalAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "proposal-author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAdded" +// }, +// { +// "type": "VoteAdded", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "author", +// "value": "g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm" +// }, +// { +// "key": "option", +// "value": "YES" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitVoteAdded" +// }, +// { +// "type": "ProposalAccepted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "EmitProposalAccepted" +// }, +// { +// "type": "ProposalExecuted", +// "attrs": [ +// { +// "key": "proposal-id", +// "value": "0" +// }, +// { +// "key": "exec-status", +// "value": "accepted" +// } +// ], +// "pkg_path": "gno.land/r/gov/dao/v2", +// "func": "ExecuteProposal" +// } +// ] diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno new file mode 100644 index 00000000000..13ca572c512 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno @@ -0,0 +1,132 @@ +package main + +import ( + "gno.land/p/demo/dao" + "gno.land/r/gov/dao/bridge" + govdaov2 "gno.land/r/gov/dao/v2" + "gno.land/r/sys/params" +) + +func init() { + mExec := params.NewStringPropExecutor("prop1.string", "value1") + title := "Setting prop1.string param" + comment := "setting prop1.string param" + prop := dao.ProposalRequest{ + Title: title, + Description: comment, + Executor: mExec, + } + id := bridge.GovDAO().Propose(prop) + println("new prop", id) +} + +func main() { + println("--") + println(govdaov2.Render("")) + println("--") + println(govdaov2.Render("0")) + println("--") + bridge.GovDAO().VoteOnProposal(0, "YES") + println("--") + println(govdaov2.Render("0")) + println("--") + bridge.GovDAO().ExecuteProposal(0) + println("--") + println(govdaov2.Render("0")) +} + +// Output: +// new prop 0 +// -- +// # GovDAO Proposals +// +// ## [Prop #0 - Setting prop1.string param](/r/gov/dao/v2:0) +// +// **Status: ACTIVE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// +// -- +// # Proposal #0 - Setting prop1.string param +// +// ## Description +// +// setting prop1.string param +// +// ## Proposal information +// +// **Status: ACTIVE** +// +// **Voting stats:** +// - YES 0 (0%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 10 (100%) +// +// +// **Threshold met: FALSE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// #### [[Vote YES](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=YES)] - [[Vote NO](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=NO)] - [[Vote ABSTAIN](/r/gov/dao/v2$help&func=VoteOnProposal&id=0&option=ABSTAIN)] +// +// +// -- +// -- +// # Proposal #0 - Setting prop1.string param +// +// ## Description +// +// setting prop1.string param +// +// ## Proposal information +// +// **Status: ACCEPTED** +// +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. +// +// +// -- +// -- +// # Proposal #0 - Setting prop1.string param +// +// ## Description +// +// setting prop1.string param +// +// ## Proposal information +// +// **Status: EXECUTION SUCCESSFUL** +// +// **Voting stats:** +// - YES 10 (100%) +// - NO 0 (0%) +// - ABSTAIN 0 (0%) +// - MISSING VOTES 0 (0%) +// +// +// **Threshold met: TRUE** +// +// **Author: g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm** +// +// ### Actions +// +// The voting period for this proposal is over. +// +// diff --git a/examples/gno.land/r/gov/dao/v2/render.gno b/examples/gno.land/r/gov/dao/v2/render.gno new file mode 100644 index 00000000000..57b7b601523 --- /dev/null +++ b/examples/gno.land/r/gov/dao/v2/render.gno @@ -0,0 +1,123 @@ +package govdao + +import ( + "strconv" + "strings" + + "gno.land/p/demo/dao" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" + "gno.land/r/demo/users" +) + +func Render(path string) string { + var out string + + if path == "" { + out += "# GovDAO Proposals\n\n" + numProposals := d.Size() + + if numProposals == 0 { + out += "No proposals found :(" // corner case + return out + } + + offset := uint64(0) + if numProposals >= 10 { + offset = uint64(numProposals) - 10 + } + + // Fetch the last 10 proposals + proposals := d.Proposals(offset, uint64(10)) + for i := len(proposals) - 1; i >= 0; i-- { + prop := proposals[i] + + title := prop.Title() + if len(title) > 40 { + title = title[:40] + "..." + } + + propID := offset + uint64(i) + out += ufmt.Sprintf("## [Prop #%d - %s](/r/gov/dao/v2:%d)\n\n", propID, title, propID) + out += ufmt.Sprintf("**Status: %s**\n\n", strings.ToUpper(prop.Status().String())) + + user := users.GetUserByAddress(prop.Author()) + authorDisplayText := prop.Author().String() + if user != nil { + authorDisplayText = ufmt.Sprintf("[%s](/r/demo/users:%s)", user.Name, user.Name) + } + + out += ufmt.Sprintf("**Author: %s**\n\n", authorDisplayText) + + if i != 0 { + out += "---\n\n" + } + } + + return out + } + + // Display the detailed proposal + idx, err := strconv.Atoi(path) + if err != nil { + return "404: Invalid proposal ID" + } + + // Fetch the proposal + prop, err := d.ProposalByID(uint64(idx)) + if err != nil { + return ufmt.Sprintf("unable to fetch proposal, %s", err.Error()) + } + + // Render the proposal page + out += renderPropPage(prop, idx) + + return out +} + +func renderPropPage(prop dao.Proposal, idx int) string { + var out string + + out += ufmt.Sprintf("# Proposal #%d - %s\n\n", idx, prop.Title()) + out += prop.Render() + out += renderAuthor(prop) + out += renderActionBar(prop, idx) + out += "\n\n" + + return out +} + +func renderAuthor(p dao.Proposal) string { + var out string + + authorUsername := "" + user := users.GetUserByAddress(p.Author()) + if user != nil { + authorUsername = user.Name + } + + if authorUsername != "" { + out += ufmt.Sprintf("**Author: [%s](/r/demo/users:%s)**\n\n", authorUsername, authorUsername) + } else { + out += ufmt.Sprintf("**Author: %s**\n\n", p.Author().String()) + } + + return out +} + +func renderActionBar(p dao.Proposal, idx int) string { + var out string + + out += "### Actions\n\n" + if p.Status() == dao.Active { + out += ufmt.Sprintf("#### [[Vote YES](%s)] - [[Vote NO](%s)] - [[Vote ABSTAIN](%s)]", + txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "YES"), + txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "NO"), + txlink.Call("VoteOnProposal", "id", strconv.Itoa(idx), "option", "ABSTAIN"), + ) + } else { + out += "The voting period for this proposal is over." + } + + return out +} diff --git a/examples/gno.land/r/gov/dao/voter.gno b/examples/gno.land/r/gov/dao/voter.gno deleted file mode 100644 index 99223210791..00000000000 --- a/examples/gno.land/r/gov/dao/voter.gno +++ /dev/null @@ -1,91 +0,0 @@ -package govdao - -import ( - "std" - - "gno.land/p/demo/ufmt" -) - -const ( - yay = "YES" - nay = "NO" - - msgNoMoreVotesAllowed = "no more votes allowed" - msgAlreadyVoted = "caller already voted" - msgWrongVotingValue = "voting values must be YES or NO" -) - -func NewPercentageVoter(percent int) *PercentageVoter { - if percent < 0 || percent > 100 { - panic("percent value must be between 0 and 100") - } - - return &PercentageVoter{ - percentage: percent, - } -} - -// PercentageVoter is a system based on the amount of received votes. -// When the specified treshold is reached, the voting process finishes. -type PercentageVoter struct { - percentage int - - voters []std.Address - yes int - no int -} - -func (pv *PercentageVoter) IsAccepted(voters []std.Address) bool { - if len(voters) == 0 { - return true // special case - } - - return pv.percent(voters) >= pv.percentage -} - -func (pv *PercentageVoter) IsFinished(voters []std.Address) bool { - return pv.yes+pv.no >= len(voters) -} - -func (pv *PercentageVoter) Status(voters []std.Address) string { - return ufmt.Sprintf("YES: %d, NO: %d, percent: %d, members: %d", pv.yes, pv.no, pv.percent(voters), len(voters)) -} - -func (pv *PercentageVoter) Vote(voters []std.Address, caller std.Address, flag string) { - if pv.IsFinished(voters) { - panic(msgNoMoreVotesAllowed) - } - - if pv.alreadyVoted(caller) { - panic(msgAlreadyVoted) - } - - switch flag { - case yay: - pv.yes++ - pv.voters = append(pv.voters, caller) - case nay: - pv.no++ - pv.voters = append(pv.voters, caller) - default: - panic(msgWrongVotingValue) - } -} - -func (pv *PercentageVoter) percent(voters []std.Address) int { - if len(voters) == 0 { - return 0 - } - - return int((float32(pv.yes) / float32(len(voters))) * 100) -} - -func (pv *PercentageVoter) alreadyVoted(addr std.Address) bool { - for _, v := range pv.voters { - if v == addr { - return true - } - } - - return false -} diff --git a/examples/gno.land/r/leon/config/config.gno b/examples/gno.land/r/leon/config/config.gno new file mode 100644 index 00000000000..bc800ec8263 --- /dev/null +++ b/examples/gno.land/r/leon/config/config.gno @@ -0,0 +1,63 @@ +package config + +import ( + "errors" + "std" +) + +var ( + main std.Address // leon's main address + backup std.Address // backup address + + ErrInvalidAddr = errors.New("leon's config: invalid address") + ErrUnauthorized = errors.New("leon's config: unauthorized") +) + +func init() { + main = "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5" +} + +func Address() std.Address { + return main +} + +func Backup() std.Address { + return backup +} + +func SetAddress(a std.Address) error { + if !a.IsValid() { + return ErrInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + main = a + return nil +} + +func SetBackup(a std.Address) error { + if !a.IsValid() { + return ErrInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + backup = a + return nil +} + +func checkAuthorized() error { + caller := std.PrevRealm().Addr() + isAuthorized := caller == main || caller == backup + + if !isAuthorized { + return ErrUnauthorized + } + + return nil +} diff --git a/examples/gno.land/r/leon/config/gno.mod b/examples/gno.land/r/leon/config/gno.mod new file mode 100644 index 00000000000..e8cd5cd85b7 --- /dev/null +++ b/examples/gno.land/r/leon/config/gno.mod @@ -0,0 +1 @@ +module gno.land/r/leon/config diff --git a/examples/gno.land/r/leon/hof/datasource.gno b/examples/gno.land/r/leon/hof/datasource.gno new file mode 100644 index 00000000000..180c4880177 --- /dev/null +++ b/examples/gno.land/r/leon/hof/datasource.gno @@ -0,0 +1,77 @@ +package hof + +import ( + "errors" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" + "gno.land/p/jeronimoalbi/datasource" +) + +func NewDatasource() Datasource { + return Datasource{exhibition} +} + +type Datasource struct { + exhibition *Exhibition +} + +func (ds Datasource) Size() int { return ds.exhibition.itemsSorted.Size() } + +func (ds Datasource) Records(q datasource.Query) datasource.Iterator { + return &iterator{ + exhibition: ds.exhibition, + index: q.Offset, + maxIndex: q.Offset + q.Count, + } +} + +func (ds Datasource) Record(id string) (datasource.Record, error) { + v, found := ds.exhibition.itemsSorted.Get(id) + if !found { + return nil, errors.New("realm submission not found") + } + return record{v.(*Item)}, nil +} + +type record struct { + item *Item +} + +func (r record) ID() string { return r.item.id.String() } +func (r record) String() string { return r.item.pkgpath } + +func (r record) Fields() (datasource.Fields, error) { + fields := avl.NewTree() + fields.Set( + "details", + ufmt.Sprintf("Votes: ⏶ %d - ⏷ %d", r.item.upvote.Size(), r.item.downvote.Size()), + ) + return fields, nil +} + +func (r record) Content() (string, error) { + content := ufmt.Sprintf("# Submission #%d\n\n", int(r.item.id)) + content += r.item.Render(false) + return content, nil +} + +type iterator struct { + exhibition *Exhibition + index, maxIndex int + record *record +} + +func (it iterator) Record() datasource.Record { return it.record } +func (it iterator) Err() error { return nil } + +func (it *iterator) Next() bool { + if it.index >= it.maxIndex || it.index >= it.exhibition.itemsSorted.Size() { + return false + } + + _, v := it.exhibition.itemsSorted.GetByIndex(it.index) + it.record = &record{v.(*Item)} + it.index++ + return true +} diff --git a/examples/gno.land/r/leon/hof/datasource_test.gno b/examples/gno.land/r/leon/hof/datasource_test.gno new file mode 100644 index 00000000000..376f981875f --- /dev/null +++ b/examples/gno.land/r/leon/hof/datasource_test.gno @@ -0,0 +1,157 @@ +package hof + +import ( + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" + "gno.land/p/jeronimoalbi/datasource" +) + +var ( + _ datasource.Datasource = (*Datasource)(nil) + _ datasource.Record = (*record)(nil) + _ datasource.ContentRecord = (*record)(nil) + _ datasource.Iterator = (*iterator)(nil) +) + +func TestDatasourceRecords(t *testing.T) { + cases := []struct { + name string + items []*Item + recordIDs []string + options []datasource.QueryOption + }{ + { + name: "all items", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000001", "0000002", "0000003"}, + }, + { + name: "with offset", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000002", "0000003"}, + options: []datasource.QueryOption{datasource.WithOffset(1)}, + }, + { + name: "with count", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000001", "0000002"}, + options: []datasource.QueryOption{datasource.WithCount(2)}, + }, + { + name: "with offset and count", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + recordIDs: []string{"0000002"}, + options: []datasource.QueryOption{ + datasource.WithOffset(1), + datasource.WithCount(1), + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Initialize a local instance of exhibition + exhibition := &Exhibition{itemsSorted: avl.NewTree()} + for _, item := range tc.items { + exhibition.itemsSorted.Set(item.id.String(), item) + } + + // Get a records iterator + ds := Datasource{exhibition} + query := datasource.NewQuery(tc.options...) + iter := ds.Records(query) + + // Start asserting + urequire.Equal(t, len(tc.items), ds.Size(), "datasource size") + + var records []datasource.Record + for iter.Next() { + records = append(records, iter.Record()) + } + urequire.Equal(t, len(tc.recordIDs), len(records), "record count") + + for i, r := range records { + uassert.Equal(t, tc.recordIDs[i], r.ID()) + } + }) + } +} + +func TestDatasourceRecord(t *testing.T) { + cases := []struct { + name string + items []*Item + id string + err string + }{ + { + name: "found", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + id: "0000001", + }, + { + name: "no found", + items: []*Item{{id: 1}, {id: 2}, {id: 3}}, + id: "42", + err: "realm submission not found", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Initialize a local instance of exhibition + exhibition := &Exhibition{itemsSorted: avl.NewTree()} + for _, item := range tc.items { + exhibition.itemsSorted.Set(item.id.String(), item) + } + + // Get a single record + ds := Datasource{exhibition} + r, err := ds.Record(tc.id) + + // Start asserting + if tc.err != "" { + uassert.ErrorContains(t, err, tc.err) + return + } + + urequire.NoError(t, err, "no error") + urequire.NotEqual(t, nil, r, "record not nil") + uassert.Equal(t, tc.id, r.ID()) + }) + } +} + +func TestItemRecord(t *testing.T) { + pkgpath := "gno.land/r/demo/test" + item := Item{ + id: 1, + pkgpath: pkgpath, + blockNum: 42, + upvote: avl.NewTree(), + downvote: avl.NewTree(), + } + item.downvote.Set("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", struct{}{}) + item.upvote.Set("g1w4ek2u33ta047h6lta047h6lta047h6ldvdwpn", struct{}{}) + item.upvote.Set("g1w4ek2u3jta047h6lta047h6lta047h6l9huexc", struct{}{}) + + r := record{&item} + + uassert.Equal(t, "0000001", r.ID()) + uassert.Equal(t, pkgpath, r.String()) + + fields, _ := r.Fields() + details, found := fields.Get("details") + urequire.True(t, found, "details field") + uassert.Equal(t, "Votes: ⏶ 2 - ⏷ 1", details) + + content, _ := r.Content() + wantContent := "# Submission #1\n\n\n```\ngno.land/r/demo/test\n```\n\nby demo\n\n" + + "[View realm](/r/demo/test)\n\nSubmitted at Block #42\n\n" + + "#### [2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land/r/demo/test) - " + + "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land/r/demo/test)\n\n" + uassert.Equal(t, wantContent, content) +} diff --git a/examples/gno.land/r/leon/hof/errors.gno b/examples/gno.land/r/leon/hof/errors.gno new file mode 100644 index 00000000000..7277f65fa76 --- /dev/null +++ b/examples/gno.land/r/leon/hof/errors.gno @@ -0,0 +1,11 @@ +package hof + +import ( + "errors" +) + +var ( + ErrNoSuchItem = errors.New("hof: no such item exists") + ErrDoubleUpvote = errors.New("hof: cannot upvote twice") + ErrDoubleDownvote = errors.New("hof: cannot downvote twice") +) diff --git a/examples/gno.land/r/leon/hof/gno.mod b/examples/gno.land/r/leon/hof/gno.mod new file mode 100644 index 00000000000..f4720eb2b5a --- /dev/null +++ b/examples/gno.land/r/leon/hof/gno.mod @@ -0,0 +1 @@ +module gno.land/r/leon/hof diff --git a/examples/gno.land/r/leon/hof/hof.gno b/examples/gno.land/r/leon/hof/hof.gno new file mode 100644 index 00000000000..147a0dd1a95 --- /dev/null +++ b/examples/gno.land/r/leon/hof/hof.gno @@ -0,0 +1,134 @@ +// Package hof is the hall of fame realm. +// The Hall of Fame is an exhibition that holds items. Users can add their realms to the Hall of Fame by +// importing the Hall of Fame realm and calling hof.Register() from their init function. +package hof + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/pausable" + "gno.land/p/demo/seqid" +) + +var ( + exhibition *Exhibition + + // Safe objects + Ownable *ownable.Ownable + Pausable *pausable.Pausable +) + +type ( + Exhibition struct { + itemCounter seqid.ID + description string + items *avl.Tree // pkgPath > Item + itemsSorted *avl.Tree // same data but sorted, storing pointers + } + + Item struct { + id seqid.ID + pkgpath string + blockNum int64 + upvote *avl.Tree // std.Addr > struct{}{} + downvote *avl.Tree // std.Addr > struct{}{} + } +) + +func init() { + exhibition = &Exhibition{ + items: avl.NewTree(), + itemsSorted: avl.NewTree(), + } + + Ownable = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5")) + Pausable = pausable.NewFromOwnable(Ownable) +} + +// Register registers your realm to the Hall of Fame +// Should be called from within code +func Register() { + if Pausable.IsPaused() { + return + } + + submission := std.PrevRealm() + pkgpath := submission.PkgPath() + + // Must be called from code + if submission.IsUser() { + return + } + + // Must not yet exist + if exhibition.items.Has(pkgpath) { + return + } + + id := exhibition.itemCounter.Next() + i := &Item{ + id: id, + pkgpath: pkgpath, + blockNum: std.GetHeight(), + upvote: avl.NewTree(), + downvote: avl.NewTree(), + } + + exhibition.items.Set(pkgpath, i) + exhibition.itemsSorted.Set(id.String(), i) + + std.Emit("Registration") +} + +func Upvote(pkgpath string) { + rawItem, ok := exhibition.items.Get(pkgpath) + if !ok { + panic(ErrNoSuchItem.Error()) + } + + item := rawItem.(*Item) + caller := std.PrevRealm().Addr().String() + + if item.upvote.Has(caller) { + panic(ErrDoubleUpvote.Error()) + } + + item.upvote.Set(caller, struct{}{}) +} + +func Downvote(pkgpath string) { + rawItem, ok := exhibition.items.Get(pkgpath) + if !ok { + panic(ErrNoSuchItem.Error()) + } + + item := rawItem.(*Item) + caller := std.PrevRealm().Addr().String() + + if item.downvote.Has(caller) { + panic(ErrDoubleDownvote.Error()) + } + + item.downvote.Set(caller, struct{}{}) +} + +func Delete(pkgpath string) { + if !Ownable.CallerIsOwner() { + panic(ownable.ErrUnauthorized.Error()) + } + + i, ok := exhibition.items.Get(pkgpath) + if !ok { + panic(ErrNoSuchItem.Error()) + } + + if _, removed := exhibition.itemsSorted.Remove(i.(*Item).id.String()); !removed { + panic(ErrNoSuchItem.Error()) + } + + if _, removed := exhibition.items.Remove(pkgpath); !removed { + panic(ErrNoSuchItem.Error()) + } +} diff --git a/examples/gno.land/r/leon/hof/hof_test.gno b/examples/gno.land/r/leon/hof/hof_test.gno new file mode 100644 index 00000000000..4d6f70eab88 --- /dev/null +++ b/examples/gno.land/r/leon/hof/hof_test.gno @@ -0,0 +1,134 @@ +package hof + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +const rlmPath = "gno.land/r/gnoland/home" + +var ( + admin = Ownable.Owner() + adminRealm = std.NewUserRealm(admin) + alice = testutils.TestAddress("alice") +) + +func TestRegister(t *testing.T) { + // Test user realm register + aliceRealm := std.NewUserRealm(alice) + std.TestSetRealm(aliceRealm) + + Register() + uassert.False(t, itemExists(t, rlmPath)) + + // Test register while paused + std.TestSetRealm(adminRealm) + Pausable.Pause() + + // Set legitimate caller + std.TestSetRealm(std.NewCodeRealm(rlmPath)) + + Register() + uassert.False(t, itemExists(t, rlmPath)) + + // Unpause + std.TestSetRealm(adminRealm) + Pausable.Unpause() + + // Set legitimate caller + std.TestSetRealm(std.NewCodeRealm(rlmPath)) + Register() + + // Find registered items + uassert.True(t, itemExists(t, rlmPath)) +} + +func TestUpvote(t *testing.T) { + raw, _ := exhibition.items.Get(rlmPath) + item := raw.(*Item) + + rawSorted, _ := exhibition.itemsSorted.Get(item.id.String()) + itemSorted := rawSorted.(*Item) + + // 0 upvotes by default + urequire.Equal(t, item.upvote.Size(), 0) + + std.TestSetRealm(adminRealm) + + urequire.NotPanics(t, func() { + Upvote(rlmPath) + }) + + // Check both trees for 1 upvote + uassert.Equal(t, item.upvote.Size(), 1) + uassert.Equal(t, itemSorted.upvote.Size(), 1) + + // Check double upvote + uassert.PanicsWithMessage(t, ErrDoubleUpvote.Error(), func() { + Upvote(rlmPath) + }) +} + +func TestDownvote(t *testing.T) { + raw, _ := exhibition.items.Get(rlmPath) + item := raw.(*Item) + + rawSorted, _ := exhibition.itemsSorted.Get(item.id.String()) + itemSorted := rawSorted.(*Item) + + // 0 downvotes by default + urequire.Equal(t, item.downvote.Size(), 0) + + userRealm := std.NewUserRealm(alice) + std.TestSetRealm(userRealm) + + urequire.NotPanics(t, func() { + Downvote(rlmPath) + }) + + // Check both trees for 1 upvote + uassert.Equal(t, item.downvote.Size(), 1) + uassert.Equal(t, itemSorted.downvote.Size(), 1) + + // Check double downvote + uassert.PanicsWithMessage(t, ErrDoubleDownvote.Error(), func() { + Downvote(rlmPath) + }) +} + +func TestDelete(t *testing.T) { + userRealm := std.NewUserRealm(admin) + std.TestSetRealm(userRealm) + std.TestSetOrigCaller(admin) + + uassert.PanicsWithMessage(t, ErrNoSuchItem.Error(), func() { + Delete("nonexistentpkgpath") + }) + + i, _ := exhibition.items.Get(rlmPath) + id := i.(*Item).id + + uassert.NotPanics(t, func() { + Delete(rlmPath) + }) + + uassert.False(t, exhibition.items.Has(rlmPath)) + uassert.False(t, exhibition.itemsSorted.Has(id.String())) +} + +func itemExists(t *testing.T, rlmPath string) bool { + t.Helper() + + i, ok1 := exhibition.items.Get(rlmPath) + ok2 := false + + if ok1 { + _, ok2 = exhibition.itemsSorted.Get(i.(*Item).id.String()) + } + + return ok1 && ok2 +} diff --git a/examples/gno.land/r/leon/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno new file mode 100644 index 00000000000..868262bedc7 --- /dev/null +++ b/examples/gno.land/r/leon/hof/render.gno @@ -0,0 +1,113 @@ +package hof + +import ( + "strings" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/fqname" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +const ( + pageSize = 5 +) + +func Render(path string) string { + out := "# Hall of Fame\n\n" + + dashboardEnabled := path == "dashboard" + + if dashboardEnabled { + out += renderDashboard() + } + + out += exhibition.Render(path, dashboardEnabled) + + return out +} + +func (e Exhibition) Render(path string, dashboard bool) string { + out := ufmt.Sprintf("%s\n\n", e.description) + + if e.items.Size() == 0 { + out += "No items in this exhibition currently.\n\n" + return out + } + + out += "
\n\n" + + page := pager.NewPager(e.itemsSorted, pageSize, false).MustGetPageByPath(path) + + for i := len(page.Items) - 1; i >= 0; i-- { + item := page.Items[i] + + out += "
\n\n" + id, _ := seqid.FromString(item.Key) + out += ufmt.Sprintf("### Submission #%d\n\n", int(id)) + out += item.Value.(*Item).Render(dashboard) + out += "
" + } + + out += "
\n\n" + + out += page.Picker() + + return out +} + +func (i Item) Render(dashboard bool) string { + out := ufmt.Sprintf("\n```\n%s\n```\n\n", i.pkgpath) + out += ufmt.Sprintf("by %s\n\n", strings.Split(i.pkgpath, "/")[2]) + out += ufmt.Sprintf("[View realm](%s)\n\n", strings.TrimPrefix(i.pkgpath, "gno.land")) // gno.land/r/leon/home > /r/leon/home + out += ufmt.Sprintf("Submitted at Block #%d\n\n", i.blockNum) + + out += ufmt.Sprintf("#### [%d👍](%s) - [%d👎](%s)\n\n", + i.upvote.Size(), txlink.Call("Upvote", "pkgpath", i.pkgpath), + i.downvote.Size(), txlink.Call("Downvote", "pkgpath", i.pkgpath), + ) + + if dashboard { + out += ufmt.Sprintf("[Delete](%s)", txlink.Call("Delete", "pkgpath", i.pkgpath)) + } + + return out +} + +func renderDashboard() string { + out := "---\n\n" + out += "## Dashboard\n\n" + out += ufmt.Sprintf("Total submissions: %d\n\n", exhibition.items.Size()) + + out += ufmt.Sprintf("Exhibition admin: %s\n\n", Ownable.Owner().String()) + + if !Pausable.IsPaused() { + out += ufmt.Sprintf("[Pause exhibition](%s)\n\n", txlink.Call("Pause")) + } else { + out += ufmt.Sprintf("[Unpause exhibition](%s)\n\n", txlink.Call("Unpause")) + } + + out += "---\n\n" + + return out +} + +func RenderExhibWidget(itemsToRender int) string { + if itemsToRender < 1 { + return "" + } + + out := "" + i := 0 + exhibition.items.Iterate("", "", func(key string, value interface{}) bool { + item := value.(*Item) + + out += ufmt.Sprintf("- %s\n", fqname.RenderLink(item.pkgpath, "")) + + i++ + return i >= itemsToRender + }) + + return out +} diff --git a/examples/gno.land/r/leon/home/gno.mod b/examples/gno.land/r/leon/home/gno.mod new file mode 100644 index 00000000000..56fea265e29 --- /dev/null +++ b/examples/gno.land/r/leon/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/leon/home diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno new file mode 100644 index 00000000000..cf33260cc6b --- /dev/null +++ b/examples/gno.land/r/leon/home/home.gno @@ -0,0 +1,136 @@ +package home + +import ( + "std" + "strconv" + + "gno.land/p/demo/ufmt" + + "gno.land/r/demo/art/gnoface" + "gno.land/r/demo/art/millipede" + "gno.land/r/demo/mirror" + "gno.land/r/leon/config" + "gno.land/r/leon/hof" +) + +var ( + pfp string // link to profile picture + pfpCaption string // profile picture caption + abtMe [2]string +) + +func init() { + pfp = "https://i.imgflip.com/91vskx.jpg" + pfpCaption = "[My favourite painting & pfp](https://en.wikipedia.org/wiki/Wanderer_above_the_Sea_of_Fog)" + abtMe = [2]string{ + `### About me +Hi, I'm Leon, a DevRel Engineer at gno.land. I am a tech enthusiast, +life-long learner, and sharer of knowledge.`, + `### Contributions +My contributions to gno.land can mainly be found +[here](https://github.com/gnolang/gno/issues?q=sort:updated-desc+author:leohhhn). + +TODO import r/gh +`, + } + + hof.Register() + mirror.Register(std.CurrentRealm().PkgPath(), Render) +} + +func UpdatePFP(url, caption string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + pfp = url + pfpCaption = caption +} + +func UpdateAboutMe(col1, col2 string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + abtMe[0] = col1 + abtMe[1] = col2 +} + +func Render(path string) string { + out := "# Leon's Homepage\n\n" + + out += renderAboutMe() + out += renderBlogPosts() + out += "\n\n" + out += renderArt() + + return out +} + +func renderBlogPosts() string { + out := "" + //out += "## Leon's Blog Posts" + + // todo fetch blog posts authored by @leohhhn + // and render them + return out +} + +func renderAboutMe() string { + out := "
" + + out += "
\n\n" + out += ufmt.Sprintf("![my profile pic](%s)\n\n%s\n\n", pfp, pfpCaption) + out += "
\n\n" + + out += "
\n\n" + out += abtMe[0] + "\n\n" + out += "
\n\n" + + out += "
\n\n" + out += abtMe[1] + "\n\n" + out += "
\n\n" + + out += "
\n\n" + + return out +} + +func renderArt() string { + out := `
` + "\n\n" + out += "# Gno Art\n\n" + + out += "
" + + out += renderGnoFace() + out += renderMillipede() + out += "Empty spot :/" + + out += "
\n\n" + + out += "This art is dynamic; it will change with every new block.\n\n" + out += `
` + "\n" + + return out +} + +func renderGnoFace() string { + out := "
\n\n" + out += gnoface.Render(strconv.Itoa(int(std.GetHeight()))) + out += "
\n\n" + + return out +} + +func renderMillipede() string { + out := "
\n\n" + out += "Millipede\n\n" + out += "```\n" + millipede.Draw(int(std.GetHeight())%10+1) + "```\n" + out += "
\n\n" + + return out +} + +func isAuthorized(addr std.Address) bool { + return addr == config.Address() || addr == config.Backup() +} diff --git a/examples/gno.land/r/manfred/config/config.gno b/examples/gno.land/r/manfred/config/config.gno deleted file mode 100644 index 23e90df50ff..00000000000 --- a/examples/gno.land/r/manfred/config/config.gno +++ /dev/null @@ -1,20 +0,0 @@ -package config - -import "std" - -var addr = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") - -func Addr() std.Address { - return addr -} - -func UpdateAddr(newAddr std.Address) { - AssertIsAdmin() - addr = newAddr -} - -func AssertIsAdmin() { - if std.GetOrigCaller() != addr { - panic("restricted area") - } -} diff --git a/examples/gno.land/r/manfred/config/gno.mod b/examples/gno.land/r/manfred/config/gno.mod deleted file mode 100644 index 516bf38528e..00000000000 --- a/examples/gno.land/r/manfred/config/gno.mod +++ /dev/null @@ -1 +0,0 @@ -module gno.land/r/manfred/config diff --git a/examples/gno.land/r/manfred/home/gno.mod b/examples/gno.land/r/manfred/home/gno.mod index 6e7aac70cc7..2efefe1824f 100644 --- a/examples/gno.land/r/manfred/home/gno.mod +++ b/examples/gno.land/r/manfred/home/gno.mod @@ -1,3 +1 @@ module gno.land/r/manfred/home - -require gno.land/r/manfred/config v0.0.0-latest diff --git a/examples/gno.land/r/manfred/home/home.gno b/examples/gno.land/r/manfred/home/home.gno old mode 100644 new mode 100755 index 720796a2201..56caf30d9fd --- a/examples/gno.land/r/manfred/home/home.gno +++ b/examples/gno.land/r/manfred/home/home.gno @@ -1,56 +1,5 @@ package home -import "gno.land/r/manfred/config" - -var ( - todos []string - status string - memeImgURL string -) - -func init() { - todos = append(todos, "fill this todo list...") - status = "Online" // Initial status set to "Online" - memeImgURL = "https://i.imgflip.com/7ze8dc.jpg" -} - func Render(path string) string { - content := "# Manfred's (gn)home Dashboard\n\n" - - content += "## Meme\n" - content += "![](" + memeImgURL + ")\n\n" - - content += "## Status\n" - content += status + "\n\n" - - content += "## Personal ToDo List\n" - for _, todo := range todos { - content += "- [ ] " + todo + "\n" - } - content += "\n" - - // TODO: Implement a feature to list replies on r/boards on my posts - // TODO: Maybe integrate a calendar feature for upcoming events? - - return content -} - -func AddNewTodo(todo string) { - config.AssertIsAdmin() - todos = append(todos, todo) -} - -func DeleteTodo(todoIndex int) { - config.AssertIsAdmin() - if todoIndex >= 0 && todoIndex < len(todos) { - // Remove the todo from the list by merging slices from before and after the todo - todos = append(todos[:todoIndex], todos[todoIndex+1:]...) - } else { - panic("Invalid todo index") - } -} - -func UpdateStatus(newStatus string) { - config.AssertIsAdmin() - status = newStatus + return "Moved to r/moul" } diff --git a/examples/gno.land/r/manfred/home/z1_filetest.gno b/examples/gno.land/r/manfred/home/z1_filetest.gno deleted file mode 100644 index 801efedb306..00000000000 --- a/examples/gno.land/r/manfred/home/z1_filetest.gno +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import "gno.land/r/manfred/home" - -func main() { - println(home.Render("")) -} - -// Output: -// # Manfred's (gn)home Dashboard -// -// ## Meme -// ![](https://i.imgflip.com/7ze8dc.jpg) -// -// ## Status -// Online -// -// ## Personal ToDo List -// - [ ] fill this todo list... diff --git a/examples/gno.land/r/manfred/home/z2_filetest.gno b/examples/gno.land/r/manfred/home/z2_filetest.gno deleted file mode 100644 index 316fd400867..00000000000 --- a/examples/gno.land/r/manfred/home/z2_filetest.gno +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "std" - - "gno.land/r/manfred/home" -) - -func main() { - std.TestSetOrigCaller("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") - home.AddNewTodo("aaa") - home.AddNewTodo("bbb") - home.AddNewTodo("ccc") - home.AddNewTodo("ddd") - home.AddNewTodo("eee") - home.UpdateStatus("Lorem Ipsum") - home.DeleteTodo(3) - println(home.Render("")) -} - -// Output: -// # Manfred's (gn)home Dashboard -// -// ## Meme -// ![](https://i.imgflip.com/7ze8dc.jpg) -// -// ## Status -// Lorem Ipsum -// -// ## Personal ToDo List -// - [ ] fill this todo list... -// - [ ] aaa -// - [ ] bbb -// - [ ] ddd -// - [ ] eee diff --git a/examples/gno.land/r/manfred/present/admin.gno b/examples/gno.land/r/manfred/present/admin.gno deleted file mode 100644 index 60af578b54f..00000000000 --- a/examples/gno.land/r/manfred/present/admin.gno +++ /dev/null @@ -1,96 +0,0 @@ -package present - -import ( - "std" - "strings" - - "gno.land/p/demo/avl" -) - -var ( - adminAddr std.Address - moderatorList avl.Tree - inPause bool -) - -func init() { - // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. - adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" -} - -func AdminSetAdminAddr(addr std.Address) { - assertIsAdmin() - adminAddr = addr -} - -func AdminSetInPause(state bool) { - assertIsAdmin() - inPause = state -} - -func AdminAddModerator(addr std.Address) { - assertIsAdmin() - moderatorList.Set(addr.String(), true) -} - -func AdminRemoveModerator(addr std.Address) { - assertIsAdmin() - moderatorList.Set(addr.String(), false) // XXX: delete instead? -} - -func ModAddPost(slug, title, body, publicationDate, authors, tags string) { - assertIsModerator() - - caller := std.GetOrigCaller() - tagList := strings.Split(tags, ",") - authorList := strings.Split(authors, ",") - - err := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList) - checkErr(err) -} - -func ModEditPost(slug, title, body, publicationDate, authors, tags string) { - assertIsModerator() - - tagList := strings.Split(tags, ",") - authorList := strings.Split(authors, ",") - - err := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList) - checkErr(err) -} - -func isAdmin(addr std.Address) bool { - return addr == adminAddr -} - -func isModerator(addr std.Address) bool { - _, found := moderatorList.Get(addr.String()) - return found -} - -func assertIsAdmin() { - caller := std.GetOrigCaller() - if !isAdmin(caller) { - panic("access restricted.") - } -} - -func assertIsModerator() { - caller := std.GetOrigCaller() - if isAdmin(caller) || isModerator(caller) { - return - } - panic("access restricted") -} - -func assertNotInPause() { - if inPause { - panic("access restricted (pause)") - } -} - -func checkErr(err error) { - if err != nil { - panic(err) - } -} diff --git a/examples/gno.land/r/manfred/present/gno.mod b/examples/gno.land/r/manfred/present/gno.mod deleted file mode 100644 index 5d50447e0e0..00000000000 --- a/examples/gno.land/r/manfred/present/gno.mod +++ /dev/null @@ -1,6 +0,0 @@ -module gno.land/r/manfred/present - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/blog v0.0.0-latest -) diff --git a/examples/gno.land/r/matijamarjanovic/home/config.gno b/examples/gno.land/r/matijamarjanovic/home/config.gno new file mode 100644 index 00000000000..2a9669c0b58 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/config.gno @@ -0,0 +1,64 @@ +package home + +import ( + "errors" + "std" +) + +var ( + mainAddr = std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y") // matija's main address + backupAddr std.Address // backup address + + errorInvalidAddr = errors.New("config: invalid address") + errorUnauthorized = errors.New("config: unauthorized") +) + +func Address() std.Address { + return mainAddr +} + +func Backup() std.Address { + return backupAddr +} + +func SetAddress(newAddress std.Address) error { + if !newAddress.IsValid() { + return errorInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + mainAddr = newAddress + return nil +} + +func SetBackup(newAddress std.Address) error { + if !newAddress.IsValid() { + return errorInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + backupAddr = newAddress + return nil +} + +func checkAuthorized() error { + caller := std.GetOrigCaller() + if caller != mainAddr && caller != backupAddr { + return errorUnauthorized + } + + return nil +} + +func AssertAuthorized() { + caller := std.GetOrigCaller() + if caller != mainAddr && caller != backupAddr { + panic(errorUnauthorized) + } +} diff --git a/examples/gno.land/r/matijamarjanovic/home/gno.mod b/examples/gno.land/r/matijamarjanovic/home/gno.mod new file mode 100644 index 00000000000..0457c947c01 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/matijamarjanovic/home diff --git a/examples/gno.land/r/matijamarjanovic/home/home.gno b/examples/gno.land/r/matijamarjanovic/home/home.gno new file mode 100644 index 00000000000..3757324108a --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/home.gno @@ -0,0 +1,238 @@ +package home + +import ( + "std" + "strings" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/r/leon/hof" +) + +var ( + pfp string // link to profile picture + pfpCaption string // profile picture caption + abtMe string + + modernVotes int64 + classicVotes int64 + minimalVotes int64 + currentTheme string + + modernLink string + classicLink string + minimalLink string +) + +func init() { + pfp = "https://static.artzone.ai/media/38734/conversions/IPF9dR7ro7n05CmMLLrXIojycr1qdLFxgutaaanG-w768.webp" + pfpCaption = "My profile picture - Tarantula Nebula" + abtMe = `Motivated Computer Science student with strong + analytical and problem-solving skills. Proficient in + programming and version control, with a high level of + focus and attention to detail. Eager to apply academic + knowledge to real-world projects and contribute to + innovative technology solutions. + In addition to my academic pursuits, + I enjoy traveling and staying active through weightlifting. + I have a keen interest in electronic music and often explore various genres. + I believe in maintaining a balanced lifestyle that complements my professional development.` + + modernVotes = 0 + classicVotes = 0 + minimalVotes = 0 + currentTheme = "classic" + modernLink = "https://www.google.com" + classicLink = "https://www.google.com" + minimalLink = "https://www.google.com" + hof.Register() +} + +func UpdatePFP(url, caption string) { + AssertAuthorized() + pfp = url + pfpCaption = caption +} + +func UpdateAboutMe(col1 string) { + AssertAuthorized() + abtMe = col1 +} + +func maxOfThree(a, b, c int64) int64 { + max := a + if b > max { + max = b + } + if c > max { + max = c + } + return max +} + +func VoteModern() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + modernVotes += votes + updateCurrentTheme() +} + +func VoteClassic() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + classicVotes += votes + updateCurrentTheme() +} + +func VoteMinimal() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + minimalVotes += votes + updateCurrentTheme() +} + +func updateCurrentTheme() { + maxVotes := maxOfThree(modernVotes, classicVotes, minimalVotes) + + if maxVotes == modernVotes { + currentTheme = "modern" + } else if maxVotes == classicVotes { + currentTheme = "classic" + } else { + currentTheme = "minimal" + } +} + +func CollectBalance() { + AssertAuthorized() + + banker := std.GetBanker(std.BankerTypeRealmSend) + ownerAddr := Address() + + banker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr())) +} + +func Render(path string) string { + var sb strings.Builder + + // Theme-specific header styling + switch currentTheme { + case "modern": + // Modern theme - Clean and minimalist with emojis + sb.WriteString(md.H1("🚀 Matija's Space")) + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(md.Italic(pfpCaption)) + sb.WriteString("\n") + sb.WriteString(md.HorizontalRule()) + sb.WriteString(abtMe) + sb.WriteString("\n") + + case "minimal": + // Minimal theme - No emojis, minimal formatting + sb.WriteString(md.H1("Matija Marjanovic")) + sb.WriteString("\n") + sb.WriteString(abtMe) + sb.WriteString("\n") + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(pfpCaption) + sb.WriteString("\n") + + default: // classic + // Classic theme - Traditional blog style with decorative elements + sb.WriteString(md.H1("✨ Welcome to Matija's Homepage ✨")) + sb.WriteString("\n") + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(pfpCaption) + sb.WriteString("\n") + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("About me")) + sb.WriteString("\n") + sb.WriteString(abtMe) + sb.WriteString("\n") + } + + // Theme-specific voting section + switch currentTheme { + case "modern": + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("🎨 Theme Selector")) + sb.WriteString("Choose your preferred viewing experience:\n") + items := []string{ + md.Link(ufmt.Sprintf("Modern Design (%d votes)", modernVotes), modernLink), + md.Link(ufmt.Sprintf("Classic Style (%d votes)", classicVotes), classicLink), + md.Link(ufmt.Sprintf("Minimal Look (%d votes)", minimalVotes), minimalLink), + } + sb.WriteString(md.BulletList(items)) + + case "minimal": + sb.WriteString("\n") + sb.WriteString(md.H3("Theme Selection")) + sb.WriteString(ufmt.Sprintf("Current theme: %s\n", currentTheme)) + sb.WriteString(ufmt.Sprintf("Votes - Modern: %d | Classic: %d | Minimal: %d\n", + modernVotes, classicVotes, minimalVotes)) + sb.WriteString(md.Link("Modern", modernLink)) + sb.WriteString(" | ") + sb.WriteString(md.Link("Classic", classicLink)) + sb.WriteString(" | ") + sb.WriteString(md.Link("Minimal", minimalLink)) + sb.WriteString("\n") + + default: // classic + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("✨ Theme Customization ✨")) + sb.WriteString(md.Bold("Choose Your Preferred Theme:")) + sb.WriteString("\n\n") + items := []string{ + ufmt.Sprintf("Modern 🚀 (%d votes) - %s", modernVotes, md.Link("Vote", modernLink)), + ufmt.Sprintf("Classic ✨ (%d votes) - %s", classicVotes, md.Link("Vote", classicLink)), + ufmt.Sprintf("Minimal ⚡ (%d votes) - %s", minimalVotes, md.Link("Vote", minimalLink)), + } + sb.WriteString(md.BulletList(items)) + } + + // Theme-specific footer/links section + switch currentTheme { + case "modern": + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.Link("GitHub", "https://github.com/matijamarjanovic")) + sb.WriteString(" | ") + sb.WriteString(md.Link("LinkedIn", "https://www.linkedin.com/in/matijamarjanovic")) + sb.WriteString("\n") + + case "minimal": + sb.WriteString("\n") + sb.WriteString(md.Link("GitHub", "https://github.com/matijamarjanovic")) + sb.WriteString(" | ") + sb.WriteString(md.Link("LinkedIn", "https://www.linkedin.com/in/matijamarjanovic")) + sb.WriteString("\n") + + default: // classic + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H3("✨ Connect With Me")) + items := []string{ + md.Link("🌟 GitHub", "https://github.com/matijamarjanovic"), + md.Link("💼 LinkedIn", "https://www.linkedin.com/in/matijamarjanovic"), + } + sb.WriteString(md.BulletList(items)) + } + + return sb.String() +} + +func UpdateModernLink(link string) { + AssertAuthorized() + modernLink = link +} + +func UpdateClassicLink(link string) { + AssertAuthorized() + classicLink = link +} + +func UpdateMinimalLink(link string) { + AssertAuthorized() + minimalLink = link +} diff --git a/examples/gno.land/r/matijamarjanovic/home/home_test.gno b/examples/gno.land/r/matijamarjanovic/home/home_test.gno new file mode 100644 index 00000000000..8cc6e6e5608 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/home_test.gno @@ -0,0 +1,134 @@ +package home + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +// Helper function to set up test environment +func setupTest() { + std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y")) +} + +func TestUpdatePFP(t *testing.T) { + setupTest() + pfp = "" + pfpCaption = "" + + UpdatePFP("https://example.com/pic.png", "New Caption") + + urequire.Equal(t, pfp, "https://example.com/pic.png", "Profile picture URL should be updated") + urequire.Equal(t, pfpCaption, "New Caption", "Profile picture caption should be updated") +} + +func TestUpdateAboutMe(t *testing.T) { + setupTest() + abtMe = "" + + UpdateAboutMe("This is my new bio.") + + urequire.Equal(t, abtMe, "This is my new bio.", "About Me should be updated") +} + +func TestVoteModern(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteModern() + + uassert.Equal(t, int64(75000000), modernVotes, "Modern votes should be calculated correctly") + uassert.Equal(t, "modern", currentTheme, "Theme should be updated to modern") +} + +func TestVoteClassic(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteClassic() + + uassert.Equal(t, int64(75000000), classicVotes, "Classic votes should be calculated correctly") + uassert.Equal(t, "classic", currentTheme, "Theme should be updated to classic") +} + +func TestVoteMinimal(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteMinimal() + + uassert.Equal(t, int64(75000000), minimalVotes, "Minimal votes should be calculated correctly") + uassert.Equal(t, "minimal", currentTheme, "Theme should be updated to minimal") +} + +func TestRender(t *testing.T) { + setupTest() + // Reset the state to known values + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + currentTheme = "classic" + pfp = "https://example.com/pic.png" + pfpCaption = "Test Caption" + abtMe = "Test About Me" + + out := Render("") + urequire.NotEqual(t, out, "", "Render output should not be empty") + + // Test classic theme specific content + uassert.True(t, strings.Contains(out, "✨ Welcome to Matija's Homepage ✨"), "Classic theme should have correct header") + uassert.True(t, strings.Contains(out, pfp), "Should contain profile picture URL") + uassert.True(t, strings.Contains(out, pfpCaption), "Should contain profile picture caption") + uassert.True(t, strings.Contains(out, "About me"), "Should contain About me section") + uassert.True(t, strings.Contains(out, abtMe), "Should contain about me content") + uassert.True(t, strings.Contains(out, "Theme Customization"), "Should contain theme customization section") + uassert.True(t, strings.Contains(out, "Connect With Me"), "Should contain connect section") +} + +func TestRenderModernTheme(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 100, 0, 0 + currentTheme = "modern" + updateCurrentTheme() + + out := Render("") + uassert.True(t, strings.Contains(out, "🚀 Matija's Space"), "Modern theme should have correct header") +} + +func TestRenderMinimalTheme(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 100 + currentTheme = "minimal" + updateCurrentTheme() + + out := Render("") + uassert.True(t, strings.Contains(out, "Matija Marjanovic"), "Minimal theme should have correct header") +} + +func TestUpdateLinks(t *testing.T) { + setupTest() + + newLink := "https://example.com/vote" + + UpdateModernLink(newLink) + urequire.Equal(t, modernLink, newLink, "Modern link should be updated") + + UpdateClassicLink(newLink) + urequire.Equal(t, classicLink, newLink, "Classic link should be updated") + + UpdateMinimalLink(newLink) + urequire.Equal(t, minimalLink, newLink, "Minimal link should be updated") +} diff --git a/examples/gno.land/r/morgan/guestbook/admin.gno b/examples/gno.land/r/morgan/guestbook/admin.gno new file mode 100644 index 00000000000..fb7f9e1461c --- /dev/null +++ b/examples/gno.land/r/morgan/guestbook/admin.gno @@ -0,0 +1,25 @@ +package guestbook + +import ( + "gno.land/p/demo/ownable" + "gno.land/p/demo/seqid" +) + +var owner = ownable.New() + +// AdminDelete removes the guestbook message with the given ID. +// The user will still be marked as having submitted a message, so they +// won't be able to re-submit a new message. +func AdminDelete(signatureID string) { + owner.AssertCallerIsOwner() + + id, err := seqid.FromString(signatureID) + if err != nil { + panic(err) + } + idb := id.Binary() + if !guestbook.Has(idb) { + panic("signature does not exist") + } + guestbook.Remove(idb) +} diff --git a/examples/gno.land/r/morgan/guestbook/gno.mod b/examples/gno.land/r/morgan/guestbook/gno.mod new file mode 100644 index 00000000000..ac63a4cf8cd --- /dev/null +++ b/examples/gno.land/r/morgan/guestbook/gno.mod @@ -0,0 +1 @@ +module gno.land/r/morgan/guestbook diff --git a/examples/gno.land/r/morgan/guestbook/guestbook.gno b/examples/gno.land/r/morgan/guestbook/guestbook.gno new file mode 100644 index 00000000000..be9e9db6133 --- /dev/null +++ b/examples/gno.land/r/morgan/guestbook/guestbook.gno @@ -0,0 +1,126 @@ +// Realm guestbook contains an implementation of a simple guestbook. +// Come and sign yourself up! +package guestbook + +import ( + "std" + "strconv" + "strings" + "time" + "unicode" + + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" +) + +// Signature is a single entry in the guestbook. +type Signature struct { + Message string + Author std.Address + Time time.Time +} + +const ( + maxMessageLength = 140 + maxPerPage = 25 +) + +var ( + signatureID seqid.ID + guestbook avl.Tree // id -> Signature + hasSigned avl.Tree // address -> struct{} +) + +func init() { + Sign("You reached the end of the guestbook!") +} + +const ( + errNotAUser = "this guestbook can only be signed by users" + errAlreadySigned = "you already signed the guestbook!" + errInvalidCharacterInMessage = "invalid character in message" +) + +// Sign signs the guestbook, with the specified message. +func Sign(message string) { + prev := std.PrevRealm() + switch { + case !prev.IsUser(): + panic(errNotAUser) + case hasSigned.Has(prev.Addr().String()): + panic(errAlreadySigned) + } + message = validateMessage(message) + + guestbook.Set(signatureID.Next().Binary(), Signature{ + Message: message, + Author: prev.Addr(), + // NOTE: time.Now() will yield the "block time", which is deterministic. + Time: time.Now(), + }) + hasSigned.Set(prev.Addr().String(), struct{}{}) +} + +func validateMessage(msg string) string { + if len(msg) > maxMessageLength { + panic("Keep it brief! (max " + strconv.Itoa(maxMessageLength) + " bytes!)") + } + out := "" + for _, ch := range msg { + switch { + case unicode.IsLetter(ch), + unicode.IsNumber(ch), + unicode.IsSpace(ch), + unicode.IsPunct(ch): + out += string(ch) + default: + panic(errInvalidCharacterInMessage) + } + } + return out +} + +func Render(maxID string) string { + var bld strings.Builder + + bld.WriteString("# Guestbook 📝\n\n[Come sign the guestbook!](./guestbook$help&func=Sign)\n\n---\n\n") + + var maxIDBinary string + if maxID != "" { + mid, err := seqid.FromString(maxID) + if err != nil { + panic(err) + } + + // AVL iteration is exclusive, so we need to decrease the ID value to get the "true" maximum. + mid-- + maxIDBinary = mid.Binary() + } + + var lastID seqid.ID + var printed int + guestbook.ReverseIterate("", maxIDBinary, func(key string, val interface{}) bool { + sig := val.(Signature) + message := strings.ReplaceAll(sig.Message, "\n", "\n> ") + bld.WriteString("> " + message + "\n>\n") + idValue, ok := seqid.FromBinary(key) + if !ok { + panic("invalid seqid id") + } + + bld.WriteString("> _Written by " + sig.Author.String() + " at " + sig.Time.Format(time.DateTime) + "_ (#" + idValue.String() + ")\n\n---\n\n") + lastID = idValue + + printed++ + // stop after exceeding limit + return printed >= maxPerPage + }) + + if printed == 0 { + bld.WriteString("No messages!") + } else if printed >= maxPerPage { + bld.WriteString("

Next page

") + } + + return bld.String() +} diff --git a/examples/gno.land/r/morgan/guestbook/guestbook_test.gno b/examples/gno.land/r/morgan/guestbook/guestbook_test.gno new file mode 100644 index 00000000000..b14fee45b42 --- /dev/null +++ b/examples/gno.land/r/morgan/guestbook/guestbook_test.gno @@ -0,0 +1,131 @@ +package guestbook + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" +) + +func TestSign(t *testing.T) { + guestbook = avl.Tree{} + hasSigned = avl.Tree{} + + std.TestSetRealm(std.NewUserRealm("g1user")) + Sign("Hello!") + + std.TestSetRealm(std.NewUserRealm("g1user2")) + Sign("Hello2!") + + res := Render("") + t.Log(res) + if !strings.Contains(res, "> Hello!\n>\n> _Written by g1user ") { + t.Error("does not contain first user's message") + } + if !strings.Contains(res, "> Hello2!\n>\n> _Written by g1user2 ") { + t.Error("does not contain second user's message") + } + if guestbook.Size() != 2 { + t.Error("invalid guestbook size") + } +} + +func TestSign_FromRealm(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/users")) + + defer func() { + rec := recover() + if rec == nil { + t.Fatal("expected panic") + } + recString, ok := rec.(string) + if !ok { + t.Fatal("not a string", rec) + } else if recString != errNotAUser { + t.Fatal("invalid error", recString) + } + }() + Sign("Hey!") +} + +func TestSign_Double(t *testing.T) { + // Should not allow signing twice. + guestbook = avl.Tree{} + hasSigned = avl.Tree{} + + std.TestSetRealm(std.NewUserRealm("g1user")) + Sign("Hello!") + + defer func() { + rec := recover() + if rec == nil { + t.Fatal("expected panic") + } + recString, ok := rec.(string) + if !ok { + t.Error("type assertion failed", rec) + } else if recString != errAlreadySigned { + t.Error("invalid error message", recString) + } + }() + + Sign("Hello again!") +} + +func TestSign_InvalidMessage(t *testing.T) { + // Should not allow control characters in message. + guestbook = avl.Tree{} + hasSigned = avl.Tree{} + + std.TestSetRealm(std.NewUserRealm("g1user")) + + defer func() { + rec := recover() + if rec == nil { + t.Fatal("expected panic") + } + recString, ok := rec.(string) + if !ok { + t.Error("type assertion failed", rec) + } else if recString != errInvalidCharacterInMessage { + t.Error("invalid error message", recString) + } + }() + Sign("\x00Hello!") +} + +func TestAdminDelete(t *testing.T) { + const ( + userAddr std.Address = "g1user" + adminAddr std.Address = "g1admin" + ) + + guestbook = avl.Tree{} + hasSigned = avl.Tree{} + owner = ownable.NewWithAddress(adminAddr) + signatureID = 0 + + std.TestSetRealm(std.NewUserRealm(userAddr)) + + const bad = "Very Bad Message! Nyeh heh heh!" + Sign(bad) + + if rnd := Render(""); !strings.Contains(rnd, bad) { + t.Fatal("render does not contain bad message", rnd) + } + + std.TestSetRealm(std.NewUserRealm(adminAddr)) + AdminDelete(signatureID.String()) + + if rnd := Render(""); strings.Contains(rnd, bad) { + t.Error("render contains bad message", rnd) + } + if guestbook.Size() != 0 { + t.Error("invalid guestbook size") + } + if hasSigned.Size() != 1 { + t.Error("invalid hasSigned size") + } +} diff --git a/examples/gno.land/r/morgan/home/gno.mod b/examples/gno.land/r/morgan/home/gno.mod new file mode 100644 index 00000000000..573a7e139e7 --- /dev/null +++ b/examples/gno.land/r/morgan/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/morgan/home diff --git a/examples/gno.land/r/morgan/home/home.gno b/examples/gno.land/r/morgan/home/home.gno new file mode 100644 index 00000000000..20b66b895e3 --- /dev/null +++ b/examples/gno.land/r/morgan/home/home.gno @@ -0,0 +1,14 @@ +package home + +import "gno.land/r/leon/hof" + +const staticHome = `# morgan's (gn)home + +- [📝 sign my guestbook](/r/morgan/guestbook) +` + +func init() { hof.Register() } + +func Render(path string) string { + return staticHome +} diff --git a/examples/gno.land/r/manfred/README.md b/examples/gno.land/r/moul/README.md similarity index 100% rename from examples/gno.land/r/manfred/README.md rename to examples/gno.land/r/moul/README.md diff --git a/examples/gno.land/r/moul/config/config.gno b/examples/gno.land/r/moul/config/config.gno new file mode 100644 index 00000000000..a4f24411747 --- /dev/null +++ b/examples/gno.land/r/moul/config/config.gno @@ -0,0 +1,20 @@ +package config + +import "std" + +var addr = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @moul + +func Addr() std.Address { + return addr +} + +func UpdateAddr(newAddr std.Address) { + AssertIsAdmin() + addr = newAddr +} + +func AssertIsAdmin() { + if std.GetOrigCaller() != addr { + panic("restricted area") + } +} diff --git a/examples/gno.land/r/moul/config/config_test.gno b/examples/gno.land/r/moul/config/config_test.gno new file mode 100644 index 00000000000..d912156bec0 --- /dev/null +++ b/examples/gno.land/r/moul/config/config_test.gno @@ -0,0 +1 @@ +package config diff --git a/examples/gno.land/r/moul/config/gno.mod b/examples/gno.land/r/moul/config/gno.mod new file mode 100644 index 00000000000..2029efc8fcb --- /dev/null +++ b/examples/gno.land/r/moul/config/gno.mod @@ -0,0 +1 @@ +module gno.land/r/moul/config diff --git a/examples/gno.land/r/moul/home/gno.mod b/examples/gno.land/r/moul/home/gno.mod new file mode 100644 index 00000000000..91e02df3707 --- /dev/null +++ b/examples/gno.land/r/moul/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/moul/home diff --git a/examples/gno.land/r/moul/home/home.gno b/examples/gno.land/r/moul/home/home.gno new file mode 100644 index 00000000000..1094ce29cc5 --- /dev/null +++ b/examples/gno.land/r/moul/home/home.gno @@ -0,0 +1,107 @@ +package home + +import ( + "strconv" + + "gno.land/p/demo/svg" + "gno.land/p/moul/debug" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/moul/txlink" + "gno.land/p/moul/web25" + "gno.land/r/leon/hof" + "gno.land/r/moul/config" +) + +var ( + todos []string + status string + memeImgURL string + web25config = web25.Config{URL: "https://moul.github.io/gno-moul-home-web25/"} +) + +func init() { + todos = append(todos, "fill this todo list...") + status = "Online" // Initial status set to "Online" + memeImgURL = "https://i.imgflip.com/7ze8dc.jpg" + hof.Register() +} + +func Render(path string) string { + content := web25config.Render(path) + var d debug.Debug + + content += md.H1("Manfred's (gn)home Dashboard") + + content += md.H2("Meme") + content += md.Paragraph( + md.Image("meme", memeImgURL), + ) + + content += md.H2("Status") + content += md.Paragraph(status) + content += md.Paragraph(md.Link("update", txlink.Call("UpdateStatus"))) + + d.Log("hello world!") + + content += md.H2("Personal TODO List (bullet list)") + for i, todo := range todos { + idstr := strconv.Itoa(i) + deleteLink := md.Link("x", txlink.Call("DeleteTodo", "idx", idstr)) + content += md.BulletItem(todo + " " + deleteLink) + } + content += md.BulletItem(md.Link("[new]", txlink.Call("AddTodo"))) + + content += md.H2("Personal TODO List (table)") + table := mdtable.Table{ + Headers: []string{"ID", "Item", "Links"}, + } + for i, todo := range todos { + idstr := strconv.Itoa(i) + deleteLink := md.Link("[del]", txlink.Call("DeleteTodo", "idx", idstr)) + table.Append([]string{"#" + idstr, todo, deleteLink}) + } + content += table.String() + + content += md.H2("SVG Example") + content += md.Paragraph("this feature may not work with the current gnoweb version and/or configuration.") + content += md.Paragraph(svg.Canvas{ + Width: 500, Height: 500, + Elems: []svg.Elem{ + svg.Rectangle{50, 50, 100, 100, "red"}, + svg.Circle{50, 50, 100, "red"}, + svg.Text{100, 100, "hello world!", "magenta"}, + }, + }.String()) + + content += md.H2("Debug") + content += md.Paragraph("this feature may not work with the current gnoweb version and/or configuration.") + content += md.Paragraph( + md.Link("toggle debug", debug.ToggleURL(path)), + ) + + // TODO: my r/boards posts + // TODO: my r/events events + content += d.Render(path) + return content +} + +func AddTodo(todo string) { + config.AssertIsAdmin() + todos = append(todos, todo) +} + +func DeleteTodo(idx int) { + config.AssertIsAdmin() + if idx >= 0 && idx < len(todos) { + // Remove the todo from the list by merging slices from before and after the todo + todos = append(todos[:idx], todos[idx+1:]...) + } else { + panic("Invalid todo index") + } +} + +func UpdateStatus(newStatus string) { + config.AssertIsAdmin() + status = newStatus +} diff --git a/examples/gno.land/r/moul/home/z1_filetest.gno b/examples/gno.land/r/moul/home/z1_filetest.gno new file mode 100644 index 00000000000..b9d7d91a702 --- /dev/null +++ b/examples/gno.land/r/moul/home/z1_filetest.gno @@ -0,0 +1,37 @@ +package main + +import "gno.land/r/moul/home" + +func main() { + println(home.Render("")) +} + +// Output: +// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience. +// # Manfred's (gn)home Dashboard +// ## Meme +// ![meme](https://i.imgflip.com/7ze8dc.jpg) +// +// ## Status +// Online +// +// [update](/r/moul/home$help&func=UpdateStatus) +// +// ## Personal TODO List (bullet list) +// - fill this todo list... [x](/r/moul/home$help&func=DeleteTodo&idx=0) +// - [\[new\]](/r/moul/home$help&func=AddTodo) +// ## Personal TODO List (table) +// | ID | Item | Links | +// | --- | --- | --- | +// | #0 | fill this todo list... | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=0) | +// ## SVG Example +// this feature may not work with the current gnoweb version and/or configuration. +// +// hello world! +// +// ## Debug +// this feature may not work with the current gnoweb version and/or configuration. +// +// [toggle debug](/r/moul/home:?debug=1) +// +// diff --git a/examples/gno.land/r/moul/home/z2_filetest.gno b/examples/gno.land/r/moul/home/z2_filetest.gno new file mode 100644 index 00000000000..f471280d8ef --- /dev/null +++ b/examples/gno.land/r/moul/home/z2_filetest.gno @@ -0,0 +1,72 @@ +package main + +import ( + "std" + + "gno.land/r/moul/home" +) + +func main() { + std.TestSetOrigCaller("g1manfred47kzduec920z88wfr64ylksmdcedlf5") + home.AddTodo("aaa") + home.AddTodo("bbb") + home.AddTodo("ccc") + home.AddTodo("ddd") + home.AddTodo("eee") + home.UpdateStatus("Lorem Ipsum") + home.DeleteTodo(3) + println(home.Render("?debug=1")) +} + +// Output: +// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience. +// # Manfred's (gn)home Dashboard +// ## Meme +// ![meme](https://i.imgflip.com/7ze8dc.jpg) +// +// ## Status +// Lorem Ipsum +// +// [update](/r/moul/home$help&func=UpdateStatus) +// +// ## Personal TODO List (bullet list) +// - fill this todo list... [x](/r/moul/home$help&func=DeleteTodo&idx=0) +// - aaa [x](/r/moul/home$help&func=DeleteTodo&idx=1) +// - bbb [x](/r/moul/home$help&func=DeleteTodo&idx=2) +// - ddd [x](/r/moul/home$help&func=DeleteTodo&idx=3) +// - eee [x](/r/moul/home$help&func=DeleteTodo&idx=4) +// - [\[new\]](/r/moul/home$help&func=AddTodo) +// ## Personal TODO List (table) +// | ID | Item | Links | +// | --- | --- | --- | +// | #0 | fill this todo list... | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=0) | +// | #1 | aaa | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=1) | +// | #2 | bbb | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=2) | +// | #3 | ddd | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=3) | +// | #4 | eee | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=4) | +// ## SVG Example +// this feature may not work with the current gnoweb version and/or configuration. +// +// hello world! +// +// ## Debug +// this feature may not work with the current gnoweb version and/or configuration. +// +// [toggle debug](/r/moul/home:) +// +//
debug +// +// ### Logs +// - hello world! +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | gno.land/r/moul/home | +// | `std.CurrentRealm().Addr()` | g1h8h57ntxadcze3f703skymfzdwa6t3ugf0nq3z | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1manfred47kzduec920z88wfr64ylksmdcedlf5 | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | +// +//
+// diff --git a/examples/gno.land/r/moul/present/admin.gno b/examples/gno.land/r/moul/present/admin.gno new file mode 100644 index 00000000000..ab99b1725c5 --- /dev/null +++ b/examples/gno.land/r/moul/present/admin.gno @@ -0,0 +1,96 @@ +package present + +import ( + "std" + "strings" + + "gno.land/p/demo/avl" +) + +var ( + adminAddr std.Address + moderatorList avl.Tree + inPause bool +) + +func init() { + // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. + adminAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" +} + +func AdminSetAdminAddr(addr std.Address) { + assertIsAdmin() + adminAddr = addr +} + +func AdminSetInPause(state bool) { + assertIsAdmin() + inPause = state +} + +func AdminAddModerator(addr std.Address) { + assertIsAdmin() + moderatorList.Set(addr.String(), true) +} + +func AdminRemoveModerator(addr std.Address) { + assertIsAdmin() + moderatorList.Set(addr.String(), false) // XXX: delete instead? +} + +func ModAddPost(slug, title, body, publicationDate, authors, tags string) { + assertIsModerator() + + caller := std.GetOrigCaller() + tagList := strings.Split(tags, ",") + authorList := strings.Split(authors, ",") + + err := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList) + checkErr(err) +} + +func ModEditPost(slug, title, body, publicationDate, authors, tags string) { + assertIsModerator() + + tagList := strings.Split(tags, ",") + authorList := strings.Split(authors, ",") + + err := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList) + checkErr(err) +} + +func isAdmin(addr std.Address) bool { + return addr == adminAddr +} + +func isModerator(addr std.Address) bool { + _, found := moderatorList.Get(addr.String()) + return found +} + +func assertIsAdmin() { + caller := std.GetOrigCaller() + if !isAdmin(caller) { + panic("access restricted.") + } +} + +func assertIsModerator() { + caller := std.GetOrigCaller() + if isAdmin(caller) || isModerator(caller) { + return + } + panic("access restricted") +} + +func assertNotInPause() { + if inPause { + panic("access restricted (pause)") + } +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/examples/gno.land/r/moul/present/gno.mod b/examples/gno.land/r/moul/present/gno.mod new file mode 100644 index 00000000000..a0a7777d0ed --- /dev/null +++ b/examples/gno.land/r/moul/present/gno.mod @@ -0,0 +1 @@ +module gno.land/r/moul/present diff --git a/examples/gno.land/r/manfred/present/present_miami23.gno b/examples/gno.land/r/moul/present/present_miami23.gno similarity index 100% rename from examples/gno.land/r/manfred/present/present_miami23.gno rename to examples/gno.land/r/moul/present/present_miami23.gno diff --git a/examples/gno.land/r/manfred/present/present_miami23_filetest.gno b/examples/gno.land/r/moul/present/present_miami23_filetest.gno similarity index 84% rename from examples/gno.land/r/manfred/present/present_miami23_filetest.gno rename to examples/gno.land/r/moul/present/present_miami23_filetest.gno index ac19d83ade4..09d332ec6e4 100644 --- a/examples/gno.land/r/manfred/present/present_miami23_filetest.gno +++ b/examples/gno.land/r/moul/present/present_miami23_filetest.gno @@ -1,7 +1,7 @@ package main import ( - "gno.land/r/manfred/present" + "gno.land/r/moul/present" ) func main() { diff --git a/examples/gno.land/r/manfred/present/presentations.gno b/examples/gno.land/r/moul/present/presentations.gno similarity index 86% rename from examples/gno.land/r/manfred/present/presentations.gno rename to examples/gno.land/r/moul/present/presentations.gno index 8a99f502e86..c5529804751 100644 --- a/examples/gno.land/r/manfred/present/presentations.gno +++ b/examples/gno.land/r/moul/present/presentations.gno @@ -8,7 +8,7 @@ import ( var b = &blog.Blog{ Title: "Manfred's Presentations", - Prefix: "/r/manfred/present:", + Prefix: "/r/moul/present:", NoBreadcrumb: true, } diff --git a/examples/gno.land/r/n2p5/config/config.gno b/examples/gno.land/r/n2p5/config/config.gno new file mode 100644 index 00000000000..42cb587eaf5 --- /dev/null +++ b/examples/gno.land/r/n2p5/config/config.gno @@ -0,0 +1,120 @@ +package config + +import ( + "std" + + "gno.land/p/demo/ufmt" + "gno.land/p/n2p5/mgroup" +) + +const ( + originalOwner = "g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t" // n2p5 +) + +var ( + adminGroup = mgroup.New(originalOwner) + description = "" +) + +// AddBackupOwner adds a backup owner to the Owner Group. +// A backup owner can claim ownership of the contract. +func AddBackupOwner(addr std.Address) { + err := adminGroup.AddBackupOwner(addr) + if err != nil { + panic(err) + } +} + +// RemoveBackupOwner removes a backup owner from the Owner Group. +// The primary owner cannot be removed. +func RemoveBackupOwner(addr std.Address) { + err := adminGroup.RemoveBackupOwner(addr) + if err != nil { + panic(err) + } +} + +// ClaimOwnership allows an authorized user in the ownerGroup +// to claim ownership of the contract. +func ClaimOwnership() { + err := adminGroup.ClaimOwnership() + if err != nil { + panic(err) + } +} + +// AddAdmin adds an admin to the Admin Group. +func AddAdmin(addr std.Address) { + err := adminGroup.AddMember(addr) + if err != nil { + panic(err) + } +} + +// RemoveAdmin removes an admin from the Admin Group. +// The primary owner cannot be removed. +func RemoveAdmin(addr std.Address) { + err := adminGroup.RemoveMember(addr) + if err != nil { + panic(err) + } +} + +// Owner returns the current owner of the claims contract. +func Owner() std.Address { + return adminGroup.Owner() +} + +// BackupOwners returns the current backup owners of the claims contract. +func BackupOwners() []string { + return adminGroup.BackupOwners() +} + +// Admins returns the current admin members of the claims contract. +func Admins() []string { + return adminGroup.Members() +} + +// IsAdmin checks if an address is in the config adminGroup. +func IsAdmin(addr std.Address) bool { + return adminGroup.IsMember(addr) +} + +// toMarkdownList formats a slice of strings as a markdown list. +func toMarkdownList(items []string) string { + var result string + for _, item := range items { + result += ufmt.Sprintf("- %s\n", item) + } + return result +} + +func Render(path string) string { + owner := adminGroup.Owner().String() + backupOwners := toMarkdownList(BackupOwners()) + adminMembers := toMarkdownList(Admins()) + return ufmt.Sprintf(` +# Config Dashboard + +This dashboard shows the current configuration owner, backup owners, and admin members. +- The owner has the exclusive ability to manage the backup owners and admin members. +- Backup owners can claim ownership of the contract and become the owner. +- Admin members are used to authorize actions in other realms, such as [my home realm](/r/n2p5/home). + +#### Owner + +%s + +#### Backup Owners + +%s + +#### Admin Members + +%s + +`, + owner, + backupOwners, + adminMembers) +} diff --git a/examples/gno.land/r/n2p5/config/gno.mod b/examples/gno.land/r/n2p5/config/gno.mod new file mode 100644 index 00000000000..29d5a74eb0a --- /dev/null +++ b/examples/gno.land/r/n2p5/config/gno.mod @@ -0,0 +1 @@ +module gno.land/r/n2p5/config diff --git a/examples/gno.land/r/n2p5/haystack/gno.mod b/examples/gno.land/r/n2p5/haystack/gno.mod new file mode 100644 index 00000000000..17c131b8370 --- /dev/null +++ b/examples/gno.land/r/n2p5/haystack/gno.mod @@ -0,0 +1 @@ +module gno.land/r/n2p5/haystack diff --git a/examples/gno.land/r/n2p5/haystack/haystack.gno b/examples/gno.land/r/n2p5/haystack/haystack.gno new file mode 100644 index 00000000000..397de1e3e3d --- /dev/null +++ b/examples/gno.land/r/n2p5/haystack/haystack.gno @@ -0,0 +1,32 @@ +package haystack + +import ( + "gno.land/p/n2p5/haystack" +) + +var storage = haystack.New() + +func Render(path string) string { + return ` +Put a Needle in the Haystack. +` +} + +// Add takes a fixed-length hex-encoded needle bytes and adds it to the haystack key-value store. +// If storage encounters an error, it will panic. +func Add(needleHex string) { + err := storage.Add(needleHex) + if err != nil { + panic(err) + } +} + +// Get takes a fixed-length hex-encoded needle hash and returns the hex-encoded needle bytes. +// If storage encounters an error, it will panic. +func Get(hashHex string) string { + needleHex, err := storage.Get(hashHex) + if err != nil { + panic(err) + } + return needleHex +} diff --git a/examples/gno.land/r/n2p5/haystack/haystack_test.gno b/examples/gno.land/r/n2p5/haystack/haystack_test.gno new file mode 100644 index 00000000000..52dadf8bf9e --- /dev/null +++ b/examples/gno.land/r/n2p5/haystack/haystack_test.gno @@ -0,0 +1,70 @@ +package haystack + +import ( + "encoding/hex" + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" + "gno.land/p/n2p5/haystack" + "gno.land/p/n2p5/haystack/needle" +) + +func TestHaystack(t *testing.T) { + t.Parallel() + // needleHex returns a hex-encoded needle and its hash for a given index. + genNeedleHex := func(i int) (string, string) { + b := make([]byte, needle.PayloadLength) + b[0] = byte(i) + n, _ := needle.New(b) + return hex.EncodeToString(n.Bytes()), hex.EncodeToString(n.Hash()) + } + + u1 := testutils.TestAddress("u1") + u2 := testutils.TestAddress("u2") + + t.Run("Add", func(t *testing.T) { + t.Parallel() + + n1, _ := genNeedleHex(1) + n2, _ := genNeedleHex(2) + n3, _ := genNeedleHex(3) + + std.TestSetOrigCaller(u1) + urequire.NotPanics(t, func() { Add(n1) }) + urequire.PanicsWithMessage(t, + haystack.ErrorDuplicateNeedle.Error(), + func() { + Add(n1) + }) + std.TestSetOrigCaller(u2) + urequire.NotPanics(t, func() { Add(n2) }) + urequire.NotPanics(t, func() { Add(n3) }) + }) + + t.Run("Get", func(t *testing.T) { + t.Parallel() + + n1, h1 := genNeedleHex(4) + _, h2 := genNeedleHex(5) + + std.TestSetOrigCaller(u1) + urequire.NotPanics(t, func() { Add(n1) }) + urequire.NotPanics(t, func() { + result := Get(h1) + urequire.Equal(t, n1, result) + }) + + std.TestSetOrigCaller(u2) + urequire.NotPanics(t, func() { + result := Get(h1) + urequire.Equal(t, n1, result) + }) + urequire.PanicsWithMessage(t, + haystack.ErrorNeedleNotFound.Error(), + func() { + Get(h2) + }) + }) +} diff --git a/examples/gno.land/r/n2p5/home/gno.mod b/examples/gno.land/r/n2p5/home/gno.mod new file mode 100644 index 00000000000..3b6ddbf86bb --- /dev/null +++ b/examples/gno.land/r/n2p5/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/n2p5/home diff --git a/examples/gno.land/r/n2p5/home/home.gno b/examples/gno.land/r/n2p5/home/home.gno new file mode 100644 index 00000000000..69b82e86d68 --- /dev/null +++ b/examples/gno.land/r/n2p5/home/home.gno @@ -0,0 +1,73 @@ +package home + +import ( + "std" + "strings" + + "gno.land/p/n2p5/chonk" + + "gno.land/r/leon/hof" + "gno.land/r/n2p5/config" +) + +var ( + active = chonk.New() + preview = chonk.New() +) + +func init() { + hof.Register() +} + +// Add appends a string to the preview Chonk. +func Add(chunk string) { + assertAdmin() + preview.Add(chunk) +} + +// Flush clears the preview Chonk. +func Flush() { + assertAdmin() + preview.Flush() +} + +// Promote promotes the preview Chonk to the active Chonk +// and creates a new preview Chonk. +func Promote() { + assertAdmin() + active = preview + preview = chonk.New() +} + +// Render returns the contents of the scanner for the active or preview Chonk +// based on the path provided. +func Render(path string) string { + var result string + scanner := getScanner(path) + for scanner.Scan() { + result += scanner.Text() + } + return result +} + +// assertAdmin panics if the caller is not an admin as defined in the config realm. +func assertAdmin() { + caller := std.PrevRealm().Addr() + if !config.IsAdmin(caller) { + panic("forbidden: must be admin") + } +} + +// getScanner returns the scanner for the active or preview Chonk based +// on the path provided. +func getScanner(path string) *chonk.Scanner { + if isPreview(path) { + return preview.Scanner() + } + return active.Scanner() +} + +// isPreview returns true if the path prefix is "preview". +func isPreview(path string) bool { + return strings.HasPrefix(path, "preview") +} diff --git a/examples/gno.land/r/n2p5/loci/gno.mod b/examples/gno.land/r/n2p5/loci/gno.mod new file mode 100644 index 00000000000..131e0d73467 --- /dev/null +++ b/examples/gno.land/r/n2p5/loci/gno.mod @@ -0,0 +1 @@ +module gno.land/r/n2p5/loci diff --git a/examples/gno.land/r/n2p5/loci/loci.gno b/examples/gno.land/r/n2p5/loci/loci.gno new file mode 100644 index 00000000000..36f282e729f --- /dev/null +++ b/examples/gno.land/r/n2p5/loci/loci.gno @@ -0,0 +1,68 @@ +package loci + +import ( + "encoding/base64" + "std" + + "gno.land/p/demo/ufmt" + "gno.land/p/n2p5/loci" +) + +var store *loci.LociStore + +func init() { + store = loci.New() +} + +// Set takes a base64 encoded string and stores it in the Loci store. +// Keyed by the address of the caller. It also emits a "set" event with +// the address of the caller. +func Set(value string) { + b, err := base64.StdEncoding.DecodeString(value) + if err != nil { + panic(err) + } + store.Set(b) + std.Emit("SetValue", "ForAddr", string(std.PrevRealm().Addr())) +} + +// Get retrieves the value stored at the provided address and +// returns it as a base64 encoded string. +func Get(addr std.Address) string { + return base64.StdEncoding.EncodeToString(store.Get(addr)) +} + +func Render(path string) string { + if path == "" { + return about + } + return renderGet(std.Address(path)) +} + +func renderGet(addr std.Address) string { + value := "```\n" + Get(addr) + "\n```" + + return ufmt.Sprintf(` +# Loci Value Viewer + +**Address:** %s + +%s + +`, addr, value) +} + +const about = ` +# Welcome to Loci + +Loci is a simple key-value store keyed by the caller's gno.land address. +Only the caller can set the value for their address, but anyone can +retrieve the value for any address. There are only two functions: Set and Get. +If you'd like to set a value, simply base64 encode any message you'd like and +it will be stored in in Loci. If you'd like to retrieve a value, simply provide +the address of the value you'd like to retrieve. + +For convenience, you can also use gnoweb to view the value for a given address, +if one exists. For instance append :g1j39fhg29uehm7twwnhvnpz3ggrm6tprhq65t0t to +this URL to view the value stored at that address. +` diff --git a/examples/gno.land/r/nemanya/config/config.gno b/examples/gno.land/r/nemanya/config/config.gno new file mode 100644 index 00000000000..795e48c94c1 --- /dev/null +++ b/examples/gno.land/r/nemanya/config/config.gno @@ -0,0 +1,63 @@ +package config + +import ( + "errors" + "std" +) + +var ( + main std.Address + backup std.Address + + ErrInvalidAddr = errors.New("Invalid address") + ErrUnauthorized = errors.New("Unauthorized") +) + +func init() { + main = "g1x9qyf6f34v2g52k4q5smn5tctmj3hl2kj7l2ql" +} + +func Address() std.Address { + return main +} + +func Backup() std.Address { + return backup +} + +func SetAddress(a std.Address) error { + if !a.IsValid() { + return ErrInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + main = a + return nil +} + +func SetBackup(a std.Address) error { + if !a.IsValid() { + return ErrInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + backup = a + return nil +} + +func checkAuthorized() error { + caller := std.PrevRealm().Addr() + isAuthorized := caller == main || caller == backup + + if !isAuthorized { + return ErrUnauthorized + } + + return nil +} diff --git a/examples/gno.land/r/nemanya/config/gno.mod b/examples/gno.land/r/nemanya/config/gno.mod new file mode 100644 index 00000000000..4388b5bd525 --- /dev/null +++ b/examples/gno.land/r/nemanya/config/gno.mod @@ -0,0 +1 @@ +module gno.land/r/nemanya/config diff --git a/examples/gno.land/r/nemanya/home/gno.mod b/examples/gno.land/r/nemanya/home/gno.mod new file mode 100644 index 00000000000..d0220197489 --- /dev/null +++ b/examples/gno.land/r/nemanya/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/nemanya/home diff --git a/examples/gno.land/r/nemanya/home/home.gno b/examples/gno.land/r/nemanya/home/home.gno new file mode 100644 index 00000000000..08e24baecfd --- /dev/null +++ b/examples/gno.land/r/nemanya/home/home.gno @@ -0,0 +1,280 @@ +package home + +import ( + "std" + "strings" + + "gno.land/p/demo/ufmt" + "gno.land/r/nemanya/config" +) + +type SocialLink struct { + URL string + Text string +} + +type Sponsor struct { + Address std.Address + Amount std.Coins +} + +type Project struct { + Name string + Description string + URL string + ImageURL string + Sponsors map[std.Address]Sponsor +} + +var ( + textArt string + aboutMe string + sponsorInfo string + socialLinks map[string]SocialLink + gnoProjects map[string]Project + otherProjects map[string]Project + totalDonations std.Coins +) + +func init() { + textArt = renderTextArt() + aboutMe = "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" + sponsorInfo = "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" + + socialLinks = map[string]SocialLink{ + "GitHub": {URL: "https://github.com/Nemanya8", Text: "Explore my repositories and open-source contributions."}, + "LinkedIn": {URL: "https://www.linkedin.com/in/nemanjamatic/", Text: "Connect with me professionally."}, + "Email Me": {URL: "mailto:matic.nemanya@gmail.com", Text: "Reach out for collaboration or inquiries."}, + } + + gnoProjects = make(map[string]Project) + otherProjects = make(map[string]Project) + + gnoProjects["Liberty Bridge"] = Project{ + Name: "Liberty Bridge", + Description: "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", + URL: "https://gno.land", + ImageURL: "https://github.com/Milosevic02/LibertyBridge/raw/main/lb_banner.png", + Sponsors: make(map[std.Address]Sponsor), + } + + otherProjects["Incognito"] = Project{ + Name: "Incognito", + Description: "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.", + URL: "https://github.com/Milosevic02/Incognito-ETHSofia", + ImageURL: "", + Sponsors: make(map[std.Address]Sponsor), + } +} + +func Render(path string) string { + var sb strings.Builder + sb.WriteString("# Hi, I'm\n") + sb.WriteString(textArt) + sb.WriteString("---\n") + sb.WriteString("## About me\n") + sb.WriteString(aboutMe) + sb.WriteString(sponsorInfo) + sb.WriteString(ufmt.Sprintf("# Total Sponsor Donations: %s\n", totalDonations.String())) + sb.WriteString("---\n") + sb.WriteString(renderProjects(gnoProjects, "Gno Projects")) + sb.WriteString("---\n") + sb.WriteString(renderProjects(otherProjects, "Other Projects")) + sb.WriteString("---\n") + sb.WriteString(renderSocialLinks()) + + return sb.String() +} + +func renderTextArt() string { + var sb strings.Builder + sb.WriteString("```\n") + sb.WriteString(" ___ ___ ___ ___ ___ ___ ___ \n") + sb.WriteString(" /\\__\\ /\\ \\ /\\__\\ /\\ \\ /\\__\\ |\\__\\ /\\ \\ \n") + sb.WriteString(" /::| | /::\\ \\ /::| | /::\\ \\ /::| | |:| | /::\\ \\ \n") + sb.WriteString(" /:|:| | /:/\\:\\ \\ /:|:| | /:/\\:\\ \\ /:|:| | |:| | /:/\\:\\ \\ \n") + sb.WriteString(" /:/|:| |__ /::\\~\\:\\ \\ /:/|:|__|__ /::\\~\\:\\ \\ /:/|:| |__ |:|__|__ /::\\~\\:\\ \\ \n") + sb.WriteString(" /:/ |:| /\\__\\ /:/\\:\\ \\:\\__\\ /:/ |::::\\__\\ /:/\\:\\ \\:\\__\\ /:/ |:| /\\__\\ /::::\\__\\ /:/\\:\\ \\:\\__\\\n") + sb.WriteString(" \\/__|:|/:/ / \\:\\~\\:\\ \\/__/ \\/__/~~/:/ / \\/__\\:\\/:/ / \\/__|:|/:/ / /:/~~/~ \\/__\\:\\/:/ / \n") + sb.WriteString(" |:/:/ / \\:\\ \\:\\__\\ /:/ / \\::/ / |:/:/ / /:/ / \\::/ / \n") + sb.WriteString(" |::/ / \\:\\ \\/__/ /:/ / /:/ / |::/ / \\/__/ /:/ / \n") + sb.WriteString(" /:/ / \\:\\__\\ /:/ / /:/ / /:/ / /:/ / \n") + sb.WriteString(" \\/__/ \\/__/ \\/__/ \\/__/ \\/__/ \\/__/ \n") + sb.WriteString("\n```\n") + return sb.String() +} + +func renderSocialLinks() string { + var sb strings.Builder + sb.WriteString("## Links\n\n") + sb.WriteString("You can find me here:\n\n") + sb.WriteString(ufmt.Sprintf("- [GitHub](%s) - %s\n", socialLinks["GitHub"].URL, socialLinks["GitHub"].Text)) + sb.WriteString(ufmt.Sprintf("- [LinkedIn](%s) - %s\n", socialLinks["LinkedIn"].URL, socialLinks["LinkedIn"].Text)) + sb.WriteString(ufmt.Sprintf("- [Email Me](%s) - %s\n", socialLinks["Email Me"].URL, socialLinks["Email Me"].Text)) + sb.WriteString("\n") + return sb.String() +} + +func renderProjects(projectsMap map[string]Project, title string) string { + var sb strings.Builder + sb.WriteString(ufmt.Sprintf("## %s\n\n", title)) + for _, project := range projectsMap { + if project.ImageURL != "" { + sb.WriteString(ufmt.Sprintf("![%s](%s)\n\n", project.Name, project.ImageURL)) + } + sb.WriteString(ufmt.Sprintf("### [%s](%s)\n\n", project.Name, project.URL)) + sb.WriteString(project.Description + "\n\n") + + if len(project.Sponsors) > 0 { + sb.WriteString(ufmt.Sprintf("#### %s Sponsors\n", project.Name)) + for _, sponsor := range project.Sponsors { + sb.WriteString(ufmt.Sprintf("- %s: %s\n", sponsor.Address.String(), sponsor.Amount.String())) + } + sb.WriteString("\n") + } + } + return sb.String() +} + +func UpdateLink(name, newURL string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + if _, exists := socialLinks[name]; !exists { + panic("Link with the given name does not exist") + } + + socialLinks[name] = SocialLink{ + URL: newURL, + Text: socialLinks[name].Text, + } +} + +func UpdateAboutMe(text string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + aboutMe = text +} + +func AddGnoProject(name, description, url, imageURL string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + project := Project{ + Name: name, + Description: description, + URL: url, + ImageURL: imageURL, + Sponsors: make(map[std.Address]Sponsor), + } + gnoProjects[name] = project +} + +func DeleteGnoProject(projectName string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + if _, exists := gnoProjects[projectName]; !exists { + panic("Project not found") + } + + delete(gnoProjects, projectName) +} + +func AddOtherProject(name, description, url, imageURL string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + project := Project{ + Name: name, + Description: description, + URL: url, + ImageURL: imageURL, + Sponsors: make(map[std.Address]Sponsor), + } + otherProjects[name] = project +} + +func RemoveOtherProject(projectName string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + if _, exists := otherProjects[projectName]; !exists { + panic("Project not found") + } + + delete(otherProjects, projectName) +} + +func isAuthorized(addr std.Address) bool { + return addr == config.Address() || addr == config.Backup() +} + +func SponsorGnoProject(projectName string) { + address := std.GetOrigCaller() + amount := std.GetOrigSend() + + if amount.AmountOf("ugnot") == 0 { + panic("Donation must include GNOT") + } + + project, exists := gnoProjects[projectName] + if !exists { + panic("Gno project not found") + } + + project.Sponsors[address] = Sponsor{ + Address: address, + Amount: project.Sponsors[address].Amount.Add(amount), + } + + totalDonations = totalDonations.Add(amount) + + gnoProjects[projectName] = project +} + +func SponsorOtherProject(projectName string) { + address := std.GetOrigCaller() + amount := std.GetOrigSend() + + if amount.AmountOf("ugnot") == 0 { + panic("Donation must include GNOT") + } + + project, exists := otherProjects[projectName] + if !exists { + panic("Other project not found") + } + + project.Sponsors[address] = Sponsor{ + Address: address, + Amount: project.Sponsors[address].Amount.Add(amount), + } + + totalDonations = totalDonations.Add(amount) + + otherProjects[projectName] = project +} + +func Withdraw() string { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + banker := std.GetBanker(std.BankerTypeRealmSend) + realmAddress := std.GetOrigPkgAddr() + coins := banker.GetCoins(realmAddress) + + if len(coins) == 0 { + return "No coins available to withdraw" + } + + banker.SendCoins(realmAddress, config.Address(), coins) + + return "Successfully withdrew all coins to config address" +} diff --git a/examples/gno.land/r/stefann/home/gno.mod b/examples/gno.land/r/stefann/home/gno.mod new file mode 100644 index 00000000000..89071aa70fb --- /dev/null +++ b/examples/gno.land/r/stefann/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/stefann/home diff --git a/examples/gno.land/r/stefann/home/home.gno b/examples/gno.land/r/stefann/home/home.gno new file mode 100644 index 00000000000..f54721ce37c --- /dev/null +++ b/examples/gno.land/r/stefann/home/home.gno @@ -0,0 +1,288 @@ +package home + +import ( + "sort" + "std" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/users" + "gno.land/r/leon/hof" + + "gno.land/r/stefann/registry" +) + +type City struct { + Name string + URL string +} + +type Sponsor struct { + Address std.Address + Amount std.Coins +} + +type Profile struct { + aboutMe []string +} + +type Travel struct { + cities []City + currentCityIndex int + jarLink string +} + +type Sponsorship struct { + maxSponsors int + sponsors *avl.Tree + DonationsCount int + sponsorsCount int +} + +var ( + profile Profile + travel Travel + sponsorship Sponsorship + owner *ownable.Ownable +) + +func init() { + owner = ownable.NewWithAddress(registry.MainAddr()) + hof.Register() + + profile = Profile{ + aboutMe: []string{ + `## About Me`, + `### 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!`, + + `## Contributions`, + `### I'm just getting started, but you can follow my journey through gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) 🔗`, + }, + } + + travel = Travel{ + cities: []City{ + {Name: "Venice", URL: "https://i.ibb.co/1mcZ7b1/venice.jpg"}, + {Name: "Tokyo", URL: "https://i.ibb.co/wNDJv3H/tokyo.jpg"}, + {Name: "São Paulo", URL: "https://i.ibb.co/yWMq2Sn/sao-paulo.jpg"}, + {Name: "Toronto", URL: "https://i.ibb.co/pb95HJB/toronto.jpg"}, + {Name: "Bangkok", URL: "https://i.ibb.co/pQy3w2g/bangkok.jpg"}, + {Name: "New York", URL: "https://i.ibb.co/6JWLm0h/new-york.jpg"}, + {Name: "Paris", URL: "https://i.ibb.co/q9vf6Hs/paris.jpg"}, + {Name: "Kandersteg", URL: "https://i.ibb.co/60DzywD/kandersteg.jpg"}, + {Name: "Rothenburg", URL: "https://i.ibb.co/cr8d2rQ/rothenburg.jpg"}, + {Name: "Capetown", URL: "https://i.ibb.co/bPGn0v3/capetown.jpg"}, + {Name: "Sydney", URL: "https://i.ibb.co/TBNzqfy/sydney.jpg"}, + {Name: "Oeschinen Lake", URL: "https://i.ibb.co/QJQwp2y/oeschinen-lake.jpg"}, + {Name: "Barra Grande", URL: "https://i.ibb.co/z4RXKc1/barra-grande.jpg"}, + {Name: "London", URL: "https://i.ibb.co/CPGtvgr/london.jpg"}, + }, + currentCityIndex: 0, + jarLink: "https://TODO", // This value should be injected through UpdateJarLink after deployment. + } + + sponsorship = Sponsorship{ + maxSponsors: 3, + sponsors: avl.NewTree(), + DonationsCount: 0, + sponsorsCount: 0, + } +} + +func UpdateCities(newCities []City) { + owner.AssertCallerIsOwner() + travel.cities = newCities +} + +func AddCities(newCities ...City) { + owner.AssertCallerIsOwner() + + travel.cities = append(travel.cities, newCities...) +} + +func UpdateJarLink(newLink string) { + owner.AssertCallerIsOwner() + travel.jarLink = newLink +} + +func UpdateAboutMe(aboutMeStr string) { + owner.AssertCallerIsOwner() + profile.aboutMe = strings.Split(aboutMeStr, "|") +} + +func AddAboutMeRows(newRows ...string) { + owner.AssertCallerIsOwner() + + profile.aboutMe = append(profile.aboutMe, newRows...) +} + +func UpdateMaxSponsors(newMax int) { + owner.AssertCallerIsOwner() + if newMax <= 0 { + panic("maxSponsors must be greater than zero") + } + sponsorship.maxSponsors = newMax +} + +func Donate() { + address := std.GetOrigCaller() + amount := std.GetOrigSend() + + if amount.AmountOf("ugnot") == 0 { + panic("Donation must include GNOT") + } + + existingAmount, exists := sponsorship.sponsors.Get(address.String()) + if exists { + updatedAmount := existingAmount.(std.Coins).Add(amount) + sponsorship.sponsors.Set(address.String(), updatedAmount) + } else { + sponsorship.sponsors.Set(address.String(), amount) + sponsorship.sponsorsCount++ + } + + travel.currentCityIndex++ + sponsorship.DonationsCount++ + + banker := std.GetBanker(std.BankerTypeRealmSend) + ownerAddr := registry.MainAddr() + banker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr())) +} + +type SponsorSlice []Sponsor + +func (s SponsorSlice) Len() int { + return len(s) +} + +func (s SponsorSlice) Less(i, j int) bool { + return s[i].Amount.AmountOf("ugnot") > s[j].Amount.AmountOf("ugnot") +} + +func (s SponsorSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func GetTopSponsors() []Sponsor { + var sponsorSlice SponsorSlice + + sponsorship.sponsors.Iterate("", "", func(key string, value interface{}) bool { + addr := std.Address(key) + amount := value.(std.Coins) + sponsorSlice = append(sponsorSlice, Sponsor{Address: addr, Amount: amount}) + return false + }) + + sort.Sort(sponsorSlice) + return sponsorSlice +} + +func GetTotalDonations() int { + total := 0 + sponsorship.sponsors.Iterate("", "", func(key string, value interface{}) bool { + total += int(value.(std.Coins).AmountOf("ugnot")) + return false + }) + return total +} + +func Render(path string) string { + out := ufmt.Sprintf("# Exploring %s!\n\n", travel.cities[travel.currentCityIndex].Name) + + out += renderAboutMe() + out += "\n\n" + out += renderTips() + + return out +} + +func renderAboutMe() string { + out := "" + + out += ufmt.Sprintf("![Current Location](%s)\n\n", travel.cities[travel.currentCityIndex%len(travel.cities)].URL) + + for _, rows := range profile.aboutMe { + out += rows + "\n\n" + } + + return out +} + +func renderTips() string { + out := "# Help Me Travel The World\n\n" + + out += ufmt.Sprintf("## I am currently in %s, tip the jar to send me somewhere else!\n\n", travel.cities[travel.currentCityIndex].Name) + out += "### **Click** the jar, **tip** in GNOT coins, and **watch** my background change as I head to a new adventure!\n\n" + + out += renderTipsJar() + "\n\n" + + out += renderSponsors() + + return out +} + +func formatAddress(address string) string { + if len(address) <= 8 { + return address + } + return address[:4] + "..." + address[len(address)-4:] +} + +func getDisplayName(addr std.Address) string { + if user := users.GetUserByAddress(addr); user != nil { + return user.Name + } + return formatAddress(addr.String()) +} + +func formatAmount(amount std.Coins) string { + ugnot := amount.AmountOf("ugnot") + if ugnot >= 1000000 { + gnot := float64(ugnot) / 1000000 + return ufmt.Sprintf("`%v`*GNOT*", gnot) + } + return ufmt.Sprintf("`%d`*ugnot*", ugnot) +} + +func renderSponsors() string { + out := "## Sponsor Leaderboard\n\n" + + if sponsorship.sponsorsCount == 0 { + return out + "No sponsors yet. Be the first to tip the jar!\n" + } + + topSponsors := GetTopSponsors() + numSponsors := len(topSponsors) + if numSponsors > sponsorship.maxSponsors { + numSponsors = sponsorship.maxSponsors + } + + for i := 0; i < numSponsors; i++ { + sponsor := topSponsors[i] + position := "" + switch i { + case 0: + position = "🥇" + case 1: + position = "🥈" + case 2: + position = "🥉" + default: + position = ufmt.Sprintf("%d.", i+1) + } + + out += ufmt.Sprintf("%s **%s** - %s\n\n", + position, + getDisplayName(sponsor.Address), + formatAmount(sponsor.Amount), + ) + } + + return out + "\n" +} + +func renderTipsJar() string { + return ufmt.Sprintf("[![Tips Jar](https://i.ibb.co/4TH9zbw/tips-jar.png)](%s)", travel.jarLink) +} diff --git a/examples/gno.land/r/stefann/home/home_test.gno b/examples/gno.land/r/stefann/home/home_test.gno new file mode 100644 index 00000000000..b8ea88670a6 --- /dev/null +++ b/examples/gno.land/r/stefann/home/home_test.gno @@ -0,0 +1,278 @@ +package home + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/testutils" +) + +func TestUpdateAboutMe(t *testing.T) { + var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8") + std.TestSetOrigCaller(owner) + + profile.aboutMe = []string{} + + UpdateAboutMe("This is my new bio.|I love coding!") + + expected := []string{"This is my new bio.", "I love coding!"} + + if len(profile.aboutMe) != len(expected) { + t.Fatalf("expected aboutMe to have length %d, got %d", len(expected), len(profile.aboutMe)) + } + + for i := range profile.aboutMe { + if profile.aboutMe[i] != expected[i] { + t.Fatalf("expected aboutMe[%d] to be %s, got %s", i, expected[i], profile.aboutMe[i]) + } + } +} + +func TestUpdateCities(t *testing.T) { + var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8") + std.TestSetOrigCaller(owner) + + travel.cities = []City{} + + newCities := []City{ + {Name: "Berlin", URL: "https://example.com/berlin.jpg"}, + {Name: "Vienna", URL: "https://example.com/vienna.jpg"}, + } + + UpdateCities(newCities) + + if len(travel.cities) != 2 { + t.Fatalf("expected 2 cities, got %d", len(travel.cities)) + } + + if travel.cities[0].Name != "Berlin" || travel.cities[1].Name != "Vienna" { + t.Fatalf("expected cities to be updated to Berlin and Vienna, got %+v", travel.cities) + } +} + +func TestUpdateJarLink(t *testing.T) { + var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8") + std.TestSetOrigCaller(owner) + + travel.jarLink = "" + + UpdateJarLink("https://example.com/jar") + + if travel.jarLink != "https://example.com/jar" { + t.Fatalf("expected jarLink to be https://example.com/jar, got %s", travel.jarLink) + } +} + +func TestUpdateMaxSponsors(t *testing.T) { + var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8") + std.TestSetOrigCaller(owner) + + sponsorship.maxSponsors = 0 + + UpdateMaxSponsors(10) + + if sponsorship.maxSponsors != 10 { + t.Fatalf("expected maxSponsors to be 10, got %d", sponsorship.maxSponsors) + } + + defer func() { + if r := recover(); r == nil { + t.Fatalf("expected panic for setting maxSponsors to 0") + } + }() + UpdateMaxSponsors(0) +} + +func TestAddCities(t *testing.T) { + var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8") + std.TestSetOrigCaller(owner) + + travel.cities = []City{} + + AddCities(City{Name: "Berlin", URL: "https://example.com/berlin.jpg"}) + + if len(travel.cities) != 1 { + t.Fatalf("expected 1 city, got %d", len(travel.cities)) + } + if travel.cities[0].Name != "Berlin" || travel.cities[0].URL != "https://example.com/berlin.jpg" { + t.Fatalf("expected city to be Berlin, got %+v", travel.cities[0]) + } + + AddCities( + City{Name: "Paris", URL: "https://example.com/paris.jpg"}, + City{Name: "Tokyo", URL: "https://example.com/tokyo.jpg"}, + ) + + if len(travel.cities) != 3 { + t.Fatalf("expected 3 cities, got %d", len(travel.cities)) + } + if travel.cities[1].Name != "Paris" || travel.cities[2].Name != "Tokyo" { + t.Fatalf("expected cities to be Paris and Tokyo, got %+v", travel.cities[1:]) + } +} + +func TestAddAboutMeRows(t *testing.T) { + var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8") + std.TestSetOrigCaller(owner) + + profile.aboutMe = []string{} + + AddAboutMeRows("I love exploring new technologies!") + + if len(profile.aboutMe) != 1 { + t.Fatalf("expected 1 aboutMe row, got %d", len(profile.aboutMe)) + } + if profile.aboutMe[0] != "I love exploring new technologies!" { + t.Fatalf("expected first aboutMe row to be 'I love exploring new technologies!', got %s", profile.aboutMe[0]) + } + + AddAboutMeRows("Travel is my passion!", "Always learning.") + + if len(profile.aboutMe) != 3 { + t.Fatalf("expected 3 aboutMe rows, got %d", len(profile.aboutMe)) + } + if profile.aboutMe[1] != "Travel is my passion!" || profile.aboutMe[2] != "Always learning." { + t.Fatalf("expected aboutMe rows to be 'Travel is my passion!' and 'Always learning.', got %+v", profile.aboutMe[1:]) + } +} + +func TestDonate(t *testing.T) { + var user = testutils.TestAddress("user") + std.TestSetOrigCaller(user) + + sponsorship.sponsors = avl.NewTree() + sponsorship.DonationsCount = 0 + sponsorship.sponsorsCount = 0 + travel.currentCityIndex = 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 500)) + std.TestSetOrigSend(coinsSent, std.NewCoins()) + Donate() + + existingAmount, exists := sponsorship.sponsors.Get(string(user)) + if !exists { + t.Fatalf("expected sponsor to be added, but it was not found") + } + + if existingAmount.(std.Coins).AmountOf("ugnot") != 500 { + t.Fatalf("expected donation amount to be 500ugnot, got %d", existingAmount.(std.Coins).AmountOf("ugnot")) + } + + if sponsorship.DonationsCount != 1 { + t.Fatalf("expected DonationsCount to be 1, got %d", sponsorship.DonationsCount) + } + + if sponsorship.sponsorsCount != 1 { + t.Fatalf("expected sponsorsCount to be 1, got %d", sponsorship.sponsorsCount) + } + + if travel.currentCityIndex != 1 { + t.Fatalf("expected currentCityIndex to be 1, got %d", travel.currentCityIndex) + } + + coinsSent = std.NewCoins(std.NewCoin("ugnot", 300)) + std.TestSetOrigSend(coinsSent, std.NewCoins()) + Donate() + + existingAmount, exists = sponsorship.sponsors.Get(string(user)) + if !exists { + t.Fatalf("expected sponsor to exist after second donation, but it was not found") + } + + if existingAmount.(std.Coins).AmountOf("ugnot") != 800 { + t.Fatalf("expected total donation amount to be 800ugnot, got %d", existingAmount.(std.Coins).AmountOf("ugnot")) + } + + if sponsorship.DonationsCount != 2 { + t.Fatalf("expected DonationsCount to be 2 after second donation, got %d", sponsorship.DonationsCount) + } + + if travel.currentCityIndex != 2 { + t.Fatalf("expected currentCityIndex to be 2 after second donation, got %d", travel.currentCityIndex) + } +} + +func TestGetTopSponsors(t *testing.T) { + var user = testutils.TestAddress("user") + std.TestSetOrigCaller(user) + + sponsorship.sponsors = avl.NewTree() + sponsorship.sponsorsCount = 0 + + sponsorship.sponsors.Set("g1address1", std.NewCoins(std.NewCoin("ugnot", 300))) + sponsorship.sponsors.Set("g1address2", std.NewCoins(std.NewCoin("ugnot", 500))) + sponsorship.sponsors.Set("g1address3", std.NewCoins(std.NewCoin("ugnot", 200))) + sponsorship.sponsorsCount = 3 + + topSponsors := GetTopSponsors() + + if len(topSponsors) != 3 { + t.Fatalf("expected 3 sponsors, got %d", len(topSponsors)) + } + + if topSponsors[0].Address.String() != "g1address2" || topSponsors[0].Amount.AmountOf("ugnot") != 500 { + t.Fatalf("expected top sponsor to be g1address2 with 500ugnot, got %s with %dugnot", topSponsors[0].Address.String(), topSponsors[0].Amount.AmountOf("ugnot")) + } + + if topSponsors[1].Address.String() != "g1address1" || topSponsors[1].Amount.AmountOf("ugnot") != 300 { + t.Fatalf("expected second sponsor to be g1address1 with 300ugnot, got %s with %dugnot", topSponsors[1].Address.String(), topSponsors[1].Amount.AmountOf("ugnot")) + } + + if topSponsors[2].Address.String() != "g1address3" || topSponsors[2].Amount.AmountOf("ugnot") != 200 { + t.Fatalf("expected third sponsor to be g1address3 with 200ugnot, got %s with %dugnot", topSponsors[2].Address.String(), topSponsors[2].Amount.AmountOf("ugnot")) + } +} + +func TestGetTotalDonations(t *testing.T) { + var user = testutils.TestAddress("user") + std.TestSetOrigCaller(user) + + sponsorship.sponsors = avl.NewTree() + sponsorship.sponsorsCount = 0 + + sponsorship.sponsors.Set("g1address1", std.NewCoins(std.NewCoin("ugnot", 300))) + sponsorship.sponsors.Set("g1address2", std.NewCoins(std.NewCoin("ugnot", 500))) + sponsorship.sponsors.Set("g1address3", std.NewCoins(std.NewCoin("ugnot", 200))) + sponsorship.sponsorsCount = 3 + + totalDonations := GetTotalDonations() + + if totalDonations != 1000 { + t.Fatalf("expected total donations to be 1000ugnot, got %dugnot", totalDonations) + } +} + +func TestRender(t *testing.T) { + travel.currentCityIndex = 0 + travel.cities = []City{ + {Name: "Venice", URL: "https://example.com/venice.jpg"}, + {Name: "Paris", URL: "https://example.com/paris.jpg"}, + } + + output := Render("") + + expectedCity := "Venice" + if !strings.Contains(output, expectedCity) { + t.Fatalf("expected output to contain city name '%s', got %s", expectedCity, output) + } + + expectedURL := "https://example.com/venice.jpg" + if !strings.Contains(output, expectedURL) { + t.Fatalf("expected output to contain city URL '%s', got %s", expectedURL, output) + } + + travel.currentCityIndex = 1 + output = Render("") + + expectedCity = "Paris" + if !strings.Contains(output, expectedCity) { + t.Fatalf("expected output to contain city name '%s', got %s", expectedCity, output) + } + + expectedURL = "https://example.com/paris.jpg" + if !strings.Contains(output, expectedURL) { + t.Fatalf("expected output to contain city URL '%s', got %s", expectedURL, output) + } +} diff --git a/examples/gno.land/r/stefann/registry/gno.mod b/examples/gno.land/r/stefann/registry/gno.mod new file mode 100644 index 00000000000..7ef0c32030f --- /dev/null +++ b/examples/gno.land/r/stefann/registry/gno.mod @@ -0,0 +1 @@ +module gno.land/r/stefann/registry diff --git a/examples/gno.land/r/stefann/registry/registry.gno b/examples/gno.land/r/stefann/registry/registry.gno new file mode 100644 index 00000000000..6f56d105e4b --- /dev/null +++ b/examples/gno.land/r/stefann/registry/registry.gno @@ -0,0 +1,51 @@ +package registry + +import ( + "errors" + "std" + + "gno.land/p/demo/ownable" +) + +var ( + mainAddr std.Address + backupAddr std.Address + owner *ownable.Ownable +) + +func init() { + mainAddr = "g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8" + backupAddr = "g13awn2575t8s2vf3svlprc4dg0e9z5wchejdxk8" + + owner = ownable.NewWithAddress(mainAddr) +} + +func MainAddr() std.Address { + return mainAddr +} + +func BackupAddr() std.Address { + return backupAddr +} + +func SetMainAddr(addr std.Address) error { + if !addr.IsValid() { + return errors.New("config: invalid address") + } + + owner.AssertCallerIsOwner() + + mainAddr = addr + return nil +} + +func SetBackupAddr(addr std.Address) error { + if !addr.IsValid() { + return errors.New("config: invalid address") + } + + owner.AssertCallerIsOwner() + + backupAddr = addr + return nil +} diff --git a/examples/gno.land/r/sys/params/gno.mod b/examples/gno.land/r/sys/params/gno.mod new file mode 100644 index 00000000000..c633412ced7 --- /dev/null +++ b/examples/gno.land/r/sys/params/gno.mod @@ -0,0 +1 @@ +module gno.land/r/sys/params diff --git a/examples/gno.land/r/sys/params/params.gno b/examples/gno.land/r/sys/params/params.gno new file mode 100644 index 00000000000..fa04c90de3f --- /dev/null +++ b/examples/gno.land/r/sys/params/params.gno @@ -0,0 +1,54 @@ +// Package params provides functions for creating parameter executors that +// interface with the Params Keeper. +// +// This package enables setting various parameter types (such as strings, +// integers, booleans, and byte slices) through the GovDAO proposal mechanism. +// Each function returns an executor that, when called, sets the specified +// parameter in the Params Keeper. +// +// The executors are designed to be used within governance proposals to modify +// parameters dynamically. The integration with the GovDAO allows for parameter +// changes to be proposed and executed in a controlled manner, ensuring that +// modifications are subject to governance processes. +// +// Example usage: +// +// executor := params.NewStringPropExecutor("exampleKey", "exampleValue") +// // This executor can be used in a governance proposal to set the parameter. +package params + +import ( + "std" + + "gno.land/p/demo/dao" + "gno.land/r/gov/dao/bridge" +) + +func NewStringPropExecutor(key string, value string) dao.Executor { + return newPropExecutor(key, func() { std.SetParamString(key, value) }) +} + +func NewInt64PropExecutor(key string, value int64) dao.Executor { + return newPropExecutor(key, func() { std.SetParamInt64(key, value) }) +} + +func NewUint64PropExecutor(key string, value uint64) dao.Executor { + return newPropExecutor(key, func() { std.SetParamUint64(key, value) }) +} + +func NewBoolPropExecutor(key string, value bool) dao.Executor { + return newPropExecutor(key, func() { std.SetParamBool(key, value) }) +} + +func NewBytesPropExecutor(key string, value []byte) dao.Executor { + return newPropExecutor(key, func() { std.SetParamBytes(key, value) }) +} + +func newPropExecutor(key string, fn func()) dao.Executor { + callback := func() error { + fn() + std.Emit("set", "k", key) + return nil + } + return bridge.GovDAO().NewGovDAOExecutor(callback) +} diff --git a/examples/gno.land/r/sys/params/params_test.gno b/examples/gno.land/r/sys/params/params_test.gno new file mode 100644 index 00000000000..eaa1ad039d3 --- /dev/null +++ b/examples/gno.land/r/sys/params/params_test.gno @@ -0,0 +1,15 @@ +package params + +import "testing" + +// Testing this package is limited because it only contains an `std.Set` method +// without a corresponding `std.Get` method. For comprehensive testing, refer to +// the tests located in the r/gov/dao/ directory, specifically in one of the +// propX_filetest.gno files. + +func TestNewStringPropExecutor(t *testing.T) { + executor := NewStringPropExecutor("foo", "bar") + if executor == nil { + t.Errorf("executor shouldn't be nil") + } +} diff --git a/examples/gno.land/r/sys/users/gno.mod b/examples/gno.land/r/sys/users/gno.mod index 774a364a272..e5e84a49faf 100644 --- a/examples/gno.land/r/sys/users/gno.mod +++ b/examples/gno.land/r/sys/users/gno.mod @@ -1,6 +1 @@ module gno.land/r/sys/users - -require ( - gno.land/p/demo/ownable v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/sys/users/verify.gno b/examples/gno.land/r/sys/users/verify.gno index 852626622e4..71869fda1a1 100644 --- a/examples/gno.land/r/sys/users/verify.gno +++ b/examples/gno.land/r/sys/users/verify.gno @@ -7,7 +7,7 @@ import ( "gno.land/r/demo/users" ) -const admin = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @moul +const admin = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul type VerifyNameFunc func(enabled bool, address std.Address, name string) bool @@ -48,8 +48,8 @@ func VerifyNameByUser(enable bool, address std.Address, name string) bool { // Enable this package. func AdminEnable() { - if err := owner.CallerIsOwner(); err != nil { - panic(err) + if !owner.CallerIsOwner() { + panic(ownable.ErrUnauthorized) } enabled = true @@ -57,8 +57,8 @@ func AdminEnable() { // Disable this package. func AdminDisable() { - if err := owner.CallerIsOwner(); err != nil { - panic(err) + if !owner.CallerIsOwner() { + panic(ownable.ErrUnauthorized) } enabled = false @@ -66,8 +66,8 @@ func AdminDisable() { // AdminUpdateVerifyCall updates the method that verifies the namespace. func AdminUpdateVerifyCall(check VerifyNameFunc) { - if err := owner.CallerIsOwner(); err != nil { - panic(err) + if !owner.CallerIsOwner() { + panic(ownable.ErrUnauthorized) } checkFunc = check @@ -75,8 +75,8 @@ func AdminUpdateVerifyCall(check VerifyNameFunc) { // AdminTransferOwnership transfers the ownership to a new owner. func AdminTransferOwnership(newOwner std.Address) error { - if err := owner.CallerIsOwner(); err != nil { - panic(err) + if !owner.CallerIsOwner() { + panic(ownable.ErrUnauthorized) } return owner.TransferOwnership(newOwner) diff --git a/examples/gno.land/r/sys/validators/gno.mod b/examples/gno.land/r/sys/validators/gno.mod deleted file mode 100644 index d9d129dd543..00000000000 --- a/examples/gno.land/r/sys/validators/gno.mod +++ /dev/null @@ -1,12 +0,0 @@ -module gno.land/r/sys/validators - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/p/demo/testutils v0.0.0-latest - gno.land/p/demo/uassert v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/gov/proposal v0.0.0-latest - gno.land/p/nt/poa v0.0.0-latest - gno.land/p/sys/validators v0.0.0-latest -) diff --git a/examples/gno.land/r/sys/validators/poc.gno b/examples/gno.land/r/sys/validators/poc.gno deleted file mode 100644 index e088b3b4293..00000000000 --- a/examples/gno.land/r/sys/validators/poc.gno +++ /dev/null @@ -1,66 +0,0 @@ -package validators - -import ( - "std" - - "gno.land/p/gov/proposal" - "gno.land/p/sys/validators" -) - -const daoPkgPath = "gno.land/r/gov/dao" - -const ( - errNoChangesProposed = "no set changes proposed" - errNotGovDAO = "caller not govdao executor" -) - -// NewPropExecutor creates a new executor that wraps a changes closure -// proposal. This wrapper is required to ensure the GovDAO Realm actually -// executed the callback. -// -// Concept adapted from: -// https://github.com/gnolang/gno/pull/1945 -func NewPropExecutor(changesFn func() []validators.Validator) proposal.Executor { - if changesFn == nil { - panic(errNoChangesProposed) - } - - callback := func() error { - // Make sure the GovDAO executor runs the valset changes - assertGovDAOCaller() - - for _, change := range changesFn() { - if change.VotingPower == 0 { - // This change request is to remove the validator - removeValidator(change.Address) - - continue - } - - // This change request is to add the validator - addValidator(change) - } - - return nil - } - - return proposal.NewExecutor(callback) -} - -// assertGovDAOCaller verifies the caller is the GovDAO executor -func assertGovDAOCaller() { - if std.PrevRealm().PkgPath() != daoPkgPath { - panic(errNotGovDAO) - } -} - -// IsValidator returns a flag indicating if the given bech32 address -// is part of the validator set -func IsValidator(addr std.Address) bool { - return vp.IsValidator(addr) -} - -// GetValidators returns the typed validator set -func GetValidators() []validators.Validator { - return vp.GetValidators() -} diff --git a/examples/gno.land/r/sys/validators/doc.gno b/examples/gno.land/r/sys/validators/v2/doc.gno similarity index 100% rename from examples/gno.land/r/sys/validators/doc.gno rename to examples/gno.land/r/sys/validators/v2/doc.gno diff --git a/examples/gno.land/r/sys/validators/v2/gno.mod b/examples/gno.land/r/sys/validators/v2/gno.mod new file mode 100644 index 00000000000..beae6e95d34 --- /dev/null +++ b/examples/gno.land/r/sys/validators/v2/gno.mod @@ -0,0 +1 @@ +module gno.land/r/sys/validators/v2 diff --git a/examples/gno.land/r/sys/validators/gnosdk.gno b/examples/gno.land/r/sys/validators/v2/gnosdk.gno similarity index 100% rename from examples/gno.land/r/sys/validators/gnosdk.gno rename to examples/gno.land/r/sys/validators/v2/gnosdk.gno diff --git a/examples/gno.land/r/sys/validators/init.gno b/examples/gno.land/r/sys/validators/v2/init.gno similarity index 100% rename from examples/gno.land/r/sys/validators/init.gno rename to examples/gno.land/r/sys/validators/v2/init.gno diff --git a/examples/gno.land/r/sys/validators/v2/poc.gno b/examples/gno.land/r/sys/validators/v2/poc.gno new file mode 100644 index 00000000000..760edc39d1e --- /dev/null +++ b/examples/gno.land/r/sys/validators/v2/poc.gno @@ -0,0 +1,52 @@ +package validators + +import ( + "std" + + "gno.land/p/demo/dao" + "gno.land/p/sys/validators" + "gno.land/r/gov/dao/bridge" +) + +const errNoChangesProposed = "no set changes proposed" + +// NewPropExecutor creates a new executor that wraps a changes closure +// proposal. This wrapper is required to ensure the GovDAO Realm actually +// executed the callback. +// +// Concept adapted from: +// https://github.com/gnolang/gno/pull/1945 +func NewPropExecutor(changesFn func() []validators.Validator) dao.Executor { + if changesFn == nil { + panic(errNoChangesProposed) + } + + callback := func() error { + for _, change := range changesFn() { + if change.VotingPower == 0 { + // This change request is to remove the validator + removeValidator(change.Address) + + continue + } + + // This change request is to add the validator + addValidator(change) + } + + return nil + } + + return bridge.GovDAO().NewGovDAOExecutor(callback) +} + +// IsValidator returns a flag indicating if the given bech32 address +// is part of the validator set +func IsValidator(addr std.Address) bool { + return vp.IsValidator(addr) +} + +// GetValidators returns the typed validator set +func GetValidators() []validators.Validator { + return vp.GetValidators() +} diff --git a/examples/gno.land/r/sys/validators/validators.gno b/examples/gno.land/r/sys/validators/v2/validators.gno similarity index 100% rename from examples/gno.land/r/sys/validators/validators.gno rename to examples/gno.land/r/sys/validators/v2/validators.gno diff --git a/examples/gno.land/r/sys/validators/validators_test.gno b/examples/gno.land/r/sys/validators/v2/validators_test.gno similarity index 100% rename from examples/gno.land/r/sys/validators/validators_test.gno rename to examples/gno.land/r/sys/validators/v2/validators_test.gno diff --git a/examples/gno.land/r/ursulovic/home/gno.mod b/examples/gno.land/r/ursulovic/home/gno.mod new file mode 100644 index 00000000000..78163ab2bb5 --- /dev/null +++ b/examples/gno.land/r/ursulovic/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/ursulovic/home diff --git a/examples/gno.land/r/ursulovic/home/home.gno b/examples/gno.land/r/ursulovic/home/home.gno new file mode 100644 index 00000000000..c03d8a66868 --- /dev/null +++ b/examples/gno.land/r/ursulovic/home/home.gno @@ -0,0 +1,159 @@ +package home + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/ownable" + "gno.land/p/moul/md" + "gno.land/r/leon/hof" + + "gno.land/r/ursulovic/registry" +) + +var ( + aboutMe string + selectedImage string + Ownable *ownable.Ownable + + githubUrl string + linkedinUrl string + connectUrl string + imageUpdatePrice int64 + + isValidUrl func(string) bool +) + +func init() { + Ownable = ownable.NewWithAddress(registry.MainAddress()) + + aboutMe = "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." + selectedImage = "https://i.ibb.co/W28NPkw/beograd.webp" + + githubUrl = "https://github.com/ursulovic" + linkedinUrl = "https://www.linkedin.com/in/ivan-ursulovic-953310190/" + imageUpdatePrice = 5000000 + isValidUrl = defaultURLValidation + hof.Register() +} + +func Render(s string) string { + var sb strings.Builder + sb.WriteString(renderAboutMe()) + sb.WriteString(renderSelectedImage()) + sb.WriteString(renderContactsUrl()) + return sb.String() +} + +func defaultURLValidation(url string) bool { + const urlPrefix string = "https://i.ibb.co/" + + if !strings.HasPrefix(url, urlPrefix) { + return false + } + + if !(strings.HasSuffix(url, ".jpg") || + strings.HasSuffix(url, ".png") || + strings.HasSuffix(url, ".gif") || + strings.HasSuffix(url, ".webp")) { + return false + } + + urlPath := strings.TrimPrefix(url, "https://i.ibb.co/") + parts := strings.Split(urlPath, "/") + + if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { + return false + } + + return true +} + +func UpdateSelectedImage(url string) { + if !isValidUrl(url) { + panic("Url is not valid!") + } + + sentCoins := std.GetOrigSend() + + if len(sentCoins) != 1 && sentCoins.AmountOf("ugnot") == imageUpdatePrice { + panic("Please send exactly " + strconv.Itoa(int(imageUpdatePrice)) + " ugnot") + } + + selectedImage = url +} + +func renderSelectedImage() string { + var sb strings.Builder + + sb.WriteString(md.HorizontalRule()) + sb.WriteString("\n") + + sb.WriteString(md.H2("📸 Featured Image")) + sb.WriteString("\n") + + sb.WriteString(md.Image("", selectedImage)) + sb.WriteString("\n") + + sb.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") + " ✨")) + + return sb.String() +} + +func renderAboutMe() string { + var sb strings.Builder + + sb.WriteString(md.H1("👋 Welcome to Ivan's Homepage!")) + sb.WriteString("\n") + + sb.WriteString(md.H2("👨‍💻 About Me")) + sb.WriteString("\n") + + sb.WriteString(md.Blockquote(aboutMe)) + + return sb.String() +} + +func renderContactsUrl() string { + var sb strings.Builder + + sb.WriteString(md.HorizontalRule()) + sb.WriteString("\n") + + sb.WriteString(md.H2("🔗 Let's Connect")) + sb.WriteString("\n") + + items := []string{ + "🐙 " + md.Link("GitHub", githubUrl), + "💼 " + md.Link("LinkedIn", linkedinUrl), + } + sb.WriteString(md.BulletList(items)) + + return sb.String() +} + +func UpdateGithubUrl(url string) { + Ownable.AssertCallerIsOwner() + githubUrl = url +} + +func UpdateLinkedinUrl(url string) { + Ownable.AssertCallerIsOwner() + linkedinUrl = url +} + +func UpdateAboutMe(text string) { + Ownable.AssertCallerIsOwner() + aboutMe = text +} + +func UpdateImagePrice(newPrice int64) { + Ownable.AssertCallerIsOwner() + imageUpdatePrice = newPrice +} + +func UpdateIsValidUrlFunction(f func(string) bool) { + Ownable.AssertCallerIsOwner() + isValidUrl = f +} diff --git a/examples/gno.land/r/ursulovic/home/home_test.gno b/examples/gno.land/r/ursulovic/home/home_test.gno new file mode 100644 index 00000000000..ff3f763d62a --- /dev/null +++ b/examples/gno.land/r/ursulovic/home/home_test.gno @@ -0,0 +1,97 @@ +package home + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" +) + +func TestUpdateGithubUrl(t *testing.T) { + caller := std.Address("g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x") + std.TestSetOrigCaller(caller) + + newUrl := "https://github.com/example" + + UpdateGithubUrl(newUrl) + + if githubUrl != newUrl { + t.Fatalf("GitHub url not updated properly!") + } +} + +func TestUpdateLinkedinUrl(t *testing.T) { + caller := std.Address("g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x") + std.TestSetOrigCaller(caller) + + newUrl := "https://www.linkedin.com/in/example" + + UpdateGithubUrl(newUrl) + + if githubUrl != newUrl { + t.Fatalf("LinkedIn url not updated properly!") + } +} + +func TestUpdateAboutMe(t *testing.T) { + caller := std.Address("g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x") + std.TestSetOrigCaller(caller) + + newAboutMe := "This is new description!" + + UpdateAboutMe(newAboutMe) + + if aboutMe != newAboutMe { + t.Fatalf("About mew not updated properly!") + } +} + +func TestUpdateSelectedImage(t *testing.T) { + var user = testutils.TestAddress("user") + std.TestSetOrigCaller(user) + + validImageUrl := "https://i.ibb.co/hLtmnX0/beautiful-rain-forest-ang-ka-nature-trail-doi-inthanon-national-park-thailand-36703721.webp" + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 5000000)) // Update to match the price expected by your function + std.TestSetOrigSend(coinsSent, std.NewCoins()) + + UpdateSelectedImage(validImageUrl) + + if selectedImage != validImageUrl { + t.Fatalf("Valid image URL rejected!") + } + + invalidImageUrl := "https://ibb.co/Kb3rQNn" + + defer func() { + if r := recover(); r == nil { + t.Fatalf("Expected panic for invalid image URL, but got no panic") + } + }() + + UpdateSelectedImage(invalidImageUrl) + + invalidCoins := std.NewCoins(std.NewCoin("ugnot", 1000000)) + std.TestSetOrigSend(invalidCoins, std.NewCoins()) + + defer func() { + if r := recover(); r == nil { + t.Fatalf("Expected panic for incorrect coin denomination or amount, but got no panic") + } + }() + + UpdateSelectedImage(validImageUrl) +} + +func TestUpdateImagePrice(t *testing.T) { + caller := std.Address("g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x") + std.TestSetOrigCaller(caller) + + var newImageUpdatePrice int64 = 3000000 + + UpdateImagePrice(newImageUpdatePrice) + + if imageUpdatePrice != newImageUpdatePrice { + t.Fatalf("Image update price not updated properly!") + } +} diff --git a/examples/gno.land/r/ursulovic/registry/gno.mod b/examples/gno.land/r/ursulovic/registry/gno.mod new file mode 100644 index 00000000000..ee1f5d38780 --- /dev/null +++ b/examples/gno.land/r/ursulovic/registry/gno.mod @@ -0,0 +1 @@ +module gno.land/r/ursulovic/registry diff --git a/examples/gno.land/r/ursulovic/registry/registry.gno b/examples/gno.land/r/ursulovic/registry/registry.gno new file mode 100644 index 00000000000..0bbd6c80df5 --- /dev/null +++ b/examples/gno.land/r/ursulovic/registry/registry.gno @@ -0,0 +1,59 @@ +package registry + +import ( + "errors" + "std" +) + +var ( + mainAddress std.Address + backupAddress std.Address + + ErrInvalidAddr = errors.New("Ivan's registry: Invalid address") + ErrUnauthorized = errors.New("Ivan's registry: Unauthorized") +) + +func init() { + mainAddress = "g1d24j8fwnc0w5q427fauyey4gdd30qgu69k6n0x" + backupAddress = "g1mw2xft3eava9kfhqw3fjj3kkf3pkammty0mtv7" +} + +func MainAddress() std.Address { + return mainAddress +} + +func BackupAddress() std.Address { + return backupAddress +} + +func SetMainAddress(addr std.Address) error { + assertAuthorized() + + if !addr.IsValid() { + return ErrInvalidAddr + } + + mainAddress = addr + return nil +} + +func SetBackupAddress(addr std.Address) error { + assertAuthorized() + + if !addr.IsValid() { + return ErrInvalidAddr + } + + backupAddress = addr + return nil +} + +// It will stay here for now, might be useful later +func assertAuthorized() { + caller := std.PrevRealm().Addr() + isAuthorized := caller == mainAddress || caller == backupAddress + + if !isAuthorized { + panic(ErrUnauthorized) + } +} diff --git a/examples/gno.land/r/x/benchmark/storage/boards.gno b/examples/gno.land/r/x/benchmark/storage/boards.gno new file mode 100644 index 00000000000..adb3d2d709c --- /dev/null +++ b/examples/gno.land/r/x/benchmark/storage/boards.gno @@ -0,0 +1,97 @@ +package storage + +import ( + "strconv" + + "gno.land/p/demo/avl" +) + +var boards avl.Tree + +type Board interface { + AddPost(title, content string) + GetPost(id int) (Post, bool) + Size() int +} + +// posts are persisted in an avl tree +type TreeBoard struct { + id int + posts *avl.Tree +} + +func (b *TreeBoard) AddPost(title, content string) { + n := b.posts.Size() + p := Post{n, title, content} + b.posts.Set(strconv.Itoa(n), p) +} + +func (b *TreeBoard) GetPost(id int) (Post, bool) { + p, ok := b.posts.Get(strconv.Itoa(id)) + if ok { + return p.(Post), ok + } else { + return Post{}, ok + } +} + +func (b *TreeBoard) Size() int { + return b.posts.Size() +} + +// posts are persisted in a map +type MapBoard struct { + id int + posts map[int]Post +} + +func (b *MapBoard) AddPost(title, content string) { + n := len(b.posts) + p := Post{n, title, content} + b.posts[n] = p +} + +func (b *MapBoard) GetPost(id int) (Post, bool) { + p, ok := b.posts[id] + if ok { + return p, ok + } else { + return Post{}, ok + } +} + +func (b *MapBoard) Size() int { + return len(b.posts) +} + +// posts are persisted in a slice +type SliceBoard struct { + id int + posts []Post +} + +func (b *SliceBoard) AddPost(title, content string) { + n := len(b.posts) + p := Post{n, title, content} + b.posts = append(b.posts, p) +} + +func (b *SliceBoard) GetPost(id int) (Post, bool) { + if id < len(b.posts) { + p := b.posts[id] + + return p, true + } else { + return Post{}, false + } +} + +func (b *SliceBoard) Size() int { + return len(b.posts) +} + +type Post struct { + id int + title string + content string +} diff --git a/examples/gno.land/r/x/benchmark/storage/forum.gno b/examples/gno.land/r/x/benchmark/storage/forum.gno new file mode 100644 index 00000000000..8f1b3734de6 --- /dev/null +++ b/examples/gno.land/r/x/benchmark/storage/forum.gno @@ -0,0 +1,64 @@ +package storage + +import ( + "strconv" + + "gno.land/p/demo/avl" +) + +func init() { + // we write to three common data structure for persistence + // avl.Tree, map and slice. + posts0 := avl.NewTree() + b0 := &TreeBoard{0, posts0} + boards.Set(strconv.Itoa(0), b0) + + posts1 := make(map[int]Post) + b1 := &MapBoard{1, posts1} + boards.Set(strconv.Itoa(1), b1) + + posts2 := []Post{} + b2 := &SliceBoard{2, posts2} + boards.Set(strconv.Itoa(2), b2) +} + +// post to all boards. +func AddPost(title, content string) { + for i := 0; i < boards.Size(); i++ { + boardId := strconv.Itoa(i) + b, ok := boards.Get(boardId) + if ok { + b.(Board).AddPost(title, content) + } + } +} + +func GetPost(boardId, postId int) string { + b, ok := boards.Get(strconv.Itoa(boardId)) + var res string + + if ok { + p, ok := b.(Board).GetPost(postId) + if ok { + res = p.title + "," + p.content + } + } + return res +} + +func GetPostSize(boardId int) int { + b, ok := boards.Get(strconv.Itoa(boardId)) + var res int + + if ok { + res = b.(Board).Size() + } else { + res = -1 + } + + return res +} + +func GetBoardSize() int { + return boards.Size() +} diff --git a/examples/gno.land/r/x/benchmark/storage/gno.mod b/examples/gno.land/r/x/benchmark/storage/gno.mod new file mode 100644 index 00000000000..04bea3012f3 --- /dev/null +++ b/examples/gno.land/r/x/benchmark/storage/gno.mod @@ -0,0 +1 @@ +module gno.land/r/x/benchmark/storage diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/example.gno b/examples/gno.land/r/x/jeronimo_render_proxy/example.gno new file mode 100644 index 00000000000..7b3da098232 --- /dev/null +++ b/examples/gno.land/r/x/jeronimo_render_proxy/example.gno @@ -0,0 +1,20 @@ +package example + +func Render(string) string { + return `# Render Proxy + +This example shows how proxying render calls can be used to allow updating realms to new +versions while keeping the same realm path. The idea is to have a simple "parent" realm +that only keeps track of the latest realm version and forwards all render calls to it. + +By only focusing on the 'Render()' function the proxy realm keeps its public functions +stable allowing each version to update their public functions and exposed types without +needing to also update the proxy realm. + +Any interaction or transaction must be sent to the latest, or target, realm version while +render calls are sent to the proxy realm. + +Each realm version registers itself on deployment as the latest available version, and +its allowed to do so because each versioned realm path shares the proxy realm path. +` +} diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/gno.mod b/examples/gno.land/r/x/jeronimo_render_proxy/gno.mod new file mode 100644 index 00000000000..9236b28f5ad --- /dev/null +++ b/examples/gno.land/r/x/jeronimo_render_proxy/gno.mod @@ -0,0 +1 @@ +module gno.land/r/x/jeronimo_render_proxy diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/home.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/home.gno new file mode 100644 index 00000000000..c73e99cc583 --- /dev/null +++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/home.gno @@ -0,0 +1,52 @@ +package home + +import ( + "std" + "strings" +) + +// RenderFn defines the type for the render function of realms. +type RenderFn func(string) string + +var current = struct { + realmPath string + renderFn RenderFn +}{} + +// CurrentRealmPath returns the path of the realm that is currently registered. +func CurrentRealmPath() string { + return current.realmPath +} + +// Register registers a render function of a realm. +func Register(fn RenderFn) { + if fn == nil { + panic("render function must not be nil") + } + + proxyPath := std.CurrentRealm().PkgPath() + callerPath := std.PrevRealm().PkgPath() + if !strings.HasPrefix(callerPath, proxyPath+"/") { + panic("caller realm path must start with " + proxyPath) + } + + current.renderFn = fn + current.realmPath = callerPath +} + +// URL returns a URL that links to the proxy realm. +func URL(renderPath string) string { + url := "http://" + std.CurrentRealm().PkgPath() + if renderPath != "" { + url += ":" + renderPath + } + return url +} + +// Render renders the rendered Markdown of the realm that is currently registered. +func Render(path string) string { + if current.renderFn == nil { + panic("no realm has been registered") + } + return current.renderFn(path) +} diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/v1.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/v1.gno new file mode 100644 index 00000000000..8698998577c --- /dev/null +++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/v1.gno @@ -0,0 +1,16 @@ +package v1 + +import "gno.land/r/x/jeronimo_render_proxy/home" + +func init() { + // Register the private render function with the render proxy + home.Register(render) +} + +func render(string) string { + return "Rendered by v1" +} + +func Render(string) string { + return "[Home](" + home.URL("") + ")" +} diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/z_filetest.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/z_filetest.gno new file mode 100644 index 00000000000..cebe2aeb5ba --- /dev/null +++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/v1/z_filetest.gno @@ -0,0 +1,10 @@ +package main + +import "gno.land/r/x/jeronimo_render_proxy/home/v1" + +func main() { + println(v1.Render("")) +} + +// Output: +// [Home](http://gno.land/r/x/jeronimo_render_proxy/home) diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/v2.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/v2.gno new file mode 100644 index 00000000000..031f8568441 --- /dev/null +++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/v2.gno @@ -0,0 +1,16 @@ +package v2 + +import "gno.land/r/x/jeronimo_render_proxy/home" + +func init() { + // Register the private render function with the render proxy + home.Register(render) +} + +func render(string) string { + return "Rendered by v2" +} + +func Render(string) string { + return "[Home](" + home.URL("") + ")" +} diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/z_filetest.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/z_filetest.gno new file mode 100644 index 00000000000..feff15533ee --- /dev/null +++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/v2/z_filetest.gno @@ -0,0 +1,10 @@ +package main + +import "gno.land/r/x/jeronimo_render_proxy/home/v2" + +func main() { + println(v2.Render("")) +} + +// Output: +// [Home](http://gno.land/r/x/jeronimo_render_proxy/home) diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/z_a_filetest.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_a_filetest.gno new file mode 100644 index 00000000000..c7d4d7febd2 --- /dev/null +++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_a_filetest.gno @@ -0,0 +1,10 @@ +package main + +import "gno.land/r/x/jeronimo_render_proxy/home" + +func main() { + home.Render("") +} + +// Error: +// no realm has been registered diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/z_b_filetest.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_b_filetest.gno new file mode 100644 index 00000000000..6ebdace67b4 --- /dev/null +++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_b_filetest.gno @@ -0,0 +1,15 @@ +package main + +import ( + "gno.land/r/x/jeronimo_render_proxy/home" + _ "gno.land/r/x/jeronimo_render_proxy/home/v1" +) + +func main() { + println(home.CurrentRealmPath()) + println(home.Render("")) +} + +// Output: +// gno.land/r/x/jeronimo_render_proxy/home/v1 +// Rendered by v1 diff --git a/examples/gno.land/r/x/jeronimo_render_proxy/home/z_c_filetest.gno b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_c_filetest.gno new file mode 100644 index 00000000000..f85b13bc5dd --- /dev/null +++ b/examples/gno.land/r/x/jeronimo_render_proxy/home/z_c_filetest.gno @@ -0,0 +1,16 @@ +package main + +import ( + "gno.land/r/x/jeronimo_render_proxy/home" + _ "gno.land/r/x/jeronimo_render_proxy/home/v1" + _ "gno.land/r/x/jeronimo_render_proxy/home/v2" +) + +func main() { + println(home.CurrentRealmPath()) + println(home.Render("")) +} + +// Output: +// gno.land/r/x/jeronimo_render_proxy/home/v2 +// Rendered by v2 diff --git a/examples/gno.land/r/x/manfred_outfmt/gno.mod b/examples/gno.land/r/x/manfred_outfmt/gno.mod index 7044f0f72b3..e8165d847c9 100644 --- a/examples/gno.land/r/x/manfred_outfmt/gno.mod +++ b/examples/gno.land/r/x/manfred_outfmt/gno.mod @@ -1,5 +1,3 @@ // Draft module gno.land/r/x/manfred_outfmt - -require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/gno.land/Makefile b/gno.land/Makefile index 7b2afd5779f..075560f44a9 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -47,6 +47,12 @@ install.gnoland:; go install ./cmd/gnoland install.gnoweb:; go install ./cmd/gnoweb install.gnokey:; go install ./cmd/gnokey +.PHONY: dev.gnoweb generate.gnoweb +dev.gnoweb: + make -C ./pkg/gnoweb dev +generate.gnoweb: + make -C ./pkg/gnoweb generate + .PHONY: fclean fclean: clean rm -rf gnoland-data genesis.json diff --git a/gno.land/README.md b/gno.land/README.md index 7da2a8574de..8f7f9c32945 100644 --- a/gno.land/README.md +++ b/gno.land/README.md @@ -1,6 +1,6 @@ -# Gno.land +# gno.land -Gno.land is a layer-1 blockchain that integrates various cutting-edge technologies, including [Tendermint2](../tm2), [GnoVM](../gnovm), Proof-of-Contributions consensus mechanism, on-chain governance through a new DAO framework with support for sub-DAOs, and a unique licensing model that allows open-source code to be monetized by default. +gno.land is a layer-1 blockchain that integrates various cutting-edge technologies, including [Tendermint2](../tm2), [GnoVM](../gnovm), Proof-of-Contributions consensus mechanism, on-chain governance through a new DAO framework with support for sub-DAOs, and a unique licensing model that allows open-source code to be monetized by default. ## Getting started @@ -12,7 +12,7 @@ To add a web interface and faucet to your localnet, use [`gnoweb`](./cmd/gnoweb) ## Interchain -Gno.land aims to offer security, high-quality contract libraries, and scalability to other Gnolang chains, while also prioritizing interoperability with existing and emerging chains. +gno.land aims to offer security, high-quality contract libraries, and scalability to other Gnolang chains, while also prioritizing interoperability with existing and emerging chains. Post mainnet launch, gno.land aims to integrate IBCv1 to connect with existing Cosmos chains and implement ICS1 for security through the existing chains. Afterwards, the platform plans to improve IBC by adding new capabilities for interchain smart-contracts. diff --git a/gno.land/cmd/gnoland/config_get_test.go b/gno.land/cmd/gnoland/config_get_test.go index f2ddc5ca6d0..84cf0ba3d37 100644 --- a/gno.land/cmd/gnoland/config_get_test.go +++ b/gno.land/cmd/gnoland/config_get_test.go @@ -289,14 +289,6 @@ func TestConfig_Get_Base(t *testing.T) { }, true, }, - { - "filter peers flag fetched", - "filter_peers", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.FilterPeers, unmarshalJSONCommon[bool](t, value)) - }, - false, - }, } verifyGetTestTableCommon(t, testTable) @@ -616,19 +608,11 @@ func TestConfig_Get_P2P(t *testing.T) { }, true, }, - { - "upnp toggle", - "p2p.upnp", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.UPNP, unmarshalJSONCommon[bool](t, value)) - }, - false, - }, { "max inbound peers", "p2p.max_num_inbound_peers", func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.MaxNumInboundPeers, unmarshalJSONCommon[int](t, value)) + assert.Equal(t, loadedCfg.P2P.MaxNumInboundPeers, unmarshalJSONCommon[uint64](t, value)) }, false, }, @@ -636,7 +620,7 @@ func TestConfig_Get_P2P(t *testing.T) { "max outbound peers", "p2p.max_num_outbound_peers", func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.MaxNumOutboundPeers, unmarshalJSONCommon[int](t, value)) + assert.Equal(t, loadedCfg.P2P.MaxNumOutboundPeers, unmarshalJSONCommon[uint64](t, value)) }, false, }, @@ -676,15 +660,7 @@ func TestConfig_Get_P2P(t *testing.T) { "pex reactor toggle", "p2p.pex", func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.PexReactor, unmarshalJSONCommon[bool](t, value)) - }, - false, - }, - { - "seed mode", - "p2p.seed_mode", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.SeedMode, unmarshalJSONCommon[bool](t, value)) + assert.Equal(t, loadedCfg.P2P.PeerExchange, unmarshalJSONCommon[bool](t, value)) }, false, }, @@ -704,30 +680,6 @@ func TestConfig_Get_P2P(t *testing.T) { }, true, }, - { - "allow duplicate IP", - "p2p.allow_duplicate_ip", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.AllowDuplicateIP, unmarshalJSONCommon[bool](t, value)) - }, - false, - }, - { - "handshake timeout", - "p2p.handshake_timeout", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.HandshakeTimeout, unmarshalJSONCommon[time.Duration](t, value)) - }, - false, - }, - { - "dial timeout", - "p2p.dial_timeout", - func(loadedCfg *config.Config, value []byte) { - assert.Equal(t, loadedCfg.P2P.DialTimeout, unmarshalJSONCommon[time.Duration](t, value)) - }, - false, - }, } verifyGetTestTableCommon(t, testTable) diff --git a/gno.land/cmd/gnoland/config_set_test.go b/gno.land/cmd/gnoland/config_set_test.go index cb831f0e502..39880313043 100644 --- a/gno.land/cmd/gnoland/config_set_test.go +++ b/gno.land/cmd/gnoland/config_set_test.go @@ -244,19 +244,6 @@ func TestConfig_Set_Base(t *testing.T) { assert.Equal(t, value, loadedCfg.ProfListenAddress) }, }, - { - "filter peers flag updated", - []string{ - "filter_peers", - "true", - }, - func(loadedCfg *config.Config, value string) { - boolVal, err := strconv.ParseBool(value) - require.NoError(t, err) - - assert.Equal(t, boolVal, loadedCfg.FilterPeers) - }, - }, } verifySetTestTableCommon(t, testTable) @@ -505,19 +492,6 @@ func TestConfig_Set_P2P(t *testing.T) { assert.Equal(t, value, loadedCfg.P2P.PersistentPeers) }, }, - { - "upnp toggle updated", - []string{ - "p2p.upnp", - "false", - }, - func(loadedCfg *config.Config, value string) { - boolVal, err := strconv.ParseBool(value) - require.NoError(t, err) - - assert.Equal(t, boolVal, loadedCfg.P2P.UPNP) - }, - }, { "max inbound peers updated", []string{ @@ -588,20 +562,7 @@ func TestConfig_Set_P2P(t *testing.T) { boolVal, err := strconv.ParseBool(value) require.NoError(t, err) - assert.Equal(t, boolVal, loadedCfg.P2P.PexReactor) - }, - }, - { - "seed mode updated", - []string{ - "p2p.seed_mode", - "false", - }, - func(loadedCfg *config.Config, value string) { - boolVal, err := strconv.ParseBool(value) - require.NoError(t, err) - - assert.Equal(t, boolVal, loadedCfg.P2P.SeedMode) + assert.Equal(t, boolVal, loadedCfg.P2P.PeerExchange) }, }, { @@ -614,39 +575,6 @@ func TestConfig_Set_P2P(t *testing.T) { assert.Equal(t, value, loadedCfg.P2P.PrivatePeerIDs) }, }, - { - "allow duplicate IPs updated", - []string{ - "p2p.allow_duplicate_ip", - "false", - }, - func(loadedCfg *config.Config, value string) { - boolVal, err := strconv.ParseBool(value) - require.NoError(t, err) - - assert.Equal(t, boolVal, loadedCfg.P2P.AllowDuplicateIP) - }, - }, - { - "handshake timeout updated", - []string{ - "p2p.handshake_timeout", - "1s", - }, - func(loadedCfg *config.Config, value string) { - assert.Equal(t, value, loadedCfg.P2P.HandshakeTimeout.String()) - }, - }, - { - "dial timeout updated", - []string{ - "p2p.dial_timeout", - "1s", - }, - func(loadedCfg *config.Config, value string) { - assert.Equal(t, value, loadedCfg.P2P.DialTimeout.String()) - }, - }, } verifySetTestTableCommon(t, testTable) diff --git a/gno.land/cmd/gnoland/genesis.go b/gno.land/cmd/gnoland/genesis.go deleted file mode 100644 index 37c0f8f2926..00000000000 --- a/gno.land/cmd/gnoland/genesis.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "flag" - - "github.com/gnolang/gno/tm2/pkg/commands" -) - -func newGenesisCmd(io commands.IO) *commands.Command { - cmd := commands.NewCommand( - commands.Metadata{ - Name: "genesis", - ShortUsage: "genesis [flags] [...]", - ShortHelp: "gno genesis manipulation suite", - LongHelp: "Gno genesis.json manipulation suite, for managing genesis parameters", - }, - commands.NewEmptyConfig(), - commands.HelpExec, - ) - - cmd.AddSubCommands( - newGenerateCmd(io), - newValidatorCmd(io), - newVerifyCmd(io), - newBalancesCmd(io), - newTxsCmd(io), - ) - - return cmd -} - -// commonCfg is the common -// configuration for genesis commands -// that require a genesis.json -type commonCfg struct { - genesisPath string -} - -func (c *commonCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.genesisPath, - "genesis-path", - "./genesis.json", - "the path to the genesis.json", - ) -} diff --git a/gno.land/cmd/gnoland/genesis_balances.go b/gno.land/cmd/gnoland/genesis_balances.go deleted file mode 100644 index c8cd1c539f5..00000000000 --- a/gno.land/cmd/gnoland/genesis_balances.go +++ /dev/null @@ -1,39 +0,0 @@ -package main - -import ( - "flag" - - "github.com/gnolang/gno/tm2/pkg/commands" -) - -type balancesCfg struct { - commonCfg -} - -// newBalancesCmd creates the genesis balances subcommand -func newBalancesCmd(io commands.IO) *commands.Command { - cfg := &balancesCfg{} - - cmd := commands.NewCommand( - commands.Metadata{ - Name: "balances", - ShortUsage: "balances [flags]", - ShortHelp: "manages genesis.json account balances", - LongHelp: "Manipulates the initial genesis.json account balances (pre-mines)", - }, - cfg, - commands.HelpExec, - ) - - cmd.AddSubCommands( - newBalancesAddCmd(cfg, io), - newBalancesRemoveCmd(cfg, io), - newBalancesExportCmd(cfg, io), - ) - - return cmd -} - -func (c *balancesCfg) RegisterFlags(fs *flag.FlagSet) { - c.commonCfg.RegisterFlags(fs) -} diff --git a/gno.land/cmd/gnoland/genesis_balances_add.go b/gno.land/cmd/gnoland/genesis_balances_add.go deleted file mode 100644 index f9a898715c8..00000000000 --- a/gno.land/cmd/gnoland/genesis_balances_add.go +++ /dev/null @@ -1,298 +0,0 @@ -package main - -import ( - "bufio" - "context" - "errors" - "flag" - "fmt" - "io" - "os" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/sdk/bank" - "github.com/gnolang/gno/tm2/pkg/std" - - _ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" -) - -var ( - errNoBalanceSource = errors.New("at least one balance source must be set") - errBalanceParsingAborted = errors.New("balance parsing aborted") - errInvalidAddress = errors.New("invalid address encountered") -) - -type balancesAddCfg struct { - rootCfg *balancesCfg - - balanceSheet string - singleEntries commands.StringArr - parseExport string -} - -// newBalancesAddCmd creates the genesis balances add subcommand -func newBalancesAddCmd(rootCfg *balancesCfg, io commands.IO) *commands.Command { - cfg := &balancesAddCfg{ - rootCfg: rootCfg, - } - - return commands.NewCommand( - commands.Metadata{ - Name: "add", - ShortUsage: "balances add [flags]", - ShortHelp: "adds balances to the genesis.json", - }, - cfg, - func(ctx context.Context, _ []string) error { - return execBalancesAdd(ctx, cfg, io) - }, - ) -} - -func (c *balancesAddCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.balanceSheet, - "balance-sheet", - "", - "the path to the balance file containing addresses in the format
="+ugnot.Denom, - ) - - fs.Var( - &c.singleEntries, - "single", - "the direct balance addition in the format
="+ugnot.Denom, - ) - - fs.StringVar( - &c.parseExport, - "parse-export", - "", - "the path to the transaction export containing a list of transactions (JSONL)", - ) -} - -func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) error { - // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) - } - - // Validate the source is set correctly - var ( - singleEntriesSet = len(cfg.singleEntries) != 0 - balanceSheetSet = cfg.balanceSheet != "" - txFileSet = cfg.parseExport != "" - ) - - if !singleEntriesSet && !balanceSheetSet && !txFileSet { - return errNoBalanceSource - } - - finalBalances := gnoland.NewBalances() - - // Get the balance sheet from the source - if singleEntriesSet { - balances, err := gnoland.GetBalancesFromEntries(cfg.singleEntries...) - if err != nil { - return fmt.Errorf("unable to get balances from entries, %w", err) - } - - finalBalances.LeftMerge(balances) - } - - if balanceSheetSet { - // Open the balance sheet - file, loadErr := os.Open(cfg.balanceSheet) - if loadErr != nil { - return fmt.Errorf("unable to open balance sheet, %w", loadErr) - } - - balances, err := gnoland.GetBalancesFromSheet(file) - if err != nil { - return fmt.Errorf("unable to get balances from balance sheet, %w", err) - } - - finalBalances.LeftMerge(balances) - } - - if txFileSet { - // Open the transactions file - file, loadErr := os.Open(cfg.parseExport) - if loadErr != nil { - return fmt.Errorf("unable to open transactions file, %w", loadErr) - } - - balances, err := getBalancesFromTransactions(ctx, io, file) - if err != nil { - return fmt.Errorf("unable to get balances from tx file, %w", err) - } - - finalBalances.LeftMerge(balances) - } - - // Initialize genesis app state if it is not initialized already - if genesis.AppState == nil { - genesis.AppState = gnoland.GnoGenesisState{} - } - - // Construct the initial genesis balance sheet - state := genesis.AppState.(gnoland.GnoGenesisState) - genesisBalances, err := mapGenesisBalancesFromState(state) - if err != nil { - return err - } - - // Merge the two balance sheets, with the input - // having precedence over the genesis balances - finalBalances.LeftMerge(genesisBalances) - - // Save the balances - state.Balances = finalBalances.List() - genesis.AppState = state - - // Save the updated genesis - if err := genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil { - return fmt.Errorf("unable to save genesis.json, %w", err) - } - - io.Printfln( - "%d pre-mines saved", - len(finalBalances), - ) - - io.Println() - - for address, balance := range finalBalances { - io.Printfln("%s:%d%s", address.String(), balance, ugnot.Denom) - } - - return nil -} - -// getBalancesFromTransactions constructs a balance map based on MsgSend messages. -// This way of determining the final balance sheet is not valid, since it doesn't take into -// account different message types (ex. MsgCall) that can initialize accounts with some balance values. -// The right way to do this sort of initialization is to spin up an in-memory node -// and execute the entire transaction history to determine touched accounts and final balances, -// and construct a balance sheet based off of this information -func getBalancesFromTransactions( - ctx context.Context, - io commands.IO, - reader io.Reader, -) (gnoland.Balances, error) { - balances := gnoland.NewBalances() - - scanner := bufio.NewScanner(reader) - - for scanner.Scan() { - select { - case <-ctx.Done(): - return nil, errBalanceParsingAborted - default: - // Parse the amino JSON - var tx std.Tx - - line := scanner.Bytes() - - if err := amino.UnmarshalJSON(line, &tx); err != nil { - io.ErrPrintfln( - "invalid amino JSON encountered: %q", - string(line), - ) - - continue - } - - feeAmount := std.NewCoins(tx.Fee.GasFee) - if feeAmount.AmountOf(ugnot.Denom) <= 0 { - io.ErrPrintfln( - "invalid gas fee amount encountered: %q", - tx.Fee.GasFee.String(), - ) - } - - for _, msg := range tx.Msgs { - if msg.Type() != "send" { - continue - } - - msgSend := msg.(bank.MsgSend) - - sendAmount := msgSend.Amount - if sendAmount.AmountOf(ugnot.Denom) <= 0 { - io.ErrPrintfln( - "invalid send amount encountered: %s", - msgSend.Amount.String(), - ) - continue - } - - // This way of determining final account balances is not really valid, - // because we take into account only the ugnot transfer messages (MsgSend) - // and not other message types (like MsgCall), that can also - // initialize accounts with some gnoland. Because of this, - // we can run into a situation where a message send amount or fee - // causes an accounts balance to go < 0. In these cases, - // we initialize the account (it is present in the balance sheet), but - // with the balance of 0 - - from := balances[msgSend.FromAddress].Amount - to := balances[msgSend.ToAddress].Amount - - to = to.Add(sendAmount) - - if from.IsAllLT(sendAmount) || from.IsAllLT(feeAmount) { - // Account cannot cover send amount / fee - // (see message above) - from = std.NewCoins(std.NewCoin(ugnot.Denom, 0)) - } - - if from.IsAllGT(sendAmount) { - from = from.Sub(sendAmount) - } - - if from.IsAllGT(feeAmount) { - from = from.Sub(feeAmount) - } - - // Set new balance - balances[msgSend.FromAddress] = gnoland.Balance{ - Address: msgSend.FromAddress, - Amount: from, - } - balances[msgSend.ToAddress] = gnoland.Balance{ - Address: msgSend.ToAddress, - Amount: to, - } - } - } - } - - // Check for scanning errors - if err := scanner.Err(); err != nil { - return nil, fmt.Errorf( - "error encountered while reading file, %w", - err, - ) - } - - return balances, nil -} - -// mapGenesisBalancesFromState extracts the initial account balances from the -// genesis app state -func mapGenesisBalancesFromState(state gnoland.GnoGenesisState) (gnoland.Balances, error) { - // Construct the initial genesis balance sheet - genesisBalances := gnoland.NewBalances() - - for _, balance := range state.Balances { - genesisBalances[balance.Address] = balance - } - - return genesisBalances, nil -} diff --git a/gno.land/cmd/gnoland/genesis_balances_add_test.go b/gno.land/cmd/gnoland/genesis_balances_add_test.go deleted file mode 100644 index 8f2879f9c57..00000000000 --- a/gno.land/cmd/gnoland/genesis_balances_add_test.go +++ /dev/null @@ -1,581 +0,0 @@ -package main - -import ( - "bytes" - "context" - "fmt" - "strings" - "testing" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/sdk/bank" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenesis_Balances_Add(t *testing.T) { - t.Parallel() - - t.Run("invalid genesis", func(t *testing.T) { - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "add", - "--genesis-path", - "dummy-path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) - }) - - t.Run("no sources selected", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "add", - "--genesis-path", - tempGenesis.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errNoBalanceSource.Error()) - }) - - t.Run("invalid genesis path", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "add", - "--genesis-path", - "dummy-path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) - }) - - t.Run("balances from entries", func(t *testing.T) { - t.Parallel() - - dummyKeys := getDummyKeys(t, 2) - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "add", - "--genesis-path", - tempGenesis.Name(), - } - - amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) - - for _, dummyKey := range dummyKeys { - args = append(args, "--single") - args = append( - args, - fmt.Sprintf( - "%s=%s", - dummyKey.Address().String(), - ugnot.ValueString(amount.AmountOf(ugnot.Denom)), - ), - ) - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the genesis was updated - genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) - require.NoError(t, loadErr) - - require.NotNil(t, genesis.AppState) - - state, ok := genesis.AppState.(gnoland.GnoGenesisState) - require.True(t, ok) - - require.Equal(t, len(dummyKeys), len(state.Balances)) - - for _, balance := range state.Balances { - // Find the appropriate key - // (the genesis is saved with randomized balance order) - found := false - for _, dummyKey := range dummyKeys { - if dummyKey.Address().String() == balance.Address.String() { - assert.Equal(t, amount, balance.Amount) - - found = true - break - } - } - - if !found { - t.Fatalf("unexpected entry with address %s found", balance.Address.String()) - } - } - }) - - t.Run("balances from sheet", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - dummyKeys := getDummyKeys(t, 10) - amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) - - balances := make([]string, len(dummyKeys)) - - // Add a random comment to the balances file output - balances = append(balances, "#comment\n") - - for index, key := range dummyKeys { - balances[index] = fmt.Sprintf( - "%s=%s", - key.Address().String(), - ugnot.ValueString(amount.AmountOf(ugnot.Denom)), - ) - } - - // Write the balance sheet to a file - balanceSheet, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - _, err := balanceSheet.WriteString(strings.Join(balances, "\n")) - require.NoError(t, err) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "add", - "--genesis-path", - tempGenesis.Name(), - "--balance-sheet", - balanceSheet.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the genesis was updated - genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) - require.NoError(t, loadErr) - - require.NotNil(t, genesis.AppState) - - state, ok := genesis.AppState.(gnoland.GnoGenesisState) - require.True(t, ok) - - require.Equal(t, len(dummyKeys), len(state.Balances)) - - for _, balance := range state.Balances { - // Find the appropriate key - // (the genesis is saved with randomized balance order) - found := false - for _, dummyKey := range dummyKeys { - if dummyKey.Address().String() == balance.Address.String() { - assert.Equal(t, amount, balance.Amount) - - found = true - break - } - } - - if !found { - t.Fatalf("unexpected entry with address %s found", balance.Address.String()) - } - } - }) - - t.Run("balances from transactions", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - var ( - dummyKeys = getDummyKeys(t, 10) - amount = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) - amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) - gasFee = std.NewCoin(ugnot.Denom, 1000000) - txs = make([]std.Tx, 0) - ) - - sender := dummyKeys[0] - for _, dummyKey := range dummyKeys[1:] { - tx := std.Tx{ - Msgs: []std.Msg{ - bank.MsgSend{ - FromAddress: sender.Address(), - ToAddress: dummyKey.Address(), - Amount: amountCoins, - }, - }, - Fee: std.Fee{ - GasWanted: 10, - GasFee: gasFee, - }, - Signatures: make([]std.Signature, 0), - } - - txs = append(txs, tx) - } - - // Marshal the transactions into amino JSON - marshalledTxs := make([]string, 0, len(txs)) - - for _, tx := range txs { - marshalledTx, err := amino.MarshalJSON(tx) - require.NoError(t, err) - - marshalledTxs = append(marshalledTxs, string(marshalledTx)) - } - - // Write the transactions to a file - txsFile, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - _, err := txsFile.WriteString(strings.Join(marshalledTxs, "\n")) - require.NoError(t, err) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "add", - "--genesis-path", - tempGenesis.Name(), - "--parse-export", - txsFile.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the genesis was updated - genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) - require.NoError(t, loadErr) - - require.NotNil(t, genesis.AppState) - - state, ok := genesis.AppState.(gnoland.GnoGenesisState) - require.True(t, ok) - - require.Equal(t, len(dummyKeys), len(state.Balances)) - - for _, balance := range state.Balances { - // Find the appropriate key - // (the genesis is saved with randomized balance order) - found := false - for index, dummyKey := range dummyKeys { - checkAmount := amount - if index == 0 { - // the first address should - // have a balance of 0 - checkAmount = std.NewCoins(std.NewCoin(ugnot.Denom, 0)) - } - - if dummyKey.Address().String() == balance.Address.String() { - assert.True(t, balance.Amount.IsEqual(checkAmount)) - - found = true - break - } - } - - if !found { - t.Fatalf("unexpected entry with address %s found", balance.Address.String()) - } - } - }) - - t.Run("balances overwrite", func(t *testing.T) { - t.Parallel() - - dummyKeys := getDummyKeys(t, 10) - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - state := gnoland.GnoGenesisState{ - // Set an initial balance value - Balances: []gnoland.Balance{ - { - Address: dummyKeys[0].Address(), - Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 100)), - }, - }, - } - genesis.AppState = state - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "add", - "--genesis-path", - tempGenesis.Name(), - } - - amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) - - for _, dummyKey := range dummyKeys { - args = append(args, "--single") - args = append( - args, - fmt.Sprintf( - "%s=%s", - dummyKey.Address().String(), - ugnot.ValueString(amount.AmountOf(ugnot.Denom)), - ), - ) - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the genesis was updated - genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) - require.NoError(t, loadErr) - - require.NotNil(t, genesis.AppState) - - state, ok := genesis.AppState.(gnoland.GnoGenesisState) - require.True(t, ok) - - require.Equal(t, len(dummyKeys), len(state.Balances)) - - for _, balance := range state.Balances { - // Find the appropriate key - // (the genesis is saved with randomized balance order) - found := false - for _, dummyKey := range dummyKeys { - if dummyKey.Address().String() == balance.Address.String() { - assert.Equal(t, amount, balance.Amount) - - found = true - break - } - } - - if !found { - t.Fatalf("unexpected entry with address %s found", balance.Address.String()) - } - } - }) -} - -func TestBalances_GetBalancesFromTransactions(t *testing.T) { - t.Parallel() - - t.Run("valid transactions", func(t *testing.T) { - t.Parallel() - - var ( - dummyKeys = getDummyKeys(t, 10) - amount = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) - amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) - gasFee = std.NewCoin(ugnot.Denom, 1000000) - txs = make([]std.Tx, 0) - ) - - sender := dummyKeys[0] - for _, dummyKey := range dummyKeys[1:] { - tx := std.Tx{ - Msgs: []std.Msg{ - bank.MsgSend{ - FromAddress: sender.Address(), - ToAddress: dummyKey.Address(), - Amount: amountCoins, - }, - }, - Fee: std.Fee{ - GasWanted: 10, - GasFee: gasFee, - }, - Signatures: make([]std.Signature, 0), - } - - txs = append(txs, tx) - } - - // Marshal the transactions into amino JSON - marshalledTxs := make([]string, 0, len(txs)) - - for _, tx := range txs { - marshalledTx, err := amino.MarshalJSON(tx) - require.NoError(t, err) - - marshalledTxs = append(marshalledTxs, string(marshalledTx)) - } - - mockErr := new(bytes.Buffer) - io := commands.NewTestIO() - io.SetErr(commands.WriteNopCloser(mockErr)) - - reader := strings.NewReader(strings.Join(marshalledTxs, "\n")) - balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader) - require.NoError(t, err) - - // Validate the balance map - assert.Len(t, balanceMap, len(dummyKeys)) - for _, key := range dummyKeys[1:] { - assert.Equal(t, amount, balanceMap[key.Address()].Amount) - } - - assert.Equal(t, std.Coins{}, balanceMap[sender.Address()].Amount) - }) - - t.Run("malformed transaction, invalid fee amount", func(t *testing.T) { - t.Parallel() - - var ( - dummyKeys = getDummyKeys(t, 10) - amountCoins = std.NewCoins(std.NewCoin(ugnot.Denom, 10)) - gasFee = std.NewCoin("gnos", 1) // invalid fee - txs = make([]std.Tx, 0) - ) - - sender := dummyKeys[0] - for _, dummyKey := range dummyKeys[1:] { - tx := std.Tx{ - Msgs: []std.Msg{ - bank.MsgSend{ - FromAddress: sender.Address(), - ToAddress: dummyKey.Address(), - Amount: amountCoins, - }, - }, - Fee: std.Fee{ - GasWanted: 10, - GasFee: gasFee, - }, - Signatures: make([]std.Signature, 0), - } - - txs = append(txs, tx) - } - - // Marshal the transactions into amino JSON - marshalledTxs := make([]string, 0, len(txs)) - - for _, tx := range txs { - marshalledTx, err := amino.MarshalJSON(tx) - require.NoError(t, err) - - marshalledTxs = append(marshalledTxs, string(marshalledTx)) - } - - mockErr := new(bytes.Buffer) - io := commands.NewTestIO() - io.SetErr(commands.WriteNopCloser(mockErr)) - - reader := strings.NewReader(strings.Join(marshalledTxs, "\n")) - balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader) - require.NoError(t, err) - - assert.NotNil(t, balanceMap) - assert.Contains(t, mockErr.String(), "invalid gas fee amount") - }) - - t.Run("malformed transaction, invalid send amount", func(t *testing.T) { - t.Parallel() - - var ( - dummyKeys = getDummyKeys(t, 10) - amountCoins = std.NewCoins(std.NewCoin("gnogno", 10)) // invalid send amount - gasFee = std.NewCoin(ugnot.Denom, 1) - txs = make([]std.Tx, 0) - ) - - sender := dummyKeys[0] - for _, dummyKey := range dummyKeys[1:] { - tx := std.Tx{ - Msgs: []std.Msg{ - bank.MsgSend{ - FromAddress: sender.Address(), - ToAddress: dummyKey.Address(), - Amount: amountCoins, - }, - }, - Fee: std.Fee{ - GasWanted: 10, - GasFee: gasFee, - }, - Signatures: make([]std.Signature, 0), - } - - txs = append(txs, tx) - } - - // Marshal the transactions into amino JSON - marshalledTxs := make([]string, 0, len(txs)) - - for _, tx := range txs { - marshalledTx, err := amino.MarshalJSON(tx) - require.NoError(t, err) - - marshalledTxs = append(marshalledTxs, string(marshalledTx)) - } - - mockErr := new(bytes.Buffer) - io := commands.NewTestIO() - io.SetErr(commands.WriteNopCloser(mockErr)) - - reader := strings.NewReader(strings.Join(marshalledTxs, "\n")) - balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader) - require.NoError(t, err) - - assert.NotNil(t, balanceMap) - assert.Contains(t, mockErr.String(), "invalid send amount") - }) -} diff --git a/gno.land/cmd/gnoland/genesis_balances_export.go b/gno.land/cmd/gnoland/genesis_balances_export.go deleted file mode 100644 index ec05d115b97..00000000000 --- a/gno.land/cmd/gnoland/genesis_balances_export.go +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" -) - -// newBalancesExportCmd creates the genesis balances export subcommand -func newBalancesExportCmd(balancesCfg *balancesCfg, io commands.IO) *commands.Command { - return commands.NewCommand( - commands.Metadata{ - Name: "export", - ShortUsage: "balances export [flags] ", - ShortHelp: "exports the balances from the genesis.json", - LongHelp: "Exports the balances from the genesis.json to an output file", - }, - commands.NewEmptyConfig(), - func(_ context.Context, args []string) error { - return execBalancesExport(balancesCfg, io, args) - }, - ) -} - -func execBalancesExport(cfg *balancesCfg, io commands.IO, args []string) error { - // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) - } - - // Load the genesis state - if genesis.AppState == nil { - return errAppStateNotSet - } - - state := genesis.AppState.(gnoland.GnoGenesisState) - if len(state.Balances) == 0 { - io.Println("No genesis balances to export") - - return nil - } - - // Make sure the output file path is specified - if len(args) == 0 { - return errNoOutputFile - } - - // Open output file - outputFile, err := os.OpenFile( - args[0], - os.O_RDWR|os.O_CREATE|os.O_APPEND, - 0o755, - ) - if err != nil { - return fmt.Errorf("unable to create output file, %w", err) - } - - // Save the balances - for _, balance := range state.Balances { - if _, err = outputFile.WriteString( - fmt.Sprintf("%s\n", balance), - ); err != nil { - return fmt.Errorf("unable to write to output, %w", err) - } - } - - io.Printfln( - "Exported %d balances", - len(state.Balances), - ) - - return nil -} diff --git a/gno.land/cmd/gnoland/genesis_balances_export_test.go b/gno.land/cmd/gnoland/genesis_balances_export_test.go deleted file mode 100644 index bd1f6152246..00000000000 --- a/gno.land/cmd/gnoland/genesis_balances_export_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package main - -import ( - "bufio" - "context" - "testing" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// getDummyBalances generates dummy balance lines -func getDummyBalances(t *testing.T, count int) []gnoland.Balance { - t.Helper() - - dummyKeys := getDummyKeys(t, count) - amount := std.NewCoins(std.NewCoin(ugnot.Denom, 10)) - - balances := make([]gnoland.Balance, len(dummyKeys)) - - for index, key := range dummyKeys { - balances[index] = gnoland.Balance{ - Address: key.Address(), - Amount: amount, - } - } - - return balances -} - -func TestGenesis_Balances_Export(t *testing.T) { - t.Parallel() - - t.Run("invalid genesis file", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "export", - "--genesis-path", - "dummy-path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) - }) - - t.Run("invalid genesis app state", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - genesis.AppState = nil // no app state - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "export", - "--genesis-path", - tempGenesis.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error()) - }) - - t.Run("no output file specified", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - genesis.AppState = gnoland.GnoGenesisState{ - Balances: getDummyBalances(t, 1), - } - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "export", - "--genesis-path", - tempGenesis.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errNoOutputFile.Error()) - }) - - t.Run("valid balances export", func(t *testing.T) { - t.Parallel() - - // Generate dummy balances - balances := getDummyBalances(t, 10) - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - genesis.AppState = gnoland.GnoGenesisState{ - Balances: balances, - } - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Prepare the output file - outputFile, outputCleanup := testutils.NewTestFile(t) - t.Cleanup(outputCleanup) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "export", - "--genesis-path", - tempGenesis.Name(), - outputFile.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the transactions were written down - scanner := bufio.NewScanner(outputFile) - - outputBalances := make([]gnoland.Balance, 0) - for scanner.Scan() { - var balance gnoland.Balance - err := balance.Parse(scanner.Text()) - require.NoError(t, err) - - outputBalances = append(outputBalances, balance) - } - - require.NoError(t, scanner.Err()) - - assert.Len(t, outputBalances, len(balances)) - - for index, balance := range outputBalances { - assert.Equal(t, balances[index], balance) - } - }) -} diff --git a/gno.land/cmd/gnoland/genesis_balances_remove.go b/gno.land/cmd/gnoland/genesis_balances_remove.go deleted file mode 100644 index 58a02319c8d..00000000000 --- a/gno.land/cmd/gnoland/genesis_balances_remove.go +++ /dev/null @@ -1,103 +0,0 @@ -package main - -import ( - "context" - "errors" - "flag" - "fmt" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" -) - -var ( - errUnableToLoadGenesis = errors.New("unable to load genesis") - errBalanceNotFound = errors.New("genesis balances entry does not exist") -) - -type balancesRemoveCfg struct { - rootCfg *balancesCfg - - address string -} - -// newBalancesRemoveCmd creates the genesis balances remove subcommand -func newBalancesRemoveCmd(rootCfg *balancesCfg, io commands.IO) *commands.Command { - cfg := &balancesRemoveCfg{ - rootCfg: rootCfg, - } - - return commands.NewCommand( - commands.Metadata{ - Name: "remove", - ShortUsage: "balances remove [flags]", - ShortHelp: "removes the balance information of a specific account", - }, - cfg, - func(_ context.Context, _ []string) error { - return execBalancesRemove(cfg, io) - }, - ) -} - -func (c *balancesRemoveCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.address, - "address", - "", - "the address of the account whose balance information should be removed from genesis.json", - ) -} - -func execBalancesRemove(cfg *balancesRemoveCfg, io commands.IO) error { - // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath) - if loadErr != nil { - return fmt.Errorf("%w, %w", errUnableToLoadGenesis, loadErr) - } - - // Validate the address - address, err := crypto.AddressFromString(cfg.address) - if err != nil { - return fmt.Errorf("%w, %w", errInvalidAddress, err) - } - - // Check if the genesis state is set at all - if genesis.AppState == nil { - return errAppStateNotSet - } - - // Construct the initial genesis balance sheet - state := genesis.AppState.(gnoland.GnoGenesisState) - genesisBalances, err := mapGenesisBalancesFromState(state) - if err != nil { - return err - } - - // Check if the genesis balance for the account is present - _, exists := genesisBalances[address] - if !exists { - return errBalanceNotFound - } - - // Drop the account pre-mine - delete(genesisBalances, address) - - // Save the balances - state.Balances = genesisBalances.List() - genesis.AppState = state - - // Save the updated genesis - if err := genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil { - return fmt.Errorf("unable to save genesis.json, %w", err) - } - - io.Printfln( - "Pre-mine information for address %s removed", - address.String(), - ) - - return nil -} diff --git a/gno.land/cmd/gnoland/genesis_balances_remove_test.go b/gno.land/cmd/gnoland/genesis_balances_remove_test.go deleted file mode 100644 index ed11836ba4d..00000000000 --- a/gno.land/cmd/gnoland/genesis_balances_remove_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package main - -import ( - "context" - "testing" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenesis_Balances_Remove(t *testing.T) { - t.Parallel() - - t.Run("invalid genesis", func(t *testing.T) { - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "remove", - "--genesis-path", - "dummy-path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) - }) - - t.Run("genesis app state not set", func(t *testing.T) { - t.Parallel() - - dummyKey := getDummyKey(t) - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - genesis.AppState = nil // not set - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "remove", - "--genesis-path", - tempGenesis.Name(), - "--address", - dummyKey.Address().String(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.ErrorContains(t, cmdErr, errAppStateNotSet.Error()) - }) - - t.Run("address is present", func(t *testing.T) { - t.Parallel() - - dummyKey := getDummyKey(t) - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - state := gnoland.GnoGenesisState{ - // Set an initial balance value - Balances: []gnoland.Balance{ - { - Address: dummyKey.Address(), - Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 100)), - }, - }, - } - genesis.AppState = state - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "remove", - "--genesis-path", - tempGenesis.Name(), - "--address", - dummyKey.Address().String(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the genesis was updated - genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) - require.NoError(t, loadErr) - - require.NotNil(t, genesis.AppState) - - state, ok := genesis.AppState.(gnoland.GnoGenesisState) - require.True(t, ok) - - assert.Len(t, state.Balances, 0) - }) - - t.Run("address not present", func(t *testing.T) { - t.Parallel() - - dummyKey := getDummyKey(t) - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - state := gnoland.GnoGenesisState{ - Balances: []gnoland.Balance{}, // Empty initial balance - } - genesis.AppState = state - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "balances", - "remove", - "--genesis-path", - tempGenesis.Name(), - "--address", - dummyKey.Address().String(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.ErrorContains(t, cmdErr, errBalanceNotFound.Error()) - }) -} diff --git a/gno.land/cmd/gnoland/genesis_generate.go b/gno.land/cmd/gnoland/genesis_generate.go deleted file mode 100644 index 751ac14ae62..00000000000 --- a/gno.land/cmd/gnoland/genesis_generate.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "time" - - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" -) - -var defaultChainID = "dev" - -type generateCfg struct { - outputPath string - chainID string - genesisTime int64 - blockMaxTxBytes int64 - blockMaxDataBytes int64 - blockMaxGas int64 - blockTimeIota int64 -} - -// newGenerateCmd creates the genesis generate subcommand -func newGenerateCmd(io commands.IO) *commands.Command { - cfg := &generateCfg{} - - return commands.NewCommand( - commands.Metadata{ - Name: "generate", - ShortUsage: "generate [flags]", - ShortHelp: "generates a fresh genesis.json", - LongHelp: "Generates a node's genesis.json based on specified parameters", - }, - cfg, - func(_ context.Context, _ []string) error { - return execGenerate(cfg, io) - }, - ) -} - -func (c *generateCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.outputPath, - "output-path", - "./genesis.json", - "the output path for the genesis.json", - ) - - fs.Int64Var( - &c.genesisTime, - "genesis-time", - time.Now().Unix(), - "the genesis creation time. Defaults to current time", - ) - - fs.StringVar( - &c.chainID, - "chain-id", - defaultChainID, - "the ID of the chain", - ) - - fs.Int64Var( - &c.blockMaxTxBytes, - "block-max-tx-bytes", - types.MaxBlockTxBytes, - "the max size of the block transaction", - ) - - fs.Int64Var( - &c.blockMaxDataBytes, - "block-max-data-bytes", - types.MaxBlockDataBytes, - "the max size of the block data", - ) - - fs.Int64Var( - &c.blockMaxGas, - "block-max-gas", - types.MaxBlockMaxGas, - "the max gas limit for the block", - ) - - fs.Int64Var( - &c.blockTimeIota, - "block-time-iota", - types.BlockTimeIotaMS, - "the block time iota (in ms)", - ) -} - -func execGenerate(cfg *generateCfg, io commands.IO) error { - // Start with the default configuration - genesis := getDefaultGenesis() - - // Set the genesis time - if cfg.genesisTime > 0 { - genesis.GenesisTime = time.Unix(cfg.genesisTime, 0) - } - - // Set the chain ID - if cfg.chainID != "" { - genesis.ChainID = cfg.chainID - } - - // Set the max tx bytes - if cfg.blockMaxTxBytes > 0 { - genesis.ConsensusParams.Block.MaxTxBytes = cfg.blockMaxTxBytes - } - - // Set the max data bytes - if cfg.blockMaxDataBytes > 0 { - genesis.ConsensusParams.Block.MaxDataBytes = cfg.blockMaxDataBytes - } - - // Set the max block gas - if cfg.blockMaxGas > 0 { - genesis.ConsensusParams.Block.MaxGas = cfg.blockMaxGas - } - - // Set the block time IOTA - if cfg.blockTimeIota > 0 { - genesis.ConsensusParams.Block.TimeIotaMS = cfg.blockTimeIota - } - - // Validate the genesis - if validateErr := genesis.ValidateAndComplete(); validateErr != nil { - return fmt.Errorf("unable to validate genesis, %w", validateErr) - } - - // Save the genesis file to disk - if saveErr := genesis.SaveAs(cfg.outputPath); saveErr != nil { - return fmt.Errorf("unable to save genesis, %w", saveErr) - } - - io.Printfln("Genesis successfully generated at %s\n", cfg.outputPath) - - // Log the empty validator set warning - io.Printfln("WARN: Genesis is generated with an empty validator set") - - return nil -} - -// getDefaultGenesis returns the default genesis config -func getDefaultGenesis() *types.GenesisDoc { - return &types.GenesisDoc{ - GenesisTime: time.Now(), - ChainID: defaultChainID, - ConsensusParams: types.DefaultConsensusParams(), - } -} diff --git a/gno.land/cmd/gnoland/genesis_generate_test.go b/gno.land/cmd/gnoland/genesis_generate_test.go deleted file mode 100644 index f078a161662..00000000000 --- a/gno.land/cmd/gnoland/genesis_generate_test.go +++ /dev/null @@ -1,252 +0,0 @@ -package main - -import ( - "context" - "fmt" - "path/filepath" - "testing" - - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenesis_Generate(t *testing.T) { - t.Parallel() - - t.Run("default genesis", func(t *testing.T) { - t.Parallel() - - tempDir, cleanup := testutils.NewTestCaseDir(t) - t.Cleanup(cleanup) - - genesisPath := filepath.Join(tempDir, "genesis.json") - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "generate", - "--output-path", - genesisPath, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Load the genesis - genesis, readErr := types.GenesisDocFromFile(genesisPath) - require.NoError(t, readErr) - - // Make sure the default configuration is set - defaultGenesis := getDefaultGenesis() - defaultGenesis.GenesisTime = genesis.GenesisTime - - assert.Equal(t, defaultGenesis, genesis) - }) - - t.Run("set chain ID", func(t *testing.T) { - t.Parallel() - - chainID := "example-chain-ID" - - tempDir, cleanup := testutils.NewTestCaseDir(t) - t.Cleanup(cleanup) - - genesisPath := filepath.Join(tempDir, "genesis.json") - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "generate", - "--chain-id", - chainID, - "--output-path", - genesisPath, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Load the genesis - genesis, readErr := types.GenesisDocFromFile(genesisPath) - require.NoError(t, readErr) - - assert.Equal(t, genesis.ChainID, chainID) - }) - - t.Run("set block max tx bytes", func(t *testing.T) { - t.Parallel() - - blockMaxTxBytes := int64(100) - - tempDir, cleanup := testutils.NewTestCaseDir(t) - t.Cleanup(cleanup) - - genesisPath := filepath.Join(tempDir, "genesis.json") - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "generate", - "--block-max-tx-bytes", - fmt.Sprintf("%d", blockMaxTxBytes), - "--output-path", - genesisPath, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Load the genesis - genesis, readErr := types.GenesisDocFromFile(genesisPath) - require.NoError(t, readErr) - - assert.Equal( - t, - genesis.ConsensusParams.Block.MaxTxBytes, - blockMaxTxBytes, - ) - }) - - t.Run("set block max data bytes", func(t *testing.T) { - t.Parallel() - - blockMaxDataBytes := int64(100) - - tempDir, cleanup := testutils.NewTestCaseDir(t) - t.Cleanup(cleanup) - - genesisPath := filepath.Join(tempDir, "genesis.json") - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "generate", - "--block-max-data-bytes", - fmt.Sprintf("%d", blockMaxDataBytes), - "--output-path", - genesisPath, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Load the genesis - genesis, readErr := types.GenesisDocFromFile(genesisPath) - require.NoError(t, readErr) - - assert.Equal( - t, - genesis.ConsensusParams.Block.MaxDataBytes, - blockMaxDataBytes, - ) - }) - - t.Run("set block max gas", func(t *testing.T) { - t.Parallel() - - blockMaxGas := int64(100) - - tempDir, cleanup := testutils.NewTestCaseDir(t) - t.Cleanup(cleanup) - - genesisPath := filepath.Join(tempDir, "genesis.json") - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "generate", - "--block-max-gas", - fmt.Sprintf("%d", blockMaxGas), - "--output-path", - genesisPath, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Load the genesis - genesis, readErr := types.GenesisDocFromFile(genesisPath) - require.NoError(t, readErr) - - assert.Equal( - t, - genesis.ConsensusParams.Block.MaxGas, - blockMaxGas, - ) - }) - - t.Run("set block time iota", func(t *testing.T) { - t.Parallel() - - blockTimeIota := int64(10) - - tempDir, cleanup := testutils.NewTestCaseDir(t) - t.Cleanup(cleanup) - - genesisPath := filepath.Join(tempDir, "genesis.json") - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "generate", - "--block-time-iota", - fmt.Sprintf("%d", blockTimeIota), - "--output-path", - genesisPath, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Load the genesis - genesis, readErr := types.GenesisDocFromFile(genesisPath) - require.NoError(t, readErr) - - assert.Equal( - t, - genesis.ConsensusParams.Block.TimeIotaMS, - blockTimeIota, - ) - }) - - t.Run("invalid genesis config (chain ID)", func(t *testing.T) { - t.Parallel() - - invalidChainID := "thischainidisunusuallylongsoitwillcausethetesttofail" - - tempDir, cleanup := testutils.NewTestCaseDir(t) - t.Cleanup(cleanup) - - genesisPath := filepath.Join(tempDir, "genesis.json") - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "generate", - "--chain-id", - invalidChainID, - "--output-path", - genesisPath, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.Error(t, cmdErr) - }) -} diff --git a/gno.land/cmd/gnoland/genesis_txs.go b/gno.land/cmd/gnoland/genesis_txs.go deleted file mode 100644 index 46b8d1bd29c..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "errors" - "flag" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/std" -) - -type txsCfg struct { - commonCfg -} - -var errInvalidGenesisStateType = errors.New("invalid genesis state type") - -// newTxsCmd creates the genesis txs subcommand -func newTxsCmd(io commands.IO) *commands.Command { - cfg := &txsCfg{} - - cmd := commands.NewCommand( - commands.Metadata{ - Name: "txs", - ShortUsage: "txs [flags]", - ShortHelp: "manages the initial genesis transactions", - LongHelp: "Manages genesis transactions through input files", - }, - cfg, - commands.HelpExec, - ) - - cmd.AddSubCommands( - newTxsAddCmd(cfg, io), - newTxsRemoveCmd(cfg, io), - newTxsExportCmd(cfg, io), - newTxsListCmd(cfg, io), - ) - - return cmd -} - -func (c *txsCfg) RegisterFlags(fs *flag.FlagSet) { - c.commonCfg.RegisterFlags(fs) -} - -// appendGenesisTxs saves the given transactions to the genesis doc -func appendGenesisTxs(genesis *types.GenesisDoc, txs []std.Tx) error { - // Initialize the app state if it's not present - if genesis.AppState == nil { - genesis.AppState = gnoland.GnoGenesisState{} - } - - // Make sure the app state is the Gno genesis state - state, ok := genesis.AppState.(gnoland.GnoGenesisState) - if !ok { - return errInvalidGenesisStateType - } - - // Left merge the transactions - fileTxStore := txStore(txs) - genesisTxStore := txStore(state.Txs) - - // The genesis transactions have preference with the order - // in the genesis.json - if err := genesisTxStore.leftMerge(fileTxStore); err != nil { - return err - } - - // Save the state - state.Txs = genesisTxStore - genesis.AppState = state - - return nil -} diff --git a/gno.land/cmd/gnoland/genesis_txs_add.go b/gno.land/cmd/gnoland/genesis_txs_add.go deleted file mode 100644 index 7e7fd25b21e..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_add.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "github.com/gnolang/gno/tm2/pkg/commands" -) - -// newTxsAddCmd creates the genesis txs add subcommand -func newTxsAddCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { - cmd := commands.NewCommand( - commands.Metadata{ - Name: "add", - ShortUsage: "txs add [flags] [...]", - ShortHelp: "adds transactions into the genesis.json", - LongHelp: "Adds initial transactions to the genesis.json", - }, - commands.NewEmptyConfig(), - commands.HelpExec, - ) - - cmd.AddSubCommands( - newTxsAddSheetCmd(txsCfg, io), - newTxsAddPackagesCmd(txsCfg, io), - ) - - return cmd -} diff --git a/gno.land/cmd/gnoland/genesis_txs_add_packages.go b/gno.land/cmd/gnoland/genesis_txs_add_packages.go deleted file mode 100644 index 56d165c070b..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_add_packages.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/std" -) - -var errInvalidPackageDir = errors.New("invalid package directory") - -var ( - genesisDeployAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 - genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) -) - -// newTxsAddPackagesCmd creates the genesis txs add packages subcommand -func newTxsAddPackagesCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { - return commands.NewCommand( - commands.Metadata{ - Name: "packages", - ShortUsage: "txs add packages ", - ShortHelp: "imports transactions from the given packages into the genesis.json", - LongHelp: "Imports the transactions from a given package directory recursively to the genesis.json", - }, - commands.NewEmptyConfig(), - func(_ context.Context, args []string) error { - return execTxsAddPackages(txsCfg, io, args) - }, - ) -} - -func execTxsAddPackages( - cfg *txsCfg, - io commands.IO, - args []string, -) error { - // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) - } - - // Make sure the package dir is set - if len(args) == 0 { - return errInvalidPackageDir - } - - parsedTxs := make([]std.Tx, 0) - for _, path := range args { - // Generate transactions from the packages (recursively) - txs, err := gnoland.LoadPackagesFromDir(path, genesisDeployAddress, genesisDeployFee) - if err != nil { - return fmt.Errorf("unable to load txs from directory, %w", err) - } - - parsedTxs = append(parsedTxs, txs...) - } - - // Save the txs to the genesis.json - if err := appendGenesisTxs(genesis, parsedTxs); err != nil { - return fmt.Errorf("unable to append genesis transactions, %w", err) - } - - // Save the updated genesis - if err := genesis.SaveAs(cfg.genesisPath); err != nil { - return fmt.Errorf("unable to save genesis.json, %w", err) - } - - io.Printfln( - "Saved %d transactions to genesis.json", - len(parsedTxs), - ) - - return nil -} diff --git a/gno.land/cmd/gnoland/genesis_txs_add_packages_test.go b/gno.land/cmd/gnoland/genesis_txs_add_packages_test.go deleted file mode 100644 index 20c4f84c9ed..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_add_packages_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenesis_Txs_Add_Packages(t *testing.T) { - t.Parallel() - - t.Run("invalid genesis file", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "add", - "packages", - "--genesis-path", - "dummy-path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) - }) - - t.Run("invalid package dir", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "add", - "packages", - "--genesis-path", - tempGenesis.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errInvalidPackageDir.Error()) - }) - - t.Run("valid package", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Prepare the package - var ( - packagePath = "gno.land/p/demo/cuttlas" - dir = t.TempDir() - ) - - createFile := func(path, data string) { - file, err := os.Create(path) - require.NoError(t, err) - - _, err = file.WriteString(data) - require.NoError(t, err) - } - - // Create the gno.mod file - createFile( - filepath.Join(dir, "gno.mod"), - fmt.Sprintf("module %s\n", packagePath), - ) - - // Create a simple main.gno - createFile( - filepath.Join(dir, "main.gno"), - "package cuttlas\n\nfunc Example() string {\nreturn \"Manos arriba!\"\n}", - ) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "add", - "packages", - "--genesis-path", - tempGenesis.Name(), - dir, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the transactions were written down - updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) - require.NoError(t, err) - require.NotNil(t, updatedGenesis.AppState) - - // Fetch the state - state := updatedGenesis.AppState.(gnoland.GnoGenesisState) - - require.Equal(t, 1, len(state.Txs)) - require.Equal(t, 1, len(state.Txs[0].Msgs)) - - msgAddPkg, ok := state.Txs[0].Msgs[0].(vmm.MsgAddPackage) - require.True(t, ok) - - assert.Equal(t, packagePath, msgAddPkg.Package.Path) - }) -} diff --git a/gno.land/cmd/gnoland/genesis_txs_add_sheet.go b/gno.land/cmd/gnoland/genesis_txs_add_sheet.go deleted file mode 100644 index 261a050029c..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_add_sheet.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "os" - - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/std" -) - -var ( - errInvalidTxsFile = errors.New("unable to open transactions file") - errNoTxsFileSpecified = errors.New("no txs file specified") -) - -// newTxsAddSheetCmd creates the genesis txs add sheet subcommand -func newTxsAddSheetCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { - return commands.NewCommand( - commands.Metadata{ - Name: "sheets", - ShortUsage: "txs add sheets ", - ShortHelp: "imports transactions from the given sheets into the genesis.json", - LongHelp: "Imports the transactions from a given transactions sheet to the genesis.json", - }, - commands.NewEmptyConfig(), - func(ctx context.Context, args []string) error { - return execTxsAddSheet(ctx, txsCfg, io, args) - }, - ) -} - -func execTxsAddSheet( - ctx context.Context, - cfg *txsCfg, - io commands.IO, - args []string, -) error { - // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) - } - - // Open the transactions files - if len(args) == 0 { - return errNoTxsFileSpecified - } - - parsedTxs := make([]std.Tx, 0) - for _, file := range args { - file, loadErr := os.Open(file) - if loadErr != nil { - return fmt.Errorf("%w, %w", errInvalidTxsFile, loadErr) - } - - txs, err := std.ParseTxs(ctx, file) - if err != nil { - return fmt.Errorf("unable to parse file, %w", err) - } - - if err = file.Close(); err != nil { - return fmt.Errorf("unable to gracefully close file, %w", err) - } - - parsedTxs = append(parsedTxs, txs...) - } - - // Save the txs to the genesis.json - if err := appendGenesisTxs(genesis, parsedTxs); err != nil { - return fmt.Errorf("unable to append genesis transactions, %w", err) - } - - // Save the updated genesis - if err := genesis.SaveAs(cfg.genesisPath); err != nil { - return fmt.Errorf("unable to save genesis.json, %w", err) - } - - io.Printfln( - "Saved %d transactions to genesis.json", - len(parsedTxs), - ) - - return nil -} diff --git a/gno.land/cmd/gnoland/genesis_txs_add_sheet_test.go b/gno.land/cmd/gnoland/genesis_txs_add_sheet_test.go deleted file mode 100644 index a70446cfe6c..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_add_sheet_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package main - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/sdk/bank" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// generateDummyTxs generates dummy transactions -func generateDummyTxs(t *testing.T, count int) []std.Tx { - t.Helper() - - txs := make([]std.Tx, count) - - for i := 0; i < count; i++ { - txs[i] = std.Tx{ - Msgs: []std.Msg{ - bank.MsgSend{ - FromAddress: crypto.Address{byte(i)}, - ToAddress: crypto.Address{byte((i + 1) % count)}, - Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 1)), - }, - }, - Fee: std.Fee{ - GasWanted: 1, - GasFee: std.NewCoin(ugnot.Denom, 1000000), - }, - Memo: fmt.Sprintf("tx %d", i), - } - } - - return txs -} - -// encodeDummyTxs encodes the transactions into amino JSON -func encodeDummyTxs(t *testing.T, txs []std.Tx) []string { - t.Helper() - - encodedTxs := make([]string, 0, len(txs)) - - for _, tx := range txs { - encodedTx, err := amino.MarshalJSON(tx) - if err != nil { - t.Fatalf("unable to marshal tx, %v", err) - } - - encodedTxs = append(encodedTxs, string(encodedTx)) - } - - return encodedTxs -} - -func TestGenesis_Txs_Add_Sheets(t *testing.T) { - t.Parallel() - - t.Run("invalid genesis file", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "add", - "sheets", - "--genesis-path", - "dummy-path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) - }) - - t.Run("invalid txs file", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "add", - "sheets", - "--genesis-path", - tempGenesis.Name(), - "dummy-tx-file", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errInvalidTxsFile.Error()) - }) - - t.Run("no txs file", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "add", - "sheets", - "--genesis-path", - tempGenesis.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errNoTxsFileSpecified.Error()) - }) - - t.Run("malformed txs file", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "add", - "sheets", - "--genesis-path", - tempGenesis.Name(), - tempGenesis.Name(), // invalid txs file - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, "unable to parse file") - }) - - t.Run("valid txs file", func(t *testing.T) { - t.Parallel() - - // Generate dummy txs - txs := generateDummyTxs(t, 10) - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Prepare the transactions file - txsFile, txsCleanup := testutils.NewTestFile(t) - t.Cleanup(txsCleanup) - - _, err := txsFile.WriteString( - strings.Join( - encodeDummyTxs(t, txs), - "\n", - ), - ) - require.NoError(t, err) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "add", - "sheets", - "--genesis-path", - tempGenesis.Name(), - txsFile.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the transactions were written down - updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) - require.NoError(t, err) - require.NotNil(t, updatedGenesis.AppState) - - // Fetch the state - state := updatedGenesis.AppState.(gnoland.GnoGenesisState) - - assert.Len(t, state.Txs, len(txs)) - - for index, tx := range state.Txs { - assert.Equal(t, txs[index], tx) - } - }) - - t.Run("existing genesis txs", func(t *testing.T) { - t.Parallel() - - // Generate dummy txs - txs := generateDummyTxs(t, 10) - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - genesisState := gnoland.GnoGenesisState{ - Txs: txs[0 : len(txs)/2], - } - - genesis.AppState = genesisState - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Prepare the transactions file - txsFile, txsCleanup := testutils.NewTestFile(t) - t.Cleanup(txsCleanup) - - _, err := txsFile.WriteString( - strings.Join( - encodeDummyTxs(t, txs), - "\n", - ), - ) - require.NoError(t, err) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "add", - "sheets", - "--genesis-path", - tempGenesis.Name(), - txsFile.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the transactions were written down - updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) - require.NoError(t, err) - require.NotNil(t, updatedGenesis.AppState) - - // Fetch the state - state := updatedGenesis.AppState.(gnoland.GnoGenesisState) - - assert.Len(t, state.Txs, len(txs)) - - for index, tx := range state.Txs { - assert.Equal(t, txs[index], tx) - } - }) -} diff --git a/gno.land/cmd/gnoland/genesis_txs_export.go b/gno.land/cmd/gnoland/genesis_txs_export.go deleted file mode 100644 index bf54236b31f..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_export.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "os" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" -) - -var errNoOutputFile = errors.New("no output file path specified") - -// newTxsExportCmd creates the genesis txs export subcommand -func newTxsExportCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { - return commands.NewCommand( - commands.Metadata{ - Name: "export", - ShortUsage: "txs export [flags] ", - ShortHelp: "exports the transactions from the genesis.json", - LongHelp: "Exports the transactions from the genesis.json to an output file", - }, - commands.NewEmptyConfig(), - func(_ context.Context, args []string) error { - return execTxsExport(txsCfg, io, args) - }, - ) -} - -func execTxsExport(cfg *txsCfg, io commands.IO, args []string) error { - // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) - } - - // Load the genesis state - if genesis.AppState == nil { - return errAppStateNotSet - } - - state := genesis.AppState.(gnoland.GnoGenesisState) - if len(state.Txs) == 0 { - io.Println("No genesis transactions to export") - - return nil - } - - // Make sure the output file path is specified - if len(args) == 0 { - return errNoOutputFile - } - - // Open output file - outputFile, err := os.OpenFile( - args[0], - os.O_RDWR|os.O_CREATE|os.O_APPEND, - 0o755, - ) - if err != nil { - return fmt.Errorf("unable to create output file, %w", err) - } - - // Save the transactions - for _, tx := range state.Txs { - // Marshal tx individual tx into JSON - jsonData, err := amino.MarshalJSON(tx) - if err != nil { - return fmt.Errorf("unable to marshal JSON data, %w", err) - } - - // Write the JSON data as a line to the file - if _, err = outputFile.Write(jsonData); err != nil { - return fmt.Errorf("unable to write to output, %w", err) - } - - // Write a newline character to separate JSON objects - if _, err = outputFile.WriteString("\n"); err != nil { - return fmt.Errorf("unable to write newline output, %w", err) - } - } - - io.Printfln( - "Exported %d transactions", - len(state.Txs), - ) - - return nil -} diff --git a/gno.land/cmd/gnoland/genesis_txs_export_test.go b/gno.land/cmd/gnoland/genesis_txs_export_test.go deleted file mode 100644 index 9927f671efb..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_export_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package main - -import ( - "bufio" - "context" - "testing" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenesis_Txs_Export(t *testing.T) { - t.Parallel() - - t.Run("invalid genesis file", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "export", - "--genesis-path", - "dummy-path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) - }) - - t.Run("invalid genesis app state", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - genesis.AppState = nil // no app state - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "export", - "--genesis-path", - tempGenesis.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error()) - }) - - t.Run("no output file specified", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - genesis.AppState = gnoland.GnoGenesisState{ - Txs: generateDummyTxs(t, 1), - } - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "export", - "--genesis-path", - tempGenesis.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errNoOutputFile.Error()) - }) - - t.Run("valid txs export", func(t *testing.T) { - t.Parallel() - - // Generate dummy txs - txs := generateDummyTxs(t, 10) - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - genesis.AppState = gnoland.GnoGenesisState{ - Txs: txs, - } - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Prepare the output file - outputFile, outputCleanup := testutils.NewTestFile(t) - t.Cleanup(outputCleanup) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "export", - "--genesis-path", - tempGenesis.Name(), - outputFile.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the transactions were written down - scanner := bufio.NewScanner(outputFile) - - outputTxs := make([]std.Tx, 0) - for scanner.Scan() { - var tx std.Tx - - require.NoError(t, amino.UnmarshalJSON(scanner.Bytes(), &tx)) - - outputTxs = append(outputTxs, tx) - } - - require.NoError(t, scanner.Err()) - - assert.Len(t, outputTxs, len(txs)) - - for index, tx := range outputTxs { - assert.Equal(t, txs[index], tx) - } - }) -} diff --git a/gno.land/cmd/gnoland/genesis_txs_list.go b/gno.land/cmd/gnoland/genesis_txs_list.go deleted file mode 100644 index c68fbc30803..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_list.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "bytes" - "context" - "errors" - "fmt" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" -) - -var ErrWrongGenesisType = errors.New("genesis state is not using the correct Gno Genesis type") - -// newTxsListCmd list all transactions on the specified genesis file -func newTxsListCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { - cmd := commands.NewCommand( - commands.Metadata{ - Name: "list", - ShortUsage: "txs list [flags] [...]", - ShortHelp: "lists transactions existing on genesis.json", - LongHelp: "Lists transactions existing on genesis.json", - }, - commands.NewEmptyConfig(), - func(ctx context.Context, args []string) error { - return execTxsListCmd(io, txsCfg) - }, - ) - - return cmd -} - -func execTxsListCmd(io commands.IO, cfg *txsCfg) error { - genesis, err := types.GenesisDocFromFile(cfg.genesisPath) - if err != nil { - return fmt.Errorf("%w, %w", errUnableToLoadGenesis, err) - } - - gs, ok := genesis.AppState.(gnoland.GnoGenesisState) - if !ok { - return ErrWrongGenesisType - } - - b, err := amino.MarshalJSONIndent(gs.Txs, "", " ") - if err != nil { - return errors.New("error marshalling data to amino JSON") - } - - buf := bytes.NewBuffer(b) - _, err = buf.WriteTo(io.Out()) - - return err -} diff --git a/gno.land/cmd/gnoland/genesis_txs_list_test.go b/gno.land/cmd/gnoland/genesis_txs_list_test.go deleted file mode 100644 index d18c2f4d641..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_list_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "bytes" - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/testutils" -) - -func TestGenesis_List_All(t *testing.T) { - t.Parallel() - - t.Run("invalid genesis path", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "list", - "--genesis-path", - "", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorIs(t, cmdErr, errUnableToLoadGenesis) - }) - - t.Run("list all txs", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - // Generate dummy txs - txs := generateDummyTxs(t, 10) - - genesis := getDefaultGenesis() - genesis.AppState = gnoland.GnoGenesisState{ - Txs: txs, - } - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - cio := commands.NewTestIO() - buf := bytes.NewBuffer(nil) - cio.SetOut(commands.WriteNopCloser(buf)) - - cmd := newRootCmd(cio) - args := []string{ - "genesis", - "txs", - "list", - "--genesis-path", - tempGenesis.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - require.Len(t, buf.String(), 4442) - }) -} diff --git a/gno.land/cmd/gnoland/genesis_txs_remove.go b/gno.land/cmd/gnoland/genesis_txs_remove.go deleted file mode 100644 index 49c650f4670..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_remove.go +++ /dev/null @@ -1,108 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "strings" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/amino" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/std" -) - -var ( - errAppStateNotSet = errors.New("genesis app state not set") - errNoTxHashSpecified = errors.New("no transaction hashes specified") - errTxNotFound = errors.New("transaction not present in genesis.json") -) - -// newTxsRemoveCmd creates the genesis txs remove subcommand -func newTxsRemoveCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { - return commands.NewCommand( - commands.Metadata{ - Name: "remove", - ShortUsage: "txs remove ", - ShortHelp: "removes the transactions from the genesis.json", - LongHelp: "Removes the transactions using the transaction hash", - }, - commands.NewEmptyConfig(), - func(_ context.Context, args []string) error { - return execTxsRemove(txsCfg, io, args) - }, - ) -} - -func execTxsRemove(cfg *txsCfg, io commands.IO, args []string) error { - // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) - } - - // Check if the genesis state is set at all - if genesis.AppState == nil { - return errAppStateNotSet - } - - // Make sure the transaction hashes are set - if len(args) == 0 { - return errNoTxHashSpecified - } - - state := genesis.AppState.(gnoland.GnoGenesisState) - - for _, inputHash := range args { - index := -1 - - for indx, tx := range state.Txs { - // Find the hash of the transaction - hash, err := getTxHash(tx) - if err != nil { - return fmt.Errorf("unable to generate tx hash, %w", err) - } - - // Check if the hashes match - if strings.ToLower(hash) == strings.ToLower(inputHash) { - index = indx - - break - } - } - - if index < 0 { - return errTxNotFound - } - - state.Txs = append(state.Txs[:index], state.Txs[index+1:]...) - - io.Printfln( - "Transaction %s removed from genesis.json", - inputHash, - ) - } - - genesis.AppState = state - - // Save the updated genesis - if err := genesis.SaveAs(cfg.genesisPath); err != nil { - return fmt.Errorf("unable to save genesis.json, %w", err) - } - - return nil -} - -// getTxHash returns the hex hash representation of -// the transaction (Amino encoded) -func getTxHash(tx std.Tx) (string, error) { - encodedTx, err := amino.Marshal(tx) - if err != nil { - return "", fmt.Errorf("unable to marshal transaction, %w", err) - } - - txHash := types.Tx(encodedTx).Hash() - - return fmt.Sprintf("%X", txHash), nil -} diff --git a/gno.land/cmd/gnoland/genesis_txs_remove_test.go b/gno.land/cmd/gnoland/genesis_txs_remove_test.go deleted file mode 100644 index ff5af479449..00000000000 --- a/gno.land/cmd/gnoland/genesis_txs_remove_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package main - -import ( - "context" - "testing" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenesis_Txs_Remove(t *testing.T) { - t.Parallel() - - t.Run("invalid genesis file", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "remove", - "--genesis-path", - "dummy-path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) - }) - - t.Run("invalid genesis app state", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - genesis.AppState = nil // no app state - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "remove", - "--genesis-path", - tempGenesis.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error()) - }) - t.Run("no transaction hash specified", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - // Generate dummy txs - txs := generateDummyTxs(t, 10) - - genesis := getDefaultGenesis() - genesis.AppState = gnoland.GnoGenesisState{ - Txs: txs, - } - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "remove", - "--genesis-path", - tempGenesis.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errNoTxHashSpecified.Error()) - }) - - t.Run("transaction removed", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - // Generate dummy txs - txs := generateDummyTxs(t, 10) - - genesis := getDefaultGenesis() - genesis.AppState = gnoland.GnoGenesisState{ - Txs: txs, - } - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - txHash, err := getTxHash(txs[0]) - require.NoError(t, err) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "txs", - "remove", - "--genesis-path", - tempGenesis.Name(), - txHash, - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - - // Validate the transaction was removed - updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) - require.NoError(t, err) - require.NotNil(t, updatedGenesis.AppState) - - // Fetch the state - state := updatedGenesis.AppState.(gnoland.GnoGenesisState) - - assert.Len(t, state.Txs, len(txs)-1) - - for _, tx := range state.Txs { - genesisTxHash, err := getTxHash(tx) - require.NoError(t, err) - - assert.NotEqual(t, txHash, genesisTxHash) - } - }) -} diff --git a/gno.land/cmd/gnoland/genesis_validator.go b/gno.land/cmd/gnoland/genesis_validator.go deleted file mode 100644 index 91d3e4af7dd..00000000000 --- a/gno.land/cmd/gnoland/genesis_validator.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "flag" - - "github.com/gnolang/gno/tm2/pkg/commands" -) - -type validatorCfg struct { - commonCfg - - address string -} - -// newValidatorCmd creates the genesis validator subcommand -func newValidatorCmd(io commands.IO) *commands.Command { - cfg := &validatorCfg{ - commonCfg: commonCfg{}, - } - - cmd := commands.NewCommand( - commands.Metadata{ - Name: "validator", - ShortUsage: "validator [flags]", - ShortHelp: "validator set management in genesis.json", - LongHelp: "Manipulates the genesis.json validator set", - }, - cfg, - commands.HelpExec, - ) - - cmd.AddSubCommands( - newValidatorAddCmd(cfg, io), - newValidatorRemoveCmd(cfg, io), - ) - - return cmd -} - -func (c *validatorCfg) RegisterFlags(fs *flag.FlagSet) { - c.commonCfg.RegisterFlags(fs) - - fs.StringVar( - &c.address, - "address", - "", - "the gno bech32 address of the validator", - ) -} diff --git a/gno.land/cmd/gnoland/genesis_validator_add.go b/gno.land/cmd/gnoland/genesis_validator_add.go deleted file mode 100644 index 6c44ad93f89..00000000000 --- a/gno.land/cmd/gnoland/genesis_validator_add.go +++ /dev/null @@ -1,137 +0,0 @@ -package main - -import ( - "context" - "errors" - "flag" - "fmt" - - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" - _ "github.com/gnolang/gno/tm2/pkg/crypto/keys" -) - -var ( - errInvalidPower = errors.New("invalid validator power") - errInvalidName = errors.New("invalid validator name") - errPublicKeyAddressMismatch = errors.New("provided public key and address do not match") - errAddressPresent = errors.New("validator with same address already present in genesis.json") -) - -type validatorAddCfg struct { - rootCfg *validatorCfg - - pubKey string - name string - power int64 -} - -// newValidatorAddCmd creates the genesis validator add subcommand -func newValidatorAddCmd(validatorCfg *validatorCfg, io commands.IO) *commands.Command { - cfg := &validatorAddCfg{ - rootCfg: validatorCfg, - } - - return commands.NewCommand( - commands.Metadata{ - Name: "add", - ShortUsage: "validator add [flags]", - ShortHelp: "adds a new validator to the genesis.json", - }, - cfg, - func(_ context.Context, _ []string) error { - return execValidatorAdd(cfg, io) - }, - ) -} - -func (c *validatorAddCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.pubKey, - "pub-key", - "", - "the bech32 string representation of the validator's public key", - ) - - fs.StringVar( - &c.name, - "name", - "", - "the name of the validator (must be unique)", - ) - - fs.Int64Var( - &c.power, - "power", - 1, - "the voting power of the validator (must be > 0)", - ) -} - -func execValidatorAdd(cfg *validatorAddCfg, io commands.IO) error { - // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) - } - - // Check the validator address - address, err := crypto.AddressFromString(cfg.rootCfg.address) - if err != nil { - return fmt.Errorf("invalid validator address, %w", err) - } - - // Check the voting power - if cfg.power < 1 { - return errInvalidPower - } - - // Check the name - if cfg.name == "" { - return errInvalidName - } - - // Check the public key - pubKey, err := crypto.PubKeyFromBech32(cfg.pubKey) - if err != nil { - return fmt.Errorf("invalid validator public key, %w", err) - } - - // Check the public key matches the address - if pubKey.Address() != address { - return errPublicKeyAddressMismatch - } - - validator := types.GenesisValidator{ - Address: address, - PubKey: pubKey, - Power: cfg.power, - Name: cfg.name, - } - - // Check if the validator exists - for _, genesisValidator := range genesis.Validators { - // There is no need to check if the public keys match - // since the address is derived from it, and the derivation - // is checked already - if validator.Address == genesisValidator.Address { - return errAddressPresent - } - } - - // Add the validator - genesis.Validators = append(genesis.Validators, validator) - - // Save the updated genesis - if err := genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil { - return fmt.Errorf("unable to save genesis.json, %w", err) - } - - io.Printfln( - "Validator with address %s added to genesis file", - cfg.rootCfg.address, - ) - - return nil -} diff --git a/gno.land/cmd/gnoland/genesis_validator_add_test.go b/gno.land/cmd/gnoland/genesis_validator_add_test.go deleted file mode 100644 index 528255b3029..00000000000 --- a/gno.land/cmd/gnoland/genesis_validator_add_test.go +++ /dev/null @@ -1,301 +0,0 @@ -package main - -import ( - "context" - "testing" - - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/crypto/bip39" - "github.com/gnolang/gno/tm2/pkg/crypto/hd" - "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" - "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// getDummyKey generates a random public key, -// and returns the key info -func getDummyKey(t *testing.T) crypto.PubKey { - t.Helper() - - mnemonic, err := client.GenerateMnemonic(256) - require.NoError(t, err) - - seed := bip39.NewSeed(mnemonic, "") - - return generateKeyFromSeed(seed, 0).PubKey() -} - -// generateKeyFromSeed generates a private key from -// the provided seed and index -func generateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey { - pathParams := hd.NewFundraiserParams(0, crypto.CoinType, index) - - masterPriv, ch := hd.ComputeMastersFromSeed(seed) - - //nolint:errcheck // This derivation can never error out, since the path params - // are always going to be valid - derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, pathParams.String()) - - return secp256k1.PrivKeySecp256k1(derivedPriv) -} - -// getDummyKeys generates random keys for testing -func getDummyKeys(t *testing.T, count int) []crypto.PubKey { - t.Helper() - - dummyKeys := make([]crypto.PubKey, count) - - for i := 0; i < count; i++ { - dummyKeys[i] = getDummyKey(t) - } - - return dummyKeys -} - -func TestGenesis_Validator_Add(t *testing.T) { - t.Parallel() - - t.Run("invalid genesis file", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "add", - "--genesis-path", - "dummy-path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) - }) - - t.Run("invalid validator address", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "add", - "--genesis-path", - tempGenesis.Name(), - "--address", - "dummyaddress", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, "invalid validator address") - }) - - t.Run("invalid voting power", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - key := getDummyKey(t) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "add", - "--genesis-path", - tempGenesis.Name(), - "--address", - key.Address().String(), - "--power", - "-1", // invalid voting power - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorIs(t, cmdErr, errInvalidPower) - }) - - t.Run("invalid validator name", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - key := getDummyKey(t) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "add", - "--genesis-path", - tempGenesis.Name(), - "--address", - key.Address().String(), - "--name", - "", // invalid validator name - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errInvalidName.Error()) - }) - - t.Run("invalid public key", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - key := getDummyKey(t) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "add", - "--genesis-path", - tempGenesis.Name(), - "--address", - key.Address().String(), - "--name", - "example", - "--pub-key", - "invalidkey", // invalid pub key - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, "invalid validator public key") - }) - - t.Run("public key address mismatch", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - dummyKeys := getDummyKeys(t, 2) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "add", - "--genesis-path", - tempGenesis.Name(), - "--address", - dummyKeys[0].Address().String(), - "--name", - "example", - "--pub-key", - crypto.PubKeyToBech32(dummyKeys[1]), // another key - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errPublicKeyAddressMismatch.Error()) - }) - - t.Run("validator with same address exists", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - dummyKeys := getDummyKeys(t, 2) - genesis := getDefaultGenesis() - - // Set an existing validator - genesis.Validators = append(genesis.Validators, types.GenesisValidator{ - Address: dummyKeys[0].Address(), - PubKey: dummyKeys[0], - Power: 1, - Name: "example", - }) - - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "add", - "--genesis-path", - tempGenesis.Name(), - "--address", - dummyKeys[0].Address().String(), - "--name", - "example", - "--pub-key", - crypto.PubKeyToBech32(dummyKeys[0]), // another key - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errAddressPresent.Error()) - }) - - t.Run("valid genesis validator", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - key := getDummyKey(t) - genesis := getDefaultGenesis() - - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "add", - "--genesis-path", - tempGenesis.Name(), - "--address", - key.Address().String(), - "--name", - "example", - "--pub-key", - crypto.PubKeyToBech32(key), // another key - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - }) -} diff --git a/gno.land/cmd/gnoland/genesis_validator_remove.go b/gno.land/cmd/gnoland/genesis_validator_remove.go deleted file mode 100644 index 48a15a9abaf..00000000000 --- a/gno.land/cmd/gnoland/genesis_validator_remove.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" -) - -var errValidatorNotPresent = errors.New("validator not present in genesis.json") - -// newValidatorRemoveCmd creates the genesis validator remove subcommand -func newValidatorRemoveCmd(rootCfg *validatorCfg, io commands.IO) *commands.Command { - return commands.NewCommand( - commands.Metadata{ - Name: "remove", - ShortUsage: "validator remove [flags]", - ShortHelp: "removes a validator from the genesis.json", - }, - commands.NewEmptyConfig(), - func(_ context.Context, _ []string) error { - return execValidatorRemove(rootCfg, io) - }, - ) -} - -func execValidatorRemove(cfg *validatorCfg, io commands.IO) error { - // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) - } - - // Check the validator address - address, err := crypto.AddressFromString(cfg.address) - if err != nil { - return fmt.Errorf("invalid validator address, %w", err) - } - - index := -1 - - for indx, validator := range genesis.Validators { - if validator.Address == address { - index = indx - - break - } - } - - if index < 0 { - return errors.New("validator not present in genesis.json") - } - - // Drop the validator - genesis.Validators = append(genesis.Validators[:index], genesis.Validators[index+1:]...) - - // Save the updated genesis - if err := genesis.SaveAs(cfg.genesisPath); err != nil { - return fmt.Errorf("unable to save genesis.json, %w", err) - } - - io.Printfln( - "Validator with address %s removed from genesis file", - cfg.address, - ) - - return nil -} diff --git a/gno.land/cmd/gnoland/genesis_validator_remove_test.go b/gno.land/cmd/gnoland/genesis_validator_remove_test.go deleted file mode 100644 index e73e867c5c3..00000000000 --- a/gno.land/cmd/gnoland/genesis_validator_remove_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package main - -import ( - "context" - "testing" - - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenesis_Validator_Remove(t *testing.T) { - t.Parallel() - - t.Run("invalid genesis file", func(t *testing.T) { - t.Parallel() - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "remove", - "--genesis-path", - "dummy-path", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) - }) - - t.Run("invalid validator address", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - genesis := getDefaultGenesis() - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "remove", - "--genesis-path", - tempGenesis.Name(), - "--address", - "dummyaddress", - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, "invalid validator address") - }) - - t.Run("validator not found", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - dummyKeys := getDummyKeys(t, 2) - genesis := getDefaultGenesis() - - // Set an existing validator - genesis.Validators = append(genesis.Validators, types.GenesisValidator{ - Address: dummyKeys[0].Address(), - PubKey: dummyKeys[0], - Power: 1, - Name: "example", - }) - - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "remove", - "--genesis-path", - tempGenesis.Name(), - "--address", - dummyKeys[1].Address().String(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorContains(t, cmdErr, errValidatorNotPresent.Error()) - }) - - t.Run("validator removed", func(t *testing.T) { - t.Parallel() - - tempGenesis, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - dummyKey := getDummyKey(t) - - genesis := getDefaultGenesis() - - // Set an existing validator - genesis.Validators = append(genesis.Validators, types.GenesisValidator{ - Address: dummyKey.Address(), - PubKey: dummyKey, - Power: 1, - Name: "example", - }) - - require.NoError(t, genesis.SaveAs(tempGenesis.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "validator", - "remove", - "--genesis-path", - tempGenesis.Name(), - "--address", - dummyKey.Address().String(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.NoError(t, cmdErr) - }) -} diff --git a/gno.land/cmd/gnoland/genesis_verify.go b/gno.land/cmd/gnoland/genesis_verify.go deleted file mode 100644 index 112b075a58c..00000000000 --- a/gno.land/cmd/gnoland/genesis_verify.go +++ /dev/null @@ -1,79 +0,0 @@ -package main - -import ( - "context" - "errors" - "flag" - "fmt" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" -) - -var errInvalidGenesisState = errors.New("invalid genesis state type") - -type verifyCfg struct { - commonCfg -} - -// newVerifyCmd creates the genesis verify subcommand -func newVerifyCmd(io commands.IO) *commands.Command { - cfg := &verifyCfg{} - - return commands.NewCommand( - commands.Metadata{ - Name: "verify", - ShortUsage: "verify [flags]", - ShortHelp: "verifies a genesis.json", - LongHelp: "Verifies a node's genesis.json", - }, - cfg, - func(_ context.Context, _ []string) error { - return execVerify(cfg, io) - }, - ) -} - -func (c *verifyCfg) RegisterFlags(fs *flag.FlagSet) { - c.commonCfg.RegisterFlags(fs) -} - -func execVerify(cfg *verifyCfg, io commands.IO) error { - // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) - } - - // Verify it - if validateErr := genesis.Validate(); validateErr != nil { - return fmt.Errorf("unable to verify genesis, %w", validateErr) - } - - // Validate the genesis state - if genesis.AppState != nil { - state, ok := genesis.AppState.(gnoland.GnoGenesisState) - if !ok { - return errInvalidGenesisState - } - - // Validate the initial transactions - for _, tx := range state.Txs { - if validateErr := tx.ValidateBasic(); validateErr != nil { - return fmt.Errorf("invalid transacton, %w", validateErr) - } - } - - // Validate the initial balances - for _, balance := range state.Balances { - if err := balance.Verify(); err != nil { - return fmt.Errorf("invalid balance: %w", err) - } - } - } - - io.Printfln("Genesis at %s is valid", cfg.genesisPath) - - return nil -} diff --git a/gno.land/cmd/gnoland/genesis_verify_test.go b/gno.land/cmd/gnoland/genesis_verify_test.go deleted file mode 100644 index 9c93519e495..00000000000 --- a/gno.land/cmd/gnoland/genesis_verify_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package main - -import ( - "context" - "testing" - "time" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto/mock" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gnolang/gno/tm2/pkg/testutils" - "github.com/stretchr/testify/require" -) - -func TestGenesis_Verify(t *testing.T) { - t.Parallel() - - getValidTestGenesis := func() *types.GenesisDoc { - key := mock.GenPrivKey().PubKey() - - return &types.GenesisDoc{ - GenesisTime: time.Now(), - ChainID: "valid-chain-id", - ConsensusParams: types.DefaultConsensusParams(), - Validators: []types.GenesisValidator{ - { - Address: key.Address(), - PubKey: key, - Power: 1, - Name: "valid validator", - }, - }, - } - } - - t.Run("invalid txs", func(t *testing.T) { - t.Parallel() - - tempFile, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - g := getValidTestGenesis() - - g.AppState = gnoland.GnoGenesisState{ - Balances: []gnoland.Balance{}, - Txs: []std.Tx{ - {}, - }, - } - - require.NoError(t, g.SaveAs(tempFile.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "verify", - "--genesis-path", - tempFile.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.Error(t, cmdErr) - }) - - t.Run("invalid balances", func(t *testing.T) { - t.Parallel() - - tempFile, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - g := getValidTestGenesis() - - g.AppState = gnoland.GnoGenesisState{ - Balances: []gnoland.Balance{ - {}, - }, - Txs: []std.Tx{}, - } - - require.NoError(t, g.SaveAs(tempFile.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "verify", - "--genesis-path", - tempFile.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.Error(t, cmdErr) - }) - - t.Run("valid genesis", func(t *testing.T) { - t.Parallel() - - tempFile, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - g := getValidTestGenesis() - g.AppState = gnoland.GnoGenesisState{ - Balances: []gnoland.Balance{}, - Txs: []std.Tx{}, - } - - require.NoError(t, g.SaveAs(tempFile.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "verify", - "--genesis-path", - tempFile.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - }) - - t.Run("valid genesis, no state", func(t *testing.T) { - t.Parallel() - - tempFile, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - g := getValidTestGenesis() - require.NoError(t, g.SaveAs(tempFile.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "verify", - "--genesis-path", - tempFile.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.NoError(t, cmdErr) - }) - - t.Run("invalid genesis state", func(t *testing.T) { - t.Parallel() - - tempFile, cleanup := testutils.NewTestFile(t) - t.Cleanup(cleanup) - - g := getValidTestGenesis() - g.AppState = "Totally invalid state" - require.NoError(t, g.SaveAs(tempFile.Name())) - - // Create the command - cmd := newRootCmd(commands.NewTestIO()) - args := []string{ - "genesis", - "verify", - "--genesis-path", - tempFile.Name(), - } - - // Run the command - cmdErr := cmd.ParseAndRun(context.Background(), args) - require.Error(t, cmdErr) - }) -} diff --git a/gno.land/cmd/gnoland/integration_test.go b/gno.land/cmd/gnoland/integration_test.go deleted file mode 100644 index 37451df9704..00000000000 --- a/gno.land/cmd/gnoland/integration_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "testing" - - "github.com/gnolang/gno/gno.land/pkg/integration" -) - -func TestTestdata(t *testing.T) { - integration.RunGnolandTestscripts(t, "testdata") -} diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index 8df716b1fed..c6143ab9cd3 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -5,12 +5,8 @@ import ( "os" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/peterbourgon/ff/v3" - "github.com/peterbourgon/ff/v3/fftoml" ) -const flagConfigFlag = "flag-config-path" - func main() { cmd := newRootCmd(commands.NewDefaultIO()) @@ -21,11 +17,7 @@ func newRootCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ ShortUsage: " [flags] [...]", - ShortHelp: "starts the gnoland blockchain node", - Options: []ff.Option{ - ff.WithConfigFileFlag(flagConfigFlag), - ff.WithConfigFileParser(fftoml.Parser), - }, + ShortHelp: "manages the gnoland blockchain node", }, commands.NewEmptyConfig(), commands.HelpExec, @@ -33,7 +25,6 @@ func newRootCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newStartCmd(io), - newGenesisCmd(io), newSecretsCmd(io), newConfigCmd(io), ) diff --git a/gno.land/cmd/gnoland/secrets_common.go b/gno.land/cmd/gnoland/secrets_common.go index d40e90f6b48..500336e3489 100644 --- a/gno.land/cmd/gnoland/secrets_common.go +++ b/gno.land/cmd/gnoland/secrets_common.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" ) var ( @@ -54,7 +54,7 @@ func isValidDirectory(dirPath string) bool { } type secretData interface { - privval.FilePVKey | privval.FilePVLastSignState | p2p.NodeKey + privval.FilePVKey | privval.FilePVLastSignState | types.NodeKey } // readSecretData reads the secret data from the given path @@ -145,7 +145,7 @@ func validateValidatorStateSignature( } // validateNodeKey validates the node's p2p key -func validateNodeKey(key *p2p.NodeKey) error { +func validateNodeKey(key *types.NodeKey) error { if key.PrivKey == nil { return errInvalidNodeKey } diff --git a/gno.land/cmd/gnoland/secrets_common_test.go b/gno.land/cmd/gnoland/secrets_common_test.go index 34592c3bd8f..38c4772c705 100644 --- a/gno.land/cmd/gnoland/secrets_common_test.go +++ b/gno.land/cmd/gnoland/secrets_common_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -26,7 +26,7 @@ func TestCommon_SaveReadData(t *testing.T) { t.Run("invalid data read path", func(t *testing.T) { t.Parallel() - readData, err := readSecretData[p2p.NodeKey]("") + readData, err := readSecretData[types.NodeKey]("") assert.Nil(t, readData) assert.ErrorContains( @@ -44,7 +44,7 @@ func TestCommon_SaveReadData(t *testing.T) { require.NoError(t, saveSecretData("totally valid key", path)) - readData, err := readSecretData[p2p.NodeKey](path) + readData, err := readSecretData[types.NodeKey](path) require.Nil(t, readData) assert.ErrorContains(t, err, "unable to unmarshal data") @@ -59,7 +59,7 @@ func TestCommon_SaveReadData(t *testing.T) { require.NoError(t, saveSecretData(key, path)) - readKey, err := readSecretData[p2p.NodeKey](path) + readKey, err := readSecretData[types.NodeKey](path) require.NoError(t, err) assert.Equal(t, key, readKey) diff --git a/gno.land/cmd/gnoland/secrets_get.go b/gno.land/cmd/gnoland/secrets_get.go index 8d111516816..0a0a714f6ee 100644 --- a/gno.land/cmd/gnoland/secrets_get.go +++ b/gno.land/cmd/gnoland/secrets_get.go @@ -12,7 +12,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" ) var errInvalidSecretsGetArgs = errors.New("invalid number of secrets get arguments provided") @@ -169,7 +169,7 @@ func readValidatorState(path string) (*validatorStateInfo, error) { // readNodeID reads the node p2p info from the given path func readNodeID(path string) (*nodeIDInfo, error) { - nodeKey, err := readSecretData[p2p.NodeKey](path) + nodeKey, err := readSecretData[types.NodeKey](path) if err != nil { return nil, fmt.Errorf("unable to read node key, %w", err) } @@ -199,7 +199,7 @@ func readNodeID(path string) (*nodeIDInfo, error) { // constructP2PAddress constructs the P2P address other nodes can use // to connect directly -func constructP2PAddress(nodeID p2p.ID, listenAddress string) string { +func constructP2PAddress(nodeID types.ID, listenAddress string) string { var ( address string parts = strings.SplitN(listenAddress, "://", 2) diff --git a/gno.land/cmd/gnoland/secrets_get_test.go b/gno.land/cmd/gnoland/secrets_get_test.go index 66e6e3509fc..3dfe0c727dd 100644 --- a/gno.land/cmd/gnoland/secrets_get_test.go +++ b/gno.land/cmd/gnoland/secrets_get_test.go @@ -13,7 +13,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -66,7 +66,7 @@ func TestSecrets_Get_All(t *testing.T) { // Get the node key nodeKeyPath := filepath.Join(tempDir, defaultNodeKeyName) - nodeKey, err := readSecretData[p2p.NodeKey](nodeKeyPath) + nodeKey, err := readSecretData[types.NodeKey](nodeKeyPath) require.NoError(t, err) // Get the validator private key diff --git a/gno.land/cmd/gnoland/secrets_init.go b/gno.land/cmd/gnoland/secrets_init.go index 58dd0783f66..9a7ddd106c3 100644 --- a/gno.land/cmd/gnoland/secrets_init.go +++ b/gno.land/cmd/gnoland/secrets_init.go @@ -12,7 +12,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" ) var errOverwriteNotEnabled = errors.New("overwrite not enabled") @@ -200,10 +200,6 @@ func generateLastSignValidatorState() *privval.FilePVLastSignState { } // generateNodeKey generates the p2p node key -func generateNodeKey() *p2p.NodeKey { - privKey := ed25519.GenPrivKey() - - return &p2p.NodeKey{ - PrivKey: privKey, - } +func generateNodeKey() *types.NodeKey { + return types.GenerateNodeKey() } diff --git a/gno.land/cmd/gnoland/secrets_init_test.go b/gno.land/cmd/gnoland/secrets_init_test.go index 20e061447f5..7be3650fb4b 100644 --- a/gno.land/cmd/gnoland/secrets_init_test.go +++ b/gno.land/cmd/gnoland/secrets_init_test.go @@ -7,7 +7,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -37,7 +37,7 @@ func verifyValidatorState(t *testing.T, path string) { func verifyNodeKey(t *testing.T, path string) { t.Helper() - nodeKey, err := readSecretData[p2p.NodeKey](path) + nodeKey, err := readSecretData[types.NodeKey](path) require.NoError(t, err) assert.NoError(t, validateNodeKey(nodeKey)) diff --git a/gno.land/cmd/gnoland/secrets_verify.go b/gno.land/cmd/gnoland/secrets_verify.go index 32e563c1c6f..15fef6649ec 100644 --- a/gno.land/cmd/gnoland/secrets_verify.go +++ b/gno.land/cmd/gnoland/secrets_verify.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" ) type secretsVerifyCfg struct { @@ -146,7 +146,7 @@ func readAndVerifyValidatorState(path string, io commands.IO) (*privval.FilePVLa // readAndVerifyNodeKey reads the node p2p key from the given path and verifies it func readAndVerifyNodeKey(path string, io commands.IO) error { - nodeKey, err := readSecretData[p2p.NodeKey](path) + nodeKey, err := readSecretData[types.NodeKey](path) if err != nil { return fmt.Errorf("unable to read node p2p key, %w", err) } diff --git a/gno.land/cmd/gnoland/secrets_verify_test.go b/gno.land/cmd/gnoland/secrets_verify_test.go index 513d7c8b503..67630aaaa4a 100644 --- a/gno.land/cmd/gnoland/secrets_verify_test.go +++ b/gno.land/cmd/gnoland/secrets_verify_test.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/privval" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/p2p/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -347,7 +347,7 @@ func TestSecrets_Verify_Single(t *testing.T) { dirPath := t.TempDir() path := filepath.Join(dirPath, defaultNodeKeyName) - invalidNodeKey := &p2p.NodeKey{ + invalidNodeKey := &types.NodeKey{ PrivKey: nil, // invalid } diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 21f0cb4b1a6..4f380031be4 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -14,6 +14,7 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/log" "github.com/gnolang/gno/gnovm/pkg/gnoenv" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" @@ -25,6 +26,8 @@ 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/std" "github.com/gnolang/gno/tm2/pkg/telemetry" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -42,18 +45,20 @@ 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 { - 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 - genesisBalancesFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 - genesisTxsFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 - genesisRemote string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 - genesisFile string - chainID string - dataDir string - genesisMaxVMCycles int64 - config string - lazyInit bool + 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 + genesisBalancesFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + genesisTxsFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + genesisRemote string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952 + genesisFile string + chainID string + dataDir string + lazyInit bool logLevel string logFormat string @@ -79,7 +84,6 @@ func newStartCmd(io commands.IO) *commands.Command { func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { gnoroot := gnoenv.RootDir() defaultGenesisBalancesFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt") - defaultGenesisTxsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.jsonl") fs.BoolVar( &c.skipFailingGenesisTxs, @@ -88,6 +92,13 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { "don't panic when replaying invalid genesis txs", ) + fs.BoolVar( + &c.skipGenesisSigVerification, + "skip-genesis-sig-verification", + false, + "don't panic when replaying invalidly signed genesis txs", + ) + fs.StringVar( &c.genesisBalancesFile, "genesis-balances-file", @@ -98,7 +109,7 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.genesisTxsFile, "genesis-txs-file", - defaultGenesisTxsFile, + "", "initial txs to replay", ) @@ -137,20 +148,6 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { "replacement for '%%REMOTE%%' in genesis", ) - fs.Int64Var( - &c.genesisMaxVMCycles, - "genesis-max-vm-cycles", - 100_000_000, - "set maximum allowed vm cycles per operation. Zero means no limit.", - ) - - fs.StringVar( - &c.config, - flagConfigFlag, - "", - "the flag config file (optional)", - ) - fs.StringVar( &c.logLevel, "log-level", @@ -225,7 +222,7 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { ) // Init a new genesis.json - if err := lazyInitGenesis(io, c, genesisPath, privateKey.GetPubKey()); err != nil { + if err := lazyInitGenesis(io, c, genesisPath, privateKey.Key.PrivKey); err != nil { return fmt.Errorf("unable to initialize genesis.json, %w", err) } } @@ -242,9 +239,19 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error { // Create a top-level shared event switch evsw := events.NewEventSwitch() + minGasPrices := cfg.Application.MinGasPrices // Create application and node - cfg.LocalApp, err = gnoland.NewApp(nodeDir, c.skipFailingGenesisTxs, evsw, logger) + cfg.LocalApp, err = gnoland.NewApp( + nodeDir, + gnoland.GenesisAppConfig{ + SkipFailingTxs: c.skipFailingGenesisTxs, + SkipSigVerification: c.skipGenesisSigVerification, + }, + evsw, + logger, + minGasPrices, + ) if err != nil { return fmt.Errorf("unable to create the Gnoland app, %w", err) } @@ -340,7 +347,7 @@ func lazyInitGenesis( io commands.IO, c *startCfg, genesisPath string, - publicKey crypto.PubKey, + privateKey crypto.PrivKey, ) error { // Check if the genesis.json is present if osm.FileExists(genesisPath) { @@ -348,7 +355,7 @@ func lazyInitGenesis( } // Generate the new genesis.json file - if err := generateGenesisFile(genesisPath, publicKey, c); err != nil { + if err := generateGenesisFile(genesisPath, privateKey, c); err != nil { return fmt.Errorf("unable to generate genesis file, %w", err) } @@ -373,24 +380,38 @@ func initializeLogger(io io.WriteCloser, logLevel, logFormat string) (*zap.Logge return log.GetZapLoggerFn(format)(io, level), nil } -func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) error { +func generateGenesisFile(genesisFile string, privKey crypto.PrivKey, c *startCfg) error { + var ( + pubKey = privKey.PubKey() + // There is an active constraint for gno.land transactions: + // + // All transaction messages' (MsgSend, MsgAddPkg...) "author" field, + // specific to the message type ("creator", "sender"...), must match + // the signature address contained in the transaction itself. + // This means that if MsgSend is originating from address A, + // the owner of the private key for address A needs to sign the transaction + // containing the message. Every message in a transaction needs to + // originate from the same account that signed the transaction + txSender = pubKey.Address() + ) + gen := &bft.GenesisDoc{} gen.GenesisTime = time.Now() gen.ChainID = c.chainID gen.ConsensusParams = abci.ConsensusParams{ Block: &abci.BlockParams{ // TODO: update limits. - MaxTxBytes: 1_000_000, // 1MB, - MaxDataBytes: 2_000_000, // 2MB, - MaxGas: 100_000_000, // 100M gas - TimeIotaMS: 100, // 100ms + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 3_000_000_000, // 3B gas + TimeIotaMS: 100, // 100ms }, } gen.Validators = []bft.GenesisValidator{ { - Address: pk.Address(), - PubKey: pk, + Address: pubKey.Address(), + PubKey: pubKey, Power: 10, Name: "testvalidator", }, @@ -404,25 +425,46 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro // Load examples folder examplesDir := filepath.Join(c.gnoRootDir, "examples") - pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, genesisDeployAddress, genesisDeployFee) + pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, txSender, genesisDeployFee) if err != nil { return fmt.Errorf("unable to load examples folder: %w", err) } // Load Genesis TXs - genesisTxs, err := gnoland.LoadGenesisTxsFile(c.genesisTxsFile, c.chainID, c.genesisRemote) - if err != nil { - return fmt.Errorf("unable to load genesis txs file: %w", err) + var genesisTxs []gnoland.TxWithMetadata + + if c.genesisTxsFile != "" { + genesisTxs, err = gnoland.LoadGenesisTxsFile(c.genesisTxsFile, c.chainID, c.genesisRemote) + if err != nil { + return fmt.Errorf("unable to load genesis txs file: %w", err) + } } genesisTxs = append(pkgsTxs, genesisTxs...) - // Construct genesis AppState. - gen.AppState = gnoland.GnoGenesisState{ - Balances: balances, - Txs: genesisTxs, + // Sign genesis transactions, with the default key (test1) + if err = gnoland.SignGenesisTxs(genesisTxs, privKey, c.chainID); err != nil { + return fmt.Errorf("unable to sign genesis txs: %w", err) } + // Make sure the genesis transaction author has sufficient + // balance to cover transaction deployments in genesis. + // + // During the init-chainer process, the account that authors the + // genesis transactions needs to have a sufficient balance + // to cover outstanding transaction costs. + // Since the cost can't be estimated upfront at this point, the balance + // set is an arbitrary value based on a "best guess" basis. + // There should be a larger discussion if genesis transactions should consume gas, at all + deployerBalance := int64(len(genesisTxs)) * 10_000_000 // ~10 GNOT per tx + balances.Set(txSender, std.NewCoins(std.NewCoin("ugnot", deployerBalance))) + + // Construct genesis AppState. + defaultGenState := gnoland.DefaultGenState() + defaultGenState.Balances = balances.List() + defaultGenState.Txs = genesisTxs + gen.AppState = defaultGenState + // Write genesis state if err := gen.SaveAs(genesisFile); err != nil { return fmt.Errorf("unable to write genesis file %q: %w", genesisFile, err) diff --git a/gno.land/cmd/gnoland/testdata/addpkg.txtar b/gno.land/cmd/gnoland/testdata/addpkg.txtar deleted file mode 100644 index 6249d2ff7a0..00000000000 --- a/gno.land/cmd/gnoland/testdata/addpkg.txtar +++ /dev/null @@ -1,26 +0,0 @@ -# test for add package - -# load hello.gno package located in $WORK directory as gno.land/r/hello -loadpkg gno.land/r/hello $WORK - -## start a new node -gnoland start - -## execute SayHello -gnokey maketx call -pkgpath gno.land/r/hello -func SayHello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 - -## compare SayHello -stdout '\("hello world!" string\)' -stdout OK! -stdout 'GAS WANTED: 2000000' -stdout 'GAS USED: \d+' -stdout 'HEIGHT: \d+' -stdout 'EVENTS: \[\]' -stdout 'TX HASH: ' - --- hello.gno -- -package hello - -func SayHello() string { - return "hello world!" -} diff --git a/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar b/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar deleted file mode 100644 index 5a88fd6d603..00000000000 --- a/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar +++ /dev/null @@ -1,88 +0,0 @@ -loadpkg gno.land/r/demo/users -loadpkg gno.land/r/sys/users - -adduser admin -adduser gui - -patchpkg "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" $USER_ADDR_admin # use our custom admin - -gnoland start - -## When `sys/users` is disabled - -# Should be disabled by default, addpkg should work by default - -# Check if sys/users is disabled -# gui call -> sys/users.IsEnable -gnokey maketx call -pkgpath gno.land/r/sys/users -func IsEnabled -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test gui -stdout 'OK!' -stdout 'false' - -# Gui should be able to addpkg on test1 addr -# gui addpkg -> gno.land/r//mysuperpkg -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_test1/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test gui -stdout 'OK!' - -# Gui should be able to addpkg on random name -# gui addpkg -> gno.land/r/randomname/mysuperpkg -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/randomname/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test gui -stdout 'OK!' - -## When `sys/users` is enabled - -# Enable `sys/users` -# admin call -> sys/users.AdminEnable -gnokey maketx call -pkgpath gno.land/r/sys/users -func AdminEnable -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test admin -stdout 'OK!' - -# Check that `sys/users` has been enabled -# gui call -> sys/users.IsEnable -gnokey maketx call -pkgpath gno.land/r/sys/users -func IsEnabled -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test gui -stdout 'OK!' -stdout 'true' - -# Try to add a pkg an with unregistered user -# gui addpkg -> gno.land/r//one -! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_test1/one -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test gui -stderr 'unauthorized user' - -# Try to add a pkg with an unregistered user, on their own address as namespace -# gui addpkg -> gno.land/r//one -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_gui/one -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test gui -stdout 'OK!' - -## Test unregistered namespace - -# Call addpkg with admin user on gui namespace -# admin addpkg -> gno.land/r/guiland/one -! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/one -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test admin -stderr 'unauthorized user' - -## Test registered namespace - -# Test admin invites gui -# admin call -> demo/users.Invite -gnokey maketx call -pkgpath gno.land/r/demo/users -func Invite -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -args $USER_ADDR_gui admin -stdout 'OK!' - -# test gui register namespace -# gui call -> demo/users.Register -gnokey maketx call -pkgpath gno.land/r/demo/users -func Register -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -args $USER_ADDR_admin -args 'guiland' -args 'im gui' gui -stdout 'OK!' - -# Test gui publishing on guiland/one -# gui addpkg -> gno.land/r/guiland/one -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/one -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test gui -stdout 'OK!' - -# Test admin publishing on guiland/two -# admin addpkg -> gno.land/r/guiland/two -! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/two -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test admin -stderr 'unauthorized user' - --- one.gno -- -package one - -func Render(path string) string { - return "# Hello One" -} diff --git a/gno.land/cmd/gnoland/testdata/append.txtar b/gno.land/cmd/gnoland/testdata/append.txtar deleted file mode 100644 index 46b66f9524b..00000000000 --- a/gno.land/cmd/gnoland/testdata/append.txtar +++ /dev/null @@ -1,131 +0,0 @@ -loadpkg gno.land/p/demo/ufmt - -# start a new node -gnoland start - -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/append -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout OK! - -# Call Append 1 -gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 2000000 -args '1' -broadcast -chainid=tendermint_test test1 -stdout OK! - -gnokey maketx call -pkgpath gno.land/r/append -func AppendNil -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout OK! - -# Call Append 2 -gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 2000000 -args '2' -broadcast -chainid=tendermint_test test1 -stdout OK! - -# Call Append 3 -gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 2000000 -args '3' -broadcast -chainid=tendermint_test test1 -stdout OK! - -# Call render -gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 -stdout '("1-2-3-" string)' -stdout OK! - -# Call Pop -gnokey maketx call -pkgpath gno.land/r/append -func Pop -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout OK! - -# Call render -gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 -stdout '("2-3-" string)' -stdout OK! - -# Call Append 42 -gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 2000000 -args '42' -broadcast -chainid=tendermint_test test1 -stdout OK! - -# Call render -gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 -stdout '("2-3-42-" string)' -stdout OK! - -gnokey maketx call -pkgpath gno.land/r/append -func CopyAppend -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout OK! - -gnokey maketx call -pkgpath gno.land/r/append -func PopB -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout OK! - -# Call render -gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 -stdout '("2-3-42-" string)' -stdout OK! - -gnokey maketx call -pkgpath gno.land/r/append -func AppendMoreAndC -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout OK! - -gnokey maketx call -pkgpath gno.land/r/append -func ReassignC -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout OK! - -gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 -stdout '("2-3-42-70-100-" string)' -stdout OK! - -gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args 'd' -broadcast -chainid=tendermint_test test1 -stdout '("1-" string)' -stdout OK! - --- append.gno -- -package append - -import ( - "gno.land/p/demo/ufmt" -) - -type T struct{ i int } - -var a, b, d []T -var c = []T{{i: 100}} - - -func init() { - a = make([]T, 0, 1) -} - -func Pop() { - a = append(a[:0], a[1:]...) -} - -func Append(i int) { - a = append(a, T{i: i}) -} - -func CopyAppend() { - b = append(a, T{i: 50}, T{i: 60}) -} - -func PopB() { - b = append(b[:0], b[1:]...) -} - -func AppendMoreAndC() { - // Fill to capacity - a = append(a, T{i: 70}) - // Above capacity; make new array - a = append(a, c...) -} - -func ReassignC() { - c[0] = T{i: 200} -} - -func AppendNil() { - d = append(d, a...) -} - -func Render(path string) string { - source := a - if path == "d" { - source = d - } - - var s string - for i:=0;i myrlm.A: PANIC -! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -stderr 'invalid non-origin call' - -## 2. MsgCall -> myrlm.B: PASS -gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -stdout 'OK!' - -## 3. MsgCall -> myrlm.C: PASS -gnokey maketx call -pkgpath gno.land/r/myrlm -func C -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -stdout 'OK!' - -## 4. MsgCall -> r/foo.A -> myrlm.A: PANIC -! gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -stderr 'invalid non-origin call' - -## 5. MsgCall -> r/foo.B -> myrlm.B: PASS -gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -stdout 'OK!' - -## 6. MsgCall -> r/foo.C -> myrlm.C: PANIC -! gnokey maketx call -pkgpath gno.land/r/foo -func C -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -stderr 'invalid non-origin call' - -## remove due to update to maketx call can only call realm (case 7,8,9) -## 7. MsgCall -> p/demo/bar.A -> myrlm.A: PANIC -## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -## stderr 'invalid non-origin call' - -## 8. MsgCall -> p/demo/bar.B -> myrlm.B: PASS -## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -## stdout 'OK!' - -## 9. MsgCall -> p/demo/bar.C -> myrlm.C: PANIC -## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -## stderr 'invalid non-origin call' - -## 10. MsgRun -> run.main -> myrlm.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno -stderr 'invalid non-origin call' - -## 11. MsgRun -> run.main -> myrlm.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno -stdout 'OK!' - -## 12. MsgRun -> run.main -> myrlm.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmC.gno -stderr 'invalid non-origin call' - -## 13. MsgRun -> run.main -> foo.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno -stderr 'invalid non-origin call' - -## 14. MsgRun -> run.main -> foo.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno -stdout 'OK!' - -## 15. MsgRun -> run.main -> foo.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooC.gno -stderr 'invalid non-origin call' - -## 16. MsgRun -> run.main -> bar.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno -stderr 'invalid non-origin call' - -## 17. MsgRun -> run.main -> bar.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno -stdout 'OK!' - -## 18. MsgRun -> run.main -> bar.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barC.gno -stderr 'invalid non-origin call' - -## remove testcase 19 due to maketx call forced to call a realm -## 19. MsgCall -> std.AssertOriginCall: pass -## gnokey maketx call -pkgpath std -func AssertOriginCall -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -## stdout 'OK!' - -## 20. MsgRun -> std.AssertOriginCall: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno -stderr 'invalid non-origin call' - - --- r/myrlm/rlm.gno -- -package myrlm - -import "std" - -func A() { - C() -} - -func B() { - if false { - C() - } -} - -func C() { - std.AssertOriginCall() -} --- r/foo/foo.gno -- -package foo - -import "gno.land/r/myrlm" - -func A() { - myrlm.A() -} - -func B() { - myrlm.B() -} - -func C() { - myrlm.C() -} --- p/demo/bar/bar.gno -- -package bar - -import "gno.land/r/myrlm" - -func A() { - myrlm.A() -} - -func B() { - myrlm.B() -} - -func C() { - myrlm.C() -} --- run/myrlmA.gno -- -package main - -import myrlm "gno.land/r/myrlm" - -func main() { - myrlm.A() -} --- run/myrlmB.gno -- -package main - -import "gno.land/r/myrlm" - -func main() { - myrlm.B() -} --- run/myrlmC.gno -- -package main - -import "gno.land/r/myrlm" - -func main() { - myrlm.C() -} --- run/fooA.gno -- -package main - -import "gno.land/r/foo" - -func main() { - foo.A() -} --- run/fooB.gno -- -package main - -import "gno.land/r/foo" - -func main() { - foo.B() -} --- run/fooC.gno -- -package main - -import "gno.land/r/foo" - -func main() { - foo.C() -} --- run/barA.gno -- -package main - -import "gno.land/p/demo/bar" - -func main() { - bar.A() -} --- run/barB.gno -- -package main - -import "gno.land/p/demo/bar" - -func main() { - bar.B() -} --- run/barC.gno -- -package main - -import "gno.land/p/demo/bar" - -func main() { - bar.C() -} --- run/baz.gno -- -package main - -import "std" - -func main() { - std.AssertOriginCall() -} diff --git a/gno.land/cmd/gnoland/testdata/event_multi_msg.txtar b/gno.land/cmd/gnoland/testdata/event_multi_msg.txtar deleted file mode 100644 index 84afe3cc6a4..00000000000 --- a/gno.land/cmd/gnoland/testdata/event_multi_msg.txtar +++ /dev/null @@ -1,50 +0,0 @@ -# load the package from $WORK directory -loadpkg gno.land/r/demo/simple_event $WORK/event - -# start a new node -gnoland start - -## test1 account should be available on default -gnokey query auth/accounts/${USER_ADDR_test1} -stdout 'height: 0' -stdout 'data: {' -stdout ' "BaseAccount": {' -stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",' -stdout ' "coins": "[0-9]*ugnot",' # dynamic -stdout ' "public_key": null,' -stdout ' "account_number": "0",' -stdout ' "sequence": "0"' -stdout ' }' -stdout '}' -! stderr '.+' # empty - - -## sign -gnokey sign -tx-path $WORK/multi/multi_msg.tx -chainid=tendermint_test -account-number 0 -account-sequence 0 test1 -stdout 'Tx successfully signed and saved to ' - -## broadcast -gnokey broadcast $WORK/multi/multi_msg.tx -quiet=false - -stdout OK! -stdout 'GAS WANTED: 2000000' -stdout 'GAS USED: [0-9]+' -stdout 'HEIGHT: [0-9]+' -stdout 'EVENTS: \[{\"type\":\"TAG\",\"attrs\":\[{\"key\":\"KEY\",\"value\":\"value11\"}\],\"pkg_path\":\"gno.land\/r\/demo\/simple_event\",\"func\":\"Event\"},{\"type\":\"TAG\",\"attrs\":\[{\"key\":\"KEY\",\"value\":\"value22\"}\],\"pkg_path\":\"gno.land\/r\/demo\/simple_event\",\"func\":\"Event\"}\]' - - - --- event/simple_event.gno -- -package simple_event - -import ( - "std" -) - -func Event(value string) { - std.Emit("TAG", "KEY", value) -} - --- multi/multi_msg.tx -- -{"msg":[{"@type":"/vm.m_call","caller":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value11"]},{"@type":"/vm.m_call","caller":"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5","send":"","pkg_path":"gno.land/r/demo/simple_event","func":"Event","args":["value22"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":null,"memo":""} - diff --git a/gno.land/cmd/gnoland/testdata/ghverify.txtar b/gno.land/cmd/gnoland/testdata/ghverify.txtar deleted file mode 100644 index f8cd05c762f..00000000000 --- a/gno.land/cmd/gnoland/testdata/ghverify.txtar +++ /dev/null @@ -1,39 +0,0 @@ -loadpkg gno.land/r/gnoland/ghverify - -# start the node -gnoland start - -# make a verification request -gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func RequestVerification -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout OK! - -# request tasks to complete (this is done by the agent) -gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'request' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout '\("\[\{\\"id\\":\\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\\",\\"type\\":\\"0\\",\\"value_type\\":\\"string\\",\\"tasks\\":\[\{\\"gno_address\\":\\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\\",\\"github_handle\\":\\"deelawn\\"\}\]\}\]" string\)' - -# a verification request was made but there should be no verified address -gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetHandleByAddress -args 'g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout "" - -# a verification request was made but there should be no verified handle -gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetAddressByHandle -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout "" - -# fail on ingestion with a bad task ID -! gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'ingest,a' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stderr 'invalid ingest id: a' - -# the agent publishes their response to the task and the verification is complete -gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'ingest,g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5,OK' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout OK! - -# get verified github handle by gno address -gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetHandleByAddress -args 'g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout "deelawn" - -# get verified gno address by github handle -gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetAddressByHandle -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" - -gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func Render -args '' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout '\("\{\\"deelawn\\": \\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\\"\}" string\)' \ No newline at end of file diff --git a/gno.land/cmd/gnoland/testdata/gnoweb_airgapped.txtar b/gno.land/cmd/gnoland/testdata/gnoweb_airgapped.txtar deleted file mode 100644 index 3ed35a1b1d3..00000000000 --- a/gno.land/cmd/gnoland/testdata/gnoweb_airgapped.txtar +++ /dev/null @@ -1,38 +0,0 @@ -# This test ensures that the "full security with airgap" commands, on gnoweb's -# help page, work as intended. - -# load the package from $WORK directory -loadpkg gno.land/r/demo/echo - -# start the node -gnoland start - -# Query account -gnokey query auth/accounts/${USER_ADDR_test1} -stdout 'height: 0' -stdout 'data: {' -stdout ' "BaseAccount": {' -stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",' -stdout ' "coins": "[0-9]*ugnot",' # dynamic -stdout ' "public_key": null,' -stdout ' "account_number": "0",' -stdout ' "sequence": "0"' -stdout ' }' -stdout '}' -! stderr '.+' # empty - -# Create transaction -gnokey maketx call -pkgpath "gno.land/r/demo/echo" -func "Render" -gas-fee 1000000ugnot -gas-wanted 2000000 -send "" -args "HELLO" test1 -cp stdout call.tx - -# Sign -gnokey sign -tx-path $WORK/call.tx -chainid "tendermint_test" -account-number 0 -account-sequence 0 test1 -cmpenv stdout sign.stdout.golden - -gnokey broadcast $WORK/call.tx -stdout '("HELLO" string)' -stdout 'GAS WANTED: 2000000' - --- sign.stdout.golden -- - -Tx successfully signed and saved to $WORK/call.tx diff --git a/gno.land/cmd/gnoland/testdata/grc20_invalid_address.txtar b/gno.land/cmd/gnoland/testdata/grc20_invalid_address.txtar deleted file mode 100644 index da903315333..00000000000 --- a/gno.land/cmd/gnoland/testdata/grc20_invalid_address.txtar +++ /dev/null @@ -1,12 +0,0 @@ -# Test for https://github.com/gnolang/gno/pull/1799 -loadpkg gno.land/r/demo/foo20 - -gnoland start - -# execute Faucet -gnokey maketx call -pkgpath gno.land/r/demo/foo20 -func Faucet -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout 'OK!' - -# execute Transfer for invalid address -! gnokey maketx call -pkgpath gno.land/r/demo/foo20 -func Transfer -args g1ubwj0apf60hd90txhnh855fkac34rxlsvua0aa -args 1 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stderr '"gnokey" error: --= Error =--\nData: invalid address' \ No newline at end of file diff --git a/gno.land/cmd/gnoland/testdata/prevrealm.txtar b/gno.land/cmd/gnoland/testdata/prevrealm.txtar deleted file mode 100644 index 72a207fae22..00000000000 --- a/gno.land/cmd/gnoland/testdata/prevrealm.txtar +++ /dev/null @@ -1,184 +0,0 @@ -# This tests ensure the consistency of the std.PrevRealm function, in the -# following situations: -# -# -# | Num | Msg Type | Call from | Entry Point | Result | -# |-----|:--------:|:-------------------:|:---------------:|:------------:| -# | 1 | MsgCall | wallet direct | myrlm.A() | user address | -# | 2 | | | myrlm.B() | user address | -# | 3 | | through /r/foo | myrlm.A() | r/foo | -# | 4 | | | myrlm.B() | r/foo | -# | 5 | | through /p/demo/bar | myrlm.A() | user address | -# | 6 | | | myrlm.B() | user address | -# | 7 | MsgRun | wallet direct | myrlm.A() | user address | -# | 8 | | | myrlm.B() | user address | -# | 9 | | through /r/foo | myrlm.A() | r/foo | -# | 10 | | | myrlm.B() | r/foo | -# | 11 | | through /p/demo/bar | myrlm.A() | user address | -# | 12 | | | myrlm.B() | user address | -# | 13 | MsgCall | wallet direct | std.PrevRealm() | user address | -# | 14 | MsgRun | wallet direct | std.PrevRealm() | user address | - -# Init -## deploy myrlm -loadpkg gno.land/r/myrlm $WORK/r/myrlm -## deploy r/foo -loadpkg gno.land/r/foo $WORK/r/foo -## deploy p/demo/bar -loadpkg gno.land/p/demo/bar $WORK/p/demo/bar - -## start a new node -gnoland start - -env RFOO_ADDR=g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469 - -# Test cases -## 1. MsgCall -> myrlm.A: user address -gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -stdout ${USER_ADDR_test1} - -## 2. MsgCall -> myrealm.B -> myrlm.A: user address -gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -stdout ${USER_ADDR_test1} - -## 3. MsgCall -> r/foo.A -> myrlm.A: r/foo -gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -stdout ${RFOO_ADDR} - -## 4. MsgCall -> r/foo.B -> myrlm.B -> r/foo.A: r/foo -gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -stdout ${RFOO_ADDR} - -## remove due to update to maketx call can only call realm (case 5, 6, 13) -## 5. MsgCall -> p/demo/bar.A -> myrlm.A: user address -## gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -## stdout ${USER_ADDR_test1} - -## 6. MsgCall -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address -## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -## stdout ${USER_ADDR_test1} - -## 7. MsgRun -> myrlm.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno -stdout ${USER_ADDR_test1} - -## 8. MsgRun -> myrealm.B -> myrlm.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno -stdout ${USER_ADDR_test1} - -## 9. MsgRun -> r/foo.A -> myrlm.A: r/foo -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno -stdout ${RFOO_ADDR} - -## 10. MsgRun -> r/foo.B -> myrlm.B -> r/foo.A: r/foo -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno -stdout ${RFOO_ADDR} - -## 11. MsgRun -> p/demo/bar.A -> myrlm.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno -stdout ${USER_ADDR_test1} - -## 12. MsgRun -> p/demo/bar.B -> myrlm.B -> r/foo.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno -stdout ${USER_ADDR_test1} - -## 13. MsgCall -> std.PrevRealm(): user address -## gnokey maketx call -pkgpath std -func PrevRealm -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 -## stdout ${USER_ADDR_test1} - -## 14. MsgRun -> std.PrevRealm(): user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno -stdout ${USER_ADDR_test1} - --- r/myrlm/myrlm.gno -- -package myrlm - -import "std" - -func A() string { - return std.PrevRealm().Addr().String() -} - -func B() string { - return A() -} --- r/foo/foo.gno -- -package foo - -import "gno.land/r/myrlm" - -func A() string { - return myrlm.A() -} - -func B() string { - return myrlm.B() -} --- p/demo/bar/bar.gno -- -package bar - -import "gno.land/r/myrlm" - -func A() string { - return myrlm.A() -} - -func B() string { - return myrlm.B() -} --- run/myrlmA.gno -- -package main - -import myrlm "gno.land/r/myrlm" - -func main() { - println(myrlm.A()) -} --- run/myrlmB.gno -- -package main - -import "gno.land/r/myrlm" - -func main() { - println(myrlm.B()) -} --- run/fooA.gno -- -package main - -import "gno.land/r/foo" - -func main() { - println(foo.A()) -} --- run/fooB.gno -- -package main - -import "gno.land/r/foo" - -func main() { - println(foo.B()) -} --- run/barA.gno -- -package main - -import "gno.land/p/demo/bar" - -func main() { - println(bar.A()) -} --- run/barB.gno -- -package main - -import "gno.land/p/demo/bar" - -func main() { - println(bar.B()) -} --- run/baz.gno -- -package main - -import "std" - -func main() { - println(std.PrevRealm().Addr().String()) -} diff --git a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar deleted file mode 100644 index 71ef6400471..00000000000 --- a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar +++ /dev/null @@ -1,88 +0,0 @@ -# test for https://github.com/gnolang/gno/pull/875 - -## another test user, test2 -adduser test2 - -## start a new node -gnoland start - -## add realm_banker -gnokey maketx addpkg -pkgdir $WORK/short -pkgpath gno.land/r/test/realm_banker -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 - -## add realm_banker with long package_name -gnokey maketx addpkg -pkgdir $WORK/long -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 - -## test2 spend all balance -gnokey maketx send -send "9999999ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2 - -## check test2 balance -gnokey query bank/balances/${USER_ADDR_test2} -stdout '' - -## mint coin from banker -gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Mint -args ${USER_ADDR_test2} -args "ugnot" -args "31337" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 - -## check balance after minting, without patching banker will return '31337ugnot' -gnokey query bank/balances/${USER_ADDR_test2} -stdout '"31337/gno.land/r/test/realm_banker:ugnot"' - -## burn coin -gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Burn -args ${USER_ADDR_test2} -args "ugnot" -args "7" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 - -## check balance after burning -gnokey query bank/balances/${USER_ADDR_test2} -stdout '"31330/gno.land/r/test/realm_banker:ugnot"' - -## transfer 1ugnot to test2 for gas-fee of below tx -gnokey maketx send -send "1ugnot" -to ${USER_ADDR_test2} -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 - -## transfer coin -gnokey maketx send -send "1330/gno.land/r/test/realm_banker:ugnot" -to g1yr0dpfgthph7y6mepdx8afuec4q3ga2lg8tjt0 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2 - -## check sender balance -gnokey query bank/balances/${USER_ADDR_test2} -stdout '"30000/gno.land/r/test/realm_banker:ugnot"' - -## check receiver balance -gnokey query bank/balances/g1yr0dpfgthph7y6mepdx8afuec4q3ga2lg8tjt0 -stdout '"1330/gno.land/r/test/realm_banker:ugnot"' - -## mint coin from long named package with banker -gnokey maketx call -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 -gnokey query bank/balances/g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7 -stdout '"100/gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890:ugnot"' - --- short/realm_banker.gno -- -package realm_banker - -import ( - "std" -) - -func Mint(addr std.Address, denom string, amount int64) { - banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.IssueCoin(addr, denom, amount) -} - -func Burn(addr std.Address, denom string, amount int64) { - banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.RemoveCoin(addr, denom, amount) -} - --- long/realm_banker.gno -- -// package name is 130 characters long -package package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 - -import ( - "std" -) - -func Mint(addr std.Address, denom string, amount int64) { - banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.IssueCoin(addr, denom, amount) -} - -func Burn(addr std.Address, denom string, amount int64) { - banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.RemoveCoin(addr, denom, amount) -} \ No newline at end of file diff --git a/gno.land/cmd/gnoland/testdata/wugnot.txtar b/gno.land/cmd/gnoland/testdata/wugnot.txtar deleted file mode 100644 index 1640909fdb9..00000000000 --- a/gno.land/cmd/gnoland/testdata/wugnot.txtar +++ /dev/null @@ -1,45 +0,0 @@ -loadpkg gno.land/r/demo/wugnot - -gnoland start - -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 -stdout '# wrapped GNOT \(\$wugnot\)' -stdout 'Decimals..: 0' -stdout 'Total supply..: 0' -stdout 'Known accounts..: 0' -stdout 'OK!' - -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send 12345678ugnot -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout 'OK!' - -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 -stdout 'Total supply..: 12345678' -stdout 'Known accounts..: 1' -stdout 'OK!' - -# XXX: use test2 instead (depends on https://github.com/gnolang/gno/issues/1269#issuecomment-1806386069) -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send 12345678ugnot -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout 'OK!' - -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 -stdout 'Total supply..: 24691356' -stdout 'Known accounts..: 1' # should be 2 once we can use test2 -stdout 'OK!' - -# XXX: replace hardcoded address with test3 -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Transfer -gas-fee 1000000ugnot -gas-wanted 2000000 -args 'g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq' -args '10000000' -broadcast -chainid=tendermint_test test1 -stdout 'OK!' - -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 -stdout 'Total supply..: 24691356' -stdout 'Known accounts..: 2' # should be 3 once we can use test2 -stdout 'OK!' - -# XXX: use test3 instead (depends on https://github.com/gnolang/gno/issues/1269#issuecomment-1806386069) -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Withdraw -args 10000000 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -stdout 'OK!' - -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 -stdout 'Total supply..: 14691356' -stdout 'Known accounts..: 2' # should be 3 once we can use test2 -stdout 'OK!' diff --git a/gno.land/cmd/gnoland/types.go b/gno.land/cmd/gnoland/types.go deleted file mode 100644 index a48bfaf7b31..00000000000 --- a/gno.land/cmd/gnoland/types.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "github.com/gnolang/gno/tm2/pkg/std" -) - -// txStore is a wrapper for TM2 transactions -type txStore []std.Tx - -// leftMerge merges the two tx stores, with -// preference to the left -func (i *txStore) leftMerge(b txStore) error { - // Build out the tx hash map - txHashMap := make(map[string]struct{}, len(*i)) - - for _, tx := range *i { - txHash, err := getTxHash(tx) - if err != nil { - return err - } - - txHashMap[txHash] = struct{}{} - } - - for _, tx := range b { - txHash, err := getTxHash(tx) - if err != nil { - return err - } - - if _, exists := txHashMap[txHash]; !exists { - *i = append(*i, tx) - } - } - - return nil -} diff --git a/gno.land/cmd/gnoweb/CONTRIBUTING.md b/gno.land/cmd/gnoweb/CONTRIBUTING.md deleted file mode 100644 index 7d7663e8bf7..00000000000 --- a/gno.land/cmd/gnoweb/CONTRIBUTING.md +++ /dev/null @@ -1,20 +0,0 @@ -# gno.land Website - -The gno.land website has 3 main dependencies: - -1. [UmbrellaJs](https://umbrellajs.com/) for DOM operations -2. [MarkedJs](https://marked.js.org/) for Markdown to html compilation -3. [HighlightJs](https://highlightjs.org/) for golang syntax highlighting -4. [DOMPurify](https://github.com/cure53/DOMPurify) to sanitize html (and avoid xss) - -Some security considerations: -| | Umbrella Js | Marked Js | HighlightJs | DOMPurify | -|---|---|---|---|---| -| dependencies | 0 | 0 | 0 | 0 | -| sanitize content | | [no](https://marked.js.org/#usage) | [throws an error](https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/highlight.js#L741) | [yes](https://github.com/cure53/DOMPurify#readme) | - -Best Practices: - -- **When using MarkedJs**: Always run the output of the marked compiler inside `DOMPurify.sanitize` before inserting it in the dom with `.innerHtml = `. -- **When using DOMPurify**: Preferably use `{ USE_PROFILES: { html: true } }` option to allow html only. Content passed in the sanitizer must not be modified afterwards, and must directly be inserted in the DOM with innerHtml. Do not call `DOMPurify.sanitize` with the output of a previous `DOMPurify.sanitize` to avoid any mutation XSS risks. -- **When using HighlightJs**: always configure it before with `hljs.configure({throwUnescapedHTML: true})` to throw before inserting html in the page if any unexpected html children are detected. The check is done [here](https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/highlight.js#L741). diff --git a/gno.land/cmd/gnoweb/README.md b/gno.land/cmd/gnoweb/README.md index 941d5e4f67e..ccd538c8f70 100644 --- a/gno.land/cmd/gnoweb/README.md +++ b/gno.land/cmd/gnoweb/README.md @@ -2,12 +2,4 @@ The gno.land web interface. -Live demo: https://test3.gno.land/ - -## Install `gnoweb` - -Install and run a local [`gnoland`](../gnoland) instance first. - - $> git clone git@github.com:gnolang/gno.git - $> cd ./gno/gno.land - $> make install.gnoweb +Live demo: [https://gno.land/](https://gno.land/) or using `gnodev` from the directory [gnodev](../../../contribs/gnodev). diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index 547134548ff..8c0df00aa35 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -1,60 +1,196 @@ package main import ( + "context" "flag" "fmt" + "net" "net/http" "os" "time" - // for static files "github.com/gnolang/gno/gno.land/pkg/gnoweb" "github.com/gnolang/gno/gno.land/pkg/log" + "github.com/gnolang/gno/tm2/pkg/commands" + "go.uber.org/zap" "go.uber.org/zap/zapcore" - // for error types - // "github.com/gnolang/gno/tm2/pkg/sdk" // for baseapp (info, status) ) +type webCfg struct { + chainid string + remote string + remoteHelp string + bind string + faucetURL string + assetsDir string + analytics bool + json bool + html bool + verbose bool +} + +var defaultWebOptions = webCfg{ + chainid: "dev", + remote: "127.0.0.1:26657", + bind: ":8888", +} + func main() { - err := runMain(os.Args[1:]) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) - os.Exit(1) - } + var cfg webCfg + + stdio := commands.NewDefaultIO() + cmd := commands.NewCommand( + commands.Metadata{ + Name: "gnoweb", + ShortUsage: "gnoweb [flags] [path ...]", + ShortHelp: "runs gno.land web interface", + LongHelp: `gnoweb web interface`, + }, + &cfg, + func(ctx context.Context, args []string) error { + run, err := setupWeb(&cfg, args, stdio) + if err != nil { + return err + } + + return run() + }) + + cmd.Execute(context.Background(), os.Args[1:]) } -func runMain(args []string) error { - var ( - fs = flag.NewFlagSet("gnoweb", flag.ContinueOnError) - cfg = gnoweb.NewDefaultConfig() - bindAddress string - ) - fs.StringVar(&cfg.RemoteAddr, "remote", cfg.RemoteAddr, "remote gnoland node address") - fs.StringVar(&cfg.CaptchaSite, "captcha-site", cfg.CaptchaSite, "recaptcha site key (if empty, captcha are disabled)") - fs.StringVar(&cfg.FaucetURL, "faucet-url", cfg.FaucetURL, "faucet server URL") - fs.StringVar(&cfg.ViewsDir, "views-dir", cfg.ViewsDir, "views directory location") // XXX: replace with goembed - fs.StringVar(&cfg.HelpChainID, "help-chainid", cfg.HelpChainID, "help page's chainid") - fs.StringVar(&cfg.HelpRemote, "help-remote", cfg.HelpRemote, "help page's remote addr") - fs.BoolVar(&cfg.WithAnalytics, "with-analytics", cfg.WithAnalytics, "enable privacy-first analytics") - fs.StringVar(&bindAddress, "bind", "127.0.0.1:8888", "server listening address") - - if err := fs.Parse(args); err != nil { - return err +func (c *webCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.remote, + "remote", + defaultWebOptions.remote, + "remote gno.land node address", + ) + + fs.StringVar( + &c.remoteHelp, + "help-remote", + defaultWebOptions.remoteHelp, + "help page's remote address", + ) + + fs.StringVar( + &c.assetsDir, + "assets-dir", + defaultWebOptions.assetsDir, + "if not empty, will be use as assets directory", + ) + + fs.StringVar( + &c.chainid, + "help-chainid", + defaultWebOptions.chainid, + "Deprecated: use `chainid` instead", + ) + + fs.StringVar( + &c.chainid, + "chainid", + defaultWebOptions.chainid, + "target chain id", + ) + + fs.StringVar( + &c.bind, + "bind", + defaultWebOptions.bind, + "gnoweb listener", + ) + + fs.StringVar( + &c.faucetURL, + "faucet-url", + defaultWebOptions.faucetURL, + "The faucet URL will redirect the user when they access `/faucet`.", + ) + + fs.BoolVar( + &c.json, + "json", + defaultWebOptions.json, + "display log in json format", + ) + + fs.BoolVar( + &c.html, + "html", + defaultWebOptions.html, + "enable unsafe html", + ) + + fs.BoolVar( + &c.analytics, + "with-analytics", + defaultWebOptions.analytics, + "nable privacy-first analytics", + ) + + fs.BoolVar( + &c.verbose, + "v", + defaultWebOptions.verbose, + "verbose logging mode", + ) +} + +func setupWeb(cfg *webCfg, _ []string, io commands.IO) (func() error, error) { + // Setup logger + level := zapcore.InfoLevel + if cfg.verbose { + level = zapcore.DebugLevel + } + var zapLogger *zap.Logger + if cfg.json { + zapLogger = log.NewZapJSONLogger(io.Out(), level) + } else { + zapLogger = log.NewZapConsoleLogger(io.Out(), level) } + defer zapLogger.Sync() - zapLogger := log.NewZapConsoleLogger(os.Stdout, zapcore.DebugLevel) logger := log.ZapLoggerToSlog(zapLogger) - logger.Info("Running", "listener", "http://"+bindAddress) - server := &http.Server{ - Addr: bindAddress, - ReadHeaderTimeout: 60 * time.Second, - Handler: gnoweb.MakeApp(logger, cfg).Router, + // Setup app + appcfg := gnoweb.NewDefaultAppConfig() + appcfg.ChainID = cfg.chainid + appcfg.NodeRemote = cfg.remote + appcfg.RemoteHelp = cfg.remoteHelp + if appcfg.RemoteHelp == "" { + appcfg.RemoteHelp = appcfg.NodeRemote + } + appcfg.Analytics = cfg.analytics + appcfg.UnsafeHTML = cfg.html + appcfg.FaucetURL = cfg.faucetURL + appcfg.AssetsDir = cfg.assetsDir + app, err := gnoweb.NewRouter(logger, appcfg) + if err != nil { + return nil, fmt.Errorf("unable to start gnoweb app: %w", err) + } + + // Resolve binding address + bindaddr, err := net.ResolveTCPAddr("tcp", cfg.bind) + if err != nil { + return nil, fmt.Errorf("unable to resolve listener %q: %w", cfg.bind, err) } - if err := server.ListenAndServe(); err != nil { - logger.Error("HTTP server stopped", " error:", err) + logger.Info("Running", "listener", bindaddr.String()) + + // Setup server + server := &http.Server{ + Handler: app, + Addr: bindaddr.String(), + ReadHeaderTimeout: 60 * time.Second, } - return zapLogger.Sync() + return func() error { + if err := server.ListenAndServe(); err != nil { + logger.Error("HTTP server stopped", "error", err) + return commands.ExitCodeError(1) + } + return nil + }, nil } diff --git a/gno.land/cmd/gnoweb/main_test.go b/gno.land/cmd/gnoweb/main_test.go index 640c4763140..37006c18c93 100644 --- a/gno.land/cmd/gnoweb/main_test.go +++ b/gno.land/cmd/gnoweb/main_test.go @@ -1,14 +1,25 @@ package main import ( - "errors" - "flag" + "os" "testing" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/stretchr/testify/require" ) -func TestFlagHelp(t *testing.T) { - err := runMain([]string{"-h"}) - if !errors.Is(err, flag.ErrHelp) { - t.Errorf("should display usage") - } +func TestSetupWeb(t *testing.T) { + opts := defaultWebOptions + opts.bind = "127.0.0.1:0" // random port + stdio := commands.NewDefaultIO() + + // Open /dev/null as a write-only file + devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0o644) + require.NoError(t, err) + defer devNull.Close() + + stdio.SetOut(devNull) + + _, err = setupWeb(&opts, []string{}, stdio) + require.NoError(t, err) } diff --git a/gno.land/genesis/README.md b/gno.land/genesis/README.md index 55fdb3d0dfd..4fb81baaaa0 100644 --- a/gno.land/genesis/README.md +++ b/gno.land/genesis/README.md @@ -1,3 +1,3 @@ -# Gno.land genesis +# gno.land genesis **WIP: see https://github.com/gnolang/independence-day** diff --git a/gno.land/genesis/genesis_balances.txt b/gno.land/genesis/genesis_balances.txt index fa3232149c1..c372d7f9fd7 100644 --- a/gno.land/genesis/genesis_balances.txt +++ b/gno.land/genesis/genesis_balances.txt @@ -16,7 +16,8 @@ g13d7jc32adhc39erm5me38w5v7ej7lpvlnqjk73=1000000000000ugnot # faucet3 (devx) g18l9us6trqaljw39j94wzf5ftxmd9qqkvrxghd2=1000000000000ugnot # faucet4 (adena) # Contributors premine & GitHub requests (closed). -g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq=10000000000ugnot # @moul +g1manfred47kzduec920z88wfr64ylksmdcedlf5=10000000000ugnot # @moul +g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq=10000000000ugnot # @manfred g14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa=10000000000ugnot # @piux2 g15gdm49ktawvkrl88jadqpucng37yxutucuwaef=10000000000ugnot # @chadwick g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s=10000000000ugnot # @mefodica #83 diff --git a/gno.land/genesis/genesis_params.toml b/gno.land/genesis/genesis_params.toml new file mode 100644 index 00000000000..fb080024624 --- /dev/null +++ b/gno.land/genesis/genesis_params.toml @@ -0,0 +1,29 @@ + +## gno.land +["gno.land/r/sys/params.sys"] + users_pkgpath.string = "gno.land/r/sys/users" # if empty, no namespace support. + # TODO: validators_pkgpath.string = "gno.land/r/sys/validators" + # TODO: rewards_pkgpath.string = "gno.land/r/sys/rewards" + # TODO: token_lock.bool = true + +## gnovm +["gno.land/r/sys/params.vm"] + chain_domain.string = "gno.land" + # TODO: max_gas.int64 = 100_000_000 + # TODO: chain_tz.string = "UTC" + # TODO: default_storage_allowance.string = "" + +## tm2 +["gno.land/r/sys/params.tm2"] + +## misc +["gno.land/r/sys/params.misc"] + +## testing +# do not remove these lines. they are needed for a txtar integration test. +["gno.land/r/sys/params.test"] + foo.string = "bar" + foo.int64 = -1337 + foo.uint64 = 42 + foo.bool = true + #foo.bytes = todo diff --git a/gno.land/genesis/genesis_txs.jsonl b/gno.land/genesis/genesis_txs.jsonl index daf9fbdc5d4..9027d51c0ac 100644 --- a/gno.land/genesis/genesis_txs.jsonl +++ b/gno.land/genesis/genesis_txs.jsonl @@ -1,17 +1,17 @@ -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj:10\ng1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s:1\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8:1\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q:1\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj:1\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0:1\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz:1\ng187982000zsc493znqt828s90cmp6hcp2erhu6m:1\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl:1\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037:1\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5:1\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr:1\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz:1\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w:1\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz:1\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3:1\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0:1\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n:1\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac:1\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap:1\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv:1\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv:1\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq:1\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6:1\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q:1\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7:1\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k:1\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll:1\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd:1\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64:1\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw:1\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a:1\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc:1\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6:1\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6:1\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9:1\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea:1\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3:1\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp:1\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5:1\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf:1\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g:1\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r:1\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su:1\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69:1\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6:1\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"S8iMMzlOMK8dmox78R9Z8+pSsS8YaTCXrIcaHDpiOgkOy7gqoQJ0oftM0zf8zAz4xpezK8Lzg8Q0fCdXJxV76w=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1thlf3yct7n7ex70k0p62user0kn6mj6d3s0cg3\ng1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"njczE6xYdp01+CaUU/8/v0YC/NuZD06+qLind+ZZEEMNaRe/4Ln+4z7dG6HYlaWUMsyI1KCoB6NIehoE0PZ44Q=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz\ng187982000zsc493znqt828s90cmp6hcp2erhu6m\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6\ng1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t\n"]}],"fee":{"gas_wanted":"4000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"7AmlhZhsVkxCUl0bbpvpPMnIKihwtG7A5IFR6Tg4xStWLgaUr05XmWRKlO2xjstTtwbVKQT5mFL4h5wyX4SQzw=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","administrator","g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"AqCqe0cS55Ym7/BvPDoCDyPP5q8284gecVQ2PMOlq/4lJpO9Q18SOWKI15dMEBY1pT0AYyhCeTirlsM1I3Y4Cg=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1qpymzwx4l4cy6cerdyajp9ksvjsf20rk5y9rtt","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","zo_oma","Love is the encryption key\u003c3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6yg5/iiktruezVw5vZJwLlGwyrvw8RlqOToTRMWXkE2"},"signature":"GGp+bVL2eEvKecPqgcULSABYOSnSMnJzfIsR8ZIRER1GGX/fOiCReX4WKMrGLVROJVfbLQkDRwvhS4TLHlSoSQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","manfred","https://github.com/moul"]}],"fee":{"gas_wanted":"2000000","gas_fee":"200000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"9CWeNbKx+hEL+RdHplAVAFntcrAVx5mK9tMqoywuHVoreH844n3yOxddQrGfBk6T2tMBmNWakERRqWZfS+bYAQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","piupiu","@piux2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ar68lqbU2YC63fbMcYUtJhYO3/66APM/EqF7m0nUjGyz"},"signature":"pTUpP0d/XlfVe3TH1hlaoLhKadzIKG1gtQ/Ueuat72p+659RWRea58Z0mk6GgPE/EeTbhMEY45zufevBdGJVoQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5","send":"","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","anarcher","https://twitter.com/anarcher"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AjpLbKdQeH+yB/1OCB148l5GlRRrXma71hdA8EES3H7f"},"signature":"pf5xm8oWIQIOEwSGw4icPmynLXb1P1HxKfjeh8UStU1mlIBPKa7yppeIMPpAflC0o2zjFR7Axe7CimAebm3BHg=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g15gdm49ktawvkrl88jadqpucng37yxutucuwaef","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","ideamour","\u003c3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhClx4AsDuX3DNCPxhDwWnrfd4MIZmxJE4vt47ClVvT2"},"signature":"IQe64af878k6HjLDqIJeg27GXAVF6xS+96cDe2jMlxNV6+8sOcuUctp0GiWVnYfN4tpthC6d4WhBo+VlpHqkbg=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateBoard","args":["testboard"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"vzlSxEFh5jOkaSdv3rsV91v/OJKEF2qSuoCpri1u5tRWq62T7xr3KHRCF5qFnn4aQX/yE8g8f/Y//WPOCUGhJw=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Hello World","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm \nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n## Starting the `gnoland` node node/validator.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### build gnoland.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake \n```\n\n### add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mnemonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### start gnoland validator node.\n\n```bash\n./build/gnoland\n```\n\n(This can be reset with `make reset`).\n\n### start gnoland web server (optional).\n\n```bash\ngo run ./gnoland/website\n```\n\n## Signing and broadcasting transactions.\n\n### publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 2000000 \u003e addpkg.avl.unsigned.txt\n./build/gnokey query \"auth/accounts/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n./build/gnokey sign test1 --txpath addpkg.avl.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 0 \u003e addpkg.avl.signed.txt\n./build/gnokey broadcast addpkg.avl.signed.txt --remote %%REMOTE%%\n```\n\n### publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 300000000 \u003e addpkg.boards.unsigned.txt\n./build/gnokey sign test1 --txpath addpkg.boards.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 1 \u003e addpkg.boards.signed.txt\n./build/gnokey broadcast addpkg.boards.signed.txt --remote %%REMOTE%%\n```\n\n### create a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateBoard --args \"testboard\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createboard.unsigned.txt\n./build/gnokey sign test1 --txpath createboard.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 2 \u003e createboard.signed.txt\n./build/gnokey broadcast createboard.signed.txt --remote %%REMOTE%%\n```\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"testboard\\\")\"\n```\n\n### create a post of a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreatePost --args 1 --args \"Hello World\" --args#file \"./examples/gno.land/r/demo/boards/README.md\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createpost.unsigned.txt\n./build/gnokey sign test1 --txpath createpost.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 3 \u003e createpost.signed.txt\n./build/gnokey broadcast createpost.signed.txt --remote %%REMOTE%%\n```\n\n### create a comment to a post.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateReply --args 1 --args 1 --args \"A comment\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createcomment.unsigned.txt\n./build/gnokey sign test1 --txpath createcomment.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 4 \u003e createcomment.signed.txt\n./build/gnokey broadcast createcomment.signed.txt --remote %%REMOTE%%\n```\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard/1\"\n```\n\n### render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:testboard` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard\"\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"V43B1waFxhzheW9TfmCpjLdrC4dC1yjUGES5y3J6QsNar6hRpNz4G1thzWmWK7xXhg8u1PCIpxLxGczKQYhuPw=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","NFT example","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.gno](https://gno.land/r/demo/nft/nft.gno)\n - [zrealm_nft3.gno test](https://github.com/gnolang/gno/blob/master/examples/gno.land/r/demo/nft/z_3_filetest.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:testboard/1)).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"ZXfrTiHxPFQL8uSm+Tv7WXIHPMca9okhm94RAlC6YgNbB1VHQYYpoP4w+cnL3YskVzGrOZxensXa9CAZ+cNNeg=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Simple echo example with coins","This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.gno](/r/demo/banktest/banktest.gno) 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\t\"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e\nSelf explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime std.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\tstd.FormatTimestamp(act.time, \"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.\nNotice that the \"latest\" variable is defined \"globally\" within\nthe context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package\nare encapsulated within this \"data realm\", where the data is \nmutated based on transactions that can potentially cross many\nrealm and non-realm packge 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\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named\n\"Deposit\". `std.AssertOriginCall() asserts that this function was called by a\ngno transactional Message. The caller is the user who signed off on this\ntransactional message. Send is the amount of deposit sent along with this\nmessage.\n\n```go\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: std.GetTimestamp(),\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```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n\t// return if any.\n\tif returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n\t\tpkgaddr := std.GetOrigPkgAddr()\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```\n\nNotice that each realm package has an associated Cosmos address.\n\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\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\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```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:testboard/4).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"iZX/llZlNTdZMLv1goCTgK2bWqzT8enlTq56wMTCpVxJGA0BTvuEM5Nnt9vrnlG6Taqj2GuTrmEnJBkDFTmt9g=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","TASK: Describe in your words","Describe in an essay (250+ words), on your favorite medium, why you are interested in gno.land and gnolang.\n\nReply here with a URL link to your written piece as a comment, for rewards.\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"4HBNtrta8HdeHj4JTN56PBTRK8GOe31NMRRXDiyYtjozuyRdWfOGEsGjGgHWcoBUJq6DepBgD4FetdqfhZ6TNQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:BOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:gnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum","2022-05-20T13:17:22Z","","tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} -{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum","2022-05-20T13:17:23Z","","tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj:10\ng1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s:1\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8:1\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q:1\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj:1\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0:1\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz:1\ng187982000zsc493znqt828s90cmp6hcp2erhu6m:1\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl:1\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037:1\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5:1\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr:1\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz:1\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w:1\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz:1\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3:1\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0:1\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n:1\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac:1\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap:1\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv:1\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv:1\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq:1\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6:1\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q:1\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7:1\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k:1\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll:1\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd:1\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64:1\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw:1\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a:1\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc:1\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6:1\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6:1\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9:1\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea:1\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3:1\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp:1\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5:1\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf:1\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g:1\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r:1\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su:1\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69:1\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6:1\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa:10\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t:5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"S8iMMzlOMK8dmox78R9Z8+pSsS8YaTCXrIcaHDpiOgkOy7gqoQJ0oftM0zf8zAz4xpezK8Lzg8Q0fCdXJxV76w=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1thlf3yct7n7ex70k0p62user0kn6mj6d3s0cg3\ng1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\ng1manfred47kzduec920z88wfr64ylksmdcedlf5\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"njczE6xYdp01+CaUU/8/v0YC/NuZD06+qLind+ZZEEMNaRe/4Ln+4z7dG6HYlaWUMsyI1KCoB6NIehoE0PZ44Q=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/users","func":"Invite","args":["g1589c8cekvmjfmy0qrd4f3z52r7fn7rgk02667s\ng13sm84nuqed3fuank8huh7x9mupgw22uft3lcl8\ng1m6732pkrngu9vrt0g7056lvr9kcqc4mv83xl5q\ng1wg88rhzlwxjd2z4j5de5v5xq30dcf6rjq3dhsj\ng18pmaskasz7mxj6rmgrl3al58xu45a7w0l5nmc0\ng19wwhkmqlns70604ksp6rkuuu42qhtvyh05lffz\ng187982000zsc493znqt828s90cmp6hcp2erhu6m\ng1ndpsnrspdnauckytvkfv8s823t3gmpqmtky8pl\ng16ja66d65emkr0zxd2tu7xjvm7utthyhpej0037\ng1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5\ng1trkzq75ntamsnw9xnrav2v7gy2lt5g6p29yhdr\ng1rrf8s5mrmu00sx04fzfsvc399fklpeg2x0a7mz\ng19p5ntfvpt4lwq4jqsmnxsnelhf3tff9scy3w8w\ng1tue8l73d6rq4vhqdsp2sr3zhuzpure3k2rnwpz\ng14hhsss4ngx5kq77je5g0tl4vftg8qp45ceadk3\ng1768hvkh7anhd40ch4h7jdh6j3mpcs7hrat4gl0\ng15fa8kyjhu88t9dr8zzua8fwdvkngv5n8yqsm0n\ng1xhccdjcscuhgmt3quww6qdy3j3czqt3urc2eac\ng1z629z04f85k4t5gnkk5egpxw9tqxeec435esap\ng1pfldkplz9puq0v82lu9vqcve9nwrxuq9qe5ttv\ng152pn0g5qfgxr7yx8zlwjq48hytkafd8x7egsfv\ng1cf2ye686ke38vjyqakreprljum4xu6rwf5jskq\ng1c5shztyaj4gjrc5zlwmh9xhex5w7l4asffs2w6\ng1lhpx2ktk0ha3qw42raxq4m24a4c4xqxyrgv54q\ng1026p54q0j902059sm2zsv37krf0ghcl7gmhyv7\ng1n4yvwnv77frq2ccuw27dmtjkd7u4p4jg0pgm7k\ng13m7f2e6r3lh3ykxupacdt9sem2tlvmaamwjhll\ng19uxluuecjlsqvwmwu8sp6pxaaqfhk972q975xd\ng1j80fpcsumfkxypvydvtwtz3j4sdwr8c2u0lr64\ng1tjdpptuk9eysq6z38nscqyycr998xjyx3w8jvw\ng19t3n89slfemgd3mwuat4lajwcp0yxrkadgeg7a\ng1yqndt8xx92l9h494jfruz2w79swzjes3n4wqjc\ng13278z0a5ufeg80ffqxpda9dlp599t7ekregcy6\ng1ht236wjd83x96uqwh9rh3fq6pylyn78mtwq9v6\ng1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9\ng1wwppuzdns5u6c6jqpkzua24zh6ppsus6399cea\ng1k8pjnguyu36pkc8hy0ufzgpzfmj2jl78la7ek3\ng1e8umkzumtxgs8399lw0us4rclea3xl5gxy9spp\ng14qekdkj2nmmwea4ufg9n002a3pud23y8k7ugs5\ng19w2488ntfgpduzqq3sk4j5x387zynwknqdvjqf\ng1495y3z7zrej4rendysnw5kaeu4g3d7x7w0734g\ng1hygx8ga9qakhkczyrzs9drm8j8tu4qds9y5e3r\ng1f977l6wxdh3qu60kzl75vx2wmzswu68l03r8su\ng1644qje5rx6jsdqfkzmgnfcegx4dxkjh6rwqd69\ng1mzjajymvmtksdwh3wkrndwj6zls2awl9q83dh6\ng1manfred47kzduec920z88wfr64ylksmdcedlf5\ng14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa\ng14vhcdsyf83ngsrrqc92kmw8q9xakqjm0v8448t\n"]}],"fee":{"gas_wanted":"4000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"7AmlhZhsVkxCUl0bbpvpPMnIKihwtG7A5IFR6Tg4xStWLgaUr05XmWRKlO2xjstTtwbVKQT5mFL4h5wyX4SQzw=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","administrator","g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"AqCqe0cS55Ym7/BvPDoCDyPP5q8284gecVQ2PMOlq/4lJpO9Q18SOWKI15dMEBY1pT0AYyhCeTirlsM1I3Y4Cg=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1qpymzwx4l4cy6cerdyajp9ksvjsf20rk5y9rtt","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","zo_oma","Love is the encryption key\u003c3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A6yg5/iiktruezVw5vZJwLlGwyrvw8RlqOToTRMWXkE2"},"signature":"GGp+bVL2eEvKecPqgcULSABYOSnSMnJzfIsR8ZIRER1GGX/fOiCReX4WKMrGLVROJVfbLQkDRwvhS4TLHlSoSQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1manfred47kzduec920z88wfr64ylksmdcedlf5","moul","https://github.com/moul"]}],"fee":{"gas_wanted":"2000000","gas_fee":"200000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"9CWeNbKx+hEL+RdHplAVAFntcrAVx5mK9tMqoywuHVoreH844n3yOxddQrGfBk6T2tMBmNWakERRqWZfS+bYAQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1fj9jccm3zjnqspq7lp2g7lj4czyfq0s35600g9","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","piupiu","@piux2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"Ar68lqbU2YC63fbMcYUtJhYO3/66APM/EqF7m0nUjGyz"},"signature":"pTUpP0d/XlfVe3TH1hlaoLhKadzIKG1gtQ/Ueuat72p+659RWRea58Z0mk6GgPE/EeTbhMEY45zufevBdGJVoQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1ds24jj9kqjcskd0gzu24r9e4n62ggye230zuv5","send":"","pkg_path":"gno.land/r/demo/users","func":"Register","args":["g1manfred47kzduec920z88wfr64ylksmdcedlf5","anarcher","https://twitter.com/anarcher"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AjpLbKdQeH+yB/1OCB148l5GlRRrXma71hdA8EES3H7f"},"signature":"pf5xm8oWIQIOEwSGw4icPmynLXb1P1HxKfjeh8UStU1mlIBPKa7yppeIMPpAflC0o2zjFR7Axe7CimAebm3BHg=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g15gdm49ktawvkrl88jadqpucng37yxutucuwaef","send":"200000000ugnot","pkg_path":"gno.land/r/demo/users","func":"Register","args":["","ideamour","\u003c3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AhClx4AsDuX3DNCPxhDwWnrfd4MIZmxJE4vt47ClVvT2"},"signature":"IQe64af878k6HjLDqIJeg27GXAVF6xS+96cDe2jMlxNV6+8sOcuUctp0GiWVnYfN4tpthC6d4WhBo+VlpHqkbg=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateBoard","args":["testboard"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"vzlSxEFh5jOkaSdv3rsV91v/OJKEF2qSuoCpri1u5tRWq62T7xr3KHRCF5qFnn4aQX/yE8g8f/Y//WPOCUGhJw=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Hello World","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm \nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n## Starting the `gnoland` node node/validator.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### build gnoland.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake \n```\n\n### add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mnemonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### start gnoland validator node.\n\n```bash\n./build/gnoland\n```\n\n(This can be reset with `make reset`).\n\n### start gnoland web server (optional).\n\n```bash\ngo run ./gnoland/website\n```\n\n## Signing and broadcasting transactions.\n\n### publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 2000000 \u003e addpkg.avl.unsigned.txt\n./build/gnokey query \"auth/accounts/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"\n./build/gnokey sign test1 --txpath addpkg.avl.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 0 \u003e addpkg.avl.signed.txt\n./build/gnokey broadcast addpkg.avl.signed.txt --remote %%REMOTE%%\n```\n\n### publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100ugnot --gas-fee 1ugnot --gas-wanted 300000000 \u003e addpkg.boards.unsigned.txt\n./build/gnokey sign test1 --txpath addpkg.boards.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 1 \u003e addpkg.boards.signed.txt\n./build/gnokey broadcast addpkg.boards.signed.txt --remote %%REMOTE%%\n```\n\n### create a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateBoard --args \"testboard\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createboard.unsigned.txt\n./build/gnokey sign test1 --txpath createboard.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 2 \u003e createboard.signed.txt\n./build/gnokey broadcast createboard.signed.txt --remote %%REMOTE%%\n```\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"testboard\\\")\"\n```\n\n### create a post of a board with a smart contract call.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreatePost --args 1 --args \"Hello World\" --args#file \"./examples/gno.land/r/demo/boards/README.md\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createpost.unsigned.txt\n./build/gnokey sign test1 --txpath createpost.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 3 \u003e createpost.signed.txt\n./build/gnokey broadcast createpost.signed.txt --remote %%REMOTE%%\n```\n\n### create a comment to a post.\n\n```bash\n./build/gnokey maketx call test1 --pkgpath \"gno.land/r/demo/boards\" --func CreateReply --args 1 --args 1 --args \"A comment\" --gas-fee 1ugnot --gas-wanted 2000000 \u003e createcomment.unsigned.txt\n./build/gnokey sign test1 --txpath createcomment.unsigned.txt --chainid \"%%CHAINID%%\" --number 0 --sequence 4 \u003e createcomment.signed.txt\n./build/gnokey broadcast createcomment.signed.txt --remote %%REMOTE%%\n```\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard/1\"\n```\n\n### render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:testboard` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:testboard\"\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"V43B1waFxhzheW9TfmCpjLdrC4dC1yjUGES5y3J6QsNar6hRpNz4G1thzWmWK7xXhg8u1PCIpxLxGczKQYhuPw=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","NFT example","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.gno](https://gno.land/r/demo/nft/nft.gno)\n - [zrealm_nft3.gno test](https://github.com/gnolang/gno/blob/master/examples/gno.land/r/demo/nft/z_3_filetest.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:testboard/1)).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"ZXfrTiHxPFQL8uSm+Tv7WXIHPMca9okhm94RAlC6YgNbB1VHQYYpoP4w+cnL3YskVzGrOZxensXa9CAZ+cNNeg=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Simple echo example with coins","This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.gno](/r/demo/banktest/banktest.gno) 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\t\"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e\nSelf explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime std.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\tstd.FormatTimestamp(act.time, \"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.\nNotice that the \"latest\" variable is defined \"globally\" within\nthe context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package\nare encapsulated within this \"data realm\", where the data is \nmutated based on transactions that can potentially cross many\nrealm and non-realm packge 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\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named\n\"Deposit\". `std.AssertOriginCall() asserts that this function was called by a\ngno transactional Message. The caller is the user who signed off on this\ntransactional message. Send is the amount of deposit sent along with this\nmessage.\n\n```go\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: std.GetTimestamp(),\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```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n\t// return if any.\n\tif returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n\t\tpkgaddr := std.GetOrigPkgAddr()\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```\n\nNotice that each realm package has an associated Cosmos address.\n\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\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\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```\n\nYou can call this contract yourself, by vistiing [/r/demo/banktest](/r/demo/banktest) and the [quickstart guide](/r/demo/boards:testboard/4).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"iZX/llZlNTdZMLv1goCTgK2bWqzT8enlTq56wMTCpVxJGA0BTvuEM5Nnt9vrnlG6Taqj2GuTrmEnJBkDFTmt9g=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","TASK: Describe in your words","Describe in an essay (250+ words), on your favorite medium, why you are interested in gno.land and gnolang.\n\nReply here with a URL link to your written piece as a comment, for rewards.\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"4HBNtrta8HdeHj4JTN56PBTRK8GOe31NMRRXDiyYtjozuyRdWfOGEsGjGgHWcoBUJq6DepBgD4FetdqfhZ6TNQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards.GetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards$help\u0026func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:BOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards:gnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast=true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum","2022-05-20T13:17:22Z","","tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} +{"tx": {"msg":[{"@type":"/vm.m_call","caller":"g1manfred47kzduec920z88wfr64ylksmdcedlf5","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum","2022-05-20T13:17:23Z","","tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}} \ No newline at end of file diff --git a/gno.land/pkg/gnoclient/README.md b/gno.land/pkg/gnoclient/README.md index a2f00895dbd..4b3854b1bcc 100644 --- a/gno.land/pkg/gnoclient/README.md +++ b/gno.land/pkg/gnoclient/README.md @@ -1,4 +1,4 @@ -# Gno.land Go Client +# gno.land Go Client The gno.land Go client is a dedicated library for interacting seamlessly with the gno.land RPC API. This library simplifies the process of querying or sending transactions to the gno.land RPC API and interpreting the responses. @@ -18,4 +18,3 @@ The roadmap for the gno.land Go client includes: - **Initial Development:** Kickstart the development specifically for gno.land. Subsequently, transition the generic functionalities to other modules like `tm2`, `gnovm`, `gnosdk`. - **Integration:** Begin incorporating this library within various components such as `gno.land/cmd/*` and other external clients, including `gnoblog-client`, the Discord community faucet bot, and [GnoMobile](https://github.com/gnolang/gnomobile). - **Enhancements:** Once the generic client establishes a robust foundation, we aim to utilize code generation for contracts. This will streamline the creation of type-safe, contract-specific clients. - diff --git a/gno.land/pkg/gnoclient/client_queries.go b/gno.land/pkg/gnoclient/client_queries.go index 9d9d7305116..2e09842ae31 100644 --- a/gno.land/pkg/gnoclient/client_queries.go +++ b/gno.land/pkg/gnoclient/client_queries.go @@ -31,7 +31,7 @@ func (c *Client) Query(cfg QueryCfg) (*ctypes.ResultABCIQuery, error) { } if qres.Response.Error != nil { - return qres, errors.Wrap(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log) + return qres, errors.Wrapf(qres.Response.Error, "deliver transaction failed: log:%s", qres.Response.Log) } return qres, nil @@ -97,7 +97,7 @@ func (c *Client) Render(pkgPath string, args string) (string, *ctypes.ResultABCI return "", nil, errors.Wrap(err, "query render") } if qres.Response.Error != nil { - return "", nil, errors.Wrap(qres.Response.Error, "Render failed: log:%s", qres.Response.Log) + return "", nil, errors.Wrapf(qres.Response.Error, "Render failed: log:%s", qres.Response.Log) } return string(qres.Response.Data), qres, nil @@ -120,7 +120,7 @@ func (c *Client) QEval(pkgPath string, expression string) (string, *ctypes.Resul return "", nil, errors.Wrap(err, "query qeval") } if qres.Response.Error != nil { - return "", nil, errors.Wrap(qres.Response.Error, "QEval failed: log:%s", qres.Response.Log) + return "", nil, errors.Wrapf(qres.Response.Error, "QEval failed: log:%s", qres.Response.Log) } return string(qres.Response.Data), qres, nil diff --git a/gno.land/pkg/gnoclient/client_test.go b/gno.land/pkg/gnoclient/client_test.go index d7795f918bf..54a15420a66 100644 --- a/gno.land/pkg/gnoclient/client_test.go +++ b/gno.land/pkg/gnoclient/client_test.go @@ -1,13 +1,17 @@ package gnoclient import ( + "errors" "testing" + "github.com/gnolang/gno/tm2/pkg/amino" + abciErrors "github.com/gnolang/gno/tm2/pkg/bft/abci/example/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" "github.com/gnolang/gno/tm2/pkg/bft/types" @@ -17,7 +21,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" ) -var testGasFee = ugnot.ValueString(10000) +var testGasFee = ugnot.ValueString(10_000) func TestRender(t *testing.T) { t.Parallel() @@ -115,7 +119,13 @@ func TestCallSingle(t *testing.T) { res, err := client.Call(cfg, msg...) assert.NoError(t, err) require.NotNil(t, res) - assert.Equal(t, string(res.DeliverTx.Data), "it works!") + expected := "it works!" + assert.Equal(t, string(res.DeliverTx.Data), expected) + + res, err = callSigningSeparately(t, client, cfg, msg...) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, string(res.DeliverTx.Data), expected) } func TestCallMultiple(t *testing.T) { @@ -192,6 +202,10 @@ func TestCallMultiple(t *testing.T) { res, err := client.Call(cfg, msg...) assert.NoError(t, err) assert.NotNil(t, res) + + res, err = callSigningSeparately(t, client, cfg, msg...) + assert.NoError(t, err) + assert.NotNil(t, res) } func TestCallErrors(t *testing.T) { @@ -642,8 +656,8 @@ func main() { msg := vm.MsgRun{ Caller: caller.GetAddress(), - Package: &std.MemPackage{ - Files: []*std.MemFile{ + Package: &gnovm.MemPackage{ + Files: []*gnovm.MemFile{ { Name: "main.gno", Body: fileBody, @@ -656,7 +670,13 @@ func main() { res, err := client.Run(cfg, msg) assert.NoError(t, err) require.NotNil(t, res) - assert.Equal(t, "hi gnoclient!\n", string(res.DeliverTx.Data)) + expected := "hi gnoclient!\n" + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = runSigningSeparately(t, client, cfg, msg) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) } func TestRunMultiple(t *testing.T) { @@ -713,8 +733,8 @@ func main() { msg1 := vm.MsgRun{ Caller: caller.GetAddress(), - Package: &std.MemPackage{ - Files: []*std.MemFile{ + Package: &gnovm.MemPackage{ + Files: []*gnovm.MemFile{ { Name: "main1.gno", Body: fileBody, @@ -726,8 +746,8 @@ func main() { msg2 := vm.MsgRun{ Caller: caller.GetAddress(), - Package: &std.MemPackage{ - Files: []*std.MemFile{ + Package: &gnovm.MemPackage{ + Files: []*gnovm.MemFile{ { Name: "main2.gno", Body: fileBody, @@ -740,7 +760,13 @@ func main() { res, err := client.Run(cfg, msg1, msg2) assert.NoError(t, err) require.NotNil(t, res) - assert.Equal(t, "hi gnoclient!\nhi gnoclient!\n", string(res.DeliverTx.Data)) + expected := "hi gnoclient!\nhi gnoclient!\n" + assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = runSigningSeparately(t, client, cfg, msg1, msg2) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, expected, string(res.DeliverTx.Data)) } func TestRunErrors(t *testing.T) { @@ -772,10 +798,10 @@ func TestRunErrors(t *testing.T) { msgs: []vm.MsgRun{ { Caller: mockAddress, - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "", Path: "", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "file1.gno", Body: "", @@ -819,10 +845,10 @@ func TestRunErrors(t *testing.T) { msgs: []vm.MsgRun{ { Caller: mockAddress, - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "", Path: "", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "file1.gno", Body: "", @@ -850,10 +876,10 @@ func TestRunErrors(t *testing.T) { msgs: []vm.MsgRun{ { Caller: mockAddress, - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "", Path: "", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "file1.gno", Body: "", @@ -881,10 +907,10 @@ func TestRunErrors(t *testing.T) { msgs: []vm.MsgRun{ { Caller: mockAddress, - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "", Path: "", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "file1.gno", Body: "", @@ -921,7 +947,7 @@ func TestRunErrors(t *testing.T) { msgs: []vm.MsgRun{ { Caller: mockAddress, - Package: &std.MemPackage{Name: "", Path: " "}, + Package: &gnovm.MemPackage{Name: "", Path: " "}, Send: nil, }, }, @@ -971,10 +997,10 @@ func TestAddPackageErrors(t *testing.T) { msgs: []vm.MsgAddPackage{ { Creator: mockAddress, - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "", Path: "", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "file1.gno", Body: "", @@ -1018,10 +1044,10 @@ func TestAddPackageErrors(t *testing.T) { msgs: []vm.MsgAddPackage{ { Creator: mockAddress, - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "", Path: "", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "file1.gno", Body: "", @@ -1049,10 +1075,10 @@ func TestAddPackageErrors(t *testing.T) { msgs: []vm.MsgAddPackage{ { Creator: mockAddress, - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "", Path: "", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "file1.gno", Body: "", @@ -1080,10 +1106,10 @@ func TestAddPackageErrors(t *testing.T) { msgs: []vm.MsgAddPackage{ { Creator: mockAddress, - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "", Path: "", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "file1.gno", Body: "", @@ -1120,7 +1146,7 @@ func TestAddPackageErrors(t *testing.T) { msgs: []vm.MsgAddPackage{ { Creator: mockAddress, - Package: &std.MemPackage{Name: "", Path: ""}, + Package: &gnovm.MemPackage{Name: "", Path: ""}, Deposit: nil, }, }, @@ -1326,3 +1352,217 @@ func TestLatestBlockHeightErrors(t *testing.T) { }) } } + +// The same as client.Call, but test signing separately +func callSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewCallTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTxCommit(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} + +// The same as client.Run, but test signing separately +func runSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewRunTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTxCommit(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} + +// The same as client.Send, but test signing separately +func sendSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewSendTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTxCommit(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} + +// The same as client.AddPackage, but test signing separately +func addPackageSigningSeparately(t *testing.T, client Client, cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.ResultBroadcastTxCommit, error) { + t.Helper() + tx, err := NewAddPackageTx(cfg, msgs...) + assert.NoError(t, err) + require.NotNil(t, tx) + signedTx, err := client.SignTx(*tx, cfg.AccountNumber, cfg.SequenceNumber) + assert.NoError(t, err) + require.NotNil(t, signedTx) + res, err := client.BroadcastTxCommit(signedTx) + assert.NoError(t, err) + require.NotNil(t, res) + return res, nil +} + +func TestClient_EstimateGas(t *testing.T) { + t.Parallel() + + t.Run("RPC client not set", func(t *testing.T) { + t.Parallel() + + c := &Client{ + RPCClient: nil, // not set + } + + estimate, err := c.EstimateGas(&std.Tx{}) + + assert.Zero(t, estimate) + assert.ErrorIs(t, err, ErrMissingRPCClient) + }) + + t.Run("unsuccessful query, rpc error", func(t *testing.T) { + t.Parallel() + + var ( + rpcErr = errors.New("rpc error") + mockRPCClient = &mockRPCClient{ + abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) { + require.Equal(t, simulatePath, path) + + var tx std.Tx + + require.NoError(t, amino.Unmarshal(data, &tx)) + + return nil, rpcErr + }, + } + ) + + c := &Client{ + RPCClient: mockRPCClient, + } + + estimate, err := c.EstimateGas(&std.Tx{}) + + assert.Zero(t, estimate) + assert.ErrorIs(t, err, rpcErr) + }) + + t.Run("unsuccessful query, process error", func(t *testing.T) { + t.Parallel() + + var ( + response = &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + ResponseBase: abci.ResponseBase{ + Error: abciErrors.UnknownError{}, + }, + }, + } + mockRPCClient = &mockRPCClient{ + abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) { + require.Equal(t, simulatePath, path) + + var tx std.Tx + + require.NoError(t, amino.Unmarshal(data, &tx)) + + return response, nil + }, + } + ) + + c := &Client{ + RPCClient: mockRPCClient, + } + + estimate, err := c.EstimateGas(&std.Tx{}) + + assert.Zero(t, estimate) + assert.ErrorIs(t, err, abciErrors.UnknownError{}) + }) + + t.Run("invalid response format", func(t *testing.T) { + t.Parallel() + + var ( + response = &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: []byte("totally valid amino"), + }, + } + mockRPCClient = &mockRPCClient{ + abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) { + require.Equal(t, simulatePath, path) + + var tx std.Tx + + require.NoError(t, amino.Unmarshal(data, &tx)) + + return response, nil + }, + } + ) + + c := &Client{ + RPCClient: mockRPCClient, + } + + estimate, err := c.EstimateGas(&std.Tx{}) + + assert.Zero(t, estimate) + assert.ErrorContains(t, err, "unable to unmarshal gas estimation response") + }) + + t.Run("valid gas estimation", func(t *testing.T) { + t.Parallel() + + var ( + gasUsed = int64(100000) + deliverResp = &abci.ResponseDeliverTx{ + GasUsed: gasUsed, + } + ) + + // Encode the response + encodedResp, err := amino.Marshal(deliverResp) + require.NoError(t, err) + + var ( + response = &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: encodedResp, // valid amino binary + }, + } + mockRPCClient = &mockRPCClient{ + abciQuery: func(path string, data []byte) (*ctypes.ResultABCIQuery, error) { + require.Equal(t, simulatePath, path) + + var tx std.Tx + + require.NoError(t, amino.Unmarshal(data, &tx)) + + return response, nil + }, + } + ) + + c := &Client{ + RPCClient: mockRPCClient, + } + + estimate, err := c.EstimateGas(&std.Tx{}) + + require.NoError(t, err) + assert.Equal(t, gasUsed, estimate) + }) +} diff --git a/gno.land/pkg/gnoclient/client_txs.go b/gno.land/pkg/gnoclient/client_txs.go index c113ea21944..ab520eceda1 100644 --- a/gno.land/pkg/gnoclient/client_txs.go +++ b/gno.land/pkg/gnoclient/client_txs.go @@ -1,8 +1,11 @@ package gnoclient import ( + "fmt" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/sdk/bank" @@ -16,6 +19,8 @@ var ( ErrMissingRPCClient = errors.New("missing RPCClient") ) +const simulatePath = ".app/simulate" + // BaseTxCfg defines the base transaction configuration, shared by all message types type BaseTxCfg struct { GasFee string // Gas fee @@ -35,6 +40,16 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcas return nil, err } + tx, err := NewCallTx(cfg, msgs...) + if err != nil { + return nil, err + } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) +} + +// NewCallTx makes an unsigned transaction from one or more MsgCall. +// The Caller field must be set. +func NewCallTx(cfg BaseTxCfg, msgs ...vm.MsgCall) (*std.Tx, error) { // Validate base transaction config if err := cfg.validateBaseTxConfig(); err != nil { return nil, err @@ -57,14 +72,12 @@ func (c *Client) Call(cfg BaseTxCfg, msgs ...vm.MsgCall) (*ctypes.ResultBroadcas } // Pack transaction - tx := std.Tx{ + return &std.Tx{ Msgs: vmMsgs, Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: cfg.Memo, - } - - return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) + }, nil } // Run executes one or more MsgRun calls on the blockchain @@ -77,6 +90,16 @@ func (c *Client) Run(cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastT return nil, err } + tx, err := NewRunTx(cfg, msgs...) + if err != nil { + return nil, err + } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) +} + +// NewRunTx makes an unsigned transaction from one or more MsgRun. +// The Caller field must be set. +func NewRunTx(cfg BaseTxCfg, msgs ...vm.MsgRun) (*std.Tx, error) { // Validate base transaction config if err := cfg.validateBaseTxConfig(); err != nil { return nil, err @@ -99,14 +122,12 @@ func (c *Client) Run(cfg BaseTxCfg, msgs ...vm.MsgRun) (*ctypes.ResultBroadcastT } // Pack transaction - tx := std.Tx{ + return &std.Tx{ Msgs: vmMsgs, Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: cfg.Memo, - } - - return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) + }, nil } // Send executes one or more MsgSend calls on the blockchain @@ -119,6 +140,16 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadc return nil, err } + tx, err := NewSendTx(cfg, msgs...) + if err != nil { + return nil, err + } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) +} + +// NewSendTx makes an unsigned transaction from one or more MsgSend. +// The FromAddress field must be set. +func NewSendTx(cfg BaseTxCfg, msgs ...bank.MsgSend) (*std.Tx, error) { // Validate base transaction config if err := cfg.validateBaseTxConfig(); err != nil { return nil, err @@ -141,14 +172,12 @@ func (c *Client) Send(cfg BaseTxCfg, msgs ...bank.MsgSend) (*ctypes.ResultBroadc } // Pack transaction - tx := std.Tx{ + return &std.Tx{ Msgs: vmMsgs, Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: cfg.Memo, - } - - return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) + }, nil } // AddPackage executes one or more AddPackage calls on the blockchain @@ -161,6 +190,16 @@ func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.Re return nil, err } + tx, err := NewAddPackageTx(cfg, msgs...) + if err != nil { + return nil, err + } + return c.signAndBroadcastTxCommit(*tx, cfg.AccountNumber, cfg.SequenceNumber) +} + +// NewAddPackageTx makes an unsigned transaction from one or more MsgAddPackage. +// The Creator field must be set. +func NewAddPackageTx(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*std.Tx, error) { // Validate base transaction config if err := cfg.validateBaseTxConfig(); err != nil { return nil, err @@ -183,18 +222,29 @@ func (c *Client) AddPackage(cfg BaseTxCfg, msgs ...vm.MsgAddPackage) (*ctypes.Re } // Pack transaction - tx := std.Tx{ + return &std.Tx{ Msgs: vmMsgs, Fee: std.NewFee(cfg.GasWanted, gasFeeCoins), Signatures: nil, Memo: cfg.Memo, - } - - return c.signAndBroadcastTxCommit(tx, cfg.AccountNumber, cfg.SequenceNumber) + }, nil } // signAndBroadcastTxCommit signs a transaction and broadcasts it, returning the result func (c *Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumber uint64) (*ctypes.ResultBroadcastTxCommit, error) { + signedTx, err := c.SignTx(tx, accountNumber, sequenceNumber) + if err != nil { + return nil, err + } + return c.BroadcastTxCommit(signedTx) +} + +// SignTx signs a transaction and returns a signed tx ready for broadcasting. +// If accountNumber or sequenceNumber is 0 then query the blockchain for the value. +func (c *Client) SignTx(tx std.Tx, accountNumber, sequenceNumber uint64) (*std.Tx, error) { + if err := c.validateSigner(); err != nil { + return nil, err + } caller, err := c.Signer.Info() if err != nil { return nil, err @@ -218,7 +268,15 @@ func (c *Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumb if err != nil { return nil, errors.Wrap(err, "sign") } + return signedTx, nil +} +// BroadcastTxCommit marshals and broadcasts the signed transaction, returning the result. +// If the result has a delivery error, then return a wrapped error. +func (c *Client) BroadcastTxCommit(signedTx *std.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + if err := c.validateRPCClient(); err != nil { + return nil, err + } bz, err := amino.Marshal(signedTx) if err != nil { return nil, errors.Wrap(err, "marshaling tx binary bytes") @@ -230,13 +288,53 @@ func (c *Client) signAndBroadcastTxCommit(tx std.Tx, accountNumber, sequenceNumb } if bres.CheckTx.IsErr() { - return bres, errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + return bres, errors.Wrapf(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) } if bres.DeliverTx.IsErr() { - return bres, errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + return bres, errors.Wrapf(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) } return bres, nil } -// TODO: Add more functionality, examples, and unit tests. +// EstimateGas returns the least amount of gas required +// for the transaction to go through on the chain (minimum gas wanted). +// The estimation process assumes the transaction is properly signed +func (c *Client) EstimateGas(tx *std.Tx) (int64, error) { + // Make sure the RPC client is set + if err := c.validateRPCClient(); err != nil { + return 0, err + } + + // Prepare the transaction. + // The transaction needs to be amino-binary encoded + // in order to be estimated + encodedTx, err := amino.Marshal(tx) + if err != nil { + return 0, fmt.Errorf("unable to marshal tx: %w", err) + } + + // Perform the simulation query + resp, err := c.RPCClient.ABCIQuery(simulatePath, encodedTx) + if err != nil { + return 0, fmt.Errorf("unable to perform ABCI query: %w", err) + } + + // Extract the query response + if err = resp.Response.Error; err != nil { + return 0, fmt.Errorf("error encountered during ABCI query: %w", err) + } + + var deliverTx abci.ResponseDeliverTx + if err = amino.Unmarshal(resp.Response.Value, &deliverTx); err != nil { + return 0, fmt.Errorf("unable to unmarshal gas estimation response: %w", err) + } + + if err = deliverTx.Error; err != nil { + return 0, fmt.Errorf("error encountered during gas estimation: %w", err) + } + + // Return the actual value returned by the node + // for executing the transaction + return deliverTx.GasUsed, nil +} diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index f2e5026aa9a..bfcaaec999e 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -1,28 +1,36 @@ package gnoclient import ( + "path/filepath" "testing" "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/tm2/pkg/sdk/bank" - "github.com/gnolang/gno/tm2/pkg/std" - + "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCallSingle_Integration(t *testing.T) { - // Set up in-memory node - config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + // Setup packages + rootdir := gnoenv.RootDir() + config := integration.TestingMinimalNodeConfig(gnoenv.RootDir()) + meta := loadpkgs(t, rootdir, "gno.land/r/demo/deep/very/deep") + state := config.Genesis.AppState.(gnoland.GnoGenesisState) + state.Txs = append(state.Txs, meta...) + config.Genesis.AppState = state + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) defer node.Stop() @@ -39,8 +47,8 @@ func TestCallSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasFee: ugnot.ValueString(2100000), + GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -66,11 +74,22 @@ func TestCallSingle_Integration(t *testing.T) { got := string(res.DeliverTx.Data) assert.Equal(t, expected, got) + + res, err = callSigningSeparately(t, client, baseCfg, msg) + require.NoError(t, err) + got = string(res.DeliverTx.Data) + assert.Equal(t, expected, got) } func TestCallMultiple_Integration(t *testing.T) { - // Set up in-memory node - config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + // Setup packages + rootdir := gnoenv.RootDir() + config := integration.TestingMinimalNodeConfig(gnoenv.RootDir()) + meta := loadpkgs(t, rootdir, "gno.land/r/demo/deep/very/deep") + state := config.Genesis.AppState.(gnoland.GnoGenesisState) + state.Txs = append(state.Txs, meta...) + config.Genesis.AppState = state + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) defer node.Stop() @@ -87,8 +106,8 @@ func TestCallMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasFee: ugnot.ValueString(2100000), + GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -123,11 +142,16 @@ func TestCallMultiple_Integration(t *testing.T) { got := string(res.DeliverTx.Data) assert.Equal(t, expected, got) + + res, err = callSigningSeparately(t, client, baseCfg, msg1, msg2) + require.NoError(t, err) + got = string(res.DeliverTx.Data) + assert.Equal(t, expected, got) } func TestSendSingle_Integration(t *testing.T) { // Set up in-memory node - config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + config := integration.TestingMinimalNodeConfig(gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) defer node.Stop() @@ -144,8 +168,8 @@ func TestSendSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasFee: ugnot.ValueString(2100000), + GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -176,11 +200,22 @@ func TestSendSingle_Integration(t *testing.T) { got := account.GetCoins() assert.Equal(t, expected, got) + + res, err = sendSigningSeparately(t, client, baseCfg, msg) + require.NoError(t, err) + assert.Equal(t, "", string(res.DeliverTx.Data)) + + // Get the new account balance + account, _, err = client.QueryAccount(toAddress) + require.NoError(t, err) + expected2 := std.Coins{{Denom: ugnot.Denom, Amount: int64(2 * amount)}} + got = account.GetCoins() + assert.Equal(t, expected2, got) } func TestSendMultiple_Integration(t *testing.T) { // Set up in-memory node - config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + config := integration.TestingMinimalNodeConfig(gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) defer node.Stop() @@ -197,8 +232,8 @@ func TestSendMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasFee: ugnot.ValueString(2100000), + GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -237,12 +272,30 @@ func TestSendMultiple_Integration(t *testing.T) { got := account.GetCoins() assert.Equal(t, expected, got) + + res, err = sendSigningSeparately(t, client, baseCfg, msg1, msg2) + require.NoError(t, err) + assert.Equal(t, "", string(res.DeliverTx.Data)) + + // Get the new account balance + account, _, err = client.QueryAccount(toAddress) + require.NoError(t, err) + expected2 := std.Coins{{Denom: ugnot.Denom, Amount: int64(2 * (amount1 + amount2))}} + got = account.GetCoins() + assert.Equal(t, expected2, got) } // Run tests func TestRunSingle_Integration(t *testing.T) { + // Setup packages + rootdir := gnoenv.RootDir() + config := integration.TestingMinimalNodeConfig(gnoenv.RootDir()) + meta := loadpkgs(t, rootdir, "gno.land/p/demo/ufmt", "gno.land/r/demo/tests") + state := config.Genesis.AppState.(gnoland.GnoGenesisState) + state.Txs = append(state.Txs, meta...) + config.Genesis.AppState = state + // Set up in-memory node - config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) defer node.Stop() @@ -258,8 +311,8 @@ func TestRunSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasFee: ugnot.ValueString(2100000), + GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -284,9 +337,9 @@ func main() { // Make Msg configs msg := vm.MsgRun{ Caller: caller.GetAddress(), - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "main", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "main.gno", Body: fileBody, @@ -300,12 +353,27 @@ func main() { assert.NoError(t, err) require.NotNil(t, res) assert.Equal(t, string(res.DeliverTx.Data), "- before: 0\n- after: 10\n") + + res, err = runSigningSeparately(t, client, baseCfg, msg) + assert.NoError(t, err) + require.NotNil(t, res) + assert.Equal(t, string(res.DeliverTx.Data), "- before: 10\n- after: 20\n") } // Run tests func TestRunMultiple_Integration(t *testing.T) { // Set up in-memory node - config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + rootdir := gnoenv.RootDir() + config := integration.TestingMinimalNodeConfig(rootdir) + meta := loadpkgs(t, rootdir, + "gno.land/p/demo/ufmt", + "gno.land/r/demo/tests", + "gno.land/r/demo/deep/very/deep", + ) + state := config.Genesis.AppState.(gnoland.GnoGenesisState) + state.Txs = append(state.Txs, meta...) + config.Genesis.AppState = state + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) defer node.Stop() @@ -321,8 +389,8 @@ func TestRunMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasFee: ugnot.ValueString(2300000), + GasWanted: 23000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -356,9 +424,9 @@ func main() { // Make Msg configs msg1 := vm.MsgRun{ Caller: caller.GetAddress(), - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "main", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "main.gno", Body: fileBody1, @@ -369,9 +437,9 @@ func main() { } msg2 := vm.MsgRun{ Caller: caller.GetAddress(), - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "main", - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "main.gno", Body: fileBody2, @@ -387,11 +455,17 @@ func main() { assert.NoError(t, err) require.NotNil(t, res) assert.Equal(t, expected, string(res.DeliverTx.Data)) + + res, err = runSigningSeparately(t, client, baseCfg, msg1, msg2) + require.NoError(t, err) + require.NotNil(t, res) + expected2 := "- before: 10\n- after: 20\nhi gnoclient!\n" + assert.Equal(t, expected2, string(res.DeliverTx.Data)) } func TestAddPackageSingle_Integration(t *testing.T) { // Set up in-memory node - config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + config := integration.TestingMinimalNodeConfig(gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) defer node.Stop() @@ -408,8 +482,8 @@ func TestAddPackageSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasFee: ugnot.ValueString(2100000), + GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -431,10 +505,10 @@ func Echo(str string) string { // Make Msg config msg := vm.MsgAddPackage{ Creator: caller.GetAddress(), - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "echo", Path: deploymentPath, - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: fileName, Body: body, @@ -460,11 +534,23 @@ func Echo(str string) string { baseAcc, _, err := client.QueryAccount(gnolang.DerivePkgAddr(deploymentPath)) require.NoError(t, err) assert.Equal(t, baseAcc.GetCoins(), deposit) + + // Test signing separately (using a different deployment path) + deploymentPathB := "gno.land/p/demo/integration/test/echo2" + msg.Package.Path = deploymentPathB + _, err = addPackageSigningSeparately(t, client, baseCfg, msg) + assert.NoError(t, err) + query, err = client.Query(QueryCfg{ + Path: "vm/qfile", + Data: []byte(deploymentPathB), + }) + require.NoError(t, err) + assert.Equal(t, string(query.Response.Data), fileName) } func TestAddPackageMultiple_Integration(t *testing.T) { // Set up in-memory node - config, _ := integration.TestingNodeConfig(t, gnoenv.RootDir()) + config := integration.TestingMinimalNodeConfig(gnoenv.RootDir()) node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNoopLogger(), config) defer node.Stop() @@ -481,8 +567,8 @@ func TestAddPackageMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ - GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasFee: ugnot.ValueString(2100000), + GasWanted: 21000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -501,7 +587,7 @@ func Echo(str string) string { body2 := `package hello func Hello(str string) string { - return "Hello " + str + "!" + return "Hello " + str + "!" }` caller, err := client.Signer.Info() @@ -509,10 +595,10 @@ func Hello(str string) string { msg1 := vm.MsgAddPackage{ Creator: caller.GetAddress(), - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "echo", Path: deploymentPath1, - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "echo.gno", Body: body1, @@ -524,10 +610,10 @@ func Hello(str string) string { msg2 := vm.MsgAddPackage{ Creator: caller.GetAddress(), - Package: &std.MemPackage{ + Package: &gnovm.MemPackage{ Name: "hello", Path: deploymentPath2, - Files: []*std.MemFile{ + Files: []*gnovm.MemFile{ { Name: "gno.mod", Body: "module gno.land/p/demo/integration/test/hello", @@ -571,6 +657,27 @@ func Hello(str string) string { baseAcc, _, err = client.QueryAccount(gnolang.DerivePkgAddr(deploymentPath2)) require.NoError(t, err) assert.Equal(t, baseAcc.GetCoins(), deposit) + + // Test signing separately (using a different deployment path) + deploymentPath1B := "gno.land/p/demo/integration/test/echo2" + deploymentPath2B := "gno.land/p/demo/integration/test/hello2" + msg1.Package.Path = deploymentPath1B + msg2.Package.Path = deploymentPath2B + _, err = addPackageSigningSeparately(t, client, baseCfg, msg1, msg2) + assert.NoError(t, err) + query, err = client.Query(QueryCfg{ + Path: "vm/qfile", + Data: []byte(deploymentPath1B), + }) + require.NoError(t, err) + assert.Equal(t, string(query.Response.Data), "echo.gno") + query, err = client.Query(QueryCfg{ + Path: "vm/qfile", + Data: []byte(deploymentPath2B), + }) + require.NoError(t, err) + assert.Contains(t, string(query.Response.Data), "hello.gno") + assert.Contains(t, string(query.Response.Data), "gno.mod") } // todo add more integration tests: @@ -594,3 +701,24 @@ func newInMemorySigner(t *testing.T, chainid string) *SignerFromKeybase { ChainID: chainid, // Chain ID for transaction signing } } + +func loadpkgs(t *testing.T, rootdir string, paths ...string) []gnoland.TxWithMetadata { + t.Helper() + + loader := integration.NewPkgsLoader() + examplesDir := filepath.Join(rootdir, "examples") + for _, path := range paths { + path = filepath.Clean(path) + path = filepath.Join(examplesDir, path) + err := loader.LoadPackage(examplesDir, path, "") + require.NoErrorf(t, err, "`loadpkg` unable to load package(s) from %q: %s", path, err) + } + privKey, err := integration.GeneratePrivKeyFromMnemonic(integration.DefaultAccount_Seed, "", 0, 0) + require.NoError(t, err) + + defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) + + meta, err := loader.LoadPackages(privKey, defaultFee, nil) + require.NoError(t, err) + return meta +} diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index f4d353411f8..0826071b9f5 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -1,15 +1,19 @@ +// Package gnoland contains the bootstrapping code to launch a gno.land node. package gnoland import ( "fmt" + "io" "log/slog" "path/filepath" "strconv" + "time" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/config" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" dbm "github.com/gnolang/gno/tm2/pkg/db" "github.com/gnolang/gno/tm2/pkg/events" @@ -17,6 +21,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/sdk/auth" "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/sdk/params" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" @@ -25,48 +30,49 @@ import ( // Only goleveldb is supported for now. _ "github.com/gnolang/gno/tm2/pkg/db/_tags" _ "github.com/gnolang/gno/tm2/pkg/db/goleveldb" - "github.com/gnolang/gno/tm2/pkg/db/memdb" ) +// AppOptions contains the options to create the gno.land ABCI application. type AppOptions struct { - DB dbm.DB - // `gnoRootDir` should point to the local location of the gno repository. - // It serves as the gno equivalent of GOROOT. - GnoRootDir string - GenesisTxHandler GenesisTxHandler - Logger *slog.Logger - EventSwitch events.EventSwitch - MaxCycles int64 - // Whether to cache the result of loading the standard libraries. - // This is useful if you have to start many nodes, like in testing. - // This disables loading existing packages; so it should only be used - // on a fresh database. - CacheStdlibLoad bool + DB dbm.DB // required + Logger *slog.Logger // required + EventSwitch events.EventSwitch // required + VMOutput io.Writer // optional + SkipGenesisVerification bool // default to verify genesis transactions + InitChainerConfig // options related to InitChainer + MinGasPrices string // optional } -func NewAppOptions() *AppOptions { +// TestAppOptions provides a "ready" default [AppOptions] for use with +// [NewAppWithOptions], using the provided db. +func TestAppOptions(db dbm.DB) *AppOptions { return &AppOptions{ - GenesisTxHandler: PanicOnFailingTxHandler, - Logger: log.NewNoopLogger(), - DB: memdb.NewMemDB(), - GnoRootDir: gnoenv.RootDir(), - EventSwitch: events.NilEventSwitch(), + DB: db, + Logger: log.NewNoopLogger(), + EventSwitch: events.NewEventSwitch(), + InitChainerConfig: InitChainerConfig{ + GenesisTxResultHandler: PanicOnFailingTxResultHandler, + StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), + CacheStdlibLoad: true, + }, + SkipGenesisVerification: true, } } -func (c *AppOptions) validate() error { - if c.Logger == nil { - return fmt.Errorf("no logger provided") - } - - if c.DB == nil { +func (c AppOptions) validate() error { + // Required fields + switch { + case c.DB == nil: return fmt.Errorf("no db provided") + case c.Logger == nil: + return fmt.Errorf("no logger provided") + case c.EventSwitch == nil: + return fmt.Errorf("no event switch provided") } - return nil } -// NewAppWithOptions creates the GnoLand application with specified options +// NewAppWithOptions creates the gno.land application with specified options. func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { if err := cfg.validate(); err != nil { return nil, err @@ -76,9 +82,13 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { mainKey := store.NewStoreKey("main") baseKey := store.NewStoreKey("base") + // set sdk app options + var appOpts []func(*sdk.BaseApp) + if cfg.MinGasPrices != "" { + appOpts = append(appOpts, sdk.SetMinGasPrices(cfg.MinGasPrices)) + } // Create BaseApp. - // TODO: Add a consensus based min gas prices for the node, by default it does not check - baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey) + baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey, appOpts...) baseApp.SetAppVersion("dev") // Set mounts for BaseApp's MultiStore. @@ -86,19 +96,23 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) // Construct keepers. - acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) + paramsKpr := params.NewParamsKeeper(mainKey, "vm") + acctKpr := auth.NewAccountKeeper(mainKey, paramsKpr, ProtoGnoAccount) + gpKpr := auth.NewGasPriceKeeper(mainKey) bankKpr := bank.NewBankKeeper(acctKpr) - // XXX: Embed this ? - stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") - vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, cfg.MaxCycles) + vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) + vmk.Output = cfg.VMOutput // Set InitChainer - baseApp.SetInitChainer(InitChainer(baseApp, acctKpr, bankKpr, cfg.GenesisTxHandler)) + icc := cfg.InitChainerConfig + icc.baseApp = baseApp + icc.acctKpr, icc.bankKpr, icc.vmKpr, icc.paramsKpr, icc.gpKpr = acctKpr, bankKpr, vmk, paramsKpr, gpKpr + baseApp.SetInitChainer(icc.InitChainer) // Set AnteHandler authOptions := auth.AnteOptions{ - VerifyGenesisSignatures: false, // for development + VerifyGenesisSignatures: !cfg.SkipGenesisVerification, } authAnteHandler := auth.NewAnteHandler( acctKpr, bankKpr, auth.DefaultSigVerificationGasConsumer, authOptions) @@ -107,15 +121,31 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { func(ctx sdk.Context, tx std.Tx, simulate bool) ( newCtx sdk.Context, res sdk.Result, abort bool, ) { + // Add last gas price in the context + ctx = ctx.WithValue(auth.GasPriceContextKey{}, gpKpr.LastGasPrice(ctx)) + // Override auth params. - ctx = ctx.WithValue( - auth.AuthParamsContextKey{}, auth.DefaultParams()) + ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx)) // Continue on with default auth ante handler. newCtx, res, abort = authAnteHandler(ctx, tx, simulate) return }, ) + // Set begin and end transaction hooks. + // These are used to create gno transaction stores and commit them when finishing + // the tx - in other words, data from a failing transaction won't be persisted + // to the gno store caches. + baseApp.SetBeginTxHook(func(ctx sdk.Context) sdk.Context { + // Create Gno transaction store. + return vmk.MakeGnoTransactionStore(ctx) + }) + baseApp.SetEndTxHook(func(ctx sdk.Context, result sdk.Result) { + if result.IsOK() { + vmk.CommitGnoTransactionStore(ctx) + } + }) + // Set up the event collector c := newCollector[validatorUpdate]( cfg.EventSwitch, // global event switch filled by the node @@ -126,6 +156,8 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { baseApp.SetEndBlocker( EndBlocker( c, + acctKpr, + gpKpr, vmk, baseApp, ), @@ -134,6 +166,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Set a handler Route. baseApp.Router().AddRoute("auth", auth.NewHandler(acctKpr)) baseApp.Router().AddRoute("bank", bank.NewHandler(bankKpr)) + baseApp.Router().AddRoute("params", params.NewHandler(paramsKpr)) baseApp.Router().AddRoute("vm", vm.NewHandler(vmk)) // Load latest version. @@ -143,24 +176,49 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Initialize the VMKeeper. ms := baseApp.GetCacheMultiStore() - vmk.Initialize(cfg.Logger, ms, cfg.CacheStdlibLoad) + vmk.Initialize(cfg.Logger, ms) ms.MultiWrite() // XXX why was't this needed? return baseApp, nil } -// NewApp creates the GnoLand application. +// GenesisAppConfig wraps the most important +// genesis params relating to the App +type GenesisAppConfig struct { + SkipFailingTxs bool // does not stop the chain from starting if any tx fails + SkipSigVerification bool // does not verify the transaction signatures in genesis +} + +// NewTestGenesisAppConfig returns a testing genesis app config +func NewTestGenesisAppConfig() GenesisAppConfig { + return GenesisAppConfig{ + SkipFailingTxs: true, + SkipSigVerification: true, + } +} + +// NewApp creates the gno.land application. func NewApp( dataRootDir string, - skipFailingGenesisTxs bool, + genesisCfg GenesisAppConfig, evsw events.EventSwitch, logger *slog.Logger, + minGasPrices string, ) (abci.Application, error) { var err error - cfg := NewAppOptions() - if skipFailingGenesisTxs { - cfg.GenesisTxHandler = NoopGenesisTxHandler + cfg := &AppOptions{ + Logger: logger, + EventSwitch: evsw, + InitChainerConfig: InitChainerConfig{ + GenesisTxResultHandler: PanicOnFailingTxResultHandler, + StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), + }, + MinGasPrices: minGasPrices, + SkipGenesisVerification: genesisCfg.SkipSigVerification, + } + if genesisCfg.SkipFailingTxs { + cfg.GenesisTxResultHandler = NoopGenesisTxResultHandler } // Get main DB. @@ -169,74 +227,169 @@ func NewApp( return nil, fmt.Errorf("error initializing database %q using path %q: %w", dbm.GoLevelDBBackend, dataRootDir, err) } - cfg.Logger = logger - cfg.EventSwitch = evsw - return NewAppWithOptions(cfg) } -type GenesisTxHandler func(ctx sdk.Context, tx std.Tx, res sdk.Result) +// GenesisTxResultHandler is called in the InitChainer after a genesis +// transaction is executed. +type GenesisTxResultHandler func(ctx sdk.Context, tx std.Tx, res sdk.Result) -func NoopGenesisTxHandler(_ sdk.Context, _ std.Tx, _ sdk.Result) {} +// NoopGenesisTxResultHandler is a no-op GenesisTxResultHandler. +func NoopGenesisTxResultHandler(_ sdk.Context, _ std.Tx, _ sdk.Result) {} -func PanicOnFailingTxHandler(_ sdk.Context, _ std.Tx, res sdk.Result) { +// PanicOnFailingTxResultHandler handles genesis transactions by panicking if +// res.IsErr() returns true. +func PanicOnFailingTxResultHandler(_ sdk.Context, _ std.Tx, res sdk.Result) { if res.IsErr() { panic(res.Log) } } -// InitChainer returns a function that can initialize the chain with genesis. -func InitChainer( - baseApp *sdk.BaseApp, - acctKpr auth.AccountKeeperI, - bankKpr bank.BankKeeperI, - resHandler GenesisTxHandler, -) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { - return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { - txResponses := []abci.ResponseDeliverTx{} - - if req.AppState != nil { - // Get genesis state - genState := req.AppState.(GnoGenesisState) - - // Parse and set genesis state balances - for _, bal := range genState.Balances { - acc := acctKpr.NewAccountWithAddress(ctx, bal.Address) - acctKpr.SetAccount(ctx, acc) - err := bankKpr.SetCoins(ctx, bal.Address, bal.Amount) - if err != nil { - panic(err) - } - } +// InitChainerConfig keeps the configuration for the InitChainer. +// [NewAppWithOptions] will set [InitChainerConfig.InitChainer] as its InitChainer +// function. +type InitChainerConfig struct { + // Handles the results of each genesis transaction. + GenesisTxResultHandler + + // Standard library directory. + StdlibDir string + // Whether to keep a record of the DB operations to load standard libraries, + // so they can be quickly replicated on additional genesis executions. + // This should be used for integration testing, where InitChainer will be + // called several times. + CacheStdlibLoad bool - // Run genesis txs - for _, tx := range genState.Txs { - res := baseApp.Deliver(tx) - if res.IsErr() { - ctx.Logger().Error( - "Unable to deliver genesis tx", - "log", res.Log, - "error", res.Error, - "gas-used", res.GasUsed, - ) - } - - txResponses = append(txResponses, abci.ResponseDeliverTx{ - ResponseBase: res.ResponseBase, - GasWanted: res.GasWanted, - GasUsed: res.GasUsed, - }) - - resHandler(ctx, tx, res) + // These fields are passed directly by NewAppWithOptions, and should not be + // configurable by end-users. + baseApp *sdk.BaseApp + vmKpr vm.VMKeeperI + acctKpr auth.AccountKeeperI + bankKpr bank.BankKeeperI + paramsKpr params.ParamsKeeperI + gpKpr auth.GasPriceKeeperI +} + +// InitChainer is the function that can be used as a [sdk.InitChainer]. +func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + start := time.Now() + ctx.Logger().Debug("InitChainer: started") + + // load standard libraries; immediately committed to store so that they are + // available for use when processing the genesis transactions below. + cfg.loadStdlibs(ctx) + ctx.Logger().Debug("InitChainer: standard libraries loaded", + "elapsed", time.Since(start)) + + // load app state. AppState may be nil mostly in some minimal testing setups; + // so log a warning when that happens. + txResponses, err := cfg.loadAppState(ctx, req.AppState) + if err != nil { + return abci.ResponseInitChain{ + ResponseBase: abci.ResponseBase{ + Error: abci.StringError(err.Error()), + }, + } + } + + ctx.Logger().Debug("InitChainer: genesis transactions loaded", + "elapsed", time.Since(start)) + + // Done! + return abci.ResponseInitChain{ + Validators: req.Validators, + TxResponses: txResponses, + } +} + +func (cfg InitChainerConfig) loadStdlibs(ctx sdk.Context) { + // cache-wrapping is necessary for non-validator nodes; in the tm2 BaseApp, + // this is done using BaseApp.cacheTxContext; so we replicate it here. + ms := ctx.MultiStore() + msCache := ms.MultiCacheWrap() + + stdlibCtx := cfg.vmKpr.MakeGnoTransactionStore(ctx) + stdlibCtx = stdlibCtx.WithMultiStore(msCache) + if cfg.CacheStdlibLoad { + cfg.vmKpr.LoadStdlibCached(stdlibCtx, cfg.StdlibDir) + } else { + cfg.vmKpr.LoadStdlib(stdlibCtx, cfg.StdlibDir) + } + cfg.vmKpr.CommitGnoTransactionStore(stdlibCtx) + + msCache.MultiWrite() +} + +func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci.ResponseDeliverTx, error) { + state, ok := appState.(GnoGenesisState) + if !ok { + return nil, fmt.Errorf("invalid AppState of type %T", appState) + } + cfg.acctKpr.InitGenesis(ctx, state.Auth) + params := cfg.acctKpr.GetParams(ctx) + ctx = ctx.WithValue(auth.AuthParamsContextKey{}, params) + auth.InitChainer(ctx, cfg.gpKpr.(auth.GasPriceKeeper), params.InitialGasPrice) + + // Apply genesis balances. + for _, bal := range state.Balances { + acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) + cfg.acctKpr.SetAccount(ctx, acc) + err := cfg.bankKpr.SetCoins(ctx, bal.Address, bal.Amount) + if err != nil { + panic(err) + } + } + + // Apply genesis params. + for _, param := range state.Params { + param.register(ctx, cfg.paramsKpr) + } + + // Replay genesis txs. + txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs)) + + // Run genesis txs + for _, tx := range state.Txs { + var ( + stdTx = tx.Tx + metadata = tx.Metadata + + ctxFn sdk.ContextFn + ) + + // Check if there is metadata associated with the tx + if metadata != nil { + // Create a custom context modifier + ctxFn = func(ctx sdk.Context) sdk.Context { + // Create a copy of the header, in + // which only the timestamp information is modified + header := ctx.BlockHeader().(*bft.Header).Copy() + header.Time = time.Unix(metadata.Timestamp, 0) + + // Save the modified header + return ctx.WithBlockHeader(header) } } - // Done! - return abci.ResponseInitChain{ - Validators: req.Validators, - TxResponses: txResponses, + res := cfg.baseApp.Deliver(stdTx, ctxFn) + if res.IsErr() { + ctx.Logger().Error( + "Unable to deliver genesis tx", + "log", res.Log, + "error", res.Error, + "gas-used", res.GasUsed, + ) } + + txResponses = append(txResponses, abci.ResponseDeliverTx{ + ResponseBase: res.ResponseBase, + GasWanted: res.GasWanted, + GasUsed: res.GasUsed, + }) + + cfg.GenesisTxResultHandler(ctx, stdTx, res) } + return txResponses, nil } // endBlockerApp is the app abstraction required by any EndBlocker @@ -253,6 +406,8 @@ type endBlockerApp interface { // validator set changes func EndBlocker( collector *collector[validatorUpdate], + acctKpr auth.AccountKeeperI, + gpKpr auth.GasPriceKeeperI, vmk vm.VMKeeperI, app endBlockerApp, ) func( @@ -260,6 +415,14 @@ func EndBlocker( req abci.RequestEndBlock, ) abci.ResponseEndBlock { return func(ctx sdk.Context, _ abci.RequestEndBlock) abci.ResponseEndBlock { + // set the auth params value in the ctx. The EndBlocker will use InitialGasPrice in + // the params to calculate the updated gas price. + if acctKpr != nil { + ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx)) + } + if acctKpr != nil && gpKpr != nil { + auth.EndBlocker(ctx, gpKpr) + } // Check if there was a valset change if len(collector.getEvents()) == 0 { // No valset updates diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 852d090f3af..361d7505157 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -1,20 +1,243 @@ package gnoland import ( + "context" "errors" "fmt" "strings" "testing" + "time" - "github.com/gnolang/gno/gnovm/stdlibs/std" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" + gnostdlibs "github.com/gnolang/gno/gnovm/stdlibs/std" + "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/types" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/events" + "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/sdk/params" + "github.com/gnolang/gno/tm2/pkg/sdk/testutils" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/store" + "github.com/gnolang/gno/tm2/pkg/store/dbadapter" + "github.com/gnolang/gno/tm2/pkg/store/iavl" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// Tests that NewAppWithOptions works even when only providing a simple DB. +func TestNewAppWithOptions(t *testing.T) { + t.Parallel() + + app, err := NewAppWithOptions(TestAppOptions(memdb.NewMemDB())) + require.NoError(t, err) + bapp := app.(*sdk.BaseApp) + assert.Equal(t, "dev", bapp.AppVersion()) + assert.Equal(t, "gnoland", bapp.Name()) + + addr := crypto.AddressFromPreimage([]byte("test1")) + + appState := DefaultGenState() + appState.Balances = []Balance{ + { + Address: addr, + Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, + }, + } + appState.Txs = []TxWithMetadata{ + { + Tx: std.Tx{ + Msgs: []std.Msg{vm.NewMsgAddPackage(addr, "gno.land/r/demo", []*gnovm.MemFile{ + { + Name: "demo.gno", + Body: "package demo; func Hello() string { return `hello`; }", + }, + })}, + Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, + Signatures: []std.Signature{{}}, // one empty signature + }, + }, + } + appState.Params = []Param{ + {key: "foo", kind: "string", value: "hello"}, + {key: "foo", kind: "int64", value: int64(-42)}, + {key: "foo", kind: "uint64", value: uint64(1337)}, + {key: "foo", kind: "bool", value: true}, + {key: "foo", kind: "bytes", value: []byte{0x48, 0x69, 0x21}}, + } + + resp := bapp.InitChain(abci.RequestInitChain{ + Time: time.Now(), + ChainID: "dev", + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + }, + Validators: []abci.ValidatorUpdate{}, + AppState: appState, + }) + require.True(t, resp.IsOK(), "InitChain response: %v", resp) + + tx := amino.MustMarshal(std.Tx{ + Msgs: []std.Msg{vm.NewMsgCall(addr, nil, "gno.land/r/demo", "Hello", nil)}, + Fee: std.Fee{ + GasWanted: 100_000, + GasFee: std.Coin{ + Denom: "ugnot", + Amount: 1_000_000, + }, + }, + Signatures: []std.Signature{{}}, // one empty signature + Memo: "", + }) + dtxResp := bapp.DeliverTx(abci.RequestDeliverTx{ + RequestBase: abci.RequestBase{}, + Tx: tx, + }) + require.True(t, dtxResp.IsOK(), "DeliverTx response: %v", dtxResp) + + cres := bapp.Commit() + require.NotNil(t, cres) + + tcs := []struct { + path string + expectedVal string + }{ + {"params/vm/foo.string", `"hello"`}, + {"params/vm/foo.int64", `"-42"`}, + {"params/vm/foo.uint64", `"1337"`}, + {"params/vm/foo.bool", `true`}, + {"params/vm/foo.bytes", `"SGkh"`}, // XXX: make this test more readable + } + for _, tc := range tcs { + qres := bapp.Query(abci.RequestQuery{ + Path: tc.path, + }) + require.True(t, qres.IsOK()) + assert.Equal(t, qres.Data, []byte(tc.expectedVal)) + } +} + +func TestNewAppWithOptions_ErrNoDB(t *testing.T) { + t.Parallel() + + _, err := NewAppWithOptions(&AppOptions{}) + assert.ErrorContains(t, err, "no db provided") +} + +func TestNewApp(t *testing.T) { + // NewApp should have good defaults and manage to run InitChain. + td := t.TempDir() + + app, err := NewApp(td, NewTestGenesisAppConfig(), events.NewEventSwitch(), log.NewNoopLogger(), "") + require.NoError(t, err, "NewApp should be successful") + + resp := app.InitChain(abci.RequestInitChain{ + RequestBase: abci.RequestBase{}, + Time: time.Time{}, + ChainID: "dev", + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + Validators: []abci.ValidatorUpdate{}, + AppState: DefaultGenState(), + }) + assert.True(t, resp.IsOK(), "resp is not OK: %v", resp) +} + +// Test whether InitChainer calls to load the stdlibs correctly. +func TestInitChainer_LoadStdlib(t *testing.T) { + t.Parallel() + + t.Run("cached", func(t *testing.T) { testInitChainerLoadStdlib(t, true) }) + t.Run("uncached", func(t *testing.T) { testInitChainerLoadStdlib(t, false) }) +} + +func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper + t.Parallel() + + type gsContextType string + const ( + stdlibDir = "test-stdlib-dir" + gnoStoreKey gsContextType = "gno-store-key" + gnoStoreValue gsContextType = "gno-store-value" + ) + db := memdb.NewMemDB() + ms := store.NewCommitMultiStore(db) + baseCapKey := store.NewStoreKey("baseCapKey") + iavlCapKey := store.NewStoreKey("iavlCapKey") + + ms.MountStoreWithDB(baseCapKey, dbadapter.StoreConstructor, db) + ms.MountStoreWithDB(iavlCapKey, iavl.StoreConstructor, db) + ms.LoadLatestVersion() + testCtx := sdk.NewContext(sdk.RunTxModeDeliver, ms.MultiCacheWrap(), &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) + + // mock set-up + var ( + makeCalls int + commitCalls int + loadStdlibCalls int + loadStdlibCachedCalls int + ) + containsGnoStore := func(ctx sdk.Context) bool { + return ctx.Context().Value(gnoStoreKey) == gnoStoreValue + } + // ptr is pointer to either loadStdlibCalls or loadStdlibCachedCalls + loadStdlib := func(ptr *int) func(ctx sdk.Context, dir string) { + return func(ctx sdk.Context, dir string) { + assert.Equal(t, stdlibDir, dir, "stdlibDir should match provided dir") + assert.True(t, containsGnoStore(ctx), "should contain gno store") + *ptr++ + } + } + mock := &mockVMKeeper{ + makeGnoTransactionStoreFn: func(ctx sdk.Context) sdk.Context { + makeCalls++ + assert.False(t, containsGnoStore(ctx), "should not already contain gno store") + return ctx.WithContext(context.WithValue(ctx.Context(), gnoStoreKey, gnoStoreValue)) + }, + commitGnoTransactionStoreFn: func(ctx sdk.Context) { + commitCalls++ + assert.True(t, containsGnoStore(ctx), "should contain gno store") + }, + loadStdlibFn: loadStdlib(&loadStdlibCalls), + loadStdlibCachedFn: loadStdlib(&loadStdlibCachedCalls), + } + + // call initchainer + cfg := InitChainerConfig{ + StdlibDir: stdlibDir, + vmKpr: mock, + CacheStdlibLoad: cached, + } + // Construct keepers. + paramsKpr := params.NewParamsKeeper(iavlCapKey, "") + cfg.acctKpr = auth.NewAccountKeeper(iavlCapKey, paramsKpr, ProtoGnoAccount) + cfg.gpKpr = auth.NewGasPriceKeeper(iavlCapKey) + cfg.InitChainer(testCtx, abci.RequestInitChain{ + AppState: DefaultGenState(), + }) + + // assert number of calls + assert.Equal(t, 1, makeCalls, "should call MakeGnoTransactionStore once") + assert.Equal(t, 1, commitCalls, "should call CommitGnoTransactionStore once") + if cached { + assert.Equal(t, 0, loadStdlibCalls, "should call LoadStdlib never") + assert.Equal(t, 1, loadStdlibCachedCalls, "should call LoadStdlibCached once") + } else { + assert.Equal(t, 1, loadStdlibCalls, "should call LoadStdlib once") + assert.Equal(t, 0, loadStdlibCachedCalls, "should call LoadStdlibCached never") + } +} + // generateValidatorUpdates generates dummy validator updates func generateValidatorUpdates(t *testing.T, count int) []abci.ValidatorUpdate { t.Helper() @@ -23,7 +246,7 @@ func generateValidatorUpdates(t *testing.T, count int) []abci.ValidatorUpdate { for i := 0; i < count; i++ { // Generate a random private key - key := getDummyKey(t) + key := getDummyKey(t).PubKey() validator := abci.ValidatorUpdate{ Address: key.Address(), @@ -37,6 +260,189 @@ func generateValidatorUpdates(t *testing.T, count int) []abci.ValidatorUpdate { return validators } +func createAndSignTx( + t *testing.T, + msgs []std.Msg, + chainID string, + key crypto.PrivKey, +) std.Tx { + t.Helper() + + tx := std.Tx{ + Msgs: msgs, + Fee: std.Fee{ + GasFee: std.NewCoin("ugnot", 2000000), + GasWanted: 10000000, + }, + } + + signBytes, err := tx.GetSignBytes(chainID, 0, 0) + require.NoError(t, err) + + // Sign the tx + signedTx, err := key.Sign(signBytes) + require.NoError(t, err) + + tx.Signatures = []std.Signature{ + { + PubKey: key.PubKey(), + Signature: signedTx, + }, + } + + return tx +} + +func TestInitChainer_MetadataTxs(t *testing.T) { + var ( + currentTimestamp = time.Now() + laterTimestamp = currentTimestamp.Add(10 * 24 * time.Hour) // 10 days + + getMetadataState = func(tx std.Tx, balances []Balance) GnoGenesisState { + return GnoGenesisState{ + // Set the package deployment as the genesis tx + Txs: []TxWithMetadata{ + { + Tx: tx, + Metadata: &GnoTxMetadata{ + Timestamp: laterTimestamp.Unix(), + }, + }, + }, + // Make sure the deployer account has a balance + Balances: balances, + } + } + + getNonMetadataState = func(tx std.Tx, balances []Balance) GnoGenesisState { + return GnoGenesisState{ + Txs: []TxWithMetadata{ + { + Tx: tx, + }, + }, + Balances: balances, + } + } + ) + + testTable := []struct { + name string + genesisTime time.Time + expectedTime time.Time + stateFn func(std.Tx, []Balance) GnoGenesisState + }{ + { + "non-metadata transaction", + currentTimestamp, + currentTimestamp, + getNonMetadataState, + }, + { + "metadata transaction", + currentTimestamp, + laterTimestamp, + getMetadataState, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + var ( + db = memdb.NewMemDB() + + key = getDummyKey(t) // user account, and genesis deployer + chainID = "test" + + path = "gno.land/r/demo/metadatatx" + body = `package metadatatx + + import "time" + + // Time is initialized on deployment (genesis) + var t time.Time = time.Now() + + // GetT returns the time that was saved from genesis + func GetT() int64 { return t.Unix() } +` + ) + + // Create a fresh app instance + app, err := NewAppWithOptions(TestAppOptions(db)) + require.NoError(t, err) + + // Prepare the deploy transaction + msg := vm.MsgAddPackage{ + Creator: key.PubKey().Address(), + Package: &gnovm.MemPackage{ + Name: "metadatatx", + Path: path, + Files: []*gnovm.MemFile{ + { + Name: "file.gno", + Body: body, + }, + }, + }, + Deposit: nil, + } + + // Create the initial genesis tx + tx := createAndSignTx(t, []std.Msg{msg}, chainID, key) + + // Run the top-level init chain process + app.InitChain(abci.RequestInitChain{ + ChainID: chainID, + Time: testCase.genesisTime, + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + // Set the package deployment as the genesis tx, + // and make sure the deployer account has a balance + AppState: testCase.stateFn(tx, []Balance{ + { + // Make sure the deployer account has a balance + Address: key.PubKey().Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)), + }, + }), + }) + + // Prepare the call transaction + callMsg := vm.MsgCall{ + Caller: key.PubKey().Address(), + PkgPath: path, + Func: "GetT", + } + + tx = createAndSignTx(t, []std.Msg{callMsg}, chainID, key) + + // Marshal the transaction to Amino binary + marshalledTx, err := amino.Marshal(tx) + require.NoError(t, err) + + // Execute the call to the "GetT" method + // on the deployed Realm + resp := app.DeliverTx(abci.RequestDeliverTx{ + Tx: marshalledTx, + }) + + require.True(t, resp.IsOK()) + + // Make sure the initialized Realm state is + // the injected context timestamp from the tx metadata + assert.Contains( + t, + string(resp.Data), + fmt.Sprintf("(%d int64)", testCase.expectedTime.Unix()), + ) + }) + } +} + func TestEndBlocker(t *testing.T) { t.Parallel() @@ -81,7 +487,7 @@ func TestEndBlocker(t *testing.T) { t.Run("no collector events", func(t *testing.T) { t.Parallel() - noFilter := func(e events.Event) []validatorUpdate { + noFilter := func(_ events.Event) []validatorUpdate { return []validatorUpdate{} } @@ -89,7 +495,7 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](&mockEventSwitch{}, noFilter) // Create the EndBlocker - eb := EndBlocker(c, nil, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, nil, &mockEndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}, abci.RequestEndBlock{}) @@ -102,7 +508,7 @@ func TestEndBlocker(t *testing.T) { t.Parallel() var ( - noFilter = func(e events.Event) []validatorUpdate { + noFilter = func(_ events.Event) []validatorUpdate { return make([]validatorUpdate, 1) // 1 update } @@ -126,10 +532,10 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](mockEventSwitch, noFilter) // Fire a GnoVM event - mockEventSwitch.FireEvent(std.GnoEvent{}) + mockEventSwitch.FireEvent(gnostdlibs.GnoEvent{}) // Create the EndBlocker - eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}, abci.RequestEndBlock{}) @@ -145,7 +551,7 @@ func TestEndBlocker(t *testing.T) { t.Parallel() var ( - noFilter = func(e events.Event) []validatorUpdate { + noFilter = func(_ events.Event) []validatorUpdate { return make([]validatorUpdate, 1) // 1 update } @@ -169,10 +575,10 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](mockEventSwitch, noFilter) // Fire a GnoVM event - mockEventSwitch.FireEvent(std.GnoEvent{}) + mockEventSwitch.FireEvent(gnostdlibs.GnoEvent{}) // Create the EndBlocker - eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}, abci.RequestEndBlock{}) @@ -208,7 +614,7 @@ func TestEndBlocker(t *testing.T) { // Construct the GnoVM events vmEvents := make([]abci.Event, 0, len(changes)) for index := range changes { - event := std.GnoEvent{ + event := gnostdlibs.GnoEvent{ Type: validatorAddedEvent, PkgPath: valRealm, } @@ -217,7 +623,7 @@ func TestEndBlocker(t *testing.T) { if index%2 == 0 { changes[index].Power = 0 - event = std.GnoEvent{ + event = gnostdlibs.GnoEvent{ Type: validatorRemovedEvent, PkgPath: valRealm, } @@ -227,8 +633,8 @@ func TestEndBlocker(t *testing.T) { } // Fire the tx result event - txEvent := types.EventTx{ - Result: types.TxResult{ + txEvent := bft.EventTx{ + Result: bft.TxResult{ Response: abci.ResponseDeliverTx{ ResponseBase: abci.ResponseBase{ Events: vmEvents, @@ -240,7 +646,7 @@ func TestEndBlocker(t *testing.T) { mockEventSwitch.FireEvent(txEvent) // Create the EndBlocker - eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{}) + eb := EndBlocker(c, nil, nil, mockVMKeeper, &mockEndBlockerApp{}) // Run the EndBlocker res := eb(sdk.Context{}, abci.RequestEndBlock{}) @@ -255,3 +661,338 @@ func TestEndBlocker(t *testing.T) { } }) } + +func TestGasPriceUpdate(t *testing.T) { + app := newGasPriceTestApp(t) + + // with default initial gas price 0.1 ugnot per gas + gnoGen := gnoGenesisState(t) + + // abci inintChain + app.InitChain(abci.RequestInitChain{ + AppState: gnoGen, + ChainID: "test-chain", + ConsensusParams: &abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxGas: 10000, + }, + }, + }) + baseApp := app.(*sdk.BaseApp) + require.Equal(t, int64(0), baseApp.LastBlockHeight()) + // Case 1 + // CheckTx failed because the GasFee is less than the initial gas price. + + tx := newCounterTx(100) + tx.Fee = std.Fee{ + GasWanted: 100, + GasFee: sdk.Coin{ + Amount: 9, + Denom: "ugnot", + }, + } + txBytes, err := amino.Marshal(tx) + require.NoError(t, err) + r := app.CheckTx(abci.RequestCheckTx{Tx: txBytes}) + assert.False(t, r.IsOK(), fmt.Sprintf("%v", r)) + + // Case 2: + // A previously successful CheckTx failed after the block gas price increased. + // Check Tx Ok + tx2 := newCounterTx(100) + tx2.Fee = std.Fee{ + GasWanted: 1000, + GasFee: sdk.Coin{ + Amount: 100, + Denom: "ugnot", + }, + } + txBytes2, err := amino.Marshal(tx2) + require.NoError(t, err) + r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2}) + assert.True(t, r.IsOK(), fmt.Sprintf("%v", r)) + + // After replaying a block, the gas price increased. + header := &bft.Header{ChainID: "test-chain", Height: 1} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + // Delvier Tx consumes more than that target block gas 6000. + + tx6001 := newCounterTx(6001) + tx6001.Fee = std.Fee{ + GasWanted: 20000, + GasFee: sdk.Coin{ + Amount: 200, + Denom: "ugnot", + }, + } + txBytes6001, err := amino.Marshal(tx6001) + require.NoError(t, err) + res := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes6001}) + require.True(t, res.IsOK(), fmt.Sprintf("%v", res)) + app.EndBlock(abci.RequestEndBlock{}) + app.Commit() + + // CheckTx failed because gas price increased + r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2}) + assert.False(t, r.IsOK(), fmt.Sprintf("%v", r)) + + // Case 3: + // A previously failed CheckTx successed after block gas price reduced. + + // CheckTx Failed + r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2}) + assert.False(t, r.IsOK(), fmt.Sprintf("%v", r)) + // Replayed a Block, the gas price decrease + header = &bft.Header{ChainID: "test-chain", Height: 2} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + // Delvier Tx consumes less than that target block gas 6000. + + tx200 := newCounterTx(200) + tx200.Fee = std.Fee{ + GasWanted: 20000, + GasFee: sdk.Coin{ + Amount: 200, + Denom: "ugnot", + }, + } + txBytes200, err := amino.Marshal(tx200) + require.NoError(t, err) + + res = app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes200}) + require.True(t, res.IsOK(), fmt.Sprintf("%v", res)) + + app.EndBlock(abci.RequestEndBlock{}) + app.Commit() + + // CheckTx earlier failed tx, now is OK + r = app.CheckTx(abci.RequestCheckTx{Tx: txBytes2}) + assert.True(t, r.IsOK(), fmt.Sprintf("%v", r)) + + // Case 4 + // require matching expected GasPrice after three blocks ( increase case) + replayBlock(t, baseApp, 8000, 3) + replayBlock(t, baseApp, 8000, 4) + replayBlock(t, baseApp, 6000, 5) + + key := []byte("gasPrice") + query := abci.RequestQuery{ + Path: ".store/main/key", + Data: key, + } + qr := app.Query(query) + var gp std.GasPrice + err = amino.Unmarshal(qr.Value, &gp) + require.NoError(t, err) + require.Equal(t, "108ugnot", gp.Price.String()) + + // Case 5, + // require matching expected GasPrice after low gas blocks ( decrease below initial gas price case) + + replayBlock(t, baseApp, 5000, 6) + replayBlock(t, baseApp, 5000, 7) + replayBlock(t, baseApp, 5000, 8) + + qr = app.Query(query) + err = amino.Unmarshal(qr.Value, &gp) + require.NoError(t, err) + require.Equal(t, "102ugnot", gp.Price.String()) + + replayBlock(t, baseApp, 5000, 9) + + qr = app.Query(query) + err = amino.Unmarshal(qr.Value, &gp) + require.NoError(t, err) + require.Equal(t, "100ugnot", gp.Price.String()) +} + +func newGasPriceTestApp(t *testing.T) abci.Application { + t.Helper() + cfg := TestAppOptions(memdb.NewMemDB()) + cfg.EventSwitch = events.NewEventSwitch() + + // Capabilities keys. + mainKey := store.NewStoreKey("main") + baseKey := store.NewStoreKey("base") + + baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey) + baseApp.SetAppVersion("test") + + // Set mounts for BaseApp's MultiStore. + baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, cfg.DB) + baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) + + // Construct keepers. + paramsKpr := params.NewParamsKeeper(mainKey, "") + acctKpr := auth.NewAccountKeeper(mainKey, paramsKpr, ProtoGnoAccount) + gpKpr := auth.NewGasPriceKeeper(mainKey) + bankKpr := bank.NewBankKeeper(acctKpr) + vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) + + // Set InitChainer + icc := cfg.InitChainerConfig + icc.baseApp = baseApp + icc.acctKpr, icc.bankKpr, icc.vmKpr, icc.gpKpr = acctKpr, bankKpr, vmk, gpKpr + baseApp.SetInitChainer(icc.InitChainer) + + // Set AnteHandler + baseApp.SetAnteHandler( + // Override default AnteHandler with custom logic. + func(ctx sdk.Context, tx std.Tx, simulate bool) ( + newCtx sdk.Context, res sdk.Result, abort bool, + ) { + // Add last gas price in the context + ctx = ctx.WithValue(auth.GasPriceContextKey{}, gpKpr.LastGasPrice(ctx)) + + // Override auth params. + ctx = ctx.WithValue(auth.AuthParamsContextKey{}, acctKpr.GetParams(ctx)) + // Continue on with default auth ante handler. + if ctx.IsCheckTx() { + res := auth.EnsureSufficientMempoolFees(ctx, tx.Fee) + if !res.IsOK() { + return ctx, res, true + } + } + + newCtx = auth.SetGasMeter(false, ctx, tx.Fee.GasWanted) + + count := getTotalCount(tx) + + newCtx.GasMeter().ConsumeGas(count, "counter-ante") + res = sdk.Result{ + GasWanted: getTotalCount(tx), + } + return + }, + ) + + // Set up the event collector + c := newCollector[validatorUpdate]( + cfg.EventSwitch, // global event switch filled by the node + validatorEventFilter, // filter fn that keeps the collector valid + ) + + // Set EndBlocker + baseApp.SetEndBlocker( + EndBlocker( + c, + acctKpr, + gpKpr, + nil, + baseApp, + ), + ) + + // Set a handler Route. + baseApp.Router().AddRoute("auth", auth.NewHandler(acctKpr)) + baseApp.Router().AddRoute("bank", bank.NewHandler(bankKpr)) + baseApp.Router().AddRoute( + testutils.RouteMsgCounter, + newTestHandler( + func(ctx sdk.Context, msg sdk.Msg) sdk.Result { return sdk.Result{} }, + ), + ) + + baseApp.Router().AddRoute("vm", vm.NewHandler(vmk)) + + // Load latest version. + if err := baseApp.LoadLatestVersion(); err != nil { + t.Fatalf("failed to load the lastest state: %v", err) + } + + // Initialize the VMKeeper. + ms := baseApp.GetCacheMultiStore() + vmk.Initialize(cfg.Logger, ms) + ms.MultiWrite() // XXX why was't this needed? + + return baseApp +} + +// newTx constructs a tx with multiple counter messages. +// we can use the counter as the gas used for the message. + +func newCounterTx(counters ...int64) sdk.Tx { + msgs := make([]sdk.Msg, len(counters)) + + for i, c := range counters { + msgs[i] = testutils.MsgCounter{Counter: c} + } + tx := sdk.Tx{Msgs: msgs} + return tx +} + +func getTotalCount(tx sdk.Tx) int64 { + var c int64 + for _, m := range tx.Msgs { + c = +m.(testutils.MsgCounter).Counter + } + return c +} + +func gnoGenesisState(t *testing.T) GnoGenesisState { + t.Helper() + gen := GnoGenesisState{} + genBytes := []byte(`{ + "@type": "/gno.GenesisState", + "auth": { + "params": { + "gas_price_change_compressor": "8", + "initial_gasprice": { + "gas": "1000", + "price": "100ugnot" + }, + "max_memo_bytes": "65536", + "sig_verify_cost_ed25519": "590", + "sig_verify_cost_secp256k1": "1000", + "target_gas_ratio": "60", + "tx_sig_limit": "7", + "tx_size_cost_per_byte": "10" + } + } + }`) + err := amino.UnmarshalJSON(genBytes, &gen) + if err != nil { + t.Fatalf("failed to create genesis state: %v", err) + } + return gen +} + +func replayBlock(t *testing.T, app *sdk.BaseApp, gas int64, hight int64) { + t.Helper() + tx := newCounterTx(gas) + tx.Fee = std.Fee{ + GasWanted: 20000, + GasFee: sdk.Coin{ + Amount: 1000, + Denom: "ugnot", + }, + } + txBytes, err := amino.Marshal(tx) + require.NoError(t, err) + + header := &bft.Header{ChainID: "test-chain", Height: hight} + app.BeginBlock(abci.RequestBeginBlock{Header: header}) + // consume gas in the block + res := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes}) + require.True(t, res.IsOK(), fmt.Sprintf("%v", res)) + app.EndBlock(abci.RequestEndBlock{}) + app.Commit() +} + +type testHandler struct { + process func(sdk.Context, sdk.Msg) sdk.Result + query func(sdk.Context, abci.RequestQuery) abci.ResponseQuery +} + +func (th testHandler) Process(ctx sdk.Context, msg sdk.Msg) sdk.Result { + return th.process(ctx, msg) +} + +func (th testHandler) Query(ctx sdk.Context, req abci.RequestQuery) abci.ResponseQuery { + return th.query(ctx, req) +} + +func newTestHandler(proc func(sdk.Context, sdk.Msg) sdk.Result) sdk.Handler { + return testHandler{ + process: proc, + } +} diff --git a/gno.land/pkg/gnoland/balance_test.go b/gno.land/pkg/gnoland/balance_test.go index 99a348e9f2f..489384196ad 100644 --- a/gno.land/pkg/gnoland/balance_test.go +++ b/gno.land/pkg/gnoland/balance_test.go @@ -120,7 +120,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { for index, key := range dummyKeys { entries[index] = fmt.Sprintf( "%s=%s", - key.Address().String(), + key.PubKey().Address().String(), ugnot.ValueString(amount.AmountOf(ugnot.Denom)), ) } @@ -131,7 +131,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { // Validate the balance map assert.Len(t, balanceMap, len(dummyKeys)) for _, key := range dummyKeys { - assert.Equal(t, amount, balanceMap[key.Address()].Amount) + assert.Equal(t, amount, balanceMap[key.PubKey().Address()].Amount) } }) @@ -162,7 +162,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { t.Run("malformed balance, invalid amount", func(t *testing.T) { t.Parallel() - dummyKey := getDummyKey(t) + dummyKey := getDummyKey(t).PubKey() balances := []string{ fmt.Sprintf( @@ -194,7 +194,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { for index, key := range dummyKeys { balances[index] = fmt.Sprintf( "%s=%s", - key.Address().String(), + key.PubKey().Address().String(), ugnot.ValueString(amount.AmountOf(ugnot.Denom)), ) } @@ -206,14 +206,14 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { // Validate the balance map assert.Len(t, balanceMap, len(dummyKeys)) for _, key := range dummyKeys { - assert.Equal(t, amount, balanceMap[key.Address()].Amount) + assert.Equal(t, amount, balanceMap[key.PubKey().Address()].Amount) } }) t.Run("malformed balance, invalid amount", func(t *testing.T) { t.Parallel() - dummyKey := getDummyKey(t) + dummyKey := getDummyKey(t).PubKey() balances := []string{ fmt.Sprintf( @@ -236,9 +236,8 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { // XXX: this function should probably be exposed somewhere as it's duplicate of // cmd/genesis/... -// getDummyKey generates a random public key, -// and returns the key info -func getDummyKey(t *testing.T) crypto.PubKey { +// getDummyKey generates a random private key +func getDummyKey(t *testing.T) crypto.PrivKey { t.Helper() mnemonic, err := client.GenerateMnemonic(256) @@ -246,14 +245,14 @@ func getDummyKey(t *testing.T) crypto.PubKey { seed := bip39.NewSeed(mnemonic, "") - return generateKeyFromSeed(seed, 0).PubKey() + return generateKeyFromSeed(seed, 0) } // getDummyKeys generates random keys for testing -func getDummyKeys(t *testing.T, count int) []crypto.PubKey { +func getDummyKeys(t *testing.T, count int) []crypto.PrivKey { t.Helper() - dummyKeys := make([]crypto.PubKey, count) + dummyKeys := make([]crypto.PrivKey, count) for i := 0; i < count; i++ { dummyKeys[i] = getDummyKey(t) diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index f5f0aa56758..a754e7a4644 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -12,16 +12,23 @@ import ( bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" "github.com/gnolang/gno/tm2/pkg/std" + "github.com/pelletier/go-toml" ) +const initGasPrice = "1ugnot/1000gas" + // LoadGenesisBalancesFile loads genesis balances from the provided file path. -func LoadGenesisBalancesFile(path string) ([]Balance, error) { +func LoadGenesisBalancesFile(path string) (Balances, error) { // each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot - content := osm.MustReadFile(path) + content, err := osm.ReadFile(path) + if err != nil { + return nil, err + } lines := strings.Split(string(content), "\n") - balances := make([]Balance, 0, len(lines)) + balances := make(Balances, len(lines)) for _, line := range lines { line = strings.TrimSpace(line) @@ -49,20 +56,60 @@ func LoadGenesisBalancesFile(path string) ([]Balance, error) { return nil, fmt.Errorf("invalid balance coins %s: %w", parts[1], err) } - balances = append(balances, Balance{ - Address: addr, - Amount: coins, - }) + balances.Set(addr, coins) } return balances, nil } +// LoadGenesisParamsFile loads genesis params from the provided file path. +func LoadGenesisParamsFile(path string) ([]Param, error) { + // each param is in the form: key.kind=value + content, err := osm.ReadFile(path) + if err != nil { + return nil, err + } + + m := map[string] /*category*/ map[string] /*key*/ map[string] /*kind*/ interface{} /*value*/ {} + err = toml.Unmarshal(content, &m) + if err != nil { + return nil, err + } + + params := make([]Param, 0) + for category, keys := range m { + for key, kinds := range keys { + for kind, val := range kinds { + param := Param{ + key: category + "." + key, + kind: kind, + } + switch kind { + case "uint64": // toml + param.value = uint64(val.(int64)) + default: + param.value = val + } + if err := param.Verify(); err != nil { + return nil, err + } + params = append(params, param) + } + } + } + + return params, nil +} + // LoadGenesisTxsFile loads genesis transactions from the provided file path. // XXX: Improve the way we generate and load this file -func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]std.Tx, error) { - txs := []std.Tx{} - txsBz := osm.MustReadFile(path) +func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]TxWithMetadata, error) { + txs := make([]TxWithMetadata, 0) + + txsBz, err := osm.ReadFile(path) + if err != nil { + return nil, err + } txsLines := strings.Split(string(txsBz), "\n") for _, txLine := range txsLines { if txLine == "" { @@ -73,7 +120,7 @@ func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]st txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID) txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) - var tx std.Tx + var tx TxWithMetadata if err := amino.UnmarshalJSON([]byte(txLine), &tx); err != nil { return nil, fmt.Errorf("unable to Unmarshall txs file: %w", err) } @@ -86,7 +133,7 @@ func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]st // LoadPackagesFromDir loads gno packages from a directory. // It creates and returns a list of transactions based on these packages. -func LoadPackagesFromDir(dir string, creator bft.Address, fee std.Fee) ([]std.Tx, error) { +func LoadPackagesFromDir(dir string, creator bft.Address, fee std.Fee) ([]TxWithMetadata, error) { // list all packages from target path pkgs, err := gnomod.ListPkgs(dir) if err != nil { @@ -101,14 +148,16 @@ func LoadPackagesFromDir(dir string, creator bft.Address, fee std.Fee) ([]std.Tx // Filter out draft packages. nonDraftPkgs := sortedPkgs.GetNonDraftPkgs() - txs := []std.Tx{} + txs := make([]TxWithMetadata, 0, len(nonDraftPkgs)) for _, pkg := range nonDraftPkgs { tx, err := LoadPackage(pkg, creator, fee, nil) if err != nil { return nil, fmt.Errorf("unable to load package %q: %w", pkg.Dir, err) } - txs = append(txs, tx) + txs = append(txs, TxWithMetadata{ + Tx: tx, + }) } return txs, nil @@ -119,7 +168,7 @@ func LoadPackage(pkg gnomod.Pkg, creator bft.Address, fee std.Fee, deposit std.C var tx std.Tx // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) + memPkg := gno.MustReadMemPackage(pkg.Dir, pkg.Name) err := memPkg.Validate() if err != nil { return tx, fmt.Errorf("invalid package: %w", err) @@ -138,3 +187,20 @@ func LoadPackage(pkg gnomod.Pkg, creator bft.Address, fee std.Fee, deposit std.C return tx, nil } + +func DefaultGenState() GnoGenesisState { + authGen := auth.DefaultGenesisState() + gp, err := std.ParseGasPrice(initGasPrice) + if err != nil { + panic(err) + } + authGen.Params.InitialGasPrice = gp + + gs := GnoGenesisState{ + Balances: []Balance{}, + Txs: []TxWithMetadata{}, + Auth: authGen, + } + + return gs +} diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go index 1ff9f168bd1..62aecaf5278 100644 --- a/gno.land/pkg/gnoland/mock_test.go +++ b/gno.land/pkg/gnoland/mock_test.go @@ -45,18 +45,15 @@ func (m *mockEventSwitch) RemoveListener(listenerID string) { } } -type ( - addPackageDelegate func(sdk.Context, vm.MsgAddPackage) error - callDelegate func(sdk.Context, vm.MsgCall) (string, error) - queryEvalDelegate func(sdk.Context, string, string) (string, error) - runDelegate func(sdk.Context, vm.MsgRun) (string, error) -) - type mockVMKeeper struct { - addPackageFn addPackageDelegate - callFn callDelegate - queryFn queryEvalDelegate - runFn runDelegate + addPackageFn func(sdk.Context, vm.MsgAddPackage) error + callFn func(sdk.Context, vm.MsgCall) (string, error) + queryFn func(sdk.Context, string, string) (string, error) + runFn func(sdk.Context, vm.MsgRun) (string, error) + loadStdlibFn func(sdk.Context, string) + loadStdlibCachedFn func(sdk.Context, string) + makeGnoTransactionStoreFn func(ctx sdk.Context) sdk.Context + commitGnoTransactionStoreFn func(ctx sdk.Context) } func (m *mockVMKeeper) AddPackage(ctx sdk.Context, msg vm.MsgAddPackage) error { @@ -91,6 +88,31 @@ func (m *mockVMKeeper) Run(ctx sdk.Context, msg vm.MsgRun) (res string, err erro return "", nil } +func (m *mockVMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) { + if m.loadStdlibFn != nil { + m.loadStdlibFn(ctx, stdlibDir) + } +} + +func (m *mockVMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { + if m.loadStdlibCachedFn != nil { + m.loadStdlibCachedFn(ctx, stdlibDir) + } +} + +func (m *mockVMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { + if m.makeGnoTransactionStoreFn != nil { + return m.makeGnoTransactionStoreFn(ctx) + } + return ctx +} + +func (m *mockVMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { + if m.commitGnoTransactionStoreFn != nil { + m.commitGnoTransactionStoreFn(ctx) + } +} + type ( lastBlockHeightDelegate func() int64 loggerDelegate func() *slog.Logger diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 02691f89c3e..cc9e74a78d8 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -2,7 +2,9 @@ package gnoland import ( "fmt" + "io" "log/slog" + "path/filepath" "time" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" @@ -14,16 +16,19 @@ import ( "github.com/gnolang/gno/tm2/pkg/db" "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/events" - "github.com/gnolang/gno/tm2/pkg/p2p" - "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/p2p/types" ) type InMemoryNodeConfig struct { - PrivValidator bft.PrivValidator // identity of the validator - Genesis *bft.GenesisDoc - TMConfig *tmcfg.Config - GenesisTxHandler GenesisTxHandler - GenesisMaxVMCycles int64 + PrivValidator bft.PrivValidator // identity of the validator + Genesis *bft.GenesisDoc + TMConfig *tmcfg.Config + DB db.DB // will be initialized if nil + VMOutput io.Writer // optional + SkipGenesisVerification bool + + // If StdlibDir not set, then it's filepath.Join(TMConfig.RootDir, "gnovm", "stdlibs") + InitChainerConfig } // NewMockedPrivValidator generate a new key @@ -32,25 +37,36 @@ func NewMockedPrivValidator() bft.PrivValidator { } // NewDefaultGenesisConfig creates a default configuration for an in-memory node. -func NewDefaultGenesisConfig(chainid string) *bft.GenesisDoc { +func NewDefaultGenesisConfig(chainid, chaindomain string) *bft.GenesisDoc { + // custom chain domain + var domainParam Param + _ = domainParam.Parse("gno.land/r/sys/params.vm.chain_domain.string=" + chaindomain) + return &bft.GenesisDoc{ GenesisTime: time.Now(), ChainID: chainid, ConsensusParams: abci.ConsensusParams{ - Block: &abci.BlockParams{ - MaxTxBytes: 1_000_000, // 1MB, - MaxDataBytes: 2_000_000, // 2MB, - MaxGas: 100_000_000, // 100M gas - TimeIotaMS: 100, // 100ms - }, + Block: defaultBlockParams(), }, AppState: &GnoGenesisState{ Balances: []Balance{}, - Txs: []std.Tx{}, + Txs: []TxWithMetadata{}, + Params: []Param{ + domainParam, + }, }, } } +func defaultBlockParams() *abci.BlockParams { + return &abci.BlockParams{ + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 100_000_000, // 100M gas + TimeIotaMS: 100, // 100ms + } +} + func NewDefaultTMConfig(rootdir string) *tmcfg.Config { // We use `TestConfig` here otherwise ChainID will be empty, and // there is no other way to update it than using a config file @@ -70,7 +86,7 @@ func (cfg *InMemoryNodeConfig) validate() error { return fmt.Errorf("`TMConfig.RootDir` is required to locate `stdlibs` directory") } - if cfg.GenesisTxHandler == nil { + if cfg.GenesisTxResultHandler == nil { return fmt.Errorf("`GenesisTxHandler` is required but not provided") } @@ -87,15 +103,22 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, evsw := events.NewEventSwitch() + if cfg.StdlibDir == "" { + cfg.StdlibDir = filepath.Join(cfg.TMConfig.RootDir, "gnovm", "stdlibs") + } + // initialize db if nil + if cfg.DB == nil { + cfg.DB = memdb.NewMemDB() + } + // Initialize the application with the provided options gnoApp, err := NewAppWithOptions(&AppOptions{ - Logger: logger, - GnoRootDir: cfg.TMConfig.RootDir, - GenesisTxHandler: cfg.GenesisTxHandler, - MaxCycles: cfg.GenesisMaxVMCycles, - DB: memdb.NewMemDB(), - EventSwitch: evsw, - CacheStdlibLoad: true, + Logger: logger, + DB: cfg.DB, + EventSwitch: evsw, + InitChainerConfig: cfg.InitChainerConfig, + VMOutput: cfg.VMOutput, + SkipGenesisVerification: cfg.SkipGenesisVerification, }) if err != nil { return nil, fmt.Errorf("error initializing new app: %w", err) @@ -114,10 +137,10 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, // Create genesis factory genProvider := func() (*bft.GenesisDoc, error) { return cfg.Genesis, nil } - dbProvider := func(*node.DBContext) (db.DB, error) { return memdb.NewMemDB(), nil } + dbProvider := func(*node.DBContext) (db.DB, error) { return cfg.DB, nil } // Generate p2p node identity - nodekey := &p2p.NodeKey{PrivKey: ed25519.GenPrivKey()} + nodekey := &types.NodeKey{PrivKey: ed25519.GenPrivKey()} // Create and return the in-memory node instance return node.NewNode(cfg.TMConfig, diff --git a/gno.land/pkg/gnoland/package.go b/gno.land/pkg/gnoland/package.go index fd1afbde136..e4b2449c972 100644 --- a/gno.land/pkg/gnoland/package.go +++ b/gno.land/pkg/gnoland/package.go @@ -11,4 +11,6 @@ var Package = amino.RegisterPackage(amino.NewPackage( ).WithDependencies().WithTypes( &GnoAccount{}, "Account", GnoGenesisState{}, "GenesisState", + TxWithMetadata{}, "TxWithMetadata", + GnoTxMetadata{}, "GnoTxMetadata", )) diff --git a/gno.land/pkg/gnoland/param.go b/gno.land/pkg/gnoland/param.go new file mode 100644 index 00000000000..4c1e1190751 --- /dev/null +++ b/gno.land/pkg/gnoland/param.go @@ -0,0 +1,121 @@ +package gnoland + +import ( + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/sdk/params" +) + +type Param struct { + key string + kind string + value interface{} +} + +func (p Param) Verify() error { + // XXX: validate + return nil +} + +const ( + ParamKindString = "string" + ParamKindInt64 = "int64" + ParamKindUint64 = "uint64" + ParamKindBool = "bool" + ParamKindBytes = "bytes" +) + +func (p *Param) Parse(entry string) error { + parts := strings.SplitN(strings.TrimSpace(entry), "=", 2) // .= + if len(parts) != 2 { + return fmt.Errorf("malformed entry: %q", entry) + } + + keyWithKind := parts[0] + rawValue := parts[1] + p.kind = keyWithKind[strings.LastIndex(keyWithKind, ".")+1:] + p.key = strings.TrimSuffix(keyWithKind, "."+p.kind) + switch p.kind { + case ParamKindString: + p.value = rawValue + case ParamKindInt64: + v, err := strconv.ParseInt(rawValue, 10, 64) + if err != nil { + return err + } + p.value = v + case ParamKindBool: + v, err := strconv.ParseBool(rawValue) + if err != nil { + return err + } + p.value = v + case ParamKindUint64: + v, err := strconv.ParseUint(rawValue, 10, 64) + if err != nil { + return err + } + p.value = v + case ParamKindBytes: + v, err := hex.DecodeString(rawValue) + if err != nil { + return err + } + p.value = v + default: + return errors.New("unsupported param kind: " + p.kind + " (" + entry + ")") + } + + return p.Verify() +} + +func (p Param) String() string { + typedKey := p.key + "." + p.kind + switch p.kind { + case ParamKindString: + return fmt.Sprintf("%s=%s", typedKey, p.value) + case ParamKindInt64: + return fmt.Sprintf("%s=%d", typedKey, p.value) + case ParamKindUint64: + return fmt.Sprintf("%s=%d", typedKey, p.value) + case ParamKindBool: + if p.value.(bool) { + return fmt.Sprintf("%s=true", typedKey) + } + return fmt.Sprintf("%s=false", typedKey) + case ParamKindBytes: + return fmt.Sprintf("%s=%x", typedKey, p.value) + } + panic("invalid param kind:" + p.kind) +} + +func (p *Param) UnmarshalAmino(rep string) error { + return p.Parse(rep) +} + +func (p Param) MarshalAmino() (string, error) { + return p.String(), nil +} + +func (p Param) register(ctx sdk.Context, prk params.ParamsKeeperI) { + key := p.key + "." + p.kind + switch p.kind { + case ParamKindString: + prk.SetString(ctx, key, p.value.(string)) + case ParamKindInt64: + prk.SetInt64(ctx, key, p.value.(int64)) + case ParamKindUint64: + prk.SetUint64(ctx, key, p.value.(uint64)) + case ParamKindBool: + prk.SetBool(ctx, key, p.value.(bool)) + case ParamKindBytes: + prk.SetBytes(ctx, key, p.value.([]byte)) + default: + panic("invalid param kind: " + p.kind) + } +} diff --git a/gno.land/pkg/gnoland/param_test.go b/gno.land/pkg/gnoland/param_test.go new file mode 100644 index 00000000000..5d17aab40da --- /dev/null +++ b/gno.land/pkg/gnoland/param_test.go @@ -0,0 +1,41 @@ +package gnoland + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParam_Parse(t *testing.T) { + t.Parallel() + tests := []struct { + name string + entry string + expected Param + expectErr bool + }{ + {"valid string", "foo.string=hello", Param{key: "foo", kind: "string", value: "hello"}, false}, + {"valid int64", "foo.int64=-1337", Param{key: "foo", kind: "int64", value: int64(-1337)}, false}, + {"valid uint64", "foo.uint64=42", Param{key: "foo", kind: "uint64", value: uint64(42)}, false}, + {"valid bool", "foo.bool=true", Param{key: "foo", kind: "bool", value: true}, false}, + {"valid bytes", "foo.bytes=AAAA", Param{key: "foo", kind: "bytes", value: []byte{0xaa, 0xaa}}, false}, + {"invalid key", "invalidkey=foo", Param{}, true}, + {"invalid kind", "invalid.kind=foo", Param{}, true}, + {"invalid int64", "invalid.int64=foobar", Param{}, true}, + {"invalid uint64", "invalid.uint64=-42", Param{}, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + param := Param{} + err := param.Parse(tc.entry) + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, param) + } + }) + } +} diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 016f3279dbd..66fb2f54e8a 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -1,8 +1,15 @@ package gnoland import ( + "bufio" + "context" "errors" + "fmt" + "os" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -20,6 +27,91 @@ func ProtoGnoAccount() std.Account { } type GnoGenesisState struct { - Balances []Balance `json:"balances"` - Txs []std.Tx `json:"txs"` + Balances []Balance `json:"balances"` + Txs []TxWithMetadata `json:"txs"` + Params []Param `json:"params"` + Auth auth.GenesisState `json:"auth"` +} + +type TxWithMetadata struct { + Tx std.Tx `json:"tx"` + Metadata *GnoTxMetadata `json:"metadata,omitempty"` +} + +type GnoTxMetadata struct { + Timestamp int64 `json:"timestamp"` +} + +// ReadGenesisTxs reads the genesis txs from the given file path +func ReadGenesisTxs(ctx context.Context, path string) ([]TxWithMetadata, error) { + // Open the txs file + file, loadErr := os.Open(path) + if loadErr != nil { + return nil, fmt.Errorf("unable to open tx file %s: %w", path, loadErr) + } + defer file.Close() + + var ( + txs []TxWithMetadata + + scanner = bufio.NewScanner(file) + ) + + scanner.Buffer(make([]byte, 1_000_000), 2_000_000) + + for scanner.Scan() { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + // Parse the amino JSON + var tx TxWithMetadata + if err := amino.UnmarshalJSON(scanner.Bytes(), &tx); err != nil { + return nil, fmt.Errorf( + "unable to unmarshal amino JSON, %w", + err, + ) + } + + txs = append(txs, tx) + } + } + + // Check for scanning errors + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf( + "error encountered while reading file, %w", + err, + ) + } + + return txs, nil +} + +// SignGenesisTxs will sign all txs passed as argument using the private key. +// This signature is only valid for genesis transactions as the account number and sequence are 0 +func SignGenesisTxs(txs []TxWithMetadata, privKey crypto.PrivKey, chainID string) error { + for index, tx := range txs { + // Upon verifying genesis transactions, the account number and sequence are considered to be 0. + // The reason for this is that it is not possible to know the account number (or sequence!) in advance + // when generating the genesis transaction signature + bytes, err := tx.Tx.GetSignBytes(chainID, 0, 0) + if err != nil { + return fmt.Errorf("unable to get sign bytes for transaction, %w", err) + } + + signature, err := privKey.Sign(bytes) + if err != nil { + return fmt.Errorf("unable to sign genesis transaction, %w", err) + } + + txs[index].Tx.Signatures = []std.Signature{ + { + PubKey: privKey.PubKey(), + Signature: signature, + }, + } + } + + return nil } diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go new file mode 100644 index 00000000000..c501325bc3e --- /dev/null +++ b/gno.land/pkg/gnoland/types_test.go @@ -0,0 +1,158 @@ +package gnoland + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateTxs generates dummy transactions +func generateTxs(t *testing.T, count int) []TxWithMetadata { + t.Helper() + + txs := make([]TxWithMetadata, count) + + for i := 0; i < count; i++ { + txs[i] = TxWithMetadata{ + Tx: std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: crypto.Address{byte(i)}, + ToAddress: crypto.Address{byte(i)}, + Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 1)), + }, + }, + Fee: std.Fee{ + GasWanted: 10, + GasFee: std.NewCoin(ugnot.Denom, 1000000), + }, + Memo: fmt.Sprintf("tx %d", i), + }, + } + } + + return txs +} + +func TestReadGenesisTxs(t *testing.T) { + t.Parallel() + + createFile := func(path, data string) { + file, err := os.Create(path) + require.NoError(t, err) + + _, err = file.WriteString(data) + require.NoError(t, err) + } + + t.Run("invalid path", func(t *testing.T) { + t.Parallel() + + path := "" // invalid + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + txs, err := ReadGenesisTxs(ctx, path) + assert.Nil(t, txs) + + assert.Error(t, err) + }) + + t.Run("invalid tx format", func(t *testing.T) { + t.Parallel() + + var ( + dir = t.TempDir() + path = filepath.Join(dir, "txs.jsonl") + ) + + // Create the file + createFile( + path, + "random data", + ) + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + txs, err := ReadGenesisTxs(ctx, path) + assert.Nil(t, txs) + + assert.Error(t, err) + }) + + t.Run("valid txs", func(t *testing.T) { + t.Parallel() + + var ( + dir = t.TempDir() + path = filepath.Join(dir, "txs.jsonl") + txs = generateTxs(t, 1000) + ) + + // Create the file + file, err := os.Create(path) + require.NoError(t, err) + + // Write the transactions + for _, tx := range txs { + encodedTx, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + _, err = file.WriteString(fmt.Sprintf("%s\n", encodedTx)) + require.NoError(t, err) + } + + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + // Load the transactions + readTxs, err := ReadGenesisTxs(ctx, path) + require.NoError(t, err) + + require.Len(t, readTxs, len(txs)) + + for index, readTx := range readTxs { + assert.Equal(t, txs[index], readTx) + } + }) +} + +func TestSignGenesisTx(t *testing.T) { + t.Parallel() + + var ( + txs = generateTxs(t, 100) + privKey = secp256k1.GenPrivKey() + pubKey = privKey.PubKey() + chainID = "testing" + ) + + // Make sure the transactions are properly signed + require.NoError(t, SignGenesisTxs(txs, privKey, chainID)) + + // Make sure the signatures are valid + for _, tx := range txs { + payload, err := tx.Tx.GetSignBytes(chainID, 0, 0) + require.NoError(t, err) + + sigs := tx.Tx.GetSignatures() + require.Len(t, sigs, 1) + + assert.True(t, pubKey.Equals(sigs[0].PubKey)) + assert.True(t, pubKey.VerifyBytes(payload, sigs[0].Signature)) + } +} diff --git a/gno.land/pkg/gnoland/validators.go b/gno.land/pkg/gnoland/validators.go new file mode 100644 index 00000000000..339ebd9dcad --- /dev/null +++ b/gno.land/pkg/gnoland/validators.go @@ -0,0 +1,61 @@ +package gnoland + +import ( + "regexp" + + gnovm "github.com/gnolang/gno/gnovm/stdlibs/std" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/events" +) + +const ( + valRealm = "gno.land/r/sys/validators/v2" // XXX: make it configurable from GovDAO + valChangesFn = "GetChanges" + + validatorAddedEvent = "ValidatorAdded" + validatorRemovedEvent = "ValidatorRemoved" +) + +// XXX: replace with amino-based clean approach +var valRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) + +// validatorUpdate is a type being used for "notifying" +// that a validator change happened on-chain. The events from `r/sys/validators` +// do not pass data related to validator add / remove instances (who, what, how) +type validatorUpdate struct{} + +// validatorEventFilter filters the given event to determine if it +// is tied to a validator update +func validatorEventFilter(event events.Event) []validatorUpdate { + // Make sure the event is a new TX event + txResult, ok := event.(types.EventTx) + if !ok { + return nil + } + + // Make sure an add / remove event happened + for _, ev := range txResult.Result.Response.Events { + // Make sure the event is a GnoVM event + gnoEv, ok := ev.(gnovm.GnoEvent) + if !ok { + continue + } + + // Make sure the event is from `r/sys/validators` + if gnoEv.PkgPath != valRealm { + continue + } + + // Make sure the event is either an add / remove + switch gnoEv.Type { + case validatorAddedEvent, validatorRemovedEvent: + // We don't pass data around with the events, but a single + // notification is enough to "trigger" a VM scrape + return []validatorUpdate{{}} + default: + continue + } + } + + return nil +} diff --git a/gno.land/pkg/gnoland/vals.go b/gno.land/pkg/gnoland/vals.go deleted file mode 100644 index 1843dff3984..00000000000 --- a/gno.land/pkg/gnoland/vals.go +++ /dev/null @@ -1,61 +0,0 @@ -package gnoland - -import ( - "regexp" - - gnovm "github.com/gnolang/gno/gnovm/stdlibs/std" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/events" -) - -const ( - valRealm = "gno.land/r/sys/validators" - valChangesFn = "GetChanges" - - validatorAddedEvent = "ValidatorAdded" - validatorRemovedEvent = "ValidatorRemoved" -) - -// XXX: replace with amino-based clean approach -var valRegexp = regexp.MustCompile(`{\("([^"]*)"\s[^)]+\),\("((?:[^"]|\\")*)"\s[^)]+\),\((\d+)\s[^)]+\)}`) - -// validatorUpdate is a type being used for "notifying" -// that a validator change happened on-chain. The events from `r/sys/validators` -// do not pass data related to validator add / remove instances (who, what, how) -type validatorUpdate struct{} - -// validatorEventFilter filters the given event to determine if it -// is tied to a validator update -func validatorEventFilter(event events.Event) []validatorUpdate { - // Make sure the event is a new TX event - txResult, ok := event.(types.EventTx) - if !ok { - return nil - } - - // Make sure an add / remove event happened - for _, ev := range txResult.Result.Response.Events { - // Make sure the event is a GnoVM event - gnoEv, ok := ev.(gnovm.GnoEvent) - if !ok { - continue - } - - // Make sure the event is from `r/sys/validators` - if gnoEv.PkgPath != valRealm { - continue - } - - // Make sure the event is either an add / remove - switch gnoEv.Type { - case validatorAddedEvent, validatorRemovedEvent: - // We don't pass data around with the events, but a single - // notification is enough to "trigger" a VM scrape - return []validatorUpdate{{}} - default: - continue - } - } - - return nil -} diff --git a/gno.land/pkg/gnoweb/.gitignore b/gno.land/pkg/gnoweb/.gitignore new file mode 100644 index 00000000000..dd09eb49099 --- /dev/null +++ b/gno.land/pkg/gnoweb/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +tmp/ +.cache diff --git a/gno.land/pkg/gnoweb/Makefile b/gno.land/pkg/gnoweb/Makefile new file mode 100644 index 00000000000..8e8b6bf1a2c --- /dev/null +++ b/gno.land/pkg/gnoweb/Makefile @@ -0,0 +1,104 @@ +# Configurable arguments +DEV_REMOTE ?= 127.0.0.1:26657 +CHAIN_ID ?= test3 +PUBLIC_DIR ?= public + +# Variable Declarations +tools_run := go run -modfile ./tools/go.mod +run_reflex := $(tools_run) github.com/cespare/reflex +run_logname := go -C ./tools run ./cmd/logname + +# css config +input_css := frontend/css/input.css +output_css := $(PUBLIC_DIR)/styles.css +tw_version := 3.4.14 +tw_config_path := frontend/css/tx.config.js +templates_files := $(shell find . -iname '*.gohtml') + +# static config +src_dir_static := frontend/static +out_dir_static := $(PUBLIC_DIR) +input_static := $(shell find $(src_dir_static) -type f) +output_static := $(patsubst $(src_dir_static)/%, $(out_dir_static)/%, $(input_static)) + +# esbuild config +src_dir_js := frontend/js +out_dir_js := $(PUBLIC_DIR)/js +input_js := $(shell find $(src_dir_js) -name '*.ts') +output_js := $(patsubst $(src_dir_js)/%.ts,$(out_dir_js)/%.js,$(input_js)) +esbuild_version := 0.24.0 + +# cache +cache_dir := .cache + +############# +# Targets +############# +.PHONY: all generate fmt css ts + +# Install dependencies +all: generate + +test: + go test -v ./... + +# Generate process +generate: css ts static + +css: $(output_css) +$(output_css): $(input_css) $(templates_files) + npx -y tailwindcss@$(tw_version) -c $(tw_config_path) -i $(input_css) -o $@ --minify # tailwind + touch $@ + +ts: $(output_js) +$(out_dir_js)/%.js: $(src_dir_js)/%.ts + npx -y esbuild $< --log-level=error --bundle --outdir=$(out_dir_js) --format=esm --minify + +# Rule to copy static files while preserving directory structure +static: $(output_static) +$(out_dir_static)/%: $(src_dir_static)/% + @mkdir -p $(dir $@) + @cp -v $< $@ + +# Format process +fmt: + go fmt ./... + + ############################### + # Developments + ############################### +.PHONY: dev dev.server dev.css dev.ts deps + +# Run the development dependencies in parallel +dev: + @echo "-- starting development tools" + @PUBLIC_DIR=$(cache_dir)/public $(MAKE) -j 3 \ + dev.gnoweb \ + dev.ts \ + dev.css + +# Go server in development mode +dev.gnoweb: generate + $(run_reflex) -s -r '.*\.go(html)?' -- \ + go run ../../cmd/gnoweb -assets-dir=${PUBLIC_DIR} -chainid=${CHAIN_ID} -remote=${DEV_REMOTE} \ + 2>&1 | $(run_logname) gnoweb + +# Tailwind CSS in development mode +dev.css: generate | $(PUBLIC_DIR) + npx -y tailwindcss@$(tw_version) -c $(tw_config_path) --verbose -i $(input_css) -o $(output_css) --watch \ + 2>&1 | $(run_logname) tailwind + +# XXX: add versioning on esbuild +# TS in development mode +dev.ts: generate | $(PUBLIC_DIR) + npx -y esbuild@$(esbuild_version) $(input_js) --bundle --outdir=$(out_dir_js) --sourcemap --format=esm --watch \ + 2>&1 | $(run_logname) esbuild + +# Cleanup +clean: + rm -rf $(cache_dir) tmp +fclean: clean + rm -rf $(PUBLIC_DIR) + +# Dirs +$(PUBLIC_DIR):; mkdir -p $@ diff --git a/gno.land/pkg/gnoweb/README.md b/gno.land/pkg/gnoweb/README.md new file mode 100644 index 00000000000..287279538d8 --- /dev/null +++ b/gno.land/pkg/gnoweb/README.md @@ -0,0 +1,45 @@ +# gnoweb + +`gnoweb` is a universal web frontend for the gno.land blockchain. + +This README provides instructions on how to set up and run `gnoweb` for development purposes. + +## Prerequisites + +Before you begin, ensure you have the following software installed on your machine: + +- **Node.js**: Required for running JavaScript and CSS build tools. +- **Go**: Required for building `gnoweb` + +## Development + +To start the development environment, which runs multiple development tools in parallel, +use the following command: + +```sh +make dev +``` + +This will: + +- Start a Go server in development mode and watch for any Go files change (targeting [localhost](http://localhost:8888)). +- Enable Tailwind CSS in watch mode to automatically compile CSS changes. +- Use esbuild in watch mode to automatically transpile and bundle TypeScript changes. + +You can customize the behavior of the Go server using the `DEV_REMOTE` and +`CHAIN_ID` environment variables. For example, to use `portal-loop` as the +target, run: + +```sh +CHAIN_ID=portal-loop DEV_REMOTE=https://rpc.gno.land make dev +``` + +## Generate + +To generate the public assets for the project, including static assets (fonts, CSS and JavaScript... +files), run the following command. This should be used while editing CSS, JS, or +any asset files: + +```sh +make generate +``` diff --git a/gno.land/pkg/gnoweb/alias.go b/gno.land/pkg/gnoweb/alias.go index d7297ed9d5d..06bb3941e41 100644 --- a/gno.land/pkg/gnoweb/alias.go +++ b/gno.land/pkg/gnoweb/alias.go @@ -1,26 +1,57 @@ package gnoweb -// realm aliases +import ( + "net/http" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" +) + +// Aliases are gnoweb paths that are rewritten using [AliasAndRedirectMiddleware]. var Aliases = map[string]string{ - "/": "/r/gnoland/home", - "/about": "/r/gnoland/pages:p/about", - "/gnolang": "/r/gnoland/pages:p/gnolang", - "/ecosystem": "/r/gnoland/pages:p/ecosystem", - "/partners": "/r/gnoland/pages:p/partners", - "/testnets": "/r/gnoland/pages:p/testnets", - "/start": "/r/gnoland/pages:p/start", - "/license": "/r/gnoland/pages:p/license", - "/game-of-realms": "/r/gnoland/pages:p/gor", // XXX: replace with gor realm - "/events": "/r/gnoland/events", + "/": "/r/gnoland/home", + "/about": "/r/gnoland/pages:p/about", + "/gnolang": "/r/gnoland/pages:p/gnolang", + "/ecosystem": "/r/gnoland/pages:p/ecosystem", + "/partners": "/r/gnoland/pages:p/partners", + "/testnets": "/r/gnoland/pages:p/testnets", + "/start": "/r/gnoland/pages:p/start", + "/license": "/r/gnoland/pages:p/license", + "/contribute": "/r/gnoland/pages:p/contribute", + "/events": "/r/gnoland/events", } -// http redirects +// Redirect are gnoweb paths that are redirected using [AliasAndRedirectMiddleware]. var Redirects = map[string]string{ "/r/demo/boards:gnolang/6": "/r/demo/boards:gnolang/3", // XXX: temporary "/blog": "/r/gnoland/blog", - "/gor": "/game-of-realms", + "/gor": "/contribute", + "/game-of-realms": "/contribute", "/grants": "/partners", "/language": "/gnolang", "/getting-started": "/start", - "/gophercon24": "https://docs.gno.land", +} + +// AliasAndRedirectMiddleware redirects all incoming requests whose path matches +// any of the [Redirects] to the corresponding URL; and rewrites the URL path +// for incoming requests which match any of the [Aliases]. +func AliasAndRedirectMiddleware(next http.Handler, analytics bool) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the request path matches a redirect + if newPath, ok := Redirects[r.URL.Path]; ok { + http.Redirect(w, r, newPath, http.StatusFound) + components.RenderRedirectComponent(w, components.RedirectData{ + To: newPath, + WithAnalytics: analytics, + }) + return + } + + // Check if the request path matches an alias + if newPath, ok := Aliases[r.URL.Path]; ok { + r.URL.Path = newPath + } + + // Call the next handler + next.ServeHTTP(w, r) + }) } diff --git a/gno.land/pkg/gnoweb/app.go b/gno.land/pkg/gnoweb/app.go new file mode 100644 index 00000000000..516d3b92186 --- /dev/null +++ b/gno.land/pkg/gnoweb/app.go @@ -0,0 +1,169 @@ +package gnoweb + +import ( + "fmt" + "log/slog" + "net/http" + "path" + "strings" + + markdown "github.com/yuin/goldmark-highlighting/v2" + + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/styles" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/extension" + mdhtml "github.com/yuin/goldmark/renderer/html" +) + +// AppConfig contains configuration for the gnoweb. +type AppConfig struct { + // UnsafeHTML, if enabled, allows to use HTML in the markdown. + UnsafeHTML bool + // Analytics enables SimpleAnalytics. + Analytics bool + // NodeRemote is the remote address of the gno.land node. + NodeRemote string + // RemoteHelp is the remote of the gno.land node, as used in the help page. + RemoteHelp string + // ChainID is the chain id, used for constructing the help page. + ChainID string + // AssetsPath is the base path to the gnoweb assets. + AssetsPath string + // AssetDir, if set, will be used for assets instead of the embedded public directory. + AssetsDir string + // FaucetURL, if specified, will be the URL to which `/faucet` redirects. + FaucetURL string + // Domain is the domain used by the node. + Domain string +} + +// NewDefaultAppConfig returns a new default [AppConfig]. The default sets +// 127.0.0.1:26657 as the remote node, "dev" as the chain ID and sets up Assets +// to be served on /public/. +func NewDefaultAppConfig() *AppConfig { + const defaultRemote = "127.0.0.1:26657" + return &AppConfig{ + NodeRemote: defaultRemote, + RemoteHelp: defaultRemote, + ChainID: "dev", + AssetsPath: "/public/", + Domain: "gno.land", + } +} + +var chromaDefaultStyle = mustGetStyle("friendly") + +func mustGetStyle(name string) *chroma.Style { + s := styles.Get(name) + if s == nil { + panic("unable to get chroma style") + } + return s +} + +// NewRouter initializes the gnoweb router with the specified logger and configuration. +func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) { + // Initialize RPC Client + client, err := client.NewHTTPClient(cfg.NodeRemote) + if err != nil { + return nil, fmt.Errorf("unable to create HTTP client: %w", err) + } + + // Configure Chroma highlighter + chromaOptions := []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + chromahtml.WithLinkableLineNumbers(true, "L"), + chromahtml.WithClasses(true), + chromahtml.ClassPrefix("chroma-"), + } + chroma := chromahtml.New(chromaOptions...) + + // Configure Goldmark markdown parser + mdopts := []goldmark.Option{ + goldmark.WithExtensions( + markdown.NewHighlighting( + markdown.WithFormatOptions(chromaOptions...), + ), + extension.Table, + ), + } + if cfg.UnsafeHTML { + mdopts = append(mdopts, goldmark.WithRendererOptions(mdhtml.WithXHTML(), mdhtml.WithUnsafe())) + } + md := goldmark.New(mdopts...) + + // Configure WebClient + webcfg := HTMLWebClientConfig{ + Markdown: md, + Highlighter: NewChromaSourceHighlighter(chroma, chromaDefaultStyle), + Domain: cfg.Domain, + UnsafeHTML: cfg.UnsafeHTML, + RPCClient: client, + } + + webcli := NewHTMLClient(logger, &webcfg) + chromaStylePath := path.Join(cfg.AssetsPath, "_chroma", "style.css") + + // Setup StaticMetadata + staticMeta := StaticMetadata{ + Domain: cfg.Domain, + AssetsPath: cfg.AssetsPath, + ChromaPath: chromaStylePath, + RemoteHelp: cfg.RemoteHelp, + ChainId: cfg.ChainID, + Analytics: cfg.Analytics, + } + + // Configure WebHandler + webConfig := WebHandlerConfig{WebClient: webcli, Meta: staticMeta} + webhandler, err := NewWebHandler(logger, webConfig) + if err != nil { + return nil, fmt.Errorf("unable to create web handler: %w", err) + } + + // Setup HTTP muxer + mux := http.NewServeMux() + + // Handle web handler with alias middleware + mux.Handle("/", AliasAndRedirectMiddleware(webhandler, cfg.Analytics)) + + // Register faucet URL to `/faucet` if specified + if cfg.FaucetURL != "" { + mux.Handle("/faucet", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, cfg.FaucetURL, http.StatusFound) + components.RenderRedirectComponent(w, components.RedirectData{ + To: cfg.FaucetURL, + WithAnalytics: cfg.Analytics, + }) + })) + } + + // Handle Chroma CSS requests + // XXX: probably move this elsewhere + mux.Handle(chromaStylePath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css") + if err := chroma.WriteCSS(w, chromaDefaultStyle); err != nil { + logger.Error("unable to write CSS", "err", err) + http.NotFound(w, r) + } + })) + + // Handle assets path + // XXX: add caching + assetsBase := "/" + strings.Trim(cfg.AssetsPath, "/") + "/" + if cfg.AssetsDir != "" { + logger.Debug("using assets dir instead of embedded assets", "dir", cfg.AssetsDir) + mux.Handle(assetsBase, DevAssetHandler(assetsBase, cfg.AssetsDir)) + } else { + mux.Handle(assetsBase, AssetHandler()) + } + + // Handle status page + mux.Handle("/status.json", handlerStatusJSON(logger, client)) + + return mux, nil +} diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go new file mode 100644 index 00000000000..9f8f87b99b1 --- /dev/null +++ b/gno.land/pkg/gnoweb/app_test.go @@ -0,0 +1,159 @@ +package gnoweb + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRoutes(t *testing.T) { + const ( + ok = http.StatusOK + found = http.StatusFound + notFound = http.StatusNotFound + ) + routes := []struct { + route string + status int + substring string + }{ + {"/", ok, "Welcome"}, // Check if / returns 200 (OK) and contains "Welcome". + {"/about", ok, "blockchain"}, + {"/r/gnoland/blog", ok, ""}, // Any content + {"/r/gnoland/blog$help", ok, "AdminSetAdminAddr"}, + {"/r/gnoland/blog/", ok, "admin.gno"}, + {"/r/gnoland/blog/admin.gno", ok, ">func<"}, + {"/r/gnoland/blog$help&func=Render", ok, "Render(path)"}, + {"/r/gnoland/blog$help&func=Render&path=foo/bar", ok, `value="foo/bar"`}, + // {"/r/gnoland/blog$help&func=NonExisting", ok, "NonExisting not found"}, // XXX(TODO) + {"/r/demo/users:administrator", ok, "address"}, + {"/r/demo/users", ok, "moul"}, + {"/r/demo/users/users.gno", ok, "// State"}, + {"/r/demo/deep/very/deep", ok, "it works!"}, + {"/r/demo/deep/very/deep?arg1=val1&arg2=val2", ok, "hi ?arg1=val1&arg2=val2"}, + {"/r/demo/deep/very/deep:bob", ok, "hi bob"}, + {"/r/demo/deep/very/deep:bob?arg1=val1&arg2=val2", ok, "hi bob?arg1=val1&arg2=val2"}, + {"/r/demo/deep/very/deep$help", ok, "Render"}, + {"/r/demo/deep/very/deep/", ok, "render.gno"}, + {"/r/demo/deep/very/deep/render.gno", ok, ">package<"}, + {"/contribute", ok, "Game of Realms"}, + {"/game-of-realms", found, "/contribute"}, + {"/gor", found, "/contribute"}, + {"/blog", found, "/r/gnoland/blog"}, + {"/r/not/found/", notFound, ""}, + {"/404/not/found", notFound, ""}, + {"/아스키문자가아닌경로", notFound, ""}, + {"/%ED%85%8C%EC%8A%A4%ED%8A%B8", notFound, ""}, + {"/グノー", notFound, ""}, + {"/\u269B\uFE0F", notFound, ""}, // Unicode + {"/p/demo/flow/LICENSE", ok, "BSD 3-Clause"}, + // Test assets + {"/public/styles.css", ok, ""}, + {"/public/js/index.js", ok, ""}, + {"/public/_chroma/style.css", ok, ""}, + {"/public/imgs/gnoland.svg", ok, ""}, + } + + rootdir := gnoenv.RootDir() + genesis := integration.LoadDefaultGenesisTXsFile(t, "tendermint_test", rootdir) + config, _ := integration.TestingNodeConfig(t, rootdir, genesis...) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) + defer node.Stop() + + cfg := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr + + logger := log.NewTestingLogger(t) + + // Initialize the router with the current node's remote address + router, err := NewRouter(logger, cfg) + require.NoError(t, err) + + for _, r := range routes { + t.Run(fmt.Sprintf("test route %s", r.route), func(t *testing.T) { + t.Logf("input: %q", r.route) + request := httptest.NewRequest(http.MethodGet, r.route, nil) + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + assert.Equal(t, r.status, response.Code) + assert.Contains(t, response.Body.String(), r.substring) + }) + } +} + +func TestAnalytics(t *testing.T) { + routes := []string{ + // Special realms + "/", // Home + "/about", + "/start", + + // Redirects + "/game-of-realms", + "/getting-started", + "/blog", + "/boards", + + // Realm, source, help page + "/r/gnoland/blog", + "/r/gnoland/blog/admin.gno", + "/r/demo/users:administrator", + "/r/gnoland/blog$help", + + // Special pages + "/404-not-found", + } + + rootdir := gnoenv.RootDir() + genesis := integration.LoadDefaultGenesisTXsFile(t, "tendermint_test", rootdir) + config, _ := integration.TestingNodeConfig(t, rootdir, genesis...) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) + defer node.Stop() + + t.Run("enabled", func(t *testing.T) { + for _, route := range routes { + t.Run(route, func(t *testing.T) { + cfg := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr + cfg.Analytics = true + logger := log.NewTestingLogger(t) + + router, err := NewRouter(logger, cfg) + require.NoError(t, err) + + request := httptest.NewRequest(http.MethodGet, route, nil) + response := httptest.NewRecorder() + + router.ServeHTTP(response, request) + + assert.Contains(t, response.Body.String(), "sa.gno.services") + }) + } + }) + t.Run("disabled", func(t *testing.T) { + for _, route := range routes { + t.Run(route, func(t *testing.T) { + cfg := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr + cfg.Analytics = false + logger := log.NewTestingLogger(t) + router, err := NewRouter(logger, cfg) + require.NoError(t, err) + + request := httptest.NewRequest(http.MethodGet, route, nil) + response := httptest.NewRecorder() + + router.ServeHTTP(response, request) + + assert.NotContains(t, response.Body.String(), "sa.gno.services") + }) + } + }) +} diff --git a/gno.land/pkg/gnoweb/components/breadcrumb.go b/gno.land/pkg/gnoweb/components/breadcrumb.go new file mode 100644 index 00000000000..8eda02a9f4d --- /dev/null +++ b/gno.land/pkg/gnoweb/components/breadcrumb.go @@ -0,0 +1,19 @@ +package components + +import ( + "io" +) + +type BreadcrumbPart struct { + Name string + URL string +} + +type BreadcrumbData struct { + Parts []BreadcrumbPart + Args string +} + +func RenderBreadcrumpComponent(w io.Writer, data BreadcrumbData) error { + return tmpl.ExecuteTemplate(w, "Breadcrumb", data) +} diff --git a/gno.land/pkg/gnoweb/components/breadcrumb.gohtml b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml new file mode 100644 index 00000000000..3824eb5894f --- /dev/null +++ b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml @@ -0,0 +1,18 @@ +{{ define "breadcrumb" }} +
    + {{- range $index, $part := .Parts }} + {{- if $index }} +
  1. + {{- else }} +
  2. + {{- end }} + {{ $part.Name }} +
  3. + {{- end }} + {{- if .Args }} +
  4. + {{ .Args }} +
  5. + {{- end }} +
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/directory.go b/gno.land/pkg/gnoweb/components/directory.go new file mode 100644 index 00000000000..6e47db3b2c4 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/directory.go @@ -0,0 +1,15 @@ +package components + +import ( + "io" +) + +type DirData struct { + PkgPath string + Files []string + FileCounter int +} + +func RenderDirectoryComponent(w io.Writer, data DirData) error { + return tmpl.ExecuteTemplate(w, "renderDir", data) +} diff --git a/gno.land/pkg/gnoweb/components/directory.gohtml b/gno.land/pkg/gnoweb/components/directory.gohtml new file mode 100644 index 00000000000..2254886f7af --- /dev/null +++ b/gno.land/pkg/gnoweb/components/directory.gohtml @@ -0,0 +1,38 @@ +{{ define "renderDir" }} +
+
+ + {{ $pkgpath := .PkgPath }} +
+
+
+

{{ $pkgpath }}

+
+
+ Directory · {{ .FileCounter }} Files +
+
+ +
+ +
+
+
+ +
+{{ end }} + diff --git a/gno.land/pkg/gnoweb/components/help.go b/gno.land/pkg/gnoweb/components/help.go new file mode 100644 index 00000000000..e819705006b --- /dev/null +++ b/gno.land/pkg/gnoweb/components/help.go @@ -0,0 +1,51 @@ +package components + +import ( + "html/template" + "io" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types +) + +type HelpData struct { + // Selected function + SelectedFunc string + SelectedArgs map[string]string + + RealmName string + Functions []vm.FunctionSignature + ChainId string + Remote string + PkgPath string +} + +func registerHelpFuncs(funcs template.FuncMap) { + funcs["helpFuncSignature"] = func(fsig vm.FunctionSignature) (string, error) { + var fsigStr strings.Builder + + fsigStr.WriteString(fsig.FuncName) + fsigStr.WriteRune('(') + for i, param := range fsig.Params { + if i > 0 { + fsigStr.WriteString(", ") + } + fsigStr.WriteString(param.Name) + } + fsigStr.WriteRune(')') + + return fsigStr.String(), nil + } + + funcs["getSelectedArgValue"] = func(data HelpData, param vm.NamedType) (string, error) { + if data.SelectedArgs == nil { + return "", nil + } + + return data.SelectedArgs[param.Name], nil + } +} + +func RenderHelpComponent(w io.Writer, data HelpData) error { + return tmpl.ExecuteTemplate(w, "renderHelp", data) +} diff --git a/gno.land/pkg/gnoweb/components/help.gohtml b/gno.land/pkg/gnoweb/components/help.gohtml new file mode 100644 index 00000000000..535cb56e9d6 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/help.gohtml @@ -0,0 +1,110 @@ +{{ define "renderHelp" }} + {{ $data := . }} +
+
+
+
+

{{ .RealmName }}

+
+
+
+ + + + +
+
+ + +
+
+
+ +
+ + {{ range .Functions }} +
+

{{ .FuncName }}

+
+
+

Params

+
+ {{ $funcName := .FuncName }} + {{ range .Params }} +
+
+ + +
+
+ {{ end }} +
+
+
+
+

Command

+
+ +
gnokey maketx call -pkgpath "{{ $.PkgPath }}" -func "{{ .FuncName }}" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid "{{ $.ChainId }}"{{ range .Params }} -args ""{{ end }} -remote "{{ $.Remote }}" ADDRESS
+
+
+
+ {{ end }} + +
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/index.go b/gno.land/pkg/gnoweb/components/index.go new file mode 100644 index 00000000000..0cc020ae261 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/index.go @@ -0,0 +1,47 @@ +package components + +import ( + "context" + "html/template" + "io" + "net/url" +) + +type HeadData struct { + Title string + Description string + Canonical string + Image string + URL string + ChromaPath string + AssetsPath string + Analytics bool +} + +type HeaderData struct { + RealmPath string + Breadcrumb BreadcrumbData + WebQuery url.Values +} + +type FooterData struct { + Analytics bool + AssetsPath string +} + +type IndexData struct { + HeadData + HeaderData + FooterData + Body template.HTML +} + +func IndexComponent(data IndexData) Component { + return func(ctx context.Context, tmpl *template.Template, w io.Writer) error { + return tmpl.ExecuteTemplate(w, "index", data) + } +} + +func RenderIndexComponent(w io.Writer, data IndexData) error { + return tmpl.ExecuteTemplate(w, "index", data) +} diff --git a/gno.land/pkg/gnoweb/components/index.gohtml b/gno.land/pkg/gnoweb/components/index.gohtml new file mode 100644 index 00000000000..a87decc14bf --- /dev/null +++ b/gno.land/pkg/gnoweb/components/index.gohtml @@ -0,0 +1,159 @@ +{{ define "index" }} + + + {{ template "head" .HeadData }} + + {{ template "spritesvg" }} + + + {{ template "header" .HeaderData }} + + + {{ template "main" .Body }} + + + {{ template "footer" .FooterData }} + + +{{ end }} + +{{ define "head" }} + + + + {{ .Title }} + + + + + + + + + + + {{ if .Canonical }} + + {{ end }} + + + + + + + + + + + + + + + + + + + + + + + + + + +{{ end }} + +{{ define "header" }} +
+ +
+{{ end }} + +{{ define "main" }} + {{ . }} +{{ end }} + +{{ define "footer" }} + + +{{- if .Analytics -}} {{- template "analytics" }} {{- end -}} + +{{- end }} + +{{- define "analytics" -}} + + + +{{- end -}} diff --git a/gno.land/pkg/gnoweb/components/logosvg.gohtml b/gno.land/pkg/gnoweb/components/logosvg.gohtml new file mode 100644 index 00000000000..5ebe6460ee3 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/logosvg.gohtml @@ -0,0 +1,21 @@ +{{ define "logosvg" }} + + + + + + + + + + + + + + + + + + + +{{ end }} diff --git a/gno.land/pkg/gnoweb/components/realm.go b/gno.land/pkg/gnoweb/components/realm.go new file mode 100644 index 00000000000..027760bb382 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/realm.go @@ -0,0 +1,32 @@ +package components + +import ( + "context" + "html/template" + "io" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb/markdown" +) + +type RealmTOCData struct { + Items []*markdown.TocItem +} + +func RealmTOCComponent(data *RealmTOCData) Component { + return func(ctx context.Context, tmpl *template.Template, w io.Writer) error { + return tmpl.ExecuteTemplate(w, "renderRealmToc", data) + } +} + +func RenderRealmTOCComponent(w io.Writer, data *RealmTOCData) error { + return tmpl.ExecuteTemplate(w, "renderRealmToc", data) +} + +type RealmData struct { + Content template.HTML + TocItems *RealmTOCData +} + +func RenderRealmComponent(w io.Writer, data RealmData) error { + return tmpl.ExecuteTemplate(w, "renderRealm", data) +} diff --git a/gno.land/pkg/gnoweb/components/realm.gohtml b/gno.land/pkg/gnoweb/components/realm.gohtml new file mode 100644 index 00000000000..55f39ef36d7 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/realm.gohtml @@ -0,0 +1,41 @@ +{{ define "renderRealmToc" }} + +{{ end }} + +{{ define "renderRealm" }} +
+
+ +
+ + {{ .Content }} +
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/redirect.go b/gno.land/pkg/gnoweb/components/redirect.go new file mode 100644 index 00000000000..873ddf56ff5 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/redirect.go @@ -0,0 +1,12 @@ +package components + +import "io" + +type RedirectData struct { + To string + WithAnalytics bool +} + +func RenderRedirectComponent(w io.Writer, data RedirectData) error { + return tmpl.ExecuteTemplate(w, "renderRedirect", data) +} diff --git a/gno.land/pkg/gnoweb/components/redirect.gohtml b/gno.land/pkg/gnoweb/components/redirect.gohtml new file mode 100644 index 00000000000..45dac0981cd --- /dev/null +++ b/gno.land/pkg/gnoweb/components/redirect.gohtml @@ -0,0 +1,16 @@ +{{- define "renderRedirect" -}} + + + + + + + + Redirecting to {{.To}} + + + {{.To}} + {{- if .WithAnalytics -}} {{- template "analytics" }} {{- end -}} + + +{{- end -}} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/components/source.go b/gno.land/pkg/gnoweb/components/source.go new file mode 100644 index 00000000000..23170776657 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/source.go @@ -0,0 +1,20 @@ +package components + +import ( + "html/template" + "io" +) + +type SourceData struct { + PkgPath string + Files []string + FileName string + FileSize string + FileLines int + FileCounter int + FileSource template.HTML +} + +func RenderSourceComponent(w io.Writer, data SourceData) error { + return tmpl.ExecuteTemplate(w, "renderSource", data) +} diff --git a/gno.land/pkg/gnoweb/components/source.gohtml b/gno.land/pkg/gnoweb/components/source.gohtml new file mode 100644 index 00000000000..cb2430b504a --- /dev/null +++ b/gno.land/pkg/gnoweb/components/source.gohtml @@ -0,0 +1,57 @@ +{{ define "renderSource" }} +
+
+
+
+

{{ .FileName }}

+
+
+ {{ .FileSize }} · {{ .FileLines }} lines + +
+
+ + +
+
+ {{ .FileSource }} +
+
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/spritesvg.gohtml b/gno.land/pkg/gnoweb/components/spritesvg.gohtml new file mode 100644 index 00000000000..c061e97bf58 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/spritesvg.gohtml @@ -0,0 +1,125 @@ +{{ define "spritesvg" }} + + + Search + + + + + + + Apps + + + + Documentation + + + + Source + + + + Content + + + + File + + + + Folder + + + + + + + + + + + Download + + + + Copy + + + + + + + + + + +{{ end }} diff --git a/gno.land/pkg/gnoweb/components/status.gohtml b/gno.land/pkg/gnoweb/components/status.gohtml new file mode 100644 index 00000000000..2321d1110bd --- /dev/null +++ b/gno.land/pkg/gnoweb/components/status.gohtml @@ -0,0 +1,12 @@ +{{ define "status" }} +
+
+
+ gno land +

Error: {{ .Message }}

+

Something went wrong. Let’s find our way back!

+ Go Back Home +
+
+
+{{ end }} diff --git a/gno.land/pkg/gnoweb/components/template.go b/gno.land/pkg/gnoweb/components/template.go new file mode 100644 index 00000000000..9c08703f460 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/template.go @@ -0,0 +1,77 @@ +package components + +import ( + "bytes" + "context" + "embed" + "html/template" + "io" + "net/url" +) + +//go:embed *.gohtml +var gohtml embed.FS + +var funcMap = template.FuncMap{ + // NOTE: this method does NOT escape HTML, use with caution + "noescape_string": func(in string) template.HTML { + return template.HTML(in) //nolint:gosec + }, + // NOTE: this method does NOT escape HTML, use with caution + "noescape_bytes": func(in []byte) template.HTML { + return template.HTML(in) //nolint:gosec + }, + "queryHas": func(vals url.Values, key string) bool { + if vals == nil { + return false + } + + return vals.Has(key) + }, +} + +var tmpl = template.New("web").Funcs(funcMap) + +func init() { + registerHelpFuncs(funcMap) + tmpl.Funcs(funcMap) + + var err error + tmpl, err = tmpl.ParseFS(gohtml, "*.gohtml") + if err != nil { + panic("unable to parse embed tempalates: " + err.Error()) + } +} + +type Component func(ctx context.Context, tmpl *template.Template, w io.Writer) error + +func (c Component) Render(ctx context.Context, w io.Writer) error { + return RenderComponent(ctx, w, c) +} + +func RenderComponent(ctx context.Context, w io.Writer, c Component) error { + var render *template.Template + funcmap := template.FuncMap{ + "render": func(cf Component) (string, error) { + var buf bytes.Buffer + if err := cf(ctx, render, &buf); err != nil { + return "", err + } + + return buf.String(), nil + }, + } + + render = tmpl.Funcs(funcmap) + return c(ctx, render, w) +} + +type StatusData struct { + Message string +} + +func RenderStatusComponent(w io.Writer, message string) error { + return tmpl.ExecuteTemplate(w, "status", StatusData{ + Message: message, + }) +} diff --git a/gno.land/pkg/gnoweb/format.go b/gno.land/pkg/gnoweb/format.go new file mode 100644 index 00000000000..67911bfa985 --- /dev/null +++ b/gno.land/pkg/gnoweb/format.go @@ -0,0 +1,69 @@ +package gnoweb + +import ( + "fmt" + "io" + "path/filepath" + "strings" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/lexers" +) + +// FormatSource defines the interface for formatting source code. +type FormatSource interface { + Format(w io.Writer, fileName string, file []byte) error +} + +// ChromaSourceHighlighter implements the Highlighter interface using the Chroma library. +type ChromaSourceHighlighter struct { + *html.Formatter + style *chroma.Style +} + +// NewChromaSourceHighlighter constructs a new ChromaHighlighter with the given formatter and style. +func NewChromaSourceHighlighter(formatter *html.Formatter, style *chroma.Style) FormatSource { + return &ChromaSourceHighlighter{Formatter: formatter, style: style} +} + +// Format applies syntax highlighting to the source code using Chroma. +func (f *ChromaSourceHighlighter) Format(w io.Writer, fileName string, src []byte) error { + var lexer chroma.Lexer + + // Determine the lexer to be used based on the file extension. + switch strings.ToLower(filepath.Ext(fileName)) { + case ".gno": + lexer = lexers.Get("go") + case ".md": + lexer = lexers.Get("markdown") + case ".mod": + lexer = lexers.Get("gomod") + default: + lexer = lexers.Get("txt") // Unsupported file type, default to plain text. + } + + if lexer == nil { + return fmt.Errorf("unsupported lexer for file %q", fileName) + } + + iterator, err := lexer.Tokenise(nil, string(src)) + if err != nil { + return fmt.Errorf("unable to tokenise %q: %w", fileName, err) + } + + if err := f.Formatter.Format(w, f.style, iterator); err != nil { + return fmt.Errorf("unable to format source file %q: %w", fileName, err) + } + + return nil +} + +// noopFormat is a no-operation highlighter that writes the source code as-is. +type noopFormat struct{} + +// Format writes the source code to the writer without any formatting. +func (f *noopFormat) Format(w io.Writer, fileName string, src []byte) error { + _, err := w.Write(src) + return err +} diff --git a/gno.land/pkg/gnoweb/frontend/css/input.css b/gno.land/pkg/gnoweb/frontend/css/input.css new file mode 100644 index 00000000000..59e41ff4a7c --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/css/input.css @@ -0,0 +1,336 @@ +@font-face { + font-family: "Roboto"; + font-style: normal; + font-weight: 900; + font-display: swap; + src: url("./fonts/roboto/roboto-mono-normal.woff2") format("woff2"), url("./fonts/roboto/roboto-mono-normal.woff") format("woff"); +} + +@font-face { + font-family: "Inter var"; + font-weight: 100 900; + font-display: block; + font-style: oblique 0deg 10deg; + src: url("./fonts/intervar/Intervar.woff2") format("woff2"); +} + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html { + @apply font-interVar text-gray-600 bg-light text-200; + font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; + -webkit-font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; + text-size-adjust: 100%; + -moz-osx-font-smoothing: grayscale; + font-smoothing: antialiased; + font-variant-ligatures: contextual common-ligatures; + font-kerning: normal; + text-rendering: optimizeLegibility; + } + + svg { + @apply max-w-full max-h-full; + } + + form { + @apply my-0; + } + + .realm-content { + @apply text-200 break-words pt-10; + } + + .realm-content > *:first-child { + @apply !mt-0; + } + + .realm-content a { + @apply text-green-600 font-medium hover:underline; + } + + .realm-content h1, + .realm-content h2, + .realm-content h3, + .realm-content h4 { + @apply text-gray-900 mt-12 leading-tight; + } + + .realm-content h2, + .realm-content h2 * { + @apply font-bold; + } + + .realm-content h3, + .realm-content h3 *, + .realm-content h4, + .realm-content h4 * { + @apply font-semibold; + } + + .realm-content h1 + h2, + .realm-content h2 + h3, + .realm-content h3 + h4 { + @apply mt-4; + } + + .realm-content h1 { + @apply text-800 font-bold; + } + + .realm-content h2 { + @apply text-600; + } + + .realm-content h3 { + @apply text-400 text-gray-600 mt-10; + } + + .realm-content h4 { + @apply text-300 text-gray-600 font-medium my-6; + } + + .realm-content p { + @apply my-5; + } + + .realm-content strong { + @apply font-bold text-gray-900; + } + + .realm-content strong * { + @apply font-bold; + } + + .realm-content em { + @apply italic-subtle; + } + + .realm-content blockquote { + @apply border-l-4 border-gray-300 pl-4 text-gray-600 italic-subtle my-4; + } + + .realm-content ul, + .realm-content ol { + @apply pl-4 my-6; + } + + .realm-content ul li, + .realm-content ol li { + @apply mb-2; + } + + .realm-content img { + @apply max-w-full my-8; + } + + .realm-content figure { + @apply my-6 text-center; + } + + .realm-content figcaption { + @apply text-100 text-gray-600; + } + + .realm-content :not(pre) > code { + @apply bg-gray-100 px-1 py-0.5 rounded-sm text-[.96em] font-mono; + } + + .realm-content pre { + @apply bg-gray-50 p-4 rounded overflow-x-auto font-mono; + } + + .realm-content hr { + @apply border-t border-gray-100 my-10; + } + + .realm-content table { + @apply border-collapse my-8 block w-full max-w-full overflow-x-auto border-collapse; + } + + .realm-content th, + .realm-content td { + @apply border px-4 py-2 break-words whitespace-normal; + } + + .realm-content th { + @apply bg-gray-100 font-bold; + } + + .realm-content caption { + @apply mt-2 text-100 text-gray-600 text-left; + } + + .realm-content q { + @apply quotes; + } + + .realm-content q::before { + content: open-quote; + } + + .realm-content q::after { + content: close-quote; + } + + .realm-content ul ul, + .realm-content ul ol, + .realm-content ol ul, + .realm-content ol ol { + @apply mt-3 mb-2 pl-4; + } + + .realm-content ul { + @apply list-disc; + } + + .realm-content ol { + @apply list-decimal; + } + + .realm-content abbr[title] { + @apply border-b border-dotted cursor-help; + } + + .realm-content details { + @apply my-5; + } + + .realm-content summary { + @apply font-bold cursor-pointer; + } + + .realm-content a code { + @apply text-inherit; + } + + .realm-content video { + @apply max-w-full my-8; + } + + .realm-content math { + @apply font-mono; + } + + .realm-content small { + @apply text-100; + } + + .realm-content del { + @apply line-through; + } + + .realm-content sub { + @apply text-50 align-sub; + } + + .realm-content sup { + @apply text-50 align-super; + } + + .realm-content input, + .realm-content button { + @apply px-4 py-2 border border-gray-300; + } + + main :is(h1, h2, h3, h4) { + @apply scroll-mt-24; + } + + ::-moz-selection { + @apply bg-green-600 text-light; + } + ::selection { + @apply bg-green-600 text-light; + } +} + +@layer components { + /* header */ + .sidemenu .peer:checked + label > svg { + @apply text-green-600; + } + + /* toc */ + .toc-expend-btn:has(#toc-expend:checked) + nav { + @apply block; + } + .toc-expend-btn:has(#toc-expend:checked) .toc-expend-btn_ico { + @apply rotate-180; + } + + /* sidebar */ + .main-header:has(#sidemenu-summary:checked) + main #sidebar #sidebar-summary, + .main-header:has(#sidemenu-source:checked) + main #sidebar #sidebar-source, + .main-header:has(#sidemenu-docs:checked) + main #sidebar #sidebar-docs, + .main-header:has(#sidemenu-meta:checked) + main #sidebar #sidebar-meta { + @apply block; + } + + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main .realm-content, + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) .main-navigation { + @apply md:col-span-6; + } + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main #sidebar, + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) .sidemenu { + @apply md:col-span-4; + } + :is(.main-header:has(#sidemenu-source:checked), .main-header:has(#sidemenu-docs:checked), .main-header:has(#sidemenu-meta:checked)) + main #sidebar::before { + @apply absolute block content-[''] top-0 w-[50vw] h-full -left-7 bg-gray-100 z-min; + } + + /* chroma */ + main :is(.source-code) > pre { + @apply !bg-light overflow-scroll rounded py-4 md:py-8 px-1 md:px-3 font-mono text-100 md:text-200; + } + main .realm-content > pre a { + @apply hover:no-underline; + } + + main :is(.realm-content, .source-code) > pre .chroma-ln:target { + @apply !bg-transparent; + } + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-ln:target), + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-lnlinks:hover), + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-ln:target) .chroma-cl, + main :is(.realm-content, .source-code) > pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl { + @apply !bg-gray-100 rounded; + } + main :is(.realm-content, .source-code) > pre .chroma-ln { + @apply scroll-mt-24; + } +} + +@layer utilities { + .italic-subtle { + font-style: oblique 10deg; + } + + .quotes { + @apply italic-subtle text-[#555] border-l-4 border-l-[#ccc] pl-4 my-6 [quotes:"“"_"”"_"‘"_"’"]; + } + + .quotes::before, + .quotes::after { + @apply [content:open-quote] text-600 text-gray-300 mr-1 [vertical-align:-0.4rem]; + } + + .quotes::after { + @apply [content:close-quote]; + } + + .text-stroke { + -webkit-text-stroke: currentColor; + -webkit-text-stroke-width: 0.6px; + } + + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } +} diff --git a/gno.land/pkg/gnoweb/frontend/css/tx.config.js b/gno.land/pkg/gnoweb/frontend/css/tx.config.js new file mode 100644 index 00000000000..21b6a101dd6 --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/css/tx.config.js @@ -0,0 +1,72 @@ +const pxToRem = (px) => px / 16; + +export default { + content: ["./components/**/*.{gohtml,ts}"], + theme: { + screens: { + xs: `${pxToRem(360)}rem`, + sm: `${pxToRem(480)}rem`, + md: `${pxToRem(640)}rem`, + lg: `${pxToRem(820)}rem`, + xl: `${pxToRem(1020)}rem`, + xxl: `${pxToRem(1366)}rem`, + max: `${pxToRem(1580)}rem`, + }, + zIndex: { + min: "-1", + 1: "1", + 2: "2", + 100: "100", + max: "9999", + }, + container: { + center: true, + padding: `${pxToRem(40)}rem`, + }, + borderRadius: { + sm: `${pxToRem(4)}rem`, + DEFAULT: `${pxToRem(6)}rem`, + }, + colors: { + light: "#FFFFFF", + gray: { + 50: "#F0F0F0", // Background color + 100: "#E2E2E2", // Title dark color + 200: "#BDBDBD", // Content dark color + 300: "#999999", // Muted color + 400: "#7C7C7C", // Border color + 600: "#54595D", // Content color + 800: "#131313", // Background dark color + 900: "#080809", // Title color + }, + green: { + 400: "#2D8D72", // Primary dark color + 600: "#226C57", // Primary light color + }, + transparent: "transparent", + current: "currentColor", + inherit: "inherit", + }, + fontFamily: { + mono: ["Roboto", 'Menlo, Consolas, "Ubuntu Mono", "Roboto Mono", "DejaVu Sans Mono", monospace;'], + interVar: [ + '"Inter var"', + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji", sans-serif', + ], + }, + fontSize: { + 0: "0", + 50: `${pxToRem(12)}rem`, + 100: `${pxToRem(14)}rem`, + 200: `${pxToRem(16)}rem`, + 300: `${pxToRem(18)}rem`, + 400: `${pxToRem(20)}rem`, + 500: `${pxToRem(22)}rem`, + 600: `${pxToRem(24)}rem`, + 700: `${pxToRem(32)}rem`, + 800: `${pxToRem(38)}rem`, + 900: `${pxToRem(42)}rem`, + }, + }, + plugins: [], +}; diff --git a/gno.land/pkg/gnoweb/frontend/js/copy.ts b/gno.land/pkg/gnoweb/frontend/js/copy.ts new file mode 100644 index 00000000000..f3e5c725783 --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/copy.ts @@ -0,0 +1,105 @@ +class Copy { + private DOM: { + el: HTMLElement | null; + }; + private static FEEDBACK_DELAY = 750; + + private btnClicked: HTMLElement | null = null; + private btnClickedIcons: HTMLElement[] = []; + private isAnimationRunning: boolean = false; + + private static SELECTORS = { + button: "[data-copy-btn]", + icon: `[data-copy-icon] > use`, + content: (id: string) => `[data-copy-content="${id}"]`, + }; + + constructor() { + this.DOM = { + el: document.querySelector("main"), + }; + + if (this.DOM.el) { + this.init(); + } else { + console.warn("Copy: Main container not found."); + } + } + + private init(): void { + this.bindEvents(); + } + + private bindEvents(): void { + this.DOM.el?.addEventListener("click", this.handleClick.bind(this)); + } + + private handleClick(event: Event): void { + const target = event.target as HTMLElement; + const button = target.closest(Copy.SELECTORS.button); + + if (!button) return; + + this.btnClicked = button; + this.btnClickedIcons = Array.from(button.querySelectorAll(Copy.SELECTORS.icon)); + + const contentId = button.getAttribute("data-copy-btn"); + if (!contentId) { + console.warn("Copy: No content ID found on the button."); + return; + } + + const codeBlock = this.DOM.el?.querySelector(Copy.SELECTORS.content(contentId)); + if (codeBlock) { + this.copyToClipboard(codeBlock, this.btnClickedIcons); + } else { + console.warn(`Copy: No content found for ID "${contentId}".`); + } + } + + private sanitizeContent(codeBlock: HTMLElement): string { + const html = codeBlock.innerHTML.replace(/]*class="chroma-ln"[^>]*>[\s\S]*?<\/span>/g, ""); + + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = html; + + return tempDiv.textContent?.trim() || ""; + } + + private toggleIcons(icons: HTMLElement[]): void { + icons.forEach((icon) => { + icon.classList.toggle("hidden"); + }); + } + + private showFeedback(icons: HTMLElement[]): void { + if (!this.btnClicked || this.isAnimationRunning === true) return; + + this.isAnimationRunning = true; + this.toggleIcons(icons); + window.setTimeout(() => { + this.toggleIcons(icons); + this.isAnimationRunning = false; + }, Copy.FEEDBACK_DELAY); + } + + private async copyToClipboard(codeBlock: HTMLElement, icons: HTMLElement[]): Promise { + const sanitizedText = this.sanitizeContent(codeBlock); + + if (!navigator.clipboard) { + console.error("Copy: Clipboard API is not supported in this browser."); + this.showFeedback(icons); + return; + } + + try { + await navigator.clipboard.writeText(sanitizedText); + this.showFeedback(icons); + } catch (err) { + console.error("Copy: Error while copying text.", err); + this.showFeedback(icons); + } + } +} + +export default () => new Copy(); diff --git a/gno.land/pkg/gnoweb/frontend/js/index.ts b/gno.land/pkg/gnoweb/frontend/js/index.ts new file mode 100644 index 00000000000..3927f794b94 --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/index.ts @@ -0,0 +1,42 @@ +(() => { + interface Module { + selector: string; + path: string; + } + + const modules: Record = { + copy: { + selector: "[data-copy-btn]", + path: "/public/js/copy.js", + }, + help: { + selector: "#help", + path: "/public/js/realmhelp.js", + }, + searchBar: { + selector: "#header-searchbar", + path: "/public/js/searchbar.js", + }, + }; + + const loadModuleIfExists = async ({ selector, path }: Module): Promise => { + const element = document.querySelector(selector); + if (element) { + try { + const module = await import(path); + module.default(); + } catch (err) { + console.error(`Error while loading script ${path}:`, err); + } + } else { + console.warn(`Module not loaded: no element matches selector "${selector}"`); + } + }; + + const initModules = async (): Promise => { + const promises = Object.values(modules).map((module) => loadModuleIfExists(module)); + await Promise.all(promises); + }; + + document.addEventListener("DOMContentLoaded", initModules); +})(); diff --git a/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts new file mode 100644 index 00000000000..d72102e2a2e --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts @@ -0,0 +1,169 @@ +import { debounce } from "./utils"; + +class Help { + private DOM: { + el: HTMLElement | null; + funcs: HTMLElement[]; + addressInput: HTMLInputElement | null; + cmdModeSelect: HTMLSelectElement | null; + }; + + private funcList: HelpFunc[]; + + private static SELECTORS = { + container: "#help", + func: "[data-func]", + addressInput: "[data-role='help-input-addr']", + cmdModeSelect: "[data-role='help-select-mode']", + }; + + constructor() { + this.DOM = { + el: document.querySelector(Help.SELECTORS.container), + funcs: [], + addressInput: null, + cmdModeSelect: null, + }; + + this.funcList = []; + + if (this.DOM.el) { + this.init(); + } else { + console.warn("Help: Main container not found."); + } + } + + private init(): void { + const { el } = this.DOM; + if (!el) return; + + this.DOM.funcs = Array.from(el.querySelectorAll(Help.SELECTORS.func)); + this.DOM.addressInput = el.querySelector(Help.SELECTORS.addressInput); + this.DOM.cmdModeSelect = el.querySelector(Help.SELECTORS.cmdModeSelect); + + this.funcList = this.DOM.funcs.map((funcEl) => new HelpFunc(funcEl)); + + this.restoreAddress(); + this.bindEvents(); + } + + private restoreAddress(): void { + const { addressInput } = this.DOM; + if (addressInput) { + const storedAddress = localStorage.getItem("helpAddressInput"); + if (storedAddress) { + addressInput.value = storedAddress; + this.funcList.forEach((func) => func.updateAddr(storedAddress)); + } + } + } + + private bindEvents(): void { + const { addressInput, cmdModeSelect } = this.DOM; + + const debouncedUpdate = debounce((addressInput: HTMLInputElement) => { + const address = addressInput.value; + + localStorage.setItem("helpAddressInput", address); + this.funcList.forEach((func) => func.updateAddr(address)); + }); + addressInput?.addEventListener("input", () => debouncedUpdate(addressInput)); + + cmdModeSelect?.addEventListener("change", (e) => { + const target = e.target as HTMLSelectElement; + this.funcList.forEach((func) => func.updateMode(target.value)); + }); + } +} + +class HelpFunc { + private DOM: { + el: HTMLElement; + addrs: HTMLElement[]; + args: HTMLElement[]; + modes: HTMLElement[]; + paramInputs: HTMLInputElement[]; + }; + + private funcName: string | null; + + private static SELECTORS = { + address: "[data-role='help-code-address']", + args: "[data-role='help-code-args']", + mode: "[data-code-mode]", + paramInput: "[data-role='help-param-input']", + }; + + constructor(el: HTMLElement) { + this.DOM = { + el, + addrs: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.address)), + args: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.args)), + modes: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.mode)), + paramInputs: Array.from(el.querySelectorAll(HelpFunc.SELECTORS.paramInput)), + }; + + this.funcName = el.dataset.func || null; + + this.initializeArgs(); + this.bindEvents(); + } + + private static sanitizeArgsInput(input: HTMLInputElement) { + const paramName = input.dataset.param || ""; + const paramValue = input.value.trim(); + + if (!paramName) { + console.warn("sanitizeArgsInput: param is missing in arg input dataset."); + } + + return { paramName, paramValue }; + } + + private bindEvents(): void { + const debouncedUpdate = debounce((paramName: string, paramValue: string) => { + if (paramName) this.updateArg(paramName, paramValue); + }); + + this.DOM.el.addEventListener("input", (e) => { + const target = e.target as HTMLInputElement; + if (target.dataset.role === "help-param-input") { + const { paramName, paramValue } = HelpFunc.sanitizeArgsInput(target); + debouncedUpdate(paramName, paramValue); + } + }); + } + + private initializeArgs(): void { + this.DOM.paramInputs.forEach((input) => { + const { paramName, paramValue } = HelpFunc.sanitizeArgsInput(input); + if (paramName) this.updateArg(paramName, paramValue); + }); + } + + public updateArg(paramName: string, paramValue: string): void { + this.DOM.args + .filter((arg) => arg.dataset.arg === paramName) + .forEach((arg) => { + arg.textContent = paramValue || ""; + }); + } + + public updateAddr(addr: string): void { + this.DOM.addrs.forEach((DOMaddr) => { + DOMaddr.textContent = addr.trim() || "ADDRESS"; + }); + } + + public updateMode(mode: string): void { + this.DOM.modes.forEach((cmd) => { + const isVisible = cmd.dataset.codeMode === mode; + cmd.classList.toggle("inline", isVisible); + cmd.classList.toggle("hidden", !isVisible); + cmd.dataset.copyContent = isVisible ? `help-cmd-${this.funcName}` : ""; + }); + } +} + +export default () => new Help(); diff --git a/gno.land/pkg/gnoweb/frontend/js/searchbar.ts b/gno.land/pkg/gnoweb/frontend/js/searchbar.ts new file mode 100644 index 00000000000..6cca444aa0f --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/searchbar.ts @@ -0,0 +1,74 @@ +class SearchBar { + private DOM: { + el: HTMLElement | null; + inputSearch: HTMLInputElement | null; + breadcrumb: HTMLElement | null; + }; + + private baseUrl: string; + + private static SELECTORS = { + container: "#header-searchbar", + inputSearch: "[data-role='header-input-search']", + breadcrumb: "[data-role='header-breadcrumb-search']", + }; + + constructor() { + this.DOM = { + el: document.querySelector(SearchBar.SELECTORS.container), + inputSearch: null, + breadcrumb: null, + }; + + this.baseUrl = window.location.origin; + + if (this.DOM.el) { + this.init(); + } else { + console.warn("SearchBar: Main container not found."); + } + } + + private init(): void { + const { el } = this.DOM; + + this.DOM.inputSearch = el?.querySelector(SearchBar.SELECTORS.inputSearch) ?? null; + this.DOM.breadcrumb = el?.querySelector(SearchBar.SELECTORS.breadcrumb) ?? null; + + if (!this.DOM.inputSearch) { + console.warn("SearchBar: Input element for search not found."); + } + + this.bindEvents(); + } + + private bindEvents(): void { + this.DOM.el?.addEventListener("submit", (e) => { + e.preventDefault(); + this.searchUrl(); + }); + } + + public searchUrl(): void { + const input = this.DOM.inputSearch?.value.trim(); + + if (input) { + let url = input; + + // Check if the URL has a proper scheme + if (!/^https?:\/\//i.test(url)) { + url = `${this.baseUrl}${url.startsWith("/") ? "" : "/"}${url}`; + } + + try { + window.location.href = new URL(url).href; + } catch (error) { + console.error("SearchBar: Invalid URL. Please enter a valid URL starting with http:// or https://."); + } + } else { + console.error("SearchBar: Please enter a URL to search."); + } + } +} + +export default () => new SearchBar(); diff --git a/gno.land/pkg/gnoweb/frontend/js/utils.ts b/gno.land/pkg/gnoweb/frontend/js/utils.ts new file mode 100644 index 00000000000..83de509efa5 --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/utils.ts @@ -0,0 +1,12 @@ +export function debounce void>(func: T, delay: number = 250): (...args: Parameters) => void { + let timeoutId: ReturnType | undefined; + + return function (this: any, ...args: Parameters) { + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => { + func.apply(this, args); + }, delay); + }; +} diff --git a/gno.land/pkg/gnoweb/static/img/favicon.ico b/gno.land/pkg/gnoweb/frontend/static/favicon.ico similarity index 100% rename from gno.land/pkg/gnoweb/static/img/favicon.ico rename to gno.land/pkg/gnoweb/frontend/static/favicon.ico diff --git a/gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Intervar.woff2 b/gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Intervar.woff2 new file mode 100644 index 00000000000..891fc5cc567 Binary files /dev/null and b/gno.land/pkg/gnoweb/frontend/static/fonts/intervar/Intervar.woff2 differ diff --git a/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff b/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff new file mode 100644 index 00000000000..2c58fe2d6d7 Binary files /dev/null and b/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff differ diff --git a/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 b/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 new file mode 100644 index 00000000000..53d081f3a53 Binary files /dev/null and b/gno.land/pkg/gnoweb/frontend/static/fonts/roboto/roboto-mono-normal.woff2 differ diff --git a/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg b/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg new file mode 100644 index 00000000000..30d2f3ef56a --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/static/imgs/gnoland.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gno.land/pkg/gnoweb/gnoweb.go b/gno.land/pkg/gnoweb/gnoweb.go deleted file mode 100644 index 5377ae6a420..00000000000 --- a/gno.land/pkg/gnoweb/gnoweb.go +++ /dev/null @@ -1,509 +0,0 @@ -package gnoweb - -import ( - "bytes" - "embed" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "log/slog" - "net/http" - "net/url" - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/gnolang/gno/tm2/pkg/amino" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/gorilla/mux" - "github.com/gotuna/gotuna" - - // for static files - "github.com/gnolang/gno/gno.land/pkg/gnoweb/static" - "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types - // "github.com/gnolang/gno/tm2/pkg/sdk" // for baseapp (info, status) -) - -const ( - qFileStr = "vm/qfile" -) - -//go:embed views/* -var defaultViewsFiles embed.FS - -type Config struct { - RemoteAddr string - CaptchaSite string - FaucetURL string - ViewsDir string - HelpChainID string - HelpRemote string - WithAnalytics bool -} - -func NewDefaultConfig() Config { - return Config{ - RemoteAddr: "127.0.0.1:26657", - CaptchaSite: "", - FaucetURL: "http://localhost:5050", - ViewsDir: "", - HelpChainID: "dev", - HelpRemote: "127.0.0.1:26657", - WithAnalytics: false, - } -} - -func MakeApp(logger *slog.Logger, cfg Config) gotuna.App { - var viewFiles fs.FS - - // Get specific views directory if specified - if cfg.ViewsDir != "" { - viewFiles = os.DirFS(cfg.ViewsDir) - } else { - // Get embed views - var err error - viewFiles, err = fs.Sub(defaultViewsFiles, "views") - if err != nil { - panic("unable to get views directory from embed fs: " + err.Error()) - } - } - - app := gotuna.App{ - ViewFiles: viewFiles, - Router: gotuna.NewMuxRouter(), - Static: static.EmbeddedStatic, - } - - for from, to := range Aliases { - app.Router.Handle(from, handlerRealmAlias(logger, app, &cfg, to)) - } - - for from, to := range Redirects { - app.Router.Handle(from, handlerRedirect(logger, app, &cfg, to)) - } - // realm routes - // NOTE: see rePathPart. - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}/{filename:(?:(?:.*\\.(?:gno|md|txt|mod)$)|(?:LICENSE$))?}", handlerRealmFile(logger, app, &cfg)) - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}", handlerRealmMain(logger, app, &cfg)) - app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}:{querystr:.*}", handlerRealmRender(logger, app, &cfg)) - app.Router.Handle("/p/{filepath:.*}", handlerPackageFile(logger, app, &cfg)) - - // other - app.Router.Handle("/faucet", handlerFaucet(logger, app, &cfg)) - app.Router.Handle("/static/{path:.+}", handlerStaticFile(logger, app, &cfg)) - app.Router.Handle("/favicon.ico", handlerFavicon(logger, app, &cfg)) - - // api - app.Router.Handle("/status.json", handlerStatusJSON(logger, app, &cfg)) - - app.Router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - path := r.RequestURI - handleNotFound(logger, app, &cfg, path, w, r) - }) - return app -} - -// handlerRealmAlias is used to render official pages from realms. -// url is intended to be shorter. -// UX is intended to be more minimalistic. -// A link to the realm realm is added. -func handlerRealmAlias(logger *slog.Logger, app gotuna.App, cfg *Config, rlmpath string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rlmfullpath := "gno.land" + rlmpath - querystr := "" // XXX: "?gnoweb-alias=1" - parts := strings.Split(rlmpath, ":") - switch len(parts) { - case 1: // continue - case 2: // r/realm:querystr - rlmfullpath = "gno.land" + parts[0] - querystr = parts[1] + querystr - default: - panic("should not happen") - } - rlmname := strings.TrimPrefix(rlmfullpath, "gno.land/r/") - qpath := "vm/qrender" - data := []byte(fmt.Sprintf("%s:%s", rlmfullpath, querystr)) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, fmt.Errorf("gnoweb failed to query gnoland: %w", err)) - return - } - - queryParts := strings.Split(querystr, "/") - pathLinks := []pathLink{} - for i, part := range queryParts { - pathLinks = append(pathLinks, pathLink{ - URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"), - Text: part, - }) - } - - tmpl := app.NewTemplatingEngine() - // XXX: extract title from realm's output - // XXX: extract description from realm's output - tmpl.Set("RealmName", rlmname) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("Query", querystr) - tmpl.Set("PathLinks", pathLinks) - tmpl.Set("Contents", string(res.Data)) - tmpl.Set("Config", cfg) - tmpl.Set("IsAlias", true) - tmpl.Render(w, r, "realm_render.html", "funcs.html") - }) -} - -func handlerFaucet(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - app.NewTemplatingEngine(). - Set("Config", cfg). - Render(w, r, "faucet.html", "funcs.html") - }) -} - -func handlerStatusJSON(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - startedAt := time.Now() - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var ret struct { - Gnoland struct { - Connected bool `json:"connected"` - Error *string `json:"error,omitempty"` - Height *int64 `json:"height,omitempty"` - // processed txs - // active connections - - Version *string `json:"version,omitempty"` - // Uptime *float64 `json:"uptime-seconds,omitempty"` - // Goarch *string `json:"goarch,omitempty"` - // Goos *string `json:"goos,omitempty"` - // GoVersion *string `json:"go-version,omitempty"` - // NumCPU *int `json:"num_cpu,omitempty"` - } `json:"gnoland"` - Website struct { - // Version string `json:"version"` - Uptime float64 `json:"uptime-seconds"` - Goarch string `json:"goarch"` - Goos string `json:"goos"` - GoVersion string `json:"go-version"` - NumCPU int `json:"num_cpu"` - } `json:"website"` - } - ret.Website.Uptime = time.Since(startedAt).Seconds() - ret.Website.Goarch = runtime.GOARCH - ret.Website.Goos = runtime.GOOS - ret.Website.NumCPU = runtime.NumCPU() - ret.Website.GoVersion = runtime.Version() - - ret.Gnoland.Connected = true - res, err := makeRequest(logger, cfg, ".app/version", []byte{}) - if err != nil { - ret.Gnoland.Connected = false - errmsg := err.Error() - ret.Gnoland.Error = &errmsg - } else { - version := string(res.Value) - ret.Gnoland.Version = &version - ret.Gnoland.Height = &res.Height - } - - out, _ := json.MarshalIndent(ret, "", " ") - w.Header().Set("Content-Type", "application/json") - w.Write(out) - }) -} - -func handlerRedirect(logger *slog.Logger, app gotuna.App, cfg *Config, to string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, to, http.StatusFound) - tmpl := app.NewTemplatingEngine() - tmpl.Set("To", to) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "redirect.html", "funcs.html") - }) -} - -func handlerRealmMain(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - rlmname := vars["rlmname"] - rlmpath := "gno.land/r/" + rlmname - query := r.URL.Query() - - logger.Info("handling", "name", rlmname, "path", rlmpath) - if query.Has("help") { - // Render function helper. - funcName := query.Get("__func") - qpath := "vm/qfuncs" - data := []byte(rlmpath) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, fmt.Errorf("request failed: %w", err)) - return - } - var fsigs vm.FunctionSignatures - amino.MustUnmarshalJSON(res.Data, &fsigs) - // Fill fsigs with query parameters. - for i := range fsigs { - fsig := &(fsigs[i]) - for j := range fsig.Params { - param := &(fsig.Params[j]) - value := query.Get(param.Name) - param.Value = value - } - } - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("FuncName", funcName) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("DirPath", pathOf(rlmpath)) - tmpl.Set("FunctionSignatures", fsigs) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "realm_help.html", "funcs.html") - } else { - // Ensure realm exists. TODO optimize. - qpath := qFileStr - data := []byte(rlmpath) - _, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, errors.New("error querying realm package")) - return - } - // Render blank query path, /r/REALM:. - handleRealmRender(logger, app, cfg, w, r) - } - }) -} - -type pathLink struct { - URL string - Text string -} - -func handlerRealmRender(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handleRealmRender(logger, app, cfg, w, r) - }) -} - -func handleRealmRender(logger *slog.Logger, app gotuna.App, cfg *Config, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - rlmname := vars["rlmname"] - rlmpath := "gno.land/r/" + rlmname - querystr := vars["querystr"] - if r.URL.Path == "/r/"+rlmname+":" { - // Redirect to /r/REALM if querypath is empty. - http.Redirect(w, r, "/r/"+rlmname, http.StatusFound) - return - } - qpath := "vm/qrender" - data := []byte(fmt.Sprintf("%s:%s", rlmpath, querystr)) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - // XXX hack - if strings.Contains(err.Error(), "Render not declared") { - res = &abci.ResponseQuery{} - res.Data = []byte("realm package has no Render() function") - } else { - writeError(logger, w, err) - return - } - } - - dirdata := []byte(rlmpath) - dirres, err := makeRequest(logger, cfg, qFileStr, dirdata) - if err != nil { - writeError(logger, w, err) - return - } - hasReadme := bytes.Contains(append(dirres.Data, '\n'), []byte("README.md\n")) - - // linkify querystr. - queryParts := strings.Split(querystr, "/") - pathLinks := []pathLink{} - for i, part := range queryParts { - pathLinks = append(pathLinks, pathLink{ - URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"), - Text: part, - }) - } - // Render template. - tmpl := app.NewTemplatingEngine() - // XXX: extract title from realm's output - // XXX: extract description from realm's output - tmpl.Set("RealmName", rlmname) - tmpl.Set("RealmPath", rlmpath) - tmpl.Set("Query", querystr) - tmpl.Set("PathLinks", pathLinks) - tmpl.Set("Contents", string(res.Data)) - tmpl.Set("Config", cfg) - tmpl.Set("HasReadme", hasReadme) - tmpl.Render(w, r, "realm_render.html", "funcs.html") -} - -func handlerRealmFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - diruri := "gno.land/r/" + vars["rlmname"] - filename := vars["filename"] - renderPackageFile(logger, app, cfg, w, r, diruri, filename) - }) -} - -func handlerPackageFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - pkgpath := "gno.land/p/" + vars["filepath"] - diruri, filename := std.SplitFilepath(pkgpath) - if filename == "" && diruri == pkgpath { - // redirect to diruri + "/" - http.Redirect(w, r, "/p/"+vars["filepath"]+"/", http.StatusFound) - return - } - renderPackageFile(logger, app, cfg, w, r, diruri, filename) - }) -} - -func renderPackageFile(logger *slog.Logger, app gotuna.App, cfg *Config, w http.ResponseWriter, r *http.Request, diruri string, filename string) { - if filename == "" { - // Request is for a folder. - qpath := qFileStr - data := []byte(diruri) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, err) - return - } - files := strings.Split(string(res.Data), "\n") - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("DirURI", diruri) - tmpl.Set("DirPath", pathOf(diruri)) - tmpl.Set("Files", files) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "package_dir.html", "funcs.html") - } else { - // Request is for a file. - filepath := diruri + "/" + filename - qpath := qFileStr - data := []byte(filepath) - res, err := makeRequest(logger, cfg, qpath, data) - if err != nil { - writeError(logger, w, err) - return - } - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("DirURI", diruri) - tmpl.Set("DirPath", pathOf(diruri)) - tmpl.Set("FileName", filename) - tmpl.Set("FileContents", string(res.Data)) - tmpl.Set("Config", cfg) - tmpl.Render(w, r, "package_file.html", "funcs.html") - } -} - -func makeRequest(log *slog.Logger, cfg *Config, qpath string, data []byte) (res *abci.ResponseQuery, err error) { - opts2 := client.ABCIQueryOptions{ - // Height: height, XXX - // Prove: false, XXX - } - remote := cfg.RemoteAddr - cli, err := client.NewHTTPClient(remote) - if err != nil { - return nil, fmt.Errorf("unable to create HTTP client, %w", err) - } - - qres, err := cli.ABCIQueryWithOptions( - qpath, data, opts2) - if err != nil { - log.Error("request error", "path", qpath, "error", err) - return nil, fmt.Errorf("unable to query path %q: %w", qpath, err) - } - if qres.Response.Error != nil { - log.Error("response error", "path", qpath, "log", qres.Response.Log) - return nil, qres.Response.Error - } - return &qres.Response, nil -} - -func handlerStaticFile(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - fs := http.FS(app.Static) - fileapp := http.StripPrefix("/static", http.FileServer(fs)) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - fpath := filepath.Clean(vars["path"]) - f, err := fs.Open(fpath) - if os.IsNotExist(err) { - handleNotFound(logger, app, cfg, fpath, w, r) - return - } - stat, err := f.Stat() - if err != nil || stat.IsDir() { - handleNotFound(logger, app, cfg, fpath, w, r) - return - } - - // TODO: ModTime doesn't work for embed? - // w.Header().Set("ETag", fmt.Sprintf("%x", stat.ModTime().UnixNano())) - // w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%s", "31536000")) - fileapp.ServeHTTP(w, r) - }) -} - -func handlerFavicon(logger *slog.Logger, app gotuna.App, cfg *Config) http.Handler { - fs := http.FS(app.Static) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fpath := "img/favicon.ico" - f, err := fs.Open(fpath) - if os.IsNotExist(err) { - handleNotFound(logger, app, cfg, fpath, w, r) - return - } - w.Header().Set("Content-Type", "image/x-icon") - w.Header().Set("Cache-Control", "public, max-age=604800") // 7d - io.Copy(w, f) - }) -} - -func handleNotFound(logger *slog.Logger, app gotuna.App, cfg *Config, path string, w http.ResponseWriter, r *http.Request) { - // decode path for non-ascii characters - decodedPath, err := url.PathUnescape(path) - if err != nil { - logger.Error("failed to decode path", err) - decodedPath = path - } - w.WriteHeader(http.StatusNotFound) - app.NewTemplatingEngine(). - Set("title", "Not found"). - Set("path", decodedPath). - Set("Config", cfg). - Render(w, r, "404.html", "funcs.html") -} - -func writeError(logger *slog.Logger, w http.ResponseWriter, err error) { - if details := errors.Unwrap(err); details != nil { - logger.Error("handler", "error", err, "details", details) - } else { - logger.Error("handler", "error:", err) - } - - // XXX: writeError should return an error page template. - w.WriteHeader(500) - w.Write([]byte(err.Error())) -} - -func pathOf(diruri string) string { - parts := strings.Split(diruri, "/") - if parts[0] == "gno.land" { - return "/" + strings.Join(parts[1:], "/") - } - - panic(fmt.Sprintf("invalid dir-URI %q", diruri)) -} diff --git a/gno.land/pkg/gnoweb/gnoweb_test.go b/gno.land/pkg/gnoweb/gnoweb_test.go deleted file mode 100644 index b266dc80a6a..00000000000 --- a/gno.land/pkg/gnoweb/gnoweb_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package gnoweb - -import ( - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gnolang/gno/gno.land/pkg/integration" - "github.com/gnolang/gno/gnovm/pkg/gnoenv" - "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gotuna/gotuna/test/assert" -) - -func TestRoutes(t *testing.T) { - const ( - ok = http.StatusOK - found = http.StatusFound - notFound = http.StatusNotFound - ) - routes := []struct { - route string - status int - substring string - }{ - {"/", ok, "Welcome"}, // assert / gives 200 (OK). assert / contains "Welcome". - {"/about", ok, "blockchain"}, - {"/r/gnoland/blog", ok, ""}, // whatever content - {"/r/gnoland/blog?help", ok, "exposed"}, - {"/r/gnoland/blog/", ok, "admin.gno"}, - {"/r/gnoland/blog/admin.gno", ok, "func "}, - {"/r/demo/users:administrator", ok, "address"}, - {"/r/demo/users", ok, "manfred"}, - {"/r/demo/users/users.gno", ok, "// State"}, - {"/r/demo/deep/very/deep", ok, "it works!"}, - {"/r/demo/deep/very/deep:bob", ok, "hi bob"}, - {"/r/demo/deep/very/deep?help", ok, "exposed"}, - {"/r/demo/deep/very/deep/", ok, "render.gno"}, - {"/r/demo/deep/very/deep/render.gno", ok, "func Render("}, - {"/game-of-realms", ok, "/r/gnoland/pages:p/gor"}, - {"/gor", found, "/game-of-realms"}, - {"/blog", found, "/r/gnoland/blog"}, - {"/404-not-found", notFound, "/404-not-found"}, - {"/아스키문자가아닌경로", notFound, "/아스키문자가아닌경로"}, - {"/%ED%85%8C%EC%8A%A4%ED%8A%B8", notFound, "/테스트"}, - {"/グノー", notFound, "/グノー"}, - {"/⚛️", notFound, "/⚛️"}, - {"/p/demo/flow/LICENSE", ok, "BSD 3-Clause"}, - } - - rootdir := gnoenv.RootDir() - genesis := integration.LoadDefaultGenesisTXsFile(t, "tendermint_test", rootdir) - config, _ := integration.TestingNodeConfig(t, rootdir, genesis...) - node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) - defer node.Stop() - - cfg := NewDefaultConfig() - - logger := log.NewTestingLogger(t) - - // set the `remoteAddr` of the client to the listening address of the - // node, which is randomly assigned. - cfg.RemoteAddr = remoteAddr - app := MakeApp(logger, cfg) - - for _, r := range routes { - t.Run(fmt.Sprintf("test route %s", r.route), func(t *testing.T) { - request := httptest.NewRequest(http.MethodGet, r.route, nil) - response := httptest.NewRecorder() - app.Router.ServeHTTP(response, request) - assert.Equal(t, r.status, response.Code) - assert.Contains(t, response.Body.String(), r.substring) - }) - } -} - -func TestAnalytics(t *testing.T) { - routes := []string{ - // special realms - "/", // home - "/about", - "/start", - - // redirects - "/game-of-realms", - "/getting-started", - "/blog", - "/boards", - - // realm, source, help page - "/r/gnoland/blog", - "/r/gnoland/blog/admin.gno", - "/r/demo/users:administrator", - "/r/gnoland/blog?help", - - // special pages - "/404-not-found", - } - - rootdir := gnoenv.RootDir() - genesis := integration.LoadDefaultGenesisTXsFile(t, "tendermint_test", rootdir) - config, _ := integration.TestingNodeConfig(t, rootdir, genesis...) - node, remoteAddr := integration.TestingInMemoryNode(t, log.NewTestingLogger(t), config) - defer node.Stop() - - cfg := NewDefaultConfig() - cfg.RemoteAddr = remoteAddr - - logger := log.NewTestingLogger(t) - - t.Run("with", func(t *testing.T) { - for _, route := range routes { - t.Run(route, func(t *testing.T) { - ccfg := cfg // clone config - ccfg.WithAnalytics = true - app := MakeApp(logger, ccfg) - request := httptest.NewRequest(http.MethodGet, route, nil) - response := httptest.NewRecorder() - app.Router.ServeHTTP(response, request) - assert.Contains(t, response.Body.String(), "sa.gno.services") - }) - } - }) - t.Run("without", func(t *testing.T) { - for _, route := range routes { - t.Run(route, func(t *testing.T) { - ccfg := cfg // clone config - ccfg.WithAnalytics = false - app := MakeApp(logger, ccfg) - request := httptest.NewRequest(http.MethodGet, route, nil) - response := httptest.NewRecorder() - app.Router.ServeHTTP(response, request) - assert.Equal(t, strings.Contains(response.Body.String(), "sa.gno.services"), false) - }) - } - }) -} diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go new file mode 100644 index 00000000000..2dc51d64029 --- /dev/null +++ b/gno.land/pkg/gnoweb/handler.go @@ -0,0 +1,347 @@ +package gnoweb + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "io" + "log/slog" + "net/http" + "path/filepath" + "strings" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // For error types +) + +// StaticMetadata holds static configuration for a web handler. +type StaticMetadata struct { + Domain string + AssetsPath string + ChromaPath string + RemoteHelp string + ChainId string + Analytics bool +} + +// WebHandlerConfig configures a WebHandler. +type WebHandlerConfig struct { + Meta StaticMetadata + WebClient WebClient +} + +// validate checks if the WebHandlerConfig is valid. +func (cfg WebHandlerConfig) validate() error { + if cfg.WebClient == nil { + return errors.New("no `WebClient` configured") + } + return nil +} + +// WebHandler processes HTTP requests. +type WebHandler struct { + Logger *slog.Logger + Static StaticMetadata + Client WebClient +} + +// NewWebHandler creates a new WebHandler. +func NewWebHandler(logger *slog.Logger, cfg WebHandlerConfig) (*WebHandler, error) { + if err := cfg.validate(); err != nil { + return nil, fmt.Errorf("config validate error: %w", err) + } + + return &WebHandler{ + Client: cfg.WebClient, + Static: cfg.Meta, + Logger: logger, + }, nil +} + +// ServeHTTP handles HTTP requests. +func (h *WebHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.Logger.Debug("receiving request", "method", r.Method, "path", r.URL.Path) + + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + h.Get(w, r) +} + +// Get processes a GET HTTP request. +func (h *WebHandler) Get(w http.ResponseWriter, r *http.Request) { + var body bytes.Buffer + + start := time.Now() + defer func() { + h.Logger.Debug("request completed", + "url", r.URL.String(), + "elapsed", time.Since(start).String()) + }() + + indexData := components.IndexData{ + HeadData: components.HeadData{ + AssetsPath: h.Static.AssetsPath, + ChromaPath: h.Static.ChromaPath, + }, + FooterData: components.FooterData{ + Analytics: h.Static.Analytics, + AssetsPath: h.Static.AssetsPath, + }, + } + + status, err := h.renderPage(&body, r, &indexData) + if err != nil { + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + w.WriteHeader(status) + + // NOTE: HTML escaping should have already been done by markdown rendering package + indexData.Body = template.HTML(body.String()) //nolint:gosec + + // Render the final page with the rendered body + if err = components.RenderIndexComponent(w, indexData); err != nil { + h.Logger.Error("failed to render index component", "err", err) + } +} + +// renderPage renders the page into the given buffer and prepares the index data. +func (h *WebHandler) renderPage(body *bytes.Buffer, r *http.Request, indexData *components.IndexData) (int, error) { + gnourl, err := ParseGnoURL(r.URL) + if err != nil { + h.Logger.Warn("unable to parse url path", "path", r.URL.Path, "err", err) + return http.StatusNotFound, components.RenderStatusComponent(body, "invalid path") + } + + breadcrumb := generateBreadcrumbPaths(gnourl) + indexData.HeadData.Title = h.Static.Domain + " - " + gnourl.Path + indexData.HeaderData = components.HeaderData{ + RealmPath: gnourl.Encode(EncodePath | EncodeArgs | EncodeQuery | EncodeNoEscape), + Breadcrumb: breadcrumb, + WebQuery: gnourl.WebQuery, + } + + switch { + case gnourl.IsRealm(), gnourl.IsPure(): + return h.GetPackagePage(body, gnourl) + default: + h.Logger.Debug("invalid path: path is neither a pure package or a realm") + return http.StatusBadRequest, components.RenderStatusComponent(body, "invalid path") + } +} + +// GetPackagePage handles package pages. +func (h *WebHandler) GetPackagePage(w io.Writer, gnourl *GnoURL) (int, error) { + h.Logger.Info("component render", "path", gnourl.Path, "args", gnourl.Args) + + // Handle Help page + if gnourl.WebQuery.Has("help") { + return h.GetHelpPage(w, gnourl) + } + + // Handle Source page + if gnourl.WebQuery.Has("source") || gnourl.IsFile() { + return h.GetSourcePage(w, gnourl) + } + + // Handle Source page + if gnourl.IsDir() || gnourl.IsPure() { + return h.GetDirectoryPage(w, gnourl) + } + + // Ultimately render realm content + return h.renderRealmContent(w, gnourl) +} + +// renderRealmContent renders the content of a realm. +func (h *WebHandler) renderRealmContent(w io.Writer, gnourl *GnoURL) (int, error) { + var content bytes.Buffer + meta, err := h.Client.RenderRealm(&content, gnourl.Path, gnourl.EncodeArgs()) + if err != nil { + h.Logger.Error("unable to render realm", "err", err, "path", gnourl.EncodeArgs()) + return renderClientErrorStatusPage(w, gnourl, err) + } + + err = components.RenderRealmComponent(w, components.RealmData{ + TocItems: &components.RealmTOCData{ + Items: meta.Toc.Items, + }, + // NOTE: `RenderRealm` should ensure that HTML content is + // sanitized before rendering + Content: template.HTML(content.String()), //nolint:gosec + }) + if err != nil { + h.Logger.Error("unable to render template", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + return http.StatusOK, nil +} + +// GetHelpPage renders the help page. +func (h *WebHandler) GetHelpPage(w io.Writer, gnourl *GnoURL) (int, error) { + fsigs, err := h.Client.Functions(gnourl.Path) + if err != nil { + h.Logger.Error("unable to fetch path functions", "err", err) + return renderClientErrorStatusPage(w, gnourl, err) + } + + selArgs := make(map[string]string) + selFn := gnourl.WebQuery.Get("func") + if selFn != "" { + for _, fn := range fsigs { + if selFn != fn.FuncName { + continue + } + + for _, param := range fn.Params { + selArgs[param.Name] = gnourl.WebQuery.Get(param.Name) + } + + fsigs = []vm.FunctionSignature{fn} + break + } + } + + realmName := filepath.Base(gnourl.Path) + err = components.RenderHelpComponent(w, components.HelpData{ + SelectedFunc: selFn, + SelectedArgs: selArgs, + RealmName: realmName, + ChainId: h.Static.ChainId, + // TODO: get chain domain and use that. + PkgPath: filepath.Join(h.Static.Domain, gnourl.Path), + Remote: h.Static.RemoteHelp, + Functions: fsigs, + }) + if err != nil { + h.Logger.Error("unable to render helper", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + return http.StatusOK, nil +} + +// GetSource renders the source page. +func (h *WebHandler) GetSourcePage(w io.Writer, gnourl *GnoURL) (int, error) { + pkgPath := gnourl.Path + files, err := h.Client.Sources(pkgPath) + if err != nil { + h.Logger.Error("unable to list sources file", "path", gnourl.Path, "err", err) + return renderClientErrorStatusPage(w, gnourl, err) + } + + if len(files) == 0 { + h.Logger.Debug("no files available", "path", gnourl.Path) + return http.StatusOK, components.RenderStatusComponent(w, "no files available") + } + + var fileName string + if gnourl.IsFile() { // check path file from path first + fileName = gnourl.File + } else if file := gnourl.WebQuery.Get("file"); file != "" { + fileName = file + } + + if fileName == "" { + fileName = files[0] // fallback on the first file if + } + + var source bytes.Buffer + meta, err := h.Client.SourceFile(&source, pkgPath, fileName) + if err != nil { + h.Logger.Error("unable to get source file", "file", fileName, "err", err) + return renderClientErrorStatusPage(w, gnourl, err) + } + + fileSizeStr := fmt.Sprintf("%.2f Kb", meta.SizeKb) + err = components.RenderSourceComponent(w, components.SourceData{ + PkgPath: gnourl.Path, + Files: files, + FileName: fileName, + FileCounter: len(files), + FileLines: meta.Lines, + FileSize: fileSizeStr, + FileSource: template.HTML(source.String()), //nolint:gosec + }) + if err != nil { + h.Logger.Error("unable to render helper", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + return http.StatusOK, nil +} + +// GetDirectoryPage renders the directory page. +func (h *WebHandler) GetDirectoryPage(w io.Writer, gnourl *GnoURL) (int, error) { + pkgPath := strings.TrimSuffix(gnourl.Path, "/") + + files, err := h.Client.Sources(pkgPath) + if err != nil { + h.Logger.Error("unable to list sources file", "path", gnourl.Path, "err", err) + return renderClientErrorStatusPage(w, gnourl, err) + } + + if len(files) == 0 { + h.Logger.Debug("no files available", "path", gnourl.Path) + return http.StatusOK, components.RenderStatusComponent(w, "no files available") + } + + err = components.RenderDirectoryComponent(w, components.DirData{ + PkgPath: gnourl.Path, + Files: files, + FileCounter: len(files), + }) + if err != nil { + h.Logger.Error("unable to render directory", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "not found") + } + + return http.StatusOK, nil +} + +func renderClientErrorStatusPage(w io.Writer, _ *GnoURL, err error) (int, error) { + if err == nil { + return http.StatusOK, nil + } + + switch { + case errors.Is(err, ErrClientPathNotFound): + return http.StatusNotFound, components.RenderStatusComponent(w, err.Error()) + case errors.Is(err, ErrClientBadRequest): + return http.StatusInternalServerError, components.RenderStatusComponent(w, "bad request") + case errors.Is(err, ErrClientResponse): + fallthrough // XXX: for now fallback as internal error + default: + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } +} + +func generateBreadcrumbPaths(url *GnoURL) components.BreadcrumbData { + split := strings.Split(url.Path, "/") + + var data components.BreadcrumbData + var name string + for i := range split { + if name = split[i]; name == "" { + continue + } + + data.Parts = append(data.Parts, components.BreadcrumbPart{ + Name: name, + URL: strings.Join(split[:i+1], "/"), + }) + } + + if args := url.EncodeArgs(); args != "" { + data.Args = args + } + + return data +} diff --git a/gno.land/pkg/gnoweb/handler_test.go b/gno.land/pkg/gnoweb/handler_test.go new file mode 100644 index 00000000000..624e3390a97 --- /dev/null +++ b/gno.land/pkg/gnoweb/handler_test.go @@ -0,0 +1,112 @@ +package gnoweb_test + +import ( + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testingLogger struct { + *testing.T +} + +func (t *testingLogger) Write(b []byte) (n int, err error) { + t.T.Log(strings.TrimSpace(string(b))) + return len(b), nil +} + +// TestWebHandler_Get tests the Get method of WebHandler using table-driven tests. +func TestWebHandler_Get(t *testing.T) { + // Set up a mock package with some files and functions + mockPackage := &gnoweb.MockPackage{ + Domain: "example.com", + Path: "/r/mock/path", + Files: map[string]string{ + "render.gno": `package main; func Render(path string) { return "one more time" }`, + "gno.mod": `module example.com/r/mock/path`, + "LicEnse": `my super license`, + }, + Functions: []vm.FunctionSignature{ + {FuncName: "SuperRenderFunction", Params: []vm.NamedType{ + {Name: "my_super_arg", Type: "string"}, + }}, + }, + } + + // Create a mock web client with the mock package + webclient := gnoweb.NewMockWebClient(mockPackage) + + // Create a WebHandlerConfig with the mock web client and static metadata + config := gnoweb.WebHandlerConfig{ + WebClient: webclient, + } + + // Define test cases + cases := []struct { + Path string + Status int + Contain string // optional + Contains []string // optional + }{ + // Found + {Path: "/r/mock/path", Status: http.StatusOK, Contain: "[example.com]/r/mock/path"}, + + // Source page + {Path: "/r/mock/path/", Status: http.StatusOK, Contain: "Directory"}, + {Path: "/r/mock/path/render.gno", Status: http.StatusOK, Contain: "one more time"}, + {Path: "/r/mock/path/LicEnse", Status: http.StatusOK, Contain: "my super license"}, + {Path: "/r/mock/path$source&file=render.gno", Status: http.StatusOK, Contain: "one more time"}, + {Path: "/r/mock/path$source", Status: http.StatusOK, Contain: "module"}, // `gno.mod` by default + {Path: "/r/mock/path/license", Status: http.StatusNotFound}, + + // Help page + {Path: "/r/mock/path$help", Status: http.StatusOK, Contains: []string{ + "my_super_arg", + "SuperRenderFunction", + }}, + + // Package not found + {Path: "/r/invalid/path", Status: http.StatusNotFound, Contain: "not found"}, + + // Invalid path + {Path: "/r", Status: http.StatusBadRequest, Contain: "invalid path"}, + {Path: "/r/~!1337", Status: http.StatusNotFound, Contain: "invalid path"}, + } + + for _, tc := range cases { + t.Run(strings.TrimPrefix(tc.Path, "/"), func(t *testing.T) { + t.Logf("input: %+v", tc) + + // Initialize testing logger + logger := slog.New(slog.NewTextHandler(&testingLogger{t}, &slog.HandlerOptions{})) + + // Create a new WebHandler + handler, err := gnoweb.NewWebHandler(logger, config) + require.NoError(t, err) + + // Create a new HTTP request for each test case + req, err := http.NewRequest(http.MethodGet, tc.Path, nil) + require.NoError(t, err) + + // Create a ResponseRecorder to capture the response + rr := httptest.NewRecorder() + + // Invoke serve method + handler.ServeHTTP(rr, req) + + // Assert result + assert.Equal(t, tc.Status, rr.Code) + assert.Containsf(t, rr.Body.String(), tc.Contain, "rendered body should contain: %q", tc.Contain) + for _, contain := range tc.Contains { + assert.Containsf(t, rr.Body.String(), contain, "rendered body should contain: %q", contain) + } + }) + } +} diff --git a/gno.land/pkg/gnoweb/markdown/toc.go b/gno.land/pkg/gnoweb/markdown/toc.go new file mode 100644 index 00000000000..ceafbd7cc96 --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/toc.go @@ -0,0 +1,137 @@ +// This file is a minimal version of https://github.com/abhinav/goldmark-toc + +package markdown + +import ( + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/util" +) + +const MaxDepth = 6 + +type Toc struct { + Items []*TocItem +} + +type TocItem struct { + // Title of this item in the table of contents. + // + // This may be blank for items that don't refer to a heading, and only + // have sub-items. + Title []byte + + // ID is the identifier for the heading that this item refers to. This + // is the fragment portion of the link without the "#". + // + // This may be blank if the item doesn't have an id assigned to it, or + // if it doesn't have a title. + // + // Enable AutoHeadingID in your parser if you expected these to be set + // but they weren't. + ID []byte + + // Items references children of this item. + // + // For a heading at level 3, Items, contains the headings at level 4 + // under that section. + Items []*TocItem +} + +func (i TocItem) Anchor() string { + return "#" + string(i.ID) +} + +type TocOptions struct { + MinDepth, MaxDepth int +} + +func TocInspect(n ast.Node, src []byte, opts TocOptions) (Toc, error) { + // Appends an empty subitem to the given node + // and returns a reference to it. + appendChild := func(n *TocItem) *TocItem { + child := new(TocItem) + n.Items = append(n.Items, child) + return child + } + + // Returns the last subitem of the given node, + // creating it if necessary. + lastChild := func(n *TocItem) *TocItem { + if len(n.Items) > 0 { + return n.Items[len(n.Items)-1] + } + return appendChild(n) + } + + var root TocItem + + stack := []*TocItem{&root} // inv: len(stack) >= 1 + err := ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + // Skip non-heading node + heading, ok := n.(*ast.Heading) + if !ok { + return ast.WalkContinue, nil + } + + if opts.MinDepth > 0 && heading.Level < opts.MinDepth { + return ast.WalkSkipChildren, nil + } + + if opts.MaxDepth > 0 && heading.Level > opts.MaxDepth { + return ast.WalkSkipChildren, nil + } + + // The heading is deeper than the current depth. + // Append empty items to match the heading's level. + for len(stack) < heading.Level { + parent := stack[len(stack)-1] + stack = append(stack, lastChild(parent)) + } + + // The heading is shallower than the current depth. + // Move back up the stack until we reach the heading's level. + if len(stack) > heading.Level { + stack = stack[:heading.Level] + } + + parent := stack[len(stack)-1] + target := lastChild(parent) + if len(target.Title) > 0 || len(target.Items) > 0 { + target = appendChild(parent) + } + + target.Title = util.UnescapePunctuations(heading.Text(src)) + if id, ok := n.AttributeString("id"); ok { + target.ID, _ = id.([]byte) + } + + return ast.WalkSkipChildren, nil + }) + + root.Items = compactItems(root.Items) + + return Toc{Items: root.Items}, err +} + +// compactItems removes items with no titles +// from the given list of items. +// +// Children of removed items will be promoted to the parent item. +func compactItems(items []*TocItem) []*TocItem { + result := make([]*TocItem, 0) + for _, item := range items { + if len(item.Title) == 0 { + result = append(result, compactItems(item.Items)...) + continue + } + + item.Items = compactItems(item.Items) + result = append(result, item) + } + + return result +} diff --git a/gno.land/pkg/gnoweb/public/favicon.ico b/gno.land/pkg/gnoweb/public/favicon.ico new file mode 100644 index 00000000000..528c362c44a Binary files /dev/null and b/gno.land/pkg/gnoweb/public/favicon.ico differ diff --git a/gno.land/pkg/gnoweb/public/fonts/intervar/Intervar.woff2 b/gno.land/pkg/gnoweb/public/fonts/intervar/Intervar.woff2 new file mode 100644 index 00000000000..891fc5cc567 Binary files /dev/null and b/gno.land/pkg/gnoweb/public/fonts/intervar/Intervar.woff2 differ diff --git a/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff b/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff new file mode 100644 index 00000000000..2c58fe2d6d7 Binary files /dev/null and b/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff differ diff --git a/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 b/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 new file mode 100644 index 00000000000..53d081f3a53 Binary files /dev/null and b/gno.land/pkg/gnoweb/public/fonts/roboto/roboto-mono-normal.woff2 differ diff --git a/gno.land/pkg/gnoweb/public/imgs/gnoland.svg b/gno.land/pkg/gnoweb/public/imgs/gnoland.svg new file mode 100644 index 00000000000..30d2f3ef56a --- /dev/null +++ b/gno.land/pkg/gnoweb/public/imgs/gnoland.svg @@ -0,0 +1,4 @@ + + + + diff --git a/gno.land/pkg/gnoweb/public/js/copy.js b/gno.land/pkg/gnoweb/public/js/copy.js new file mode 100644 index 00000000000..918a30b1ca3 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/copy.js @@ -0,0 +1 @@ +var s=class o{DOM;static FEEDBACK_DELAY=750;btnClicked=null;btnClickedIcons=[];isAnimationRunning=!1;static SELECTORS={button:"[data-copy-btn]",icon:"[data-copy-icon] > use",content:t=>`[data-copy-content="${t}"]`};constructor(){this.DOM={el:document.querySelector("main")},this.DOM.el?this.init():console.warn("Copy: Main container not found.")}init(){this.bindEvents()}bindEvents(){this.DOM.el?.addEventListener("click",this.handleClick.bind(this))}handleClick(t){let e=t.target.closest(o.SELECTORS.button);if(!e)return;this.btnClicked=e,this.btnClickedIcons=Array.from(e.querySelectorAll(o.SELECTORS.icon));let i=e.getAttribute("data-copy-btn");if(!i){console.warn("Copy: No content ID found on the button.");return}let r=this.DOM.el?.querySelector(o.SELECTORS.content(i));r?this.copyToClipboard(r,this.btnClickedIcons):console.warn(`Copy: No content found for ID "${i}".`)}sanitizeContent(t){let n=t.innerHTML.replace(/]*class="chroma-ln"[^>]*>[\s\S]*?<\/span>/g,""),e=document.createElement("div");return e.innerHTML=n,e.textContent?.trim()||""}toggleIcons(t){t.forEach(n=>{n.classList.toggle("hidden")})}showFeedback(t){!this.btnClicked||this.isAnimationRunning===!0||(this.isAnimationRunning=!0,this.toggleIcons(t),window.setTimeout(()=>{this.toggleIcons(t),this.isAnimationRunning=!1},o.FEEDBACK_DELAY))}async copyToClipboard(t,n){let e=this.sanitizeContent(t);if(!navigator.clipboard){console.error("Copy: Clipboard API is not supported in this browser."),this.showFeedback(n);return}try{await navigator.clipboard.writeText(e),this.showFeedback(n)}catch(i){console.error("Copy: Error while copying text.",i),this.showFeedback(n)}}},a=()=>new s;export{a as default}; diff --git a/gno.land/pkg/gnoweb/public/js/index.js b/gno.land/pkg/gnoweb/public/js/index.js new file mode 100644 index 00000000000..e990dd91f5f --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/index.js @@ -0,0 +1 @@ +(()=>{let s={copy:{selector:"[data-copy-btn]",path:"/public/js/copy.js"},help:{selector:"#help",path:"/public/js/realmhelp.js"},searchBar:{selector:"#header-searchbar",path:"/public/js/searchbar.js"}},r=async({selector:e,path:o})=>{if(document.querySelector(e))try{(await import(o)).default()}catch(t){console.error(`Error while loading script ${o}:`,t)}else console.warn(`Module not loaded: no element matches selector "${e}"`)},l=async()=>{let e=Object.values(s).map(o=>r(o));await Promise.all(e)};document.addEventListener("DOMContentLoaded",l)})(); diff --git a/gno.land/pkg/gnoweb/public/js/realmhelp.js b/gno.land/pkg/gnoweb/public/js/realmhelp.js new file mode 100644 index 00000000000..5d4a3feeba6 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/realmhelp.js @@ -0,0 +1 @@ +function d(s,e=250){let t;return function(...a){t!==void 0&&clearTimeout(t),t=setTimeout(()=>{s.apply(this,a)},e)}}var l=class s{DOM;funcList;static SELECTORS={container:"#help",func:"[data-func]",addressInput:"[data-role='help-input-addr']",cmdModeSelect:"[data-role='help-select-mode']"};constructor(){this.DOM={el:document.querySelector(s.SELECTORS.container),funcs:[],addressInput:null,cmdModeSelect:null},this.funcList=[],this.DOM.el?this.init():console.warn("Help: Main container not found.")}init(){let{el:e}=this.DOM;e&&(this.DOM.funcs=Array.from(e.querySelectorAll(s.SELECTORS.func)),this.DOM.addressInput=e.querySelector(s.SELECTORS.addressInput),this.DOM.cmdModeSelect=e.querySelector(s.SELECTORS.cmdModeSelect),this.funcList=this.DOM.funcs.map(t=>new o(t)),this.restoreAddress(),this.bindEvents())}restoreAddress(){let{addressInput:e}=this.DOM;if(e){let t=localStorage.getItem("helpAddressInput");t&&(e.value=t,this.funcList.forEach(a=>a.updateAddr(t)))}}bindEvents(){let{addressInput:e,cmdModeSelect:t}=this.DOM,a=d(r=>{let n=r.value;localStorage.setItem("helpAddressInput",n),this.funcList.forEach(i=>i.updateAddr(n))});e?.addEventListener("input",()=>a(e)),t?.addEventListener("change",r=>{let n=r.target;this.funcList.forEach(i=>i.updateMode(n.value))})}},o=class s{DOM;funcName;static SELECTORS={address:"[data-role='help-code-address']",args:"[data-role='help-code-args']",mode:"[data-code-mode]",paramInput:"[data-role='help-param-input']"};constructor(e){this.DOM={el:e,addrs:Array.from(e.querySelectorAll(s.SELECTORS.address)),args:Array.from(e.querySelectorAll(s.SELECTORS.args)),modes:Array.from(e.querySelectorAll(s.SELECTORS.mode)),paramInputs:Array.from(e.querySelectorAll(s.SELECTORS.paramInput))},this.funcName=e.dataset.func||null,this.initializeArgs(),this.bindEvents()}static sanitizeArgsInput(e){let t=e.dataset.param||"",a=e.value.trim();return t||console.warn("sanitizeArgsInput: param is missing in arg input dataset."),{paramName:t,paramValue:a}}bindEvents(){let e=d((t,a)=>{t&&this.updateArg(t,a)});this.DOM.el.addEventListener("input",t=>{let a=t.target;if(a.dataset.role==="help-param-input"){let{paramName:r,paramValue:n}=s.sanitizeArgsInput(a);e(r,n)}})}initializeArgs(){this.DOM.paramInputs.forEach(e=>{let{paramName:t,paramValue:a}=s.sanitizeArgsInput(e);t&&this.updateArg(t,a)})}updateArg(e,t){this.DOM.args.filter(a=>a.dataset.arg===e).forEach(a=>{a.textContent=t||""})}updateAddr(e){this.DOM.addrs.forEach(t=>{t.textContent=e.trim()||"ADDRESS"})}updateMode(e){this.DOM.modes.forEach(t=>{let a=t.dataset.codeMode===e;t.classList.toggle("inline",a),t.classList.toggle("hidden",!a),t.dataset.copyContent=a?`help-cmd-${this.funcName}`:""})}},p=()=>new l;export{p as default}; diff --git a/gno.land/pkg/gnoweb/public/js/searchbar.js b/gno.land/pkg/gnoweb/public/js/searchbar.js new file mode 100644 index 00000000000..e8012b9b6d9 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/searchbar.js @@ -0,0 +1 @@ +var n=class r{DOM;baseUrl;static SELECTORS={container:"#header-searchbar",inputSearch:"[data-role='header-input-search']",breadcrumb:"[data-role='header-breadcrumb-search']"};constructor(){this.DOM={el:document.querySelector(r.SELECTORS.container),inputSearch:null,breadcrumb:null},this.baseUrl=window.location.origin,this.DOM.el?this.init():console.warn("SearchBar: Main container not found.")}init(){let{el:e}=this.DOM;this.DOM.inputSearch=e?.querySelector(r.SELECTORS.inputSearch)??null,this.DOM.breadcrumb=e?.querySelector(r.SELECTORS.breadcrumb)??null,this.DOM.inputSearch||console.warn("SearchBar: Input element for search not found."),this.bindEvents()}bindEvents(){this.DOM.el?.addEventListener("submit",e=>{e.preventDefault(),this.searchUrl()})}searchUrl(){let e=this.DOM.inputSearch?.value.trim();if(e){let t=e;/^https?:\/\//i.test(t)||(t=`${this.baseUrl}${t.startsWith("/")?"":"/"}${t}`);try{window.location.href=new URL(t).href}catch{console.error("SearchBar: Invalid URL. Please enter a valid URL starting with http:// or https://.")}}else console.error("SearchBar: Please enter a URL to search.")}},i=()=>new n;export{i as default}; diff --git a/gno.land/pkg/gnoweb/public/js/utils.js b/gno.land/pkg/gnoweb/public/js/utils.js new file mode 100644 index 00000000000..e27fb93bc1c --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/utils.js @@ -0,0 +1 @@ +function r(t,n=250){let e;return function(...i){e!==void 0&&clearTimeout(e),e=setTimeout(()=>{t.apply(this,i)},n)}}export{r as debounce}; diff --git a/gno.land/pkg/gnoweb/public/styles.css b/gno.land/pkg/gnoweb/public/styles.css new file mode 100644 index 00000000000..8e8d7ed802d --- /dev/null +++ b/gno.land/pkg/gnoweb/public/styles.css @@ -0,0 +1,3 @@ +@font-face{font-family:Roboto;font-style:normal;font-weight:900;font-display:swap;src:url(fonts/roboto/roboto-mono-normal.woff2) format("woff2"),url(fonts/roboto/roboto-mono-normal.woff) format("woff")}@font-face{font-family:Inter var;font-weight:100 900;font-display:block;font-style:oblique 0deg 10deg;src:url(fonts/intervar/Intervar.woff2) format("woff2")}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: } + +/*! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #bdbdbd}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#7c7c7c}input::placeholder,textarea::placeholder{opacity:1;color:#7c7c7c}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}html{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity));font-family:Inter var,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji,sans-serif;font-size:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-font-feature-settings:"kern" on,"liga" on,"calt" on,"zero" on;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;-moz-osx-font-smoothing:grayscale;font-smoothing:antialiased;font-variant-ligatures:contextual common-ligatures;font-kerning:normal;text-rendering:optimizeLegibility}svg{max-height:100%;max-width:100%}form{margin-top:0;margin-bottom:0}.realm-content{overflow-wrap:break-word;padding-top:2.5rem;font-size:1rem}.realm-content>:first-child{margin-top:0!important}.realm-content a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-content a:hover{text-decoration-line:underline}.realm-content h1,.realm-content h2,.realm-content h3,.realm-content h4{margin-top:3rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content h2,.realm-content h2 *{font-weight:700}.realm-content h3,.realm-content h3 *,.realm-content h4,.realm-content h4 *{font-weight:600}.realm-content h1+h2,.realm-content h2+h3,.realm-content h3+h4{margin-top:1rem}.realm-content h1{font-size:2.375rem;font-weight:700}.realm-content h2{font-size:1.5rem}.realm-content h3{margin-top:2.5rem;font-size:1.25rem}.realm-content h3,.realm-content h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content h4{margin-top:1.5rem;margin-bottom:1.5rem;font-size:1.125rem;font-weight:500}.realm-content p{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-content strong *{font-weight:700}.realm-content em{font-style:oblique 10deg}.realm-content blockquote{margin-top:1rem;margin-bottom:1rem;border-left-width:4px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity));font-style:oblique 10deg}.realm-content ol,.realm-content ul{margin-top:1.5rem;margin-bottom:1.5rem;padding-left:1rem}.realm-content ol li,.realm-content ul li{margin-bottom:.5rem}.realm-content img{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-content figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.96em}.realm-content :not(pre)>code,.realm-content pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-content hr{margin-top:2.5rem;margin-bottom:2.5rem;border-top-width:1px;--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.realm-content table{margin-top:2rem;margin-bottom:2rem;display:block;width:100%;max-width:100%;border-collapse:collapse;overflow-x:auto}.realm-content td,.realm-content th{white-space:normal;overflow-wrap:break-word;border-width:1px;padding:.5rem 1rem}.realm-content th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-content caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-content q{margin-top:1.5rem;margin-bottom:1.5rem;border-left-width:4px;--tw-border-opacity:1;border-left-color:rgb(204 204 204/var(--tw-border-opacity));padding-left:1rem;--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity));font-style:oblique 10deg;quotes:"“" "”" "‘" "’"}.realm-content q:after,.realm-content q:before{margin-right:.25rem;font-size:1.5rem;--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity));content:open-quote;vertical-align:-.4rem}.realm-content q:after{content:close-quote}.realm-content q:before{content:open-quote}.realm-content q:after{content:close-quote}.realm-content ol ol,.realm-content ol ul,.realm-content ul ol,.realm-content ul ul{margin-top:.75rem;margin-bottom:.5rem;padding-left:1rem}.realm-content ul{list-style-type:disc}.realm-content ol{list-style-type:decimal}.realm-content abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-content details{margin-top:1.25rem;margin-bottom:1.25rem}.realm-content summary{cursor:pointer;font-weight:700}.realm-content a code{color:inherit}.realm-content video{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-content math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-content small{font-size:.875rem}.realm-content del{text-decoration-line:line-through}.realm-content sub{vertical-align:sub;font-size:.75rem}.realm-content sup{vertical-align:super;font-size:.75rem}.realm-content button,.realm-content input{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));padding:.5rem 1rem}main :is(h1,h2,h3,h4){scroll-margin-top:6rem}::-moz-selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}::selection{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity));--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.sidemenu .peer:checked+label>svg{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.toc-expend-btn:has(#toc-expend:checked)+nav{display:block}.toc-expend-btn:has(#toc-expend:checked) .toc-expend-btn_ico{--tw-rotate:180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.main-header:has(#sidemenu-docs:checked)+main #sidebar #sidebar-docs,.main-header:has(#sidemenu-meta:checked)+main #sidebar #sidebar-meta,.main-header:has(#sidemenu-source:checked)+main #sidebar #sidebar-source,.main-header:has(#sidemenu-summary:checked)+main #sidebar #sidebar-summary{display:block}@media (min-width:40rem){:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .main-navigation,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main .realm-content{grid-column:span 6/span 6}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked)) .sidemenu,:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar{grid-column:span 4/span 4}}:is(.main-header:has(#sidemenu-source:checked),.main-header:has(#sidemenu-docs:checked),.main-header:has(#sidemenu-meta:checked))+main #sidebar:before{position:absolute;top:0;left:-1.75rem;z-index:-1;display:block;height:100%;width:50vw;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}main :is(.source-code)>pre{overflow:scroll;border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;padding:1rem .25rem;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;;font-size:.875rem}@media (min-width:40rem){main :is(.source-code)>pre{padding:2rem .75rem;font-size:1rem}}main .realm-content>pre a:hover{text-decoration-line:none}main :is(.realm-content,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-content,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover) .chroma-cl{border-radius:.375rem;--tw-bg-opacity:1!important;background-color:rgb(226 226 226/var(--tw-bg-opacity))!important}main :is(.realm-content,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.bottom-1{bottom:.25rem}.left-0{left:0}.right-2{right:.5rem}.right-3{right:.75rem}.top-0{top:0}.top-1\/2{top:50%}.top-14{top:3.5rem}.top-2{top:.5rem}.z-1{z-index:1}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.row-start-1{grid-row-start:1}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.mr-10{margin-right:2.5rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-full{height:100%}.max-h-screen{max-height:100vh}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-full{width:100%}.min-w-2{min-width:.5rem}.min-w-48{min-width:12rem}.max-w-screen-max{max-width:98.75rem}.shrink-0{flex-shrink:0}.grow-\[2\]{flex-grow:2}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-flow-dense{grid-auto-flow:dense}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(153 153 153/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.text-light{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.before\:content-\[\'\:\'\]:before{--tw-content:":";content:var(--tw-content)}.before\:content-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:top-0:after{content:var(--tw-content);top:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:h-full:after{content:var(--tw-content);height:100%}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:rounded-t-sm:after{content:var(--tw-content);border-top-left-radius:.25rem;border-top-right-radius:.25rem}.after\:bg-gray-100:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:border-t:first-child{border-top-width:1px}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:border-l-gray-300:focus{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-l-gray-300{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:checked~.peer-checked\:before\:content-\[\'close\'\]:before{--tw-content:"close";content:var(--tw-content)}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[ul\:empty\]\:hidden:has(ul:empty){display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\:row-start-2{grid-row-start:2}.lg\:mb-4{margin-bottom:1rem}.lg\:mt-0{margin-top:0}.lg\:mt-10{margin-top:2.5rem}.lg\:block{display:block}.lg\:hidden{display:none}.lg\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:justify-start{justify-content:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.lg\:border-none{border-style:none}.lg\:bg-transparent{background-color:transparent}.lg\:p-0{padding:0}.lg\:px-0{padding-left:0;padding-right:0}.lg\:px-2{padding-left:.5rem;padding-right:.5rem}.lg\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.lg\:pb-28{padding-bottom:7rem}.lg\:pt-2{padding-top:.5rem}.lg\:text-200{font-size:1rem}.lg\:font-semibold{font-weight:600}.lg\:hover\:bg-transparent:hover{background-color:transparent}}@media (min-width:63.75rem){.xl\:inline{display:inline}.xl\:hidden{display:none}.xl\:grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:gap-20{gap:5rem}.xl\:gap-6{gap:1.5rem}.xl\:pt-0{padding-top:0}}@media (min-width:85.375rem){.xxl\:inline-block{display:inline-block}.xxl\:h-4{height:1rem}.xxl\:w-4{width:1rem}.xxl\:gap-20{gap:5rem}.xxl\:gap-x-32{-moz-column-gap:8rem;column-gap:8rem}.xxl\:pr-1{padding-right:.25rem}} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/static.go b/gno.land/pkg/gnoweb/static.go new file mode 100644 index 00000000000..7900dcd7891 --- /dev/null +++ b/gno.land/pkg/gnoweb/static.go @@ -0,0 +1,28 @@ +package gnoweb + +import ( + "embed" + "net/http" +) + +//go:embed public/* +var assets embed.FS + +func disableCache(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", "no-store") + next.ServeHTTP(w, r) + }) +} + +// AssetHandler returns the handler to serve static assets. If cache is true, +// these will be served using the static files embedded in the binary; otherwise +// they will served from the filesystem. +func AssetHandler() http.Handler { + return http.FileServer(http.FS(assets)) +} + +func DevAssetHandler(path, dir string) http.Handler { + handler := http.StripPrefix(path, http.FileServer(http.Dir(dir))) + return disableCache(handler) +} diff --git a/gno.land/pkg/gnoweb/static/css/app.css b/gno.land/pkg/gnoweb/static/css/app.css deleted file mode 100644 index c10fc8ec0e0..00000000000 --- a/gno.land/pkg/gnoweb/static/css/app.css +++ /dev/null @@ -1,862 +0,0 @@ -/**** ROBOTO ****/ - -@font-face { - font-family: "Roboto Mono"; - font-style: normal; - font-weight: normal; - font-display: swap; - src: local("Roboto Mono Regular"), url("/static/font/roboto/RobotoMono-Regular.woff") format("woff"); - } - - @font-face { - font-family: "Roboto Mono"; - font-style: italic; - font-weight: normal; - font-display: swap; - src: local("Roboto Mono Italic"), url("/static/font/roboto/RobotoMono-Italic.woff") format("woff"); - } - - @font-face { - font-family: "Roboto Mono Bold"; - font-style: normal; - font-weight: 700; - font-display: swap; - src: local("Roboto Mono Bold"), url("/static/font/roboto/RobotoMono-Bold.woff") format("woff"); - } - - @font-face { - font-family: "Roboto Mono"; - font-style: italic; - font-weight: 700; - font-display: swap; - src: local("Roboto Mono Bold Italic"), url("/static/font/roboto/RobotoMono-BoldItalic.woff") format("woff"); - } - - -/*** DARK/LIGHT THEME COLORS ***/ - -html:not([data-theme="dark"]), -html[data-theme="light"] { - --background-color: #eee; - --input-background-color: #eee; - --text-color: #000; - --link-color: #25172a; - --muted-color: #757575; - --border-color: #d7d9db; - --icon-color: #000; - - --quote-background: #ddd; - --quote-2-background: #aaa4; - --code-background: #d7d9db; - --header-background: #373737; - --header-forground: #ffffff; - --logo-hat: #ffffff; - --logo-beard: #808080; - - --realm-help-background-color: #d7d9db9e; - --realm-help-odd-background-color: #d7d9db45; - --realm-help-code-color: #5d5d5d; - - --highlight-color: #2f3337; - --highlight-bg: #f6f6f6; - --highlight-color: #2f3337; - --highlight-comment: #656e77; - --highlight-keyword: #015692; - --highlight-attribute: #015692; - --highlight-symbol: #803378; - --highlight-namespace: #b75501; - --highlight-keyword: #015692; - --highlight-variable: #54790d; - --highlight-keyword: #015692; - --highlight-literal: #b75501; - --highlight-punctuation: #535a60; - --highlight-variable: #54790d; - --highlight-deletion: #c02d2e; - --highlight-addition: #2f6f44; -} - -html[data-theme="dark"] { - --background-color: #1e1e1e; - --input-background-color: #393939; - --text-color: #c7c7c7; - --link-color: #c7c7c7; - --muted-color: #737373; - --border-color: #606060; - --icon-color: #dddddd; - - --quote-background: #404040; - --quote-2-background: #555555; - --code-background: #606060; - --header-background: #373737; - --header-forground: #ffffff; - --logo-hat: #ffffff; - --logo-beard: #808080; - - --realm-help-background-color: #45454545; - --realm-help-odd-background-color: #4545459e; - --realm-help-code-color: #b6b6b6; - - --highlight-color: #ffffff; - --highlight-bg: #1c1b1b; - --highlight-color: #ffffff; - --highlight-comment: #999999; - --highlight-keyword: #88aece; - --highlight-attribute: #88aece; - --highlight-symbol: #c59bc1; - --highlight-namespace: #f08d49; - --highlight-keyword: #88aece; - --highlight-variable: #b5bd68; - --highlight-keyword: #88aece; - --highlight-literal: #f08d49; - --highlight-punctuation: #cccccc; - --highlight-variable: #b5bd68; - --highlight-deletion: #de7176; - --highlight-addition: #76c490; -} - -.logo-wording path {fill: var(--header-forground, #ffffff); } -.logo-beard { fill: var(--logo-beard, #808080); } -.logo-hat {fill: var(--logo-hat, #ffffff); } - -#theme-toggle { - cursor: pointer; - display: inline-block; - padding: 0; - color: var(--header-forground, #ffffff); -} - -html[data-theme="dark"] #theme-toggle-moon, -html[data-theme="light"] #theme-toggle-sun { - display: none; -} - -/*** BASE HTML ELEMENTS ***/ - -* { - box-sizing: border-box; -} - -html { - font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; - -webkit-font-feature-settings: "kern" on, "liga" on, "calt" on, "zero" on; - text-size-adjust: 100%; - -moz-osx-font-smoothing: grayscale; - font-smoothing: antialiased; - font-variant-ligatures: contextual common-ligatures; - font-kerning: normal; - text-rendering: optimizeLegibility; - -moz-text-size-adjust: none; - -webkit-text-size-adjust: none; - text-size-adjust: none; -} - -html, -body { - padding: 0; - margin: 0; - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", - "Segoe UI Symbol", "Noto Color Emoji"; background-color: var(--background-color, #eee); - color: var(--text-color, #000); - font-size: 15px; - transition: 0.25s all ease; -} - -h1, -h2, -h3, -h4, -nav { - - font-weight: 600; - letter-spacing: 0.08rem; -} - -:is(h1, h2, h3, h4) a { - text-decoration: none; -} - -h1 { - text-align: center; - font-size: 2rem; - margin-block: 4.2rem 2rem; -} - -h2 { - font-size: 1.625rem; - margin-block: 3.4rem 1.2rem; - line-height: 1.4; -} - -h3 { - font-size: 1.467rem; - margin-block: 2.6rem 1rem; -} - -p { - font-size: 1rem; - margin-block: 1.2rem; - line-height: 1.4; -} - -p:last-child:has(a:only-child) { - margin-block-start: 0.8rem; -} -.stack > p:last-child:has(a:only-child) { - margin-block-start: 0; -} - -hr { - border: none; - height: 1px; - background: var(--border-color, #d7d9db); - width: 100%; - margin-block: 1.5rem 2rem; -} - -nav { - font-weight: 400; -} - -button { - color: var(--text-color, #000); -} - -body { - height: 100%; - width: 100%; -} - -input { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -a { - color: var(--link-color, #25172a); -} - -a[href="#"] { - color: var(--muted-color, #757575); -} - -.gno-tmpl-section ul { - padding: 0; -} - -.gno-tmpl-section li , -#header li , -.footer li { - list-style: none; -} - -.gno-tmpl-section blockquote { - margin-inline: 0; -} - -li { - margin-bottom: 0.4rem; -} - -li > * { - vertical-align: middle; -} - -input { - background-color: var(--input-background-color, #eee); - border: 1px solid var(--border-color); - color: var(--text-color, #000); - width: 25em; - padding: 0.4rem 0.5rem; - max-width: 100%;x -} - -blockquote { - background-color: var(--quote-background, #ddd); -} - -blockquote blockquote { - margin: 0; - background-color: var(--quote-2-background, #aaa4); -} - -pre, code { - font-family: "Roboto Mono", "Courier New", "sans-serif"; -} -pre { - background-color: var(--code-background, #d7d9db); - margin: 0; - padding: 0.5rem; -} - -label { - margin-block-end: 0.8rem; - display: block; -} - -label > img { - margin-inline-end: 0.8rem; -} - -code { - white-space: pre-wrap; - overflow-wrap: anywhere; -} -/*** COMPOSITION ***/ -.container { - width: 100%; - max-width: 63.75rem; - margin: auto; - padding: 1.25rem; -} - -.container p > img:only-child { - max-width: 100%; -} -.gno-tmpl-page p img:only-child { - margin-inline: auto; - display: block; - max-width: 100%; -} - -.inline-list { - padding: 1rem; - display: flex; - justify-content: space-between; -} - - - -.stack, -.stack > p { - display: flex; - flex-direction: column; -} - -.stack > p { - margin: 0; -} - -.stack > a, -.stack > p > a{ - margin-block-end: 0.4rem; -} - -.column > h1, -.column > h2, -.column > h3, -.column > h4, -.column > h5, -.column > h6 { - margin-block-start: 0; -} - -.columns-2, -.columns-3 { - display: grid; - grid-template-columns: repeat(1, 1fr); - grid-gap: 3.75rem; - margin: 3.75rem auto; -} - -.footer { - text-align: center; - margin-block-start: 2rem; - background-color: var(--header-background, #d7d9db); - border-top: 1px solid var(--border-color); -} - -.footer > .logo { - display: inline-block; - margin: 1rem; - height: 1.2rem; -} - -/** 51.2rem **/ -@media screen and (min-width: 68.75rem) { - .stack, - .stack > p { - flex-direction: row; - } - .stack *:not(:first-child) { - margin-left: 3.75rem; - } - .stack > a, - .stack > p > a{ - margin-block-end: 0; - } - .columns-2 { - grid-template-columns: repeat(2, 1fr); - } - .columns-3 { - grid-template-columns: repeat(3, 1fr); - } -} - -/*** UTILITIES ***/ - -.is-hidden { - display: none; -} - -.is-muted { - color: var(--muted-color, #757575); -} - -.is-finished { - text-decoration: line-through; -} - -.is-underline { - text-decoration: underline; -} - -/*** BLOCKS ***/ -.tabs button { - border: none; - cursor: pointer; - text-decoration: underline; - padding: 0; - background: none; - color: var(--text-color, #000); -} - -.tabs button[aria-selected="true"] { - font-weight: 700; -} - -.tabs + .jumbotron { - margin-top: 2.5rem; -} -.tabs > .columns-2, -.tabs > .columns-3 { - margin-bottom: 2.5rem; -} - -.accordion-trigger { - display: block; - border: none; - cursor: pointer; - padding: 0.4rem 0; - font-size: 1.125rem; - font-weight: 700; - text-align: left; - background: none; -} - -.accordion-trigger ~ div { - padding: 0.875rem 0 2.2rem; -} - -.accordion > p { - margin-block: 0; -} -/** 51.2rem **/ -@media screen and (min-width: 68.75rem) { - .accordion .accordion-trigger ~ div { - padding: 0.875rem 0 2.2rem 2rem; - } -} - -.gor-accordion button::first-letter { - font-size: 1.5em; - color: var(--text-color, #000); -} - -.jumbotron { - border: 1px solid var(--border-color, #d7d9db); - padding: 1.4rem; - margin: 3.75rem auto; -} - -.jumbotron h1 { - text-align: left; -} - -.jumbotron > *:first-child, -.jumbotron > * > *:first-child { - margin-block-start: 0; -} - -.jumbotron > *:last-child, -.jumbotron > * > *:last-child { - margin-block-end: 0; -} - -/** 68.75rem**/ -@media screen and (min-width: 68.75rem) { - .jumbotron { - margin: 3.75rem -3.5rem; - padding: 3.5rem; - } -} - -#root { - display: flex; - flex-direction: column; - border: 1px solid var(--header-background, #d7d9db); - margin: 20px; - overflow: hidden; - /* height: calc(100vh - 40px); */ -} - -#header { - position: relative; - background-color: var(--header-background, #d7d9db); - padding: 1.333rem; - display: flex; - align-items: center; - justify-content: space-between; -} - -#header > nav { - flex-grow: 2; -} - -#header .logo { - display: flex; - align-items: center; - color: var(--link-color, #25172a); - position: absolute; - height: 2.4rem; - z-index: 2; -} - -.logo > svg { - height: 100%; -} - -#logo_path a { - text-decoration: none; -} - -#logo_path { - padding-right: 0.8rem; -} - -#logo_path a:hover { - text-decoration: underline; -} - -#realm_links a { - font-size: 0.8rem; -} - -#header_buttons { - position: relative; - width: 100%; - height: 3rem; -} - -#header_buttons nav { - height: 100%; - display: flex; - justify-content: flex-end; - align-items: center; -} - -/* enabled conditionally with