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/.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..d1ecba7ade3 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -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..b8bd5537135 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 @@ -50,6 +51,7 @@ linters-settings: 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: 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/autocounterd.yml b/.github/workflows/autocounterd.yml index 66aced0d89c..9217fe2eef2 100644 --- a/.github/workflows/autocounterd.yml +++ b/.github/workflows/autocounterd.yml @@ -1,9 +1,13 @@ name: autocounterd on: + pull_request: + branches: + - master push: paths: - misc/autocounterd + - misc/loop - .github/workflows/autocounterd.yml branches: - "master" @@ -41,7 +45,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..622baefc0de --- /dev/null +++ b/.github/workflows/benchmark-master-push.yml @@ -0,0 +1,70 @@ +name: run benchmarks when pushing on main branch + +on: + push: + branches: + - master + paths: + - contribs/** + - gno.land/** + - gnovm/** + - tm2/** + +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.yml b/.github/workflows/bot.yml new file mode 100644 index 00000000000..300a5928e25 --- /dev/null +++ b/.github/workflows/bot.yml @@ -0,0 +1,93 @@ +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 + # Prevent bot from retriggering itself and ignore event emitted by codecov + if: ${{ github.actor != vars.GH_BOT_LOGIN && github.actor != 'codecov[bot]' }} + 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 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/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/docs.yml b/.github/workflows/docs.yml index 262b341276c..c9d9af0fb6f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version-file: go.mod - name: Install dependencies run: go mod download diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 5b3c3c1fbf1..41d579c4567 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -3,7 +3,7 @@ name: examples on: pull_request: push: - branches: [ "master" ] + branches: ["master"] concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -47,7 +47,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 @@ -70,7 +70,7 @@ jobs: strategy: fail-fast: false matrix: - goversion: [ "1.22.x" ] + goversion: ["1.22.x"] runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -86,7 +86,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: [ "1.22.x" ] + go-version: ["1.22.x"] # unittests: TODO: matrix with contracts runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 11f04ca8282..41d9a2cba94 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -2,12 +2,6 @@ name: Dependency License Scanning on: workflow_dispatch: - pull_request: - paths: - - ".github/.fossa.yml" - - ".github/workflows/fossa.yml" - schedule: - - cron: '0 0 * * 6' # At 00:00 on saturdays permissions: contents: read @@ -31,7 +25,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 +41,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..1288d588100 --- /dev/null +++ b/.github/workflows/genesis-verify.yml @@ -0,0 +1,57 @@ +name: genesis-verify + +on: + pull_request: + branches: + - master + 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..1b955b52cd0 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod - run: "cd misc/gendocs && make install gen" - uses: actions/configure-pages@v5 id: pages diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 9451d6da3a1..59050f1baa4 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -13,5 +13,28 @@ jobs: 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/lint_template.yml b/.github/workflows/lint_template.yml index 65679633240..b7568d19c41 100644 --- a/.github/workflows/lint_template.yml +++ b/.github/workflows/lint_template.yml @@ -13,16 +13,16 @@ 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..5b3437b54a1 100644 --- a/.github/workflows/main_template.yml +++ b/.github/workflows/main_template.yml @@ -4,6 +4,9 @@ on: modulepath: required: true type: string + tests-extra-args: + required: false + type: string secrets: codecov-token: required: true @@ -32,6 +35,7 @@ jobs: modulepath: ${{ inputs.modulepath }} tests-timeout: "30m" go-version: "1.22.x" + tests-extra-args: ${{ inputs.tests-extra-args }} secrets: codecov-token: ${{ secrets.codecov-token }} - \ No newline at end of file + diff --git a/.github/workflows/misc.yml b/.github/workflows/misc.yml index b824235ca2b..ad2c886e2ac 100644 --- a/.github/workflows/misc.yml +++ b/.github/workflows/misc.yml @@ -12,18 +12,15 @@ on: 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 + 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 Main uses: ./.github/workflows/main_template.yml with: diff --git a/.github/workflows/mod-tidy.yml b/.github/workflows/mod-tidy.yml new file mode 100644 index 00000000000..24eab553d19 --- /dev/null +++ b/.github/workflows/mod-tidy.yml @@ -0,0 +1,26 @@ +name: Ensure go.mods are tidied + +on: + push: + branches: + - master + workflow_dispatch: + pull_request: + +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 e8f3fe4ca5c..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.6.0 - - uses: anchore/sbom-action/download-syft@v0.17.2 - - - 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..b898a149e9d 100644 --- a/.github/workflows/portal-loop.yml +++ b/.github/workflows/portal-loop.yml @@ -45,7 +45,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,13 +57,12 @@ 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: | diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml index 7eda0536532..3d194e2cb4c 100644 --- a/.github/workflows/releaser-master.yml +++ b/.github/workflows/releaser-master.yml @@ -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.6.0 - - uses: anchore/sbom-action/download-syft@v0.17.2 + - uses: sigstore/cosign-installer@v3.7.0 + - uses: anchore/sbom-action/download-syft@v0.17.8 - 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..4308f1c4a7d --- /dev/null +++ b/.github/workflows/releaser-nightly.yml @@ -0,0 +1,46 @@ +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-file: go.mod + cache: true + + - uses: sigstore/cosign-installer@v3.7.0 + - uses: anchore/sbom-action/download-syft@v0.17.8 + + - 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 index 5433582cace..309664bdcce 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -20,11 +20,11 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.22.x" + go-version-file: go.mod cache: true - - uses: sigstore/cosign-installer@v3.6.0 - - uses: anchore/sbom-action/download-syft@v0.17.2 + - uses: sigstore/cosign-installer@v3.7.0 + - uses: anchore/sbom-action/download-syft@v0.17.8 - uses: docker/login-action@v3 with: @@ -34,7 +34,7 @@ jobs: - name: Set up QEMU uses: docker/setup-qemu-action@v3 - + - uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser-pro diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml new file mode 100644 index 00000000000..55a17ac60a8 --- /dev/null +++ b/.github/workflows/stale-bot.yml @@ -0,0 +1,23 @@ +name: "Close stale PRs" +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..c7956b4caf4 100644 --- a/.github/workflows/test_template.yml +++ b/.github/workflows/test_template.yml @@ -5,11 +5,14 @@ on: required: true type: string tests-timeout: - required: true - type: string + required: true + type: string go-version: - required: true - type: string + required: true + type: string + tests-extra-args: + required: false + type: string secrets: codecov-token: required: true @@ -18,12 +21,12 @@ jobs: 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: Checkout code - uses: actions/checkout@v4 + go-version: ${{ inputs.go-version }} - name: Go test working-directory: ${{ inputs.modulepath }} env: @@ -38,11 +41,14 @@ jobs: # 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=atomic -timeout ${{ inputs.tests-timeout }} -v ./... -test.gocoverdir=$GOCOVERDIR + go test -covermode=set -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR # Print results (set +x; echo 'go coverage results:') @@ -54,11 +60,11 @@ jobs: go tool covdata textfmt -v 1 $filter -i=$GOCOVERDIR,$TXTARCOVERDIR -o gocoverage.out - name: Upload go coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: disable_search: true fail_ci_if_error: true - file: ${{ inputs.modulepath }}/gocoverage.out + files: ${{ 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 @@ -70,7 +76,7 @@ jobs: # - name: Install Go # uses: actions/setup-go@v5 # with: - # go-version: ${{ inputs.go-version }} + # go-version: ${{ inputs.go-version }} # - name: Checkout code # uses: actions/checkout@v4 # - name: Go race test 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..fd29f5e5f57 --- /dev/null +++ b/contribs/github-bot/internal/config/config.go @@ -0,0 +1,96 @@ +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 = "**SKIP**: Do not block the CI for this PR" + +// 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: "The pull request head branch must be up-to-date with its base ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/keeping-your-pull-request-in-sync-with-the-base-branch))", + If: c.Always(), + Then: r.UpToDateWith(gh, r.PR_BASE), + }, + { + 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.Or( + r.And( + r.AuthorInTeam(gh, "devrels"), + r.ReviewByTeamMembers(gh, "tech-staff", 1), + ), + r.And( + r.AuthorInTeam(gh, "tech-staff"), + r.ReviewByTeamMembers(gh, "devrels", 1), + ), + ), + }, + } + + 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.Not(c.AuthorInTeam(gh, "core-contributors")), + 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..092a441542a 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 } @@ -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 f4859889a16..b5b5a402c2a 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 ( @@ -27,7 +29,7 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect - github.com/alecthomas/chroma/v2 v2.8.0 // 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 @@ -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,10 +59,6 @@ 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.22.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -82,7 +79,7 @@ require ( github.com/rs/xid v1.6.0 // 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/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect @@ -107,6 +104,6 @@ require ( 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 af57f320257..bab6e5364e8 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -1,12 +1,12 @@ 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.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= +github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +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= @@ -91,8 +91,8 @@ 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.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= @@ -101,8 +101,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= @@ -130,16 +128,8 @@ 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= @@ -235,8 +225,8 @@ 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.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/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= @@ -326,8 +316,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 c3e70366fb2..fa9e2d11e29 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,7 +484,7 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) } // Setup node config - nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, genesis) + 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 @@ -547,10 +563,10 @@ func (n *Node) genesisTxResultHandler(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 @@ -564,10 +580,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..4a4acc232b9 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() @@ -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..eab9fc90c50 100644 --- a/contribs/gnofaucet/go.mod +++ b/contribs/gnofaucet/go.mod @@ -6,51 +6,52 @@ 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 + 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 + 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..aabe858e893 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,37 @@ 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/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= @@ -148,22 +151,22 @@ go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ 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 +176,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 +202,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..f1b316c2bee --- /dev/null +++ b/contribs/gnogenesis/go.mod @@ -0,0 +1,60 @@ +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/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/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..7ba3aede534 --- /dev/null +++ b/contribs/gnogenesis/go.sum @@ -0,0 +1,226 @@ +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/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..cf863c72116 --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_add_packages.go @@ -0,0 +1,119 @@ +package txs + +import ( + "context" + "errors" + "flag" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/crypto" + + "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" +) + +var ( + errInvalidPackageDir = errors.New("invalid package directory") + errInvalidDeployerAddr = errors.New("invalid deployer address") +) + +// Keep in sync with gno.land/cmd/start.go +var ( + defaultCreator = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 + genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) +) + +type addPkgCfg struct { + txsCfg *txsCfg + deployerAddress string +} + +func (c *addPkgCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.deployerAddress, + "deployer-address", + defaultCreator.String(), + "the address that will be used to deploy the package", + ) +} + +// 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 { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.txsCfg.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 + } + + var ( + creator = defaultCreator + err error + ) + + // Check if the deployer address is set + if cfg.deployerAddress != defaultCreator.String() { + creator, err = crypto.AddressFromString(cfg.deployerAddress) + if err != nil { + return fmt.Errorf("%w, %w", errInvalidDeployerAddr, err) + } + } + + 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) + } + + 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 +} 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..c3405d6ff8d --- /dev/null +++ b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go @@ -0,0 +1,154 @@ +package txs + +import ( + "context" + "fmt" + "os" + "path/filepath" + "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/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 := 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("invalid deployer 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 := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + t.TempDir(), // package dir + "--deployer-address", + "beep-boop", // invalid address + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidDeployerAddr) + }) + + 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() + ) + + 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 := 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) + + assert.Equal(t, packagePath, msgAddPkg.Package.Path) + }) +} 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..4f5862a0d2e --- /dev/null +++ b/contribs/gnohealth/go.mod @@ -0,0 +1,45 @@ +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/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/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..dd287d9ca84 --- /dev/null +++ b/contribs/gnohealth/go.sum @@ -0,0 +1,197 @@ +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/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/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 a8e235a5c5a..479daed22f6 100644 --- a/contribs/gnokeykc/go.mod +++ b/contribs/gnokeykc/go.mod @@ -21,7 +21,6 @@ require ( 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 @@ -59,6 +58,6 @@ require ( 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 b3bfadb3468..cacf6788d45 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -54,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= @@ -216,8 +214,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..423e4414a79 100644 --- a/contribs/gnomd/go.mod +++ b/contribs/gnomd/go.mod @@ -22,6 +22,6 @@ require ( 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/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..0ff70dd99fb 100644 --- a/contribs/gnomd/go.sum +++ b/contribs/gnomd/go.sum @@ -57,13 +57,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk 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/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..cd31adc4f6f --- /dev/null +++ b/contribs/gnomigrate/go.mod @@ -0,0 +1,55 @@ +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/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/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..7ba3aede534 --- /dev/null +++ b/contribs/gnomigrate/go.sum @@ -0,0 +1,226 @@ +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/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..4c65ca6ef0b --- /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 = outputFile.WriteString(fmt.Sprintf("%s\n", string(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/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/installation.md b/docs/getting-started/local-setup/installation.md index 272d0069ee5..e05c2f9b205 100644 --- a/docs/getting-started/local-setup/installation.md +++ b/docs/getting-started/local-setup/installation.md @@ -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). 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/gnokey/working-with-key-pairs.md b/docs/gno-tooling/cli/gnokey/working-with-key-pairs.md index ba03ca569b4..9bc29da6a18 100644 --- a/docs/gno-tooling/cli/gnokey/working-with-key-pairs.md +++ b/docs/gno-tooling/cli/gnokey/working-with-key-pairs.md @@ -38,6 +38,7 @@ 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 @@ -161,6 +162,18 @@ you can recover it using the key's mnemonic, or by importing it if it was export 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. 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 971007e5cef..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. @@ -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/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/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/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..cccdc0df645 --- /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.Tree + 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.Tree, 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.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + items = append(items, Item{Key: key, Value: value}) + return false + }) + } else { + p.Tree.ReverseIterateByOffset(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/z_0_filetest.gno b/examples/gno.land/p/demo/avl/z_0_filetest.gno index aff79ffabc6..2dce5e7f1ac 100644 --- a/examples/gno.land/p/demo/avl/z_0_filetest.gno +++ b/examples/gno.land/p/demo/avl/z_0_filetest.gno @@ -267,7 +267,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "z_0.gno", // "IsMethod": false, // "Name": "init.1", // "NativeName": "", @@ -278,7 +278,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "z_0.gno", // "Line": "10", // "PkgPath": "gno.land/r/test" // } @@ -303,7 +303,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "z_0.gno", // "IsMethod": false, // "Name": "main", // "NativeName": "", @@ -314,7 +314,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "z_0.gno", // "Line": "15", // "PkgPath": "gno.land/r/test" // } 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..97ca5ed2135 100644 --- a/examples/gno.land/p/demo/avl/z_1_filetest.gno +++ b/examples/gno.land/p/demo/avl/z_1_filetest.gno @@ -340,7 +340,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "z_1.gno", // "IsMethod": false, // "Name": "init.1", // "NativeName": "", @@ -351,7 +351,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "z_1.gno", // "Line": "10", // "PkgPath": "gno.land/r/test" // } @@ -376,7 +376,7 @@ func main() { // "Escaped": true, // "ObjectID": "a8ada09dee16d791fd406d629fe29bb0ed084a30:3" // }, -// "FileName": "main.gno", +// "FileName": "z_1.gno", // "IsMethod": false, // "Name": "main", // "NativeName": "", @@ -387,7 +387,7 @@ func main() { // "BlockNode": null, // "Location": { // "Column": "1", -// "File": "main.gno", +// "File": "z_1.gno", // "Line": "15", // "PkgPath": "gno.land/r/test" // } diff --git a/examples/gno.land/p/demo/avlhelpers/gno.mod b/examples/gno.land/p/demo/avlhelpers/gno.mod index 559f60975cf..5adffd13a43 100644 --- a/examples/gno.land/p/demo/avlhelpers/gno.mod +++ b/examples/gno.land/p/demo/avlhelpers/gno.mod @@ -1,3 +1 @@ module gno.land/p/demo/avlhelpers - -require gno.land/p/demo/avl v0.0.0-latest 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 c9e125261b5..4634bae933b 100644 --- a/examples/gno.land/p/demo/grc/grc20/token.gno +++ b/examples/gno.land/p/demo/grc/grc20/token.gno @@ -1,45 +1,240 @@ package grc20 import ( + "math/overflow" "std" + "strconv" + + "gno.land/p/demo/ufmt" ) -// token implements the Token interface. -// -// It is generated with Banker.Token(). -// It can safely be exposed 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 } + +// 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 } -func (t *token) BalanceOf(owner std.Address) uint64 { - return t.banker.BalanceOf(owner) +// 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) +// 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 } -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 { +// 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) +} + +// 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 8926fe1d6de..572dd15e7e6 100644 --- a/examples/gno.land/p/demo/int256/arithmetic.gno +++ b/examples/gno.land/p/demo/int256/arithmetic.gno @@ -1,202 +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() - - if x.neg == y.neg { - // If both numbers have the same sign, add their absolute values - z.abs.Add(x.abs, y.abs) - z.neg = x.neg - } else { - switch x.abs.Cmp(y.abs) { - case 1: // x > y - z.abs.Sub(x.abs, y.abs) - z.neg = x.neg - case -1: // x < y - z.abs.Sub(y.abs, x.abs) - z.neg = y.neg - case 0: // x == y - z.abs = uint256.NewUint(0) - } - } +const divisionByZeroError = "division by zero" +// 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() - - if x.neg != y.neg { - // If sign are different, add the absolute values - z.abs.Add(x.abs, y.abs) - z.neg = x.neg - } else { - switch x.abs.Cmp(y.abs) { - case 1: // x > y - z.abs.Sub(x.abs, y.abs) - z.neg = x.neg - case -1: // x < y - z.abs.Sub(y.abs, x.abs) - z.neg = !x.neg - case 0: // x == y - z.abs = uint256.NewUint(0) - } - } - - // Ensure zero is always positive - if z.abs.IsZero() { - z.neg = false - } + 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 && !z.abs.IsZero() // 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() - - if y.abs.IsZero() { - panic("division by zero") + // Step 1: Check for division by zero + if y.IsZero() { + panic(divisionByZeroError) } - z.abs.Div(x.abs, y.abs) - z.neg = (x.neg != y.neg) && !z.abs.IsZero() // 0 has no sign + // Step 2, 3: Calculate the absolute values of x and y + xAbs, xSign := x.Abs(), x.Sign() + yAbs, ySign := y.Abs(), y.Sign() - return z -} + // Step 4: Perform unsigned division on the absolute values + z.value.Div(xAbs, yAbs) -// 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 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() - z.abs.Mod(x.abs, y.abs) - z.neg = z.abs.Sign() > 0 && x.neg // 0 has no sign + // 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.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 4cfa306890a..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 @@ -23,7 +53,10 @@ func TestAdd(t *testing.T) { {"-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 { @@ -49,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()) } } } @@ -64,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 { @@ -93,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()) } } } @@ -109,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 { @@ -140,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()) } } } @@ -190,9 +223,11 @@ func TestSub(t *testing.T) { {"-1", "1", "-2"}, {"1", "-1", "2"}, {"-1", "-1", "0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "-115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - {x: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", y: "1", want: "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 { @@ -218,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()) } } } @@ -234,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 { @@ -262,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()) } } } @@ -276,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 { @@ -301,51 +342,7 @@ 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()) - } - } -} - -func TestMulUint256(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "1", "0"}, - {"1", "0", "0"}, - {"1", "1", "1"}, - {"1", "2", "2"}, - {"-1", "1", "-1"}, - {"-1", "3", "-3"}, - {"3", "4", "12"}, - {"-3", "4", "-12"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "-115792089237316195423570985008687907853269984665640564039457584007913129639932"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "115792089237316195423570985008687907853269984665640564039457584007913129639932"}, - } - - 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()) + t.Errorf("Mul(%s, %s) = %v, want %v", tc.x, tc.y, got.String(), want.String()) } } } @@ -364,7 +361,10 @@ func TestDiv(t *testing.T) { {"-10", "3", "-3"}, {"7", "3", "2"}, {"-7", "3", "-2"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "2", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, // Max uint256 / 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 _, tt := range tests { @@ -372,11 +372,8 @@ func TestDiv(t *testing.T) { x := MustFromDecimal(tt.x) y := MustFromDecimal(tt.y) result := Zero().Div(x, y) - if result.ToString() != tt.expected { - t.Errorf("Div(%s, %s) = %s, want %s", tt.x, tt.y, result.ToString(), tt.expected) - } - if result.abs.IsZero() && result.neg { - t.Errorf("Div(%s, %s) resulted in negative zero", tt.x, tt.y) + if result.String() != tt.expected { + t.Errorf("Div(%s, %s) = %s, want %s", tt.x, tt.y, result.String(), tt.expected) } }) } @@ -393,21 +390,19 @@ func TestDiv(t *testing.T) { }) } -func TestDivUint256(t *testing.T) { +func TestQuo(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", "10"}, + {"10", "-1", "-10"}, + {"-10", "1", "-10"}, + {"-10", "-1", "10"}, + {"10", "-3", "-3"}, + {"-10", "3", "-3"}, + {"10", "3", "3"}, } for _, tc := range tests { @@ -417,7 +412,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 +425,28 @@ func TestDivUint256(t *testing.T) { } got := New() - got.DivUint256(x, y) + got.Quo(x, y) if got.Neq(want) { - t.Errorf("DivUint256(%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 TestQuo(t *testing.T) { +func TestRem(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", "-1"}, + {"-10", "-3", "-1"}, } for _, tc := range tests { @@ -472,15 +469,15 @@ func TestQuo(t *testing.T) { } got := New() - got.Quo(x, y) + got.Rem(x, y) if got.Neq(want) { - t.Errorf("Quo(%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 TestRem(t *testing.T) { +func TestMod(t *testing.T) { tests := []struct { x, y, want string }{ @@ -492,8 +489,8 @@ func TestRem(t *testing.T) { {"-10", "-1", "0"}, {"10", "3", "1"}, {"10", "-3", "1"}, - {"-10", "3", "-1"}, - {"-10", "-3", "-1"}, + {"-10", "3", "2"}, + {"-10", "-3", "2"}, } for _, tc := range tests { @@ -516,33 +513,51 @@ func TestRem(t *testing.T) { } got := New() - got.Rem(x, y) + got.Mod(x, y) if got.Neq(want) { - t.Errorf("Rem(%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 TestMod(t *testing.T) { +func TestModeOverflow(t *testing.T) { tests := []struct { x, y, 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"}, + {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) @@ -555,17 +570,105 @@ func TestMod(t *testing.T) { continue } - want, err := FromDecimal(tc.want) + 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) + } + } +} + +func TestDivEByZero(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("DivE did not panic on division by zero") + } + }() + + 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 TestAbs(t *testing.T) { + tests := []struct { + x, want string + }{ + {"0", "0"}, + {"1", "1"}, + {"-1", "1"}, + {"-2", "2"}, + {"-100000000000", "100000000000"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) if err != nil { t.Error(err) continue } - got := New() - got.Mod(x, y) + got := x.Abs() - 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 9e264e7e46b..c8829ea754b 100644 --- a/examples/gno.land/p/demo/int256/conversion.gno +++ b/examples/gno.land/p/demo/int256/conversion.gno @@ -1,87 +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" } - - t := z.abs.Dec() - if z.neg { - return "-" + t + 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 +} - 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 z } diff --git a/examples/gno.land/p/demo/int256/conversion_test.gno b/examples/gno.land/p/demo/int256/conversion_test.gno index b085a77a15a..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 + v int64 + expect int }{ - {0, "0"}, - {1, "1"}, - {-1, "-1"}, - {9223372036854775807, "9223372036854775807"}, - {-9223372036854775808, "-9223372036854775808"}, + {0, 0}, + {1, 1}, + {-1, -1}, + {9223372036854775807, 1}, // overflow (max int64) + {-9223372036854775808, -1}, // underflow (min int64) } - 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 - }{ - {0, "0"}, - {1, "1"}, - } - - 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,77 +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 TestToString(t *testing.T) { +func TestString(t *testing.T) { tests := []struct { - name string - setup func() *Int + input string expected string }{ - { - name: "Zero from subtraction", - setup: func() *Int { - minusThree := MustFromDecimal("-3") - three := MustFromDecimal("3") - return Zero().Add(minusThree, three) - }, - expected: "0", - }, - { - name: "Zero from right shift", - setup: func() *Int { - return Zero().Rsh(One(), 1234) - }, - expected: "0", - }, - { - name: "Positive number", - setup: func() *Int { - return MustFromDecimal("42") - }, - expected: "42", - }, - { - name: "Negative number", - setup: func() *Int { - return MustFromDecimal("-42") - }, - expected: "-42", - }, - { - name: "Large positive number", - setup: func() *Int { - return MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935") - }, - expected: "115792089237316195423570985008687907853269984665640564039457584007913129639935", - }, - { - name: "Large negative number", - setup: func() *Int { - return MustFromDecimal("-115792089237316195423570985008687907853269984665640564039457584007913129639935") - }, - expected: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", - }, + {"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 { - t.Run(tt.name, func(t *testing.T) { - z := tt.setup() - result := z.ToString() - if result != tt.expected { - t.Errorf("ToString() = %s, want %s", result, tt.expected) - } - }) + 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/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/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/users/types.gno b/examples/gno.land/p/demo/users/types.gno index d28b6a8ee42..33b5388a206 100644 --- a/examples/gno.land/p/demo/users/types.gno +++ b/examples/gno.land/p/demo/users/types.gno @@ -1,5 +1,9 @@ package users +import ( + "std" +) + type AddressOrName string func (aon AddressOrName) IsName() bool { @@ -12,3 +16,14 @@ func (aon AddressOrName) GetName() (string, bool) { } return "", false } + +func (aon AddressOrName) IsValid() bool { + if len(aon) == 0 { + return false + } + if aon[0] == '@' { + // TODO: validate name better + return len(aon) >= 2 + } + return std.Address(aon).IsValid() +} diff --git a/examples/gno.land/p/demo/users/users_test.gno b/examples/gno.land/p/demo/users/users_test.gno index 82abcb9fccb..4faa5d2ec51 100644 --- a/examples/gno.land/p/demo/users/users_test.gno +++ b/examples/gno.land/p/demo/users/users_test.gno @@ -1 +1,58 @@ package users + +import ( + "testing" +) + +func TestAddressOrNameValidation(t *testing.T) { + tests := []struct { + name string + address string + expected bool + }{ + { + name: "Valid address", + address: "g18syxa0vh0vmne90mwhtynjet0zgeqf6prh3ryy", + expected: true, + }, + { + name: "Invalid address", + address: "0x123456789012345678901234567890123456789", + expected: false, + }, + { + name: "Valid name", + address: "@test", + expected: true, + }, + { + name: "Invalid name", + address: "test@", + expected: false, + }, + { + name: "Empty", + address: "", + expected: false, + }, + { + name: "Only @", + address: "@", + expected: false, + }, + { + name: "One letter", + address: "@a", + expected: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := AddressOrName(test.address).IsValid() + if result != test.expected { + t.Errorf("Expected %v, got %v", test.expected, result) + } + }) + } +} 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/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/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/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/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/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..7ef0619188f --- /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] != u2.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] != u1.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] != u2.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] != u1.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/teritori/dao_core/dao_core.gno b/examples/gno.land/p/teritori/dao_core/dao_core.gno new file mode 100644 index 00000000000..89f064149af --- /dev/null +++ b/examples/gno.land/p/teritori/dao_core/dao_core.gno @@ -0,0 +1,206 @@ +package core + +import ( + "std" + "strconv" + "strings" + + dao_interfaces "gno.land/p/teritori/dao_interfaces" +) + +// TODO: add wrapper message handler to handle multiple proposal modules messages + +type daoCore struct { + dao_interfaces.IDAOCore + + votingModule dao_interfaces.IVotingModule + proposalModules []dao_interfaces.ActivableProposalModule + activeProposalModuleCount int + realm std.Realm + registry *dao_interfaces.MessagesRegistry +} + +func NewDAOCore( + votingModuleFactory dao_interfaces.VotingModuleFactory, + proposalModulesFactories []dao_interfaces.ProposalModuleFactory, + messageHandlersFactories []dao_interfaces.MessageHandlerFactory, +) dao_interfaces.IDAOCore { + if votingModuleFactory == nil { + panic("Missing voting module factory") + } + + if len(proposalModulesFactories) == 0 { + panic("No proposal modules factories") + } + + core := &daoCore{ + realm: std.CurrentRealm(), + activeProposalModuleCount: len(proposalModulesFactories), + registry: dao_interfaces.NewMessagesRegistry(), + proposalModules: make([]dao_interfaces.ActivableProposalModule, len(proposalModulesFactories)), + } + + core.votingModule = votingModuleFactory(core) + if core.votingModule == nil { + panic("voting module factory returned nil") + } + + for i, modFactory := range proposalModulesFactories { + mod := modFactory(core) + if mod == nil { + panic("proposal module factory returned nil") + } + + core.proposalModules[i] = dao_interfaces.ActivableProposalModule{ + Enabled: true, + Module: mod, + } + } + + // this registry is specific to gno since we can't do dynamic calls + core.registry.Register(NewUpdateVotingModuleMessageHandler(core)) + core.registry.Register(NewUpdateProposalModulesMessageHandler(core)) + for _, handlerFactory := range messageHandlersFactories { + handler := handlerFactory(core) + if handler == nil { + panic("message handler factory returned nil") + } + + core.registry.Register(handler) + } + + return core +} + +// mutations + +func (d *daoCore) UpdateVotingModule(newVotingModule dao_interfaces.IVotingModule) { + if std.CurrentRealm().Addr() != d.realm.Addr() { // not sure this check necessary since the ownership system should protect against mutation from other realms + panic(ErrUnauthorized) + } + + d.votingModule = newVotingModule +} + +func (d *daoCore) UpdateProposalModules(toAdd []dao_interfaces.IProposalModule, toDisable []int) { + if std.CurrentRealm().Addr() != d.realm.Addr() { // not sure this check necessary since the ownership system should protect against mutation from other realms + panic(ErrUnauthorized) + } + + for _, module := range toAdd { + d.addProposalModule(module) + } + + for _, moduleIndex := range toDisable { + module := GetProposalModule(d, moduleIndex) + + if !module.Enabled { + panic(ErrModuleAlreadyDisabled) + } + + module.Enabled = false + + d.activeProposalModuleCount-- + if d.activeProposalModuleCount == 0 { + panic("no active proposal modules") // this -> `panic(ErrNoActiveProposalModules)` triggers `panic: reflect: reflect.Value.SetString using value obtained using unexported field` + } + } +} + +// queries + +func (d *daoCore) ProposalModules() []dao_interfaces.ActivableProposalModule { + return d.proposalModules +} + +func (d *daoCore) VotingModule() dao_interfaces.IVotingModule { + return d.votingModule +} + +func (d *daoCore) VotingPowerAtHeight(address std.Address, height int64) uint64 { + return d.VotingModule().VotingPowerAtHeight(address, height, []string{}) +} + +func (d *daoCore) ActiveProposalModuleCount() int { + return d.activeProposalModuleCount +} + +func (d *daoCore) Render(path string) string { + sb := strings.Builder{} + sb.WriteString("# DAO Core\n") + votingInfo := d.votingModule.Info() + sb.WriteString("## Voting Module: ") + sb.WriteString(votingInfo.String()) + sb.WriteRune('\n') + sb.WriteString(d.votingModule.Render("")) + sb.WriteString("## Supported Messages:\n") + sb.WriteString(d.registry.Render()) + + sb.WriteString("## Proposal Modules:\n") + for i, propMod := range d.proposalModules { + if !propMod.Enabled { + continue + } + + info := propMod.Module.Info() + sb.WriteString("### #") + sb.WriteString(strconv.Itoa(i)) + sb.WriteString(": ") + sb.WriteString(info.String()) + sb.WriteRune('\n') + sb.WriteString(propMod.Module.Render("")) + } + + sb.WriteRune('\n') + return sb.String() +} + +func (d *daoCore) Registry() *dao_interfaces.MessagesRegistry { + return d.registry +} + +// TODO: move this helper in dao interfaces + +func GetProposalModule(core dao_interfaces.IDAOCore, moduleIndex int) *dao_interfaces.ActivableProposalModule { + if moduleIndex < 0 { + panic("module index must be >= 0") + } + + mods := core.ProposalModules() + if moduleIndex >= len(mods) { + panic("invalid module index") + } + + return &mods[moduleIndex] +} + +// internal + +func (d *daoCore) executeMsgs(msgs []dao_interfaces.ExecutableMessage) { + for _, msg := range msgs { + d.registry.Execute(msg) + } +} + +func (d *daoCore) addProposalModule(proposalMod dao_interfaces.IProposalModule) { + for _, mod := range d.proposalModules { + if mod.Module != proposalMod { + continue + } + + if mod.Enabled { + panic(ErrModuleAlreadyAdded) + } + + mod.Enabled = true + d.activeProposalModuleCount++ + return + } + + d.proposalModules = append(d.proposalModules, dao_interfaces.ActivableProposalModule{ + Enabled: true, + Module: proposalMod, + }) + + d.activeProposalModuleCount++ +} diff --git a/examples/gno.land/p/teritori/dao_core/dao_core_test.gno b/examples/gno.land/p/teritori/dao_core/dao_core_test.gno new file mode 100644 index 00000000000..c0729650c3c --- /dev/null +++ b/examples/gno.land/p/teritori/dao_core/dao_core_test.gno @@ -0,0 +1,191 @@ +package core + +import ( + "std" + "testing" + + dao_interfaces "gno.land/p/teritori/dao_interfaces" +) + +type votingModule struct { + core dao_interfaces.IDAOCore +} + +func votingModuleFactory(core dao_interfaces.IDAOCore) dao_interfaces.IVotingModule { + return &votingModule{core: core} +} + +func (vm *votingModule) Core() dao_interfaces.IDAOCore { + return vm.core +} + +func (vm *votingModule) Info() dao_interfaces.ModuleInfo { + return dao_interfaces.ModuleInfo{ + Kind: "TestVoting", + Version: "21.42", + } +} + +func (vm *votingModule) ConfigJSON() string { + return "{}" +} + +func (vm *votingModule) GetMembersJSON(start, end string, limit uint64, height int64) string { + return "[]" +} + +func (vm *votingModule) Render(path string) string { + return "# Test Voting Module" +} + +func (vm *votingModule) VotingPowerAtHeight(address std.Address, height int64, resources []string) uint64 { + return 0 +} + +func (vm *votingModule) TotalPowerAtHeight(height int64, resources []string) uint64 { + return 0 +} + +type proposalModule struct { + core dao_interfaces.IDAOCore +} + +func proposalModuleFactory(core dao_interfaces.IDAOCore) dao_interfaces.IProposalModule { + return &proposalModule{core: core} +} + +func (pm *proposalModule) Core() dao_interfaces.IDAOCore { + return pm.core +} + +func (pm *proposalModule) Info() dao_interfaces.ModuleInfo { + return dao_interfaces.ModuleInfo{ + Kind: "TestProposal", + Version: "42.21", + } +} + +func (pm *proposalModule) ConfigJSON() string { + return "{}" +} + +func (pm *proposalModule) VoteJSON(proposalID int, voteJSON string) { + panic("not implemented") +} + +func (pm *proposalModule) Render(path string) string { + return "# Test Proposal Module" +} + +func (pm *proposalModule) Execute(proposalId int) { + panic("not implemented") +} + +func (pm *proposalModule) ProposeJSON(proposalJSON string) int { + panic("not implemented") +} + +func (pm *proposalModule) ProposalsJSON(limit int, startAfter string, reverse bool) string { + panic("not implemented") +} + +func (pm *proposalModule) ProposalJSON(proposalID int) string { + panic("not implemented") +} + +func TestDAOCore(t *testing.T) { + var testValue string + handler := dao_interfaces.NewCopyMessageHandler(&testValue) + handlerFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return handler + } + + core := NewDAOCore(votingModuleFactory, []dao_interfaces.ProposalModuleFactory{proposalModuleFactory}, []dao_interfaces.MessageHandlerFactory{handlerFactory}) + if core == nil { + t.Fatal("core is nil") + } + + if core.ActiveProposalModuleCount() != 1 { + t.Fatal("expected 1 active proposal module") + } + + votingMod := core.VotingModule() + if votingMod == nil { + t.Fatal("voting module is nil") + } + + if votingMod.Info().Kind != "TestVoting" { + t.Fatal("voting module has wrong kind") + } + + propMods := core.ProposalModules() + if len(propMods) != 1 { + t.Fatal("expected 1 proposal module") + } + + propMod := propMods[0] + if !propMod.Enabled { + t.Fatal("proposal module is not enabled") + } + + if propMod.Module == nil { + t.Fatal("proposal module is nil") + } + + if propMod.Module.Info().Kind != "TestProposal" { + t.Fatal("proposal module has wrong kind") + } + + registry := core.Registry() + if registry == nil { + t.Fatal("registry is nil") + } + + msg := &dao_interfaces.CopyMessage{Value: "test"} + registry.Execute(msg) + if testValue != "test" { + t.Errorf("expected testValue to be 'test', got '%s'", testValue) + } + + newProposalModule := &proposalModule{core: core} + updatePropModsMsg := &UpdateProposalModulesExecutableMessage{ + ToAdd: []dao_interfaces.IProposalModule{newProposalModule}, + ToDisable: []int{0}, + } + registry.Execute(updatePropModsMsg) + + if core.ActiveProposalModuleCount() != 1 { + t.Fatal("expected 1 active proposal module") + } + + propMods = core.ProposalModules() + if len(propMods) != 2 { + t.Fatal("expected 2 proposal modules") + } + + propMod = propMods[0] + if propMod.Enabled { + t.Errorf("old proposal module is still enabled") + } + + propMod = propMods[1] + if !propMod.Enabled { + t.Errorf("new proposal module is not enabled") + } + + if propMod.Module != newProposalModule { + t.Errorf("new proposal module is not the same as the one added") + } + + newVotingModule := &votingModule{core: core} + updateVotingModMsg := &UpdateVotingModuleExecutableMessage{ + Module: newVotingModule, + } + + registry.Execute(updateVotingModMsg) + + votingMod = core.VotingModule() + if votingMod != newVotingModule { + t.Errorf("new voting module is not the same as the one added") + } +} diff --git a/examples/gno.land/p/teritori/dao_core/errors.gno b/examples/gno.land/p/teritori/dao_core/errors.gno new file mode 100644 index 00000000000..a7299585a0a --- /dev/null +++ b/examples/gno.land/p/teritori/dao_core/errors.gno @@ -0,0 +1,15 @@ +package core + +import ( + "errors" +) + +var ( + ErrUnauthorized = errors.New("unauthorized") + ErrModuleDisabledCannotExecute = errors.New("module disabled, cannot execute") + ErrNotImplemented = errors.New("not implemented") + ErrModuleAlreadyDisabled = errors.New("module already disabled") + ErrNoActiveProposalModules = errors.New("no active proposal modules") + ErrModuleAlreadyAdded = errors.New("module already added") + ErrNotSupported = errors.New("not supported") +) diff --git a/examples/gno.land/p/teritori/dao_core/gno.mod b/examples/gno.land/p/teritori/dao_core/gno.mod new file mode 100644 index 00000000000..294b6efaa39 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_core/gno.mod @@ -0,0 +1 @@ +module gno.land/p/teritori/dao_core diff --git a/examples/gno.land/p/teritori/dao_core/messages.gno b/examples/gno.land/p/teritori/dao_core/messages.gno new file mode 100644 index 00000000000..c69ea17cfa4 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_core/messages.gno @@ -0,0 +1,101 @@ +package core + +import ( + "gno.land/p/demo/json" + dao_interfaces "gno.land/p/teritori/dao_interfaces" +) + +// UpdateProposalModules + +type UpdateProposalModulesExecutableMessage struct { + ToAdd []dao_interfaces.IProposalModule + ToDisable []int +} + +var _ dao_interfaces.ExecutableMessage = &UpdateProposalModulesExecutableMessage{} + +func (msg UpdateProposalModulesExecutableMessage) Type() string { + return "gno.land/p/teritori/dao_core.UpdateProposalModules" +} + +func (msg *UpdateProposalModulesExecutableMessage) String() string { + panic(ErrNotImplemented) +} + +func (msg *UpdateProposalModulesExecutableMessage) ToJSON() *json.Node { + panic(ErrNotImplemented) +} + +func (msg *UpdateProposalModulesExecutableMessage) FromJSON(ast *json.Node) { + panic(ErrNotImplemented) +} + +type UpdateProposalModulesMessageHandler struct { + dao dao_interfaces.IDAOCore +} + +var _ dao_interfaces.MessageHandler = &UpdateProposalModulesMessageHandler{} + +func NewUpdateProposalModulesMessageHandler(dao dao_interfaces.IDAOCore) *UpdateProposalModulesMessageHandler { + return &UpdateProposalModulesMessageHandler{dao: dao} +} + +func (handler UpdateProposalModulesMessageHandler) Type() string { + return UpdateProposalModulesExecutableMessage{}.Type() +} + +func (handler *UpdateProposalModulesMessageHandler) Execute(message dao_interfaces.ExecutableMessage) { + msg := message.(*UpdateProposalModulesExecutableMessage) + handler.dao.UpdateProposalModules(msg.ToAdd, msg.ToDisable) +} + +func (handler *UpdateProposalModulesMessageHandler) Instantiate() dao_interfaces.ExecutableMessage { + return &UpdateProposalModulesExecutableMessage{} +} + +// UpdateVotingModule + +type UpdateVotingModuleExecutableMessage struct { + Module dao_interfaces.IVotingModule +} + +var _ dao_interfaces.ExecutableMessage = &UpdateVotingModuleExecutableMessage{} + +func (msg UpdateVotingModuleExecutableMessage) Type() string { + return "gno.land/p/teritori/dao_core.UpdateVotingModule" +} + +func (msg *UpdateVotingModuleExecutableMessage) String() string { + panic(ErrNotImplemented) +} + +func (msg *UpdateVotingModuleExecutableMessage) ToJSON() *json.Node { + panic(ErrNotImplemented) +} + +func (msg *UpdateVotingModuleExecutableMessage) FromJSON(ast *json.Node) { + panic(ErrNotImplemented) +} + +type UpdateVotingModuleMessageHandler struct { + dao dao_interfaces.IDAOCore +} + +var _ dao_interfaces.MessageHandler = &UpdateVotingModuleMessageHandler{} + +func NewUpdateVotingModuleMessageHandler(dao dao_interfaces.IDAOCore) *UpdateVotingModuleMessageHandler { + return &UpdateVotingModuleMessageHandler{dao: dao} +} + +func (handler UpdateVotingModuleMessageHandler) Type() string { + return UpdateVotingModuleExecutableMessage{}.Type() +} + +func (handler *UpdateVotingModuleMessageHandler) Execute(message dao_interfaces.ExecutableMessage) { + msg := message.(*UpdateVotingModuleExecutableMessage) + handler.dao.UpdateVotingModule(msg.Module) +} + +func (handler *UpdateVotingModuleMessageHandler) Instantiate() dao_interfaces.ExecutableMessage { + return &UpdateVotingModuleExecutableMessage{} +} diff --git a/examples/gno.land/p/teritori/dao_interfaces/README.md b/examples/gno.land/p/teritori/dao_interfaces/README.md new file mode 100644 index 00000000000..601c7f566fb --- /dev/null +++ b/examples/gno.land/p/teritori/dao_interfaces/README.md @@ -0,0 +1,344 @@ +--- +id: creating-dao +--- + +# How to Create a DAO + +## Overview + +This guide will show you how to write a simple [**DAO**](https://en.wikipedia.org/wiki/Decentralized_autonomous_organization) [realm](../concepts/realms.md) in [Gno](../concepts/gno-language.md). For actually deploying the realm, please see the +[deployment](deploy.md) guide. + +We'll cover the core components that make up a DAO, walk you through the process of creating your first DAO, and provide code examples to help you get started. + +## Theoretical Foundations + +### Core Interface + +The `IDAOCore` interface ties all the other components together and offers the main entry points for interacting with the DAO. + +**Interface Definition:** + +```go +type ActivableProposalModule struct { + Enabled bool + Module IProposalModule +} + +type IDAOCore interface { + Render(path string) string + + VotingModule() IVotingModule + ProposalModules() []ActivableProposalModule + ActiveProposalModuleCount() int + Registry() *MessagesRegistry + + UpdateVotingModule(newVotingModule IVotingModule) + UpdateProposalModules(toAdd []IProposalModule, toDisable []int) +} +``` + +A default implementation is provided in the package `gno.land/p/teritori/dao_core`, and custom implementations are generally not required. + +### Voting Module + +The `gno.land/p/teritori/dao_interfaces.IVotingModule` interface defines how voting power is allocated to addresses within the DAO. + +**Interface Definition:** + +```go +type IVotingModule interface { + Info() ModuleInfo + ConfigJSON() string + Render(path string) string + VotingPowerAtHeight(address std.Address, height int64) (power uint64) + TotalPowerAtHeight(height int64) uint64 +} +``` + +There is only one implementation currently, `gno.land/p/teritori/dao_voting_group`, providing a membership-based voting power definition. + +### Proposal Modules + +A proposal module (`gno.land/p/teritori/dao_interfaces.IProposalModule`) is responsible for: +- Receiving proposals, the proposal type is defined by the module +- Managing the proposals lifecycle +- Tallying votes, the vote type is defined by the module and the associated voting power is queried from the voting module +- Executing proposals once they are passed + +**Interface Definition:** +```go +type IProposalModule interface { + Core() IDAOCore + Info() ModuleInfo + ConfigJSON() string + Render(path string) string + Execute(proposalID int) + VoteJSON(proposalID int, voteJSON string) + ProposeJSON(proposalJSON string) int + ProposalsJSON(limit int, startAfter string, reverse bool) string + ProposalJSON(proposalID int) string +} +``` + +There is only one implementation currently, `gno.land/p/teritori/dao_proposal_single`, providing a yes/no/abstain vote model with quorum and threshold. + +### Message handlers + +Proposal actions are encoded as objects implementing `ExecutableMessage` found under `gno.land/p/teritori/dao_interfaces`. +```go +type ExecutableMessage interface { + ToJSON() *json.Node + FromJSON(ast *json.Node) + + String() string + Type() string +} +``` + +They are unmarshalled and executed by message handlers implementing `gno.land/p/teritori/dao_interfaces.MessageHandler`. +```go +type MessageHandler interface { + Execute(message ExecutableMessage) + Instantiate() ExecutableMessage + Type() string +} +``` + +Message handlers are registered at core creation and new message handlers can be registered via proposals to extend the DAO capabilities. + +## Practical Implementation +In this section, we will showcase how to implement your own DAO using the `dao_maker` package suite. + +### Setting Up Your Workspace + +#### Setup the Tooling + +To setup your tooling, see [Getting Started: Local Setup](../getting-started/local-setup.md). + +#### Create a new Gno module + +- Create a new directory and move into it: `mkdir my-gno-dao && cd my-gno-dao` +- Initialize the gno module: `gno mod init gno.land/r//my_dao` + +### Creating the Voting Module + +We will start by instantiating a voting module. + +1. **Initialize the Factory** + +Modules instantiation uses the factory pattern in case the module needs to access the core. + +`my_dao.gno` +```go +package my_dao + +import ( + "gno.land/p/teritori/dao_interfaces" +) + +func init() { + votingModuleFactory := func(core dao_interfaces.IDAOCore) { + + } +} +``` + +2. **Instantiate the module** + +`my_dao.gno` +```go +package my_dao + +import ( + "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/dao_voting_group" // <- new +) + +func init() { + var group *dao_voting_group.VotingGroup // <- new + + votingModuleFactory := func(core dao_interfaces.IDAOCore) { + group = dao_voting_group.NewVotingGroup() // <- new + } +} +``` + +We need to keep a reference to the module to instantiate its message handlers later. + +3. **Add Initial Members and return the module** + +`my_dao.gno` +```go +func init() { + votingModuleFactory := func(core dao_interfaces.IDAOCore) { + group = dao_voting_group.NewVotingGroup() + group.SetMemberPower("your-address", 1) // <- new + // repeat for any other initial members you want in the DAO + return group // <- new + } +} +``` + +Now let's create a proposal module. + +### Creating the proposal module + +1. **Initialize the Factory** + +`my_dao.gno` +```go +func init() { + votingModuleFactory := func(core dao_interfaces.IDAOCore) { + // ... + } + proposalModuleFactories := []dao_interfaces.ProposalModuleFactory{ + func(core dao_interfaces.IDAOCore) dao_interfaces.IProposalModule { + + }, + } +} +``` + +2. **Configure and instantiate the Proposal Module** + +`my_dao.gno` +```go +package my_dao + +import ( + "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/dao_voting_group" + "gno.land/p/teritori/dao_proposal_single" // <- new +) + +func init() { + // ... + var proposalModule *dao_proposal_single.DAOProposalSingle + proposalModuleFactories := []dao_interfaces.ProposalModuleFactory{ + func(core dao_interfaces.IDAOCore) dao_interfaces.IProposalModule { + tt := dao_proposal_single.PercentageThresholdPercent(100) // 1% threshold + tq := dao_proposal_single.PercentageThresholdPercent(100) // 1% quorum + proposalModule = dao_proposal_single.NewDAOProposalSingle(core, &dao_proposal_single.DAOProposalSingleOpts{ + MaxVotingPeriod: time.Hour * 24 * 42, + Threshold: &dao_proposal_single.ThresholdThresholdQuorum{ + Threshold: &tt, + Quorum: &tq, + }, + }) + return proposalModule + }, + } +} +``` + +We also need to keep a reference to the module to instantiate it's message handlers later. + +### Registering Message Handlers + +Add message handlers to allow your DAO to perform specific actions when proposals are executed. + +`my_dao.gno` +```go +package my_dao + +import ( + "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/dao_voting_group" + "gno.land/p/teritori/dao_proposal_single" +) + +func init() { + // ... + messageHandlersFactories := []dao_interfaces.MessageHandlerFactory{ + // Allow to manage the voting group + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return group.UpdateMembersHandler() + }, + // Allow to update the proposal module settings + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return dao_proposal_single.NewUpdateSettingsHandler(proposalModule) + }, + } +} +``` + +### Creating the DAO Core + +Now we can create the actual DAO. + +```go +package my_dao + +import ( + "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/dao_voting_group" + "gno.land/p/teritori/dao_proposal_single" + "gno.land/p/teritori/dao_core" // <- new +) + +var ( + daoCore dao_interfaces.IDAOCore // <- new +) + +func init() { + // ... + messageHandlersFactories := []dao_interfaces.MessageHandlerFactory{ + // ... + } + daoCore = dao_core.NewDAOCore(votingModuleFactory, proposalModuleFactories, messageHandlersFactories) // <- new +} +``` + +We also need to expose the DAO methods in the realm. + +```go +func init() { + // ... +} + +func Render(path string) string { + return daoCore.Render(path) +} + +func VoteJSON(moduleIndex int, proposalID int, voteJSON string) { + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + module.Module.VoteJSON(proposalID, voteJSON) +} + +func Execute(moduleIndex int, proposalID int) { + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + module.Module.Execute(proposalID) +} + +func ProposeJSON(moduleIndex int, proposalJSON string) int { + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + return module.Module.ProposeJSON(proposalJSON) +} + +func getProposalsJSON(moduleIndex int, limit int, startAfter string, reverse bool) string { + module := dao_core.GetProposalModule(daoCore, moduleIndex) + return module.Module.ProposalsJSON(limit, startAfter, reverse) +} + +func getProposalJSON(moduleIndex int, proposalIndex int) string { + module := dao_core.GetProposalModule(daoCore, moduleIndex) + return module.Module.ProposalJSON(proposalIndex) +} +``` + +## Conclusion + +That's it! You've successfully created your first DAO using the Gno DAO framework. To expand its capabilities, you can register additional message handlers or even create new modules if you feel bold. \ No newline at end of file diff --git a/examples/gno.land/p/teritori/dao_interfaces/core.gno b/examples/gno.land/p/teritori/dao_interfaces/core.gno new file mode 100644 index 00000000000..5379638edbd --- /dev/null +++ b/examples/gno.land/p/teritori/dao_interfaces/core.gno @@ -0,0 +1,20 @@ +package dao_interfaces + +// Inspired by DA0-DA0: https://github.com/DA0-DA0/dao-contracts + +type ActivableProposalModule struct { + Enabled bool + Module IProposalModule +} + +type IDAOCore interface { + Render(path string) string + + VotingModule() IVotingModule + ProposalModules() []ActivableProposalModule + ActiveProposalModuleCount() int + Registry() *MessagesRegistry + + UpdateVotingModule(newVotingModule IVotingModule) + UpdateProposalModules(toAdd []IProposalModule, toDisable []int) +} diff --git a/examples/gno.land/p/teritori/dao_interfaces/core_testing.gno b/examples/gno.land/p/teritori/dao_interfaces/core_testing.gno new file mode 100644 index 00000000000..76e1cec0c77 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_interfaces/core_testing.gno @@ -0,0 +1,35 @@ +package dao_interfaces + +type dummyCore struct{} + +func NewDummyCore() IDAOCore { + return &dummyCore{} +} + +func (d *dummyCore) Render(path string) string { + panic("not implemented") +} + +func (d *dummyCore) VotingModule() IVotingModule { + panic("not implemented") +} + +func (d *dummyCore) ProposalModules() []ActivableProposalModule { + panic("not implemented") +} + +func (d *dummyCore) ActiveProposalModuleCount() int { + panic("not implemented") +} + +func (d *dummyCore) Registry() *MessagesRegistry { + panic("not implemented") +} + +func (d *dummyCore) UpdateVotingModule(newVotingModule IVotingModule) { + panic("not implemented") +} + +func (d *dummyCore) UpdateProposalModules(toAdd []IProposalModule, toDisable []int) { + panic("not implemented") +} diff --git a/examples/gno.land/p/teritori/dao_interfaces/gno.mod b/examples/gno.land/p/teritori/dao_interfaces/gno.mod new file mode 100644 index 00000000000..fa40dd8a400 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_interfaces/gno.mod @@ -0,0 +1 @@ +module gno.land/p/teritori/dao_interfaces diff --git a/examples/gno.land/p/teritori/dao_interfaces/messages.gno b/examples/gno.land/p/teritori/dao_interfaces/messages.gno new file mode 100644 index 00000000000..8b61ad2fe12 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_interfaces/messages.gno @@ -0,0 +1,21 @@ +package dao_interfaces + +import ( + "gno.land/p/demo/json" +) + +type ExecutableMessage interface { + ToJSON() *json.Node + FromJSON(ast *json.Node) + + String() string + Type() string +} + +type MessageHandler interface { + Execute(message ExecutableMessage) + Instantiate() ExecutableMessage + Type() string +} + +type MessageHandlerFactory func(core IDAOCore) MessageHandler diff --git a/examples/gno.land/p/teritori/dao_interfaces/messages_registry.gno b/examples/gno.land/p/teritori/dao_interfaces/messages_registry.gno new file mode 100644 index 00000000000..3d9b317eca7 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_interfaces/messages_registry.gno @@ -0,0 +1,160 @@ +package dao_interfaces + +import ( + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/json" +) + +type MessagesRegistry struct { + handlers *avl.Tree +} + +func NewMessagesRegistry() *MessagesRegistry { + registry := &MessagesRegistry{handlers: avl.NewTree()} + registry.Register(NewRegisterHandlerExecutableMessageHandler(registry)) + registry.Register(NewRemoveHandlerExecutableMessageHandler(registry)) + return registry +} + +func (r *MessagesRegistry) Register(handler MessageHandler) { + r.handlers.Set(handler.Type(), handler) +} + +func (r *MessagesRegistry) Remove(t string) { + r.handlers.Remove(t) +} + +func (r *MessagesRegistry) MessagesFromJSON(slice []*json.Node) []ExecutableMessage { + msgs := make([]ExecutableMessage, len(slice)) + for i, elem := range slice { + messageType := json.Must(elem.GetKey("type")).MustString() + payload := json.Must(elem.GetKey("payload")) + h, ok := r.handlers.Get(messageType) + if !ok { + panic("invalid ExecutableMessage: invalid message type") + } + + instance := h.(MessageHandler).Instantiate() + instance.FromJSON(payload) + msgs[i] = instance + } + + return msgs +} + +func (r *MessagesRegistry) Execute(msg ExecutableMessage) { + h, ok := r.handlers.Get(msg.Type()) + if !ok { + panic("invalid ExecutableMessage: invalid message type") + } + + h.(MessageHandler).Execute(msg) +} + +func (r *MessagesRegistry) ExecuteMessages(msgs []ExecutableMessage) { + for _, msg := range msgs { + r.Execute(msg) + } +} + +func (r *MessagesRegistry) Render() string { + sb := strings.Builder{} + r.handlers.Iterate("", "", func(key string, value interface{}) bool { + sb.WriteString("- ") + sb.WriteString(value.(MessageHandler).Type()) + sb.WriteRune('\n') + return false + }) + + return sb.String() +} + +type RegisterHandlerExecutableMessage struct { + Handler MessageHandler +} + +var _ ExecutableMessage = &RegisterHandlerExecutableMessage{} + +func (m RegisterHandlerExecutableMessage) Type() string { + return "gno.land/p/teritori/dao_interfaces.RegisterHandler" +} + +func (m *RegisterHandlerExecutableMessage) FromJSON(ast *json.Node) { + panic("not implemented") +} + +func (m *RegisterHandlerExecutableMessage) ToJSON() *json.Node { + panic("not implemented") +} + +func (m *RegisterHandlerExecutableMessage) String() string { + return m.Handler.Type() +} + +type RegisterHandlerExecutableMessageHandler struct { + registry *MessagesRegistry +} + +var _ MessageHandler = &RegisterHandlerExecutableMessageHandler{} + +func NewRegisterHandlerExecutableMessageHandler(registry *MessagesRegistry) *RegisterHandlerExecutableMessageHandler { + return &RegisterHandlerExecutableMessageHandler{registry: registry} +} + +func (h RegisterHandlerExecutableMessageHandler) Type() string { + return RegisterHandlerExecutableMessage{}.Type() +} + +func (h *RegisterHandlerExecutableMessageHandler) Instantiate() ExecutableMessage { + return &RegisterHandlerExecutableMessage{} +} + +func (h *RegisterHandlerExecutableMessageHandler) Execute(msg ExecutableMessage) { + h.registry.Register(msg.(*RegisterHandlerExecutableMessage).Handler) +} + +type RemoveHandlerExecutableMessage struct { + HandlerType string +} + +var _ ExecutableMessage = &RemoveHandlerExecutableMessage{} + +func (m RemoveHandlerExecutableMessage) Type() string { + return "gno.land/p/teritori/dao_interfaces.RemoveHandler" +} + +func (m *RemoveHandlerExecutableMessage) FromJSON(ast *json.Node) { + m.HandlerType = ast.MustString() +} + +func (m *RemoveHandlerExecutableMessage) ToJSON() *json.Node { + return json.StringNode("", m.HandlerType) +} + +func (m *RemoveHandlerExecutableMessage) String() string { + return m.HandlerType +} + +type RemoveHandlerExecutableMessageHandler struct { + registry *MessagesRegistry +} + +var _ MessageHandler = &RemoveHandlerExecutableMessageHandler{} + +func NewRemoveHandlerExecutableMessageHandler(registry *MessagesRegistry) *RemoveHandlerExecutableMessageHandler { + return &RemoveHandlerExecutableMessageHandler{registry: registry} +} + +func (h RemoveHandlerExecutableMessageHandler) Type() string { + return RemoveHandlerExecutableMessage{}.Type() +} + +func (h *RemoveHandlerExecutableMessageHandler) Instantiate() ExecutableMessage { + return &RemoveHandlerExecutableMessage{} +} + +func (h *RemoveHandlerExecutableMessageHandler) Execute(msg ExecutableMessage) { + h.registry.Remove(msg.(*RemoveHandlerExecutableMessage).HandlerType) +} diff --git a/examples/gno.land/p/teritori/dao_interfaces/messages_registry_test.gno b/examples/gno.land/p/teritori/dao_interfaces/messages_registry_test.gno new file mode 100644 index 00000000000..dfabe4d2d5a --- /dev/null +++ b/examples/gno.land/p/teritori/dao_interfaces/messages_registry_test.gno @@ -0,0 +1,55 @@ +package dao_interfaces + +import ( + "testing" + + "gno.land/p/demo/json" +) + +func TestRegistry(t *testing.T) { + registry := NewMessagesRegistry() + + var value string + msgHandler := NewCopyMessageHandler(&value) + + // Test register handler via message + registerMsg := &RegisterHandlerExecutableMessage{Handler: msgHandler} + registry.Execute(registerMsg) + + // Test messages execution + msgs := registry.MessagesFromJSON(json.Must(json.Unmarshal([]byte(`[{"type":"CopyMessage","payload":"Hello"}]`))).MustArray()) + if len(msgs) != 1 { + t.Errorf("Expected 1 message, got %d", len(msgs)) + } + + registry.Execute(msgs[0]) + if value != "Hello" { + t.Errorf("Expected value to be 'Hello', got '%s'", value) + } + + msg2 := &CopyMessage{Value: "World"} + registry.Execute(msg2) + if value != "World" { + t.Errorf("Expected value to be 'World', got '%s'", value) + } + + // Test handler removal + removeMsg := &RemoveHandlerExecutableMessage{HandlerType: msgHandler.Type()} + registry.Execute(removeMsg) + func() { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic, got none") + } + }() + registry.Execute(msg2) + }() + + // Test direct register + registry.Register(msgHandler) + msg3 := &CopyMessage{Value: "!"} + registry.Execute(msg3) + if value != "!" { + t.Errorf("Expected value to be '!', got '%s'", value) + } +} diff --git a/examples/gno.land/p/teritori/dao_interfaces/messages_testing.gno b/examples/gno.land/p/teritori/dao_interfaces/messages_testing.gno new file mode 100644 index 00000000000..9485fcbe02a --- /dev/null +++ b/examples/gno.land/p/teritori/dao_interfaces/messages_testing.gno @@ -0,0 +1,54 @@ +package dao_interfaces + +import ( + "gno.land/p/demo/json" +) + +type CopyMessage struct { + Value string +} + +func (m CopyMessage) Type() string { + return "CopyMessage" +} + +func (m *CopyMessage) String() string { + return m.Value +} + +func (m *CopyMessage) FromJSON(ast *json.Node) { + m.Value = ast.MustString() +} + +func (m *CopyMessage) ToJSON() *json.Node { + return json.StringNode("", m.Value) +} + +type CopyMessageHandler struct { + ptr *string +} + +func NewCopyMessageHandler(ptr *string) *CopyMessageHandler { + if ptr == nil { + panic("ptr cannot be nil") + } + + return &CopyMessageHandler{ptr} +} + +func (h *CopyMessageHandler) Execute(imsg ExecutableMessage) { + msg, ok := imsg.(*CopyMessage) + if !ok { + panic("Wrong message type") + } + + *h.ptr = msg.Value +} + +func (h CopyMessageHandler) Type() string { + return "CopyMessage" +} + +func (h *CopyMessageHandler) Instantiate() ExecutableMessage { + return &CopyMessage{} +} diff --git a/examples/gno.land/p/teritori/dao_interfaces/modules.gno b/examples/gno.land/p/teritori/dao_interfaces/modules.gno new file mode 100644 index 00000000000..66790acd442 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_interfaces/modules.gno @@ -0,0 +1,39 @@ +package dao_interfaces + +import ( + "std" +) + +type ModuleInfo struct { + Kind string + Version string +} + +func (mi ModuleInfo) String() string { + return mi.Kind + "@v" + mi.Version +} + +type IVotingModule interface { + Info() ModuleInfo + ConfigJSON() string + GetMembersJSON(start, end string, limit uint64, height int64) string + Render(path string) string + VotingPowerAtHeight(address std.Address, height int64, resources []string) (power uint64) + TotalPowerAtHeight(height int64, resources []string) uint64 +} + +type VotingModuleFactory func(core IDAOCore) IVotingModule + +type IProposalModule interface { + Core() IDAOCore + Info() ModuleInfo + ConfigJSON() string + Render(path string) string + Execute(proposalID int) + VoteJSON(proposalID int, voteJSON string) + ProposeJSON(proposalJSON string) int + ProposalsJSON(limit int, startAfter string, reverse bool) string + ProposalJSON(proposalID int) string +} + +type ProposalModuleFactory func(core IDAOCore) IProposalModule diff --git a/examples/gno.land/p/teritori/dao_proposal_single/dao_proposal_single.gno b/examples/gno.land/p/teritori/dao_proposal_single/dao_proposal_single.gno new file mode 100644 index 00000000000..fd67a5ef567 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_proposal_single/dao_proposal_single.gno @@ -0,0 +1,473 @@ +package dao_proposal_single + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/json" + "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/dao_utils" +) + +type DAOProposalSingleOpts struct { + /// The threshold a proposal must reach to complete. + Threshold Threshold + /// The default maximum amount of time a proposal may be voted on + /// before expiring. + MaxVotingPeriod dao_utils.Duration + /// The minimum amount of time a proposal must be open before + /// passing. A proposal may fail before this amount of time has + /// elapsed, but it will not pass. This can be useful for + /// preventing governance attacks wherein an attacker aquires a + /// large number of tokens and forces a proposal through. + MinVotingPeriod dao_utils.Duration // 0 means no minimum + /// If set to true only members may execute passed + /// proposals. Otherwise, any address may execute a passed + /// proposal. + OnlyMembersExecute bool + /// Allows changing votes before the proposal expires. If this is + /// enabled proposals will not be able to complete early as final + /// vote information is not known until the time of proposal + /// expiration. + AllowRevoting bool + /// Information about what addresses may create proposals. + // preProposeInfo PreProposeInfo + /// If set to true proposals will be closed if their execution + /// fails. Otherwise, proposals will remain open after execution + /// failure. For example, with this enabled a proposal to send 5 + /// tokens out of a DAO's treasury with 4 tokens would be closed when + /// it is executed. With this disabled, that same proposal would + /// remain open until the DAO's treasury was large enough for it to be + /// executed. + CloseProposalOnExecutionFailure bool +} + +func (opts DAOProposalSingleOpts) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "threshold": opts.Threshold.ToJSON(), + "maxVotingPeriod": opts.MaxVotingPeriod.ToJSON(), + "minVotingPeriod": opts.MinVotingPeriod.ToJSON(), + "onlyMembersExecute": json.BoolNode("", opts.OnlyMembersExecute), + "allowRevoting": json.BoolNode("", opts.AllowRevoting), + "closeProposalOnExecutionFailure": json.BoolNode("", opts.CloseProposalOnExecutionFailure), + }) +} + +type DAOProposalSingle struct { + dao_interfaces.IProposalModule + + core dao_interfaces.IDAOCore + opts *DAOProposalSingleOpts + proposals []*Proposal +} + +func NewDAOProposalSingle(core dao_interfaces.IDAOCore, opts *DAOProposalSingleOpts) *DAOProposalSingle { + if core == nil { + panic("core cannot be nil") + } + + if opts == nil { + panic("opts cannot be nil") + } + + if opts.AllowRevoting { + panic("allow revoting not implemented") + } + + if opts.OnlyMembersExecute { + panic("only members execute not implemented") + } + + if opts.CloseProposalOnExecutionFailure { + panic("close proposal on execution failure not implemented") + } + + if opts.MaxVotingPeriod == nil { + panic("max voting period cannot be nil") + } + + // TODO: support other threshold types + switch opts.Threshold.(type) { + case *ThresholdThresholdQuorum: + threshold := opts.Threshold.(*ThresholdThresholdQuorum) + switch threshold.Threshold.(type) { + case *PercentageThresholdMajority: + panic("not implemented") + + case *PercentageThresholdPercent: + if *threshold.Threshold.(*PercentageThresholdPercent) > 10000 { + panic("opts.Threshold.Threshold must be <= 100%") + } + + default: + panic("unknown Threshold type") + } + + switch threshold.Quorum.(type) { + case *PercentageThresholdMajority: + panic("not implemented") + + case *PercentageThresholdPercent: + if *threshold.Quorum.(*PercentageThresholdPercent) > 10000 { + panic("opts.Threshold.Quorum must be <= 100%") + } + + default: + panic("unknown PercentageThreshold type") + } + default: + panic("unsupported Threshold type") + } + + return &DAOProposalSingle{core: core, opts: opts} +} + +func (d *DAOProposalSingle) Render(path string) string { + minVotingPeriodStr := "No minimum voting period" + if d.opts.MinVotingPeriod != nil { + minVotingPeriodStr = "Min voting period: " + d.opts.MinVotingPeriod.String() + } + + executeStr := "Any address may execute passed proposals" + if d.opts.OnlyMembersExecute { + executeStr = "Only members may execute passed proposals" + } + + revotingStr := "Revoting is not allowed" + if d.opts.AllowRevoting { + revotingStr = "Revoting is allowed" + } + + closeOnExecFailureStr := "Proposals will remain open after execution failure" + if d.opts.CloseProposalOnExecutionFailure { + closeOnExecFailureStr = "Proposals will be closed if their execution fails" + } + + sb := strings.Builder{} + sb.WriteString("Max voting period: ") + sb.WriteString(d.opts.MaxVotingPeriod.String()) + sb.WriteString("\n\n") + sb.WriteString(minVotingPeriodStr) + sb.WriteString("\n\n") + sb.WriteString(executeStr) + sb.WriteString("\n\n") + sb.WriteString(revotingStr) + sb.WriteString("\n\n") + sb.WriteString(closeOnExecFailureStr) + sb.WriteString("\n\n") + switch d.opts.Threshold.(type) { + case *ThresholdThresholdQuorum: + threshold := d.opts.Threshold.(*ThresholdThresholdQuorum) + sb.WriteString("Threshold: ") + sb.WriteString(threshold.Threshold.String()) + sb.WriteString("\n\nQuorum: ") + sb.WriteString(threshold.Quorum.String()) + + case *ThresholdAbsolutePercentage: + threshold := d.opts.Threshold.(*ThresholdAbsolutePercentage) + sb.WriteString("Threshold (Absolute Percentage): ") + sb.WriteString(threshold.Value.String()) + + case *ThresholdAbsoluteCount: + threshold := d.opts.Threshold.(*ThresholdAbsoluteCount) + sb.WriteString("Threshold (Absolute Count): ") + sb.WriteString(strconv.FormatUint(uint64(*threshold), 10)) + + default: + panic("unsupported Threshold type") + } + + sb.WriteString("\n\n") + sb.WriteString("Proposals:\n\n") + for _, p := range d.proposals { + sb.WriteString("\\#") + sb.WriteString(strconv.Itoa(p.ID)) + sb.WriteString(": ") + sb.WriteString(p.Title) + sb.WriteString(":\n\n") + sb.WriteString(" Status: ") + sb.WriteString(p.Status.String()) + sb.WriteString("\n\n") + sb.WriteString(" Proposed by ") + sb.WriteString(p.Proposer.String()) + sb.WriteString("\n\n") + sb.WriteString(" ") + sb.WriteString(p.Description) + sb.WriteString("\n\n") + sb.WriteString(" Votes summary:") + sb.WriteString("\n\n") + sb.WriteString(" - Yes: ") + sb.WriteString(strconv.FormatUint(p.Votes.Yes, 10)) + sb.WriteRune('\n') + sb.WriteString(" - No: ") + sb.WriteString(strconv.FormatUint(p.Votes.No, 10)) + sb.WriteRune('\n') + sb.WriteString(" - Abstain: ") + sb.WriteString(strconv.FormatUint(p.Votes.Abstain, 10)) + sb.WriteString("\n\n") + sb.WriteString(" Total: ") + sb.WriteString(strconv.FormatUint(p.Votes.Total(), 10)) + sb.WriteString("\n\n") + + sb.WriteString(" Messages:") + sb.WriteString("\n\n") + for _, m := range p.Messages { + sb.WriteString(m.(dao_interfaces.ExecutableMessage).Type()) + sb.WriteString("\n\n") + sb.WriteString(m.(dao_interfaces.ExecutableMessage).String()) + sb.WriteString("\n\n") + } + + sb.WriteString(" Votes:") + sb.WriteString("\n\n") + + p.Ballots.Iterate("", "", func(k string, v interface{}) bool { + ballot := v.(Ballot) + sb.WriteString(k) + sb.WriteString(" voted ") + sb.WriteString(ballot.String()) + sb.WriteString("\n\n") + return false + }) + + sb.WriteRune('\n') + } + + return sb.String() +} + +func (d *DAOProposalSingle) Core() dao_interfaces.IDAOCore { + return d.core +} + +func (d *DAOProposalSingle) Info() dao_interfaces.ModuleInfo { + return dao_interfaces.ModuleInfo{ + Kind: "gno.land/p/teritori/dao_proposal_single", + Version: "0.1.0", + } +} + +func (d *DAOProposalSingle) ConfigJSON() string { + b, err := json.Marshal(d.opts.ToJSON()) + if err != nil { + panic(err) + } + return string(b) +} + +func (d *DAOProposalSingle) Propose(title string, description string, messages []dao_interfaces.ExecutableMessage) int { + // TODO: creation policy + + totalPower := d.core.VotingModule().TotalPowerAtHeight(0, []string{}) + + expiration := d.opts.MaxVotingPeriod.AfterCurrentBlock() + minVotingPeriod := dao_utils.Expiration(nil) + if d.opts.MinVotingPeriod != nil { + minVotingPeriod = d.opts.MinVotingPeriod.AfterCurrentBlock() + } + + id := len(d.proposals) + + prop := Proposal{ + ID: id, + Title: title, + Description: description, + Proposer: std.PrevRealm().Addr(), + StartHeight: std.GetHeight(), + MinVotingPeriod: minVotingPeriod, + Expiration: expiration, + Threshold: d.opts.Threshold.Clone(), + TotalPower: totalPower, + Messages: messages, + Status: ProposalStatusOpen, + Ballots: avl.NewTree(), + AllowRevoting: d.opts.AllowRevoting, + } + prop.updateStatus() + d.proposals = append(d.proposals, &prop) + return id +} + +func (d *DAOProposalSingle) GetBallot(proposalID int, memberAddress std.Address) Ballot { + if len(d.proposals) <= proposalID || proposalID < 0 { + panic("proposal does not exist") + } + + proposal := d.proposals[proposalID] + ballot, has := proposal.Ballots.Get(memberAddress.String()) + if !has { + panic("ballot does not exist") + } + + return ballot.(Ballot) +} + +type VoteWithRationale struct { + Vote Vote + Rationale string +} + +func (v *VoteWithRationale) FromJSON(ast *json.Node) { + obj := ast.MustObject() + v.Vote.FromJSON(obj["vote"]) + v.Rationale = obj["rationale"].MustString() +} + +func (d *DAOProposalSingle) VoteJSON(proposalID int, voteJSON string) { + var v VoteWithRationale + v.FromJSON(json.Must(json.Unmarshal([]byte(voteJSON)))) + + voter := std.PrevRealm().Addr() + + if len(d.proposals) <= proposalID || proposalID < 0 { + panic("proposal does not exist") + } + + proposal := d.proposals[proposalID] + + if proposal.Expiration.IsExpired() { + panic("proposal is expired") + } + + votePower := d.core.VotingModule().VotingPowerAtHeight(voter, proposal.StartHeight, []string{}) + if votePower == 0 { + panic("not registered") + } + + // TODO: handle revoting + if ok := proposal.Ballots.Has(voter.String()); ok { + panic("already voted") + } + + proposal.Ballots.Set(voter.String(), Ballot{ + Vote: v.Vote, + Power: votePower, + Rationale: v.Rationale, + }) + + proposal.Votes.Add(v.Vote, votePower) + + proposal.updateStatus() +} + +func (d *DAOProposalSingle) Execute(proposalID int) { + if len(d.proposals) <= proposalID || proposalID < 0 { + panic("proposal does not exist") + } + + prop := d.proposals[proposalID] + + prop.updateStatus() + if prop.Status != ProposalStatusPassed { + panic("proposal is not passed") + } + + for _, m := range prop.Messages { + d.core.Registry().Execute(m) + } + + prop.Status = ProposalStatusExecuted +} + +type ProposalRequest struct { + Title string + Description string + Messages []*json.Node +} + +func (pr *ProposalRequest) FromJSON(ast *json.Node) { + obj := ast.MustObject() + pr.Title = obj["title"].MustString() + pr.Description = obj["description"].MustString() + pr.Messages = obj["messages"].MustArray() +} + +func (d *DAOProposalSingle) ProposeJSON(proposalJSON string) int { + var req ProposalRequest + req.FromJSON(json.Must(json.Unmarshal([]byte(proposalJSON)))) + msgs := d.core.Registry().MessagesFromJSON(req.Messages) + return d.Propose(req.Title, req.Description, msgs) +} + +func (d *DAOProposalSingle) Proposals() []*Proposal { + return d.proposals +} + +func (d *DAOProposalSingle) ProposalsJSON(limit int, startAfter string, reverse bool) string { + iSlice := make([]*json.Node, len(d.proposals)) + for i, p := range d.proposals { + iSlice[i] = p.ToJSON() + } + + return json.ArrayNode("", iSlice).String() +} + +func (d *DAOProposalSingle) ProposalJSON(proposalID int) string { + if proposalID < 0 || proposalID >= len(d.proposals) { + panic("proposal does not exist") + } + + return d.proposals[proposalID].ToJSON().String() +} + +func (d *DAOProposalSingle) Threshold() Threshold { + return d.opts.Threshold +} + +func (proposal *Proposal) updateStatus() { + if proposal.Status == ProposalStatusOpen && proposal.isPassed() { + proposal.Status = ProposalStatusPassed + return + } +} + +func (proposal *Proposal) isPassed() bool { + switch proposal.Threshold.(interface{}).(type) { + case *ThresholdAbsolutePercentage: + panic("'isPassed' not implemented for 'ThresholdAbsolutePercentage'") + + case *ThresholdThresholdQuorum: + thresholdObj := proposal.Threshold.(*ThresholdThresholdQuorum) + + threshold := thresholdObj.Threshold + quorum := thresholdObj.Quorum + + totalPower := proposal.TotalPower + + if !doesVoteCountPass(proposal.Votes.Total(), totalPower, quorum) { + return false + } + + // TODO: handle expiration + options := totalPower - proposal.Votes.Abstain + return doesVoteCountPass(proposal.Votes.Yes, options, threshold) + + case *ThresholdAbsoluteCount: + panic("'isPassed' not implemented for 'ThresholdAbsoluteCount'") + + default: + panic("unknown Threshold type") + } +} + +func doesVoteCountPass(yesVotes uint64, options uint64, percent PercentageThreshold) bool { + switch percent.(type) { + case *PercentageThresholdMajority: + panic("'doesVoteCountPass' not implemented for 'PercentageThresholdMajority'") + + case *PercentageThresholdPercent: + if options == 0 { + return false + } + + percentValue := uint64(*percent.(*PercentageThresholdPercent)) + votes := yesVotes * 10000 + threshold := options * percentValue + return votes >= threshold + + default: + panic("unknown PercentageThreshold type") + } +} diff --git a/examples/gno.land/p/teritori/dao_proposal_single/gno.mod b/examples/gno.land/p/teritori/dao_proposal_single/gno.mod new file mode 100644 index 00000000000..eadc8d8a188 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_proposal_single/gno.mod @@ -0,0 +1 @@ +module gno.land/p/teritori/dao_proposal_single diff --git a/examples/gno.land/p/teritori/dao_proposal_single/proposal_test.gno b/examples/gno.land/p/teritori/dao_proposal_single/proposal_test.gno new file mode 100644 index 00000000000..62b2b482f7b --- /dev/null +++ b/examples/gno.land/p/teritori/dao_proposal_single/proposal_test.gno @@ -0,0 +1,132 @@ +package dao_proposal_single + +import ( + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/json" + dao_interfaces "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/dao_utils" +) + +type NoopMessage struct{} + +var _ dao_interfaces.ExecutableMessage = (*NoopMessage)(nil) + +func (m NoopMessage) String() string { + return "noop" +} + +func (m NoopMessage) Type() string { + return "noop-type" +} + +func (m NoopMessage) ToJSON() *json.Node { + return json.StringNode("", m.String()) +} + +func (m NoopMessage) FromJSON(ast *json.Node) { + if ast.MustString() != m.String() { + panic("invalid noop message") + } +} + +func TestThresholdToJSON(t *testing.T) { + tt := PercentageThresholdPercent(1) + tq := PercentageThresholdPercent(1) + threshold := &ThresholdThresholdQuorum{ + Threshold: &tt, + Quorum: &tq, + } + str := threshold.ToJSON().String() + expected := `{"thresholdQuorum":{"threshold":{"percent":1},"quorum":{"percent":1}}}` + if expected != str { + t.Fatalf("JSON does not match, expected %s, got %s", expected, str) + } +} + +func TestThresholdFromJSON(t *testing.T) { + s := ` {"thresholdQuorum": {"threshold" :{"percent":1},"quorum": {"percent":1}}}` + ival := ThresholdFromJSON(json.Must(json.Unmarshal([]byte(s)))) + + val, ok := ival.(*ThresholdThresholdQuorum) + if !ok { + t.Fatalf("expected ThresholdQuorum") + } + + tt, ok := val.Threshold.(*PercentageThresholdPercent) + if !ok { + t.Fatalf("expected Percent in threshold") + } + + if *tt != 1 { + t.Fatalf("expected 1, got %d", *tt) + } + + tq, ok := val.Quorum.(*PercentageThresholdPercent) + if !ok { + t.Fatalf("expected Percent in quorum") + } + + if *tq != 1 { + t.Fatalf("expected 1, got %d", *tq) + } +} + +func TestProposalJSON(t *testing.T) { + props := []Proposal{ + { + ID: 0, + Title: "Prop #0", + Description: "Wolol0\n\t\r", + Proposer: "0x1234567890", + Votes: Votes{ + Yes: 7, + No: 21, + Abstain: 42, + }, + Expiration: dao_utils.ExpirationAtHeight(1000), + Ballots: avl.NewTree(), + }, + { + ID: 1, + Title: "Prop #1", + Description: `Wolol1\"`, + Proposer: "0x1234567890", + Status: ProposalStatusExecuted, + Expiration: dao_utils.ExpirationAtHeight(2000), + Messages: []dao_interfaces.ExecutableMessage{NoopMessage{}, NoopMessage{}, NoopMessage{}}, + }, + } + + props[0].Ballots.Set("0x1234567890", Ballot{Power: 1, Vote: VoteYes, Rationale: "test"}) + iSlice := make([]*json.Node, len(props)) + for i, p := range props { + iSlice[i] = p.ToJSON() + } + + str := json.ArrayNode("", iSlice).String() + expected := `[{"id":"0","title":"Prop #0","description":"Wolol0\n\t\r","proposer":"0x1234567890","startHeight":"0","totalPower":"0","messages":[],"status":"Open","votes":{"yes":"7","no":"21","abstain":"42"},"allowRevoting":false,"ballots":{"0x1234567890":{"power":"1","vote":"Yes","rationale":"test"}},"expiration":{"atHeight":"1000"}},{"id":"1","title":"Prop #1","description":"Wolol1\\\"","proposer":"0x1234567890","startHeight":"0","totalPower":"0","messages":[{"type":"noop-type","payload":"noop"},{"type":"noop-type","payload":"noop"},{"type":"noop-type","payload":"noop"}],"status":"Executed","votes":{"yes":"0","no":"0","abstain":"0"},"allowRevoting":false,"ballots":{},"expiration":{"atHeight":"2000"}}]` + if expected != str { + t.Fatalf("JSON does not match, expected %s, got %s", expected, str) + } +} + +func TestConfig(t *testing.T) { + core := dao_interfaces.NewDummyCore() + tt := PercentageThresholdPercent(1) + tq := PercentageThresholdPercent(1) + mod := NewDAOProposalSingle(core, &DAOProposalSingleOpts{ + MaxVotingPeriod: dao_utils.DurationHeight(42), + MinVotingPeriod: dao_utils.DurationHeight(21), + Threshold: &ThresholdThresholdQuorum{ + Threshold: &tt, + Quorum: &tq, + }, + }) + conf := mod.ConfigJSON() + expected := `{"threshold":{"thresholdQuorum":{"threshold":{"percent":1},"quorum":{"percent":1}}},"maxVotingPeriod":{"height":42},"minVotingPeriod":{"height":21},"onlyMembersExecute":false,"allowRevoting":false,"closeProposalOnExecutionFailure":false}` + if expected != conf { + t.Fatalf("Config JSON does not match, expected %s, got %s", expected, conf) + } +} diff --git a/examples/gno.land/p/teritori/dao_proposal_single/threshold.gno b/examples/gno.land/p/teritori/dao_proposal_single/threshold.gno new file mode 100644 index 00000000000..5c4226fd80f --- /dev/null +++ b/examples/gno.land/p/teritori/dao_proposal_single/threshold.gno @@ -0,0 +1,151 @@ +package dao_proposal_single + +import ( + "strconv" + "strings" + + "gno.land/p/demo/json" + "gno.land/p/teritori/jsonutil" +) + +// ported from https://github.com/DA0-DA0/dao-contracts/blob/7776858e780f1ce9f038a3b06cce341dd41d2189/packages/dao-voting/src/threshold.rs + +type PercentageThreshold interface { + String() string + Clone() PercentageThreshold + ToJSON() *json.Node +} + +func PercentageThresholdFromJSON(ast *json.Node) PercentageThreshold { + variant, valueNode := jsonutil.MustUnion(ast) + switch variant { + case "majority": + return &PercentageThresholdMajority{} + + case "percent": + p := PercentageThresholdPercent(0) + p.FromJSON(valueNode) + return &p + } + + panic("unknown PercentageThreshold variant") +} + +type PercentageThresholdMajority struct{} + +func (p *PercentageThresholdMajority) String() string { + return "Majority" +} + +func (p *PercentageThresholdMajority) Clone() PercentageThreshold { + return &PercentageThresholdMajority{} +} + +func (p *PercentageThresholdMajority) ToJSON() *json.Node { + return jsonutil.UnionNode("majority", jsonutil.EmptyObjectNode()) +} + +type PercentageThresholdPercent uint16 // 4 decimals fixed point + +func (p *PercentageThresholdPercent) String() string { + sb := strings.Builder{} + sb.WriteString(strconv.FormatUint(uint64(*p)/100, 10)) + decPart := uint64(*p) % 100 + if decPart != 0 { + sb.WriteRune('.') + sb.WriteString(strconv.FormatUint(decPart, 10)) + } + + sb.WriteRune('%') + return sb.String() +} + +func (p *PercentageThresholdPercent) FromJSON(ast *json.Node) { + val := ast.MustNumeric() // FIXME: don't pass by float64 + *p = PercentageThresholdPercent(val) +} + +func (p *PercentageThresholdPercent) Clone() PercentageThreshold { + c := *p + return &c +} + +func (p *PercentageThresholdPercent) ToJSON() *json.Node { + return jsonutil.UnionNode("percent", json.NumberNode("", float64(*p))) +} + +type Threshold interface { + Clone() Threshold + ToJSON() *json.Node +} + +func ThresholdFromJSON(ast *json.Node) Threshold { + variant, valueNode := jsonutil.MustUnion(ast) + switch variant { + case "absolutePercentage": + panic("not implemented") + + case "thresholdQuorum": + t := &ThresholdThresholdQuorum{} + t.FromJSON(valueNode) + return t + + case "absoluteCount": + val := valueNode.MustNumeric() + ac := ThresholdAbsoluteCount(val) + return &ac + } + + panic("unknown Threshold variant") +} + +type ThresholdAbsolutePercentage struct { + Value PercentageThreshold +} + +func (t ThresholdAbsolutePercentage) Clone() Threshold { + c := t.Value.Clone() + return &ThresholdAbsolutePercentage{Value: c} +} + +func (t ThresholdAbsolutePercentage) ToJSON() *json.Node { + return jsonutil.UnionNode("absolutePercentage", t.Value.ToJSON()) +} + +type ThresholdThresholdQuorum struct { + Threshold PercentageThreshold + Quorum PercentageThreshold +} + +func (t *ThresholdThresholdQuorum) Clone() Threshold { + return &ThresholdThresholdQuorum{ + Threshold: t.Threshold.Clone(), + Quorum: t.Quorum.Clone(), + } +} + +// TODO: make union members json utilities bijective + +func (t *ThresholdThresholdQuorum) FromJSON(ast *json.Node) { + obj := ast.MustObject() + t.Threshold = PercentageThresholdFromJSON(obj["threshold"]) + t.Quorum = PercentageThresholdFromJSON(obj["quorum"]) +} + +func (t *ThresholdThresholdQuorum) ToJSON() *json.Node { + return jsonutil.UnionNode("thresholdQuorum", json.ObjectNode("", map[string]*json.Node{ + "threshold": t.Threshold.ToJSON(), + "quorum": t.Quorum.ToJSON(), + })) +} + +type ThresholdAbsoluteCount uint64 + +func (t *ThresholdAbsoluteCount) Clone() Threshold { + val := *t + return &val +} + +func (t *ThresholdAbsoluteCount) ToJSON() *json.Node { + return jsonutil.UnionNode("absoluteCount", jsonutil.Uint64Node(uint64(*t))) +} diff --git a/examples/gno.land/p/teritori/dao_proposal_single/types.gno b/examples/gno.land/p/teritori/dao_proposal_single/types.gno new file mode 100644 index 00000000000..b1d8e5fcedf --- /dev/null +++ b/examples/gno.land/p/teritori/dao_proposal_single/types.gno @@ -0,0 +1,216 @@ +package dao_proposal_single + +import ( + "std" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/json" + dao_interfaces "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/dao_utils" + "gno.land/p/teritori/jsonutil" +) + +type Ballot struct { + Power uint64 + Vote Vote + Rationale string +} + +func (b Ballot) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "power": jsonutil.Uint64Node(b.Power), + "vote": b.Vote.ToJSON(), + "rationale": json.StringNode("", b.Rationale), + }) +} + +func (b Ballot) String() string { + return b.Vote.String() + " with power " + strconv.FormatUint(b.Power, 10) + "" + ": " + b.Rationale +} + +type Votes struct { + Yes uint64 + No uint64 + Abstain uint64 +} + +func (v *Votes) Add(vote Vote, power uint64) { + switch vote { + case VoteYes: + v.Yes += power + case VoteNo: + v.No += power + case VoteAbstain: + v.Abstain += power + default: + panic("unknown vote kind") + } +} + +func (v *Votes) Remove(vote Vote, power uint64) { + switch vote { + case VoteYes: + v.Yes -= power + case VoteNo: + v.No -= power + case VoteAbstain: + v.Abstain -= power + default: + panic("unknown vote kind") + } +} + +func (v *Votes) Total() uint64 { + return v.Yes + v.No + v.Abstain +} + +func (v Votes) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "yes": jsonutil.Uint64Node(v.Yes), + "no": jsonutil.Uint64Node(v.No), + "abstain": jsonutil.Uint64Node(v.Abstain), + }) +} + +type Proposal struct { + ID int + Title string + Description string + Proposer std.Address + StartHeight int64 + MinVotingPeriod dao_utils.Expiration + Expiration dao_utils.Expiration + Threshold Threshold + TotalPower uint64 + Messages []dao_interfaces.ExecutableMessage + Status ProposalStatus + Votes Votes + AllowRevoting bool + + // not in DA0-DA0 implementation: + + Ballots *avl.Tree +} + +type messageWithType struct { + Type string + Message dao_interfaces.ExecutableMessage +} + +func (m *messageWithType) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "type": json.StringNode("", m.Type), + "payload": m.Message.ToJSON(), + }) +} + +func formatMessages(messages []dao_interfaces.ExecutableMessage) []*json.Node { + out := make([]*json.Node, len(messages)) + for i, m := range messages { + mt := messageWithType{ + Type: m.Type(), + Message: m, + } + out[i] = mt.ToJSON() + } + + return out +} + +func (p Proposal) ToJSON() *json.Node { + fields := map[string]*json.Node{ + "id": jsonutil.IntNode(p.ID), + "title": json.StringNode("", p.Title), + "description": json.StringNode("", p.Description), + "proposer": jsonutil.AddressNode(p.Proposer), + "startHeight": jsonutil.Int64Node(p.StartHeight), + "totalPower": jsonutil.Uint64Node(p.TotalPower), + "messages": json.ArrayNode("", formatMessages(p.Messages)), + "status": p.Status.ToJSON(), + "votes": p.Votes.ToJSON(), + "allowRevoting": json.BoolNode("", p.AllowRevoting), + "ballots": jsonutil.AVLTreeNode(p.Ballots, func(v interface{}) *json.Node { return v.(Ballot).ToJSON() }), + } + + if p.MinVotingPeriod != nil { + fields["minVotingPeriod"] = p.MinVotingPeriod.ToJSON() + } + + if p.Expiration != nil { + fields["expiration"] = p.Expiration.ToJSON() + } + + if p.Threshold != nil { + fields["threshold"] = p.Threshold.ToJSON() + } + + return json.ObjectNode("", fields) +} + +type ProposalStatus int + +const ( + ProposalStatusOpen ProposalStatus = iota + ProposalStatusPassed + ProposalStatusExecuted +) + +func (p ProposalStatus) ToJSON() *json.Node { + return json.StringNode("", p.String()) +} + +func (p ProposalStatus) String() string { + switch p { + case ProposalStatusOpen: + return "Open" + case ProposalStatusPassed: + return "Passed" + case ProposalStatusExecuted: + return "Executed" + default: + return "Unknown(" + strconv.Itoa(int(p)) + ")" + } +} + +type Vote int + +const ( + VoteYes Vote = iota + VoteNo + VoteAbstain +) + +func (v Vote) ToJSON() *json.Node { + return json.StringNode("", v.String()) +} + +func (v *Vote) FromJSON(ast *json.Node) { + v.FromString(ast.MustString()) +} + +func (v *Vote) FromString(s string) { + switch s { + case "Yes": + *v = VoteYes + case "No": + *v = VoteNo + case "Abstain": + *v = VoteAbstain + default: + panic("unknown vote kind") + } +} + +func (v Vote) String() string { + switch v { + case VoteYes: + return "Yes" + case VoteNo: + return "No" + case VoteAbstain: + return "Abstain" + default: + return "Unknown(" + strconv.Itoa(int(v)) + ")" + } +} diff --git a/examples/gno.land/p/teritori/dao_proposal_single/update_settings.gno b/examples/gno.land/p/teritori/dao_proposal_single/update_settings.gno new file mode 100644 index 00000000000..b3d682b78a0 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_proposal_single/update_settings.gno @@ -0,0 +1,79 @@ +package dao_proposal_single + +import ( + "strings" + + "gno.land/p/demo/json" + "gno.land/p/teritori/dao_interfaces" +) + +type UpdateSettingsMessage struct { + dao_interfaces.ExecutableMessage + + Threshold Threshold +} + +var _ dao_interfaces.ExecutableMessage = (*UpdateSettingsMessage)(nil) + +func (usm UpdateSettingsMessage) Type() string { + return "gno.land/p/teritori/dao_proposal_single.UpdateSettings" +} + +func (usm *UpdateSettingsMessage) String() string { + ss := []string{usm.Type()} + switch usm.Threshold.(type) { + case *ThresholdThresholdQuorum: + ss = append(ss, "Threshold type: ThresholdQuorum\n\nThreshold: "+usm.Threshold.(*ThresholdThresholdQuorum).Threshold.String()+"\n\nQuorum: "+usm.Threshold.(*ThresholdThresholdQuorum).Quorum.String()) + + default: + ss = append(ss, "Threshold type: unknown") + + } + + return strings.Join(ss, "\n--\n") +} + +func (usm *UpdateSettingsMessage) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "threshold": usm.Threshold.ToJSON(), + }) +} + +func (usm *UpdateSettingsMessage) FromJSON(ast *json.Node) { + obj := ast.MustObject() + usm.Threshold = ThresholdFromJSON(obj["threshold"]) +} + +func NewUpdateSettingsHandler(mod *DAOProposalSingle) dao_interfaces.MessageHandler { + return &updateSettingsHandler{mod: mod} +} + +type updateSettingsHandler struct { + dao_interfaces.MessageHandler + + mod *DAOProposalSingle +} + +var _ dao_interfaces.MessageHandler = (*updateSettingsHandler)(nil) + +func (h *updateSettingsHandler) Execute(message dao_interfaces.ExecutableMessage) { + usm := message.(*UpdateSettingsMessage) + + switch usm.Threshold.(type) { + case *ThresholdThresholdQuorum: + // FIXME: validate better + h.mod.opts.Threshold = usm.Threshold.(*ThresholdThresholdQuorum) + return + + default: + panic("unsupported threshold type") + } +} + +func (h updateSettingsHandler) Type() string { + return UpdateSettingsMessage{}.Type() +} + +func (h *updateSettingsHandler) Instantiate() dao_interfaces.ExecutableMessage { + return &UpdateSettingsMessage{} +} diff --git a/examples/gno.land/p/teritori/dao_utils/expiration.gno b/examples/gno.land/p/teritori/dao_utils/expiration.gno new file mode 100644 index 00000000000..e8397e8a633 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_utils/expiration.gno @@ -0,0 +1,99 @@ +package dao_utils + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/json" + "gno.land/p/teritori/jsonutil" +) + +// loosely ported from https://github.com/CosmWasm/cw-utils/blob/7fce8a214f2f1e7763b8718dcbd2a6dd07f30988/src/expiration.rs + +type ( + Expiration interface { + IsExpired() bool + ToJSON() *json.Node + String() string + } + ExpirationAtHeight int64 + ExpirationAtTime time.Time + ExpirationNever struct{} +) + +func (e ExpirationAtHeight) IsExpired() bool { + return std.GetHeight() >= int64(e) +} + +func (e ExpirationAtHeight) ToJSON() *json.Node { + val := jsonutil.Int64Node(int64(e)) + return jsonutil.UnionNode("atHeight", val) +} + +func (e ExpirationAtHeight) String() string { + return strconv.FormatInt(int64(e), 10) +} + +func (e ExpirationAtTime) IsExpired() bool { + t := time.Time(e) + now := time.Now() + return now.Equal(t) || now.After(t) +} + +func (e ExpirationAtTime) ToJSON() *json.Node { + val := jsonutil.TimeNode(time.Time(e)) + return jsonutil.UnionNode("atTime", val) +} + +func (e ExpirationAtTime) String() string { + return time.Time(e).String() +} + +func (e ExpirationNever) IsExpired() bool { + return false +} + +func (e ExpirationNever) ToJSON() *json.Node { + return jsonutil.UnionNode("never", jsonutil.EmptyObjectNode()) +} + +func (e ExpirationNever) String() string { + return "Never" +} + +type ( + Duration interface { + AfterCurrentBlock() Expiration + ToJSON() *json.Node + String() string + } + DurationHeight int64 + DurationTime time.Duration +) + +func (d DurationHeight) AfterCurrentBlock() Expiration { + return ExpirationAtHeight(std.GetHeight() + int64(d)) +} + +func (d DurationHeight) ToJSON() *json.Node { + val := json.NumberNode("", float64(d)) + return jsonutil.UnionNode("height", val) +} + +func (d DurationHeight) String() string { + return strconv.FormatInt(int64(d), 10) +} + +func (d DurationTime) AfterCurrentBlock() Expiration { + return ExpirationAtTime(time.Now().Add(time.Duration(d))) +} + +func (d DurationTime) ToJSON() *json.Node { + val := jsonutil.DurationNode(time.Duration(d)) + return jsonutil.UnionNode("time", val) +} + +func (d DurationTime) String() string { + return time.Duration(d).String() +} diff --git a/examples/gno.land/p/teritori/dao_utils/expiration_test.gno b/examples/gno.land/p/teritori/dao_utils/expiration_test.gno new file mode 100644 index 00000000000..a4d76246478 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_utils/expiration_test.gno @@ -0,0 +1,60 @@ +package dao_utils + +import ( + "testing" + "time" + + "gno.land/p/demo/json" +) + +func TestMatch(t *testing.T) { + ex := ExpirationNever{} + switch Expiration(ex).(type) { + case ExpirationNever: + t.Log("ExpirationNever") + default: + t.Errorf("expected a match") + } +} + +func TestJSONNever(t *testing.T) { + ex := ExpirationNever{} + node := ex.ToJSON() + b, err := json.Marshal(node) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := `{"never":{}}` + if string(b) != expected { + t.Errorf("expected %v, got %v", expected, string(b)) + } +} + +func TestJSONAtTime(t *testing.T) { + ex := ExpirationAtTime(time.Unix(1712830928, 0)) + node := ex.ToJSON() + b, err := json.Marshal(node) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := `{"atTime":"2024-04-11T10:22:08Z"}` + if string(b) != expected { + t.Errorf("expected %v, got %v", expected, string(b)) + } +} + +func TestJSONAtHeight(t *testing.T) { + ex := ExpirationAtHeight(123456) + node := ex.ToJSON() + b, err := json.Marshal(node) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expected := `{"atHeight":"123456"}` + if string(b) != expected { + t.Errorf("expected %v, got %v", expected, string(b)) + } +} diff --git a/examples/gno.land/p/teritori/dao_utils/gno.mod b/examples/gno.land/p/teritori/dao_utils/gno.mod new file mode 100644 index 00000000000..af1c9ddac33 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_utils/gno.mod @@ -0,0 +1 @@ +module gno.land/p/teritori/dao_utils diff --git a/examples/gno.land/p/teritori/dao_voting_group/gno.mod b/examples/gno.land/p/teritori/dao_voting_group/gno.mod new file mode 100644 index 00000000000..482dca89fbd --- /dev/null +++ b/examples/gno.land/p/teritori/dao_voting_group/gno.mod @@ -0,0 +1 @@ +module gno.land/p/teritori/dao_voting_group diff --git a/examples/gno.land/p/teritori/dao_voting_group/messages.gno b/examples/gno.land/p/teritori/dao_voting_group/messages.gno new file mode 100644 index 00000000000..4dc2c1de360 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_voting_group/messages.gno @@ -0,0 +1,62 @@ +package dao_voting_group + +import ( + "gno.land/p/demo/json" + "gno.land/p/teritori/dao_interfaces" +) + +const updateMembersType = "gno.land/p/teritori/dao_voting_group.UpdateMembers" + +type UpdateMembersExecutableMessage []Member + +var _ dao_interfaces.ExecutableMessage = (*UpdateMembersExecutableMessage)(nil) + +func (m *UpdateMembersExecutableMessage) FromJSON(ast *json.Node) { + changes := ast.MustArray() + *m = make([]Member, len(changes)) + for i, change := range changes { + (*m)[i].FromJSON(change) + } +} + +func (m *UpdateMembersExecutableMessage) ToJSON() *json.Node { + changes := make([]*json.Node, len(*m)) + for i, change := range *m { + changes[i] = change.ToJSON() + } + + return json.ArrayNode("", changes) +} + +func (m *UpdateMembersExecutableMessage) String() string { + return m.ToJSON().String() +} + +func (m *UpdateMembersExecutableMessage) Type() string { + return updateMembersType +} + +type updateMembersHandler struct { + vg *VotingGroup +} + +var _ dao_interfaces.MessageHandler = (*updateMembersHandler)(nil) + +func (h *updateMembersHandler) Type() string { + return updateMembersType +} + +func (h *updateMembersHandler) Execute(msg dao_interfaces.ExecutableMessage) { + m, ok := msg.(*UpdateMembersExecutableMessage) + if !ok { + panic("unexpected message type") + } + + for _, change := range *m { + h.vg.SetMemberPower(change.Address, change.Power) + } +} + +func (h *updateMembersHandler) Instantiate() dao_interfaces.ExecutableMessage { + return &UpdateMembersExecutableMessage{} +} diff --git a/examples/gno.land/p/teritori/dao_voting_group/voting_group.gno b/examples/gno.land/p/teritori/dao_voting_group/voting_group.gno new file mode 100644 index 00000000000..cca5c5beafd --- /dev/null +++ b/examples/gno.land/p/teritori/dao_voting_group/voting_group.gno @@ -0,0 +1,184 @@ +package dao_voting_group + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/json" + dao_interfaces "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/havl" + "gno.land/p/teritori/jsonutil" +) + +type Member struct { + Address std.Address + Power uint64 +} + +func (m Member) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "address": jsonutil.AddressNode(m.Address), + "power": jsonutil.Uint64Node(m.Power), + }) +} + +func (m *Member) FromJSON(ast *json.Node) { + obj := ast.MustObject() + m.Address = jsonutil.MustAddress(obj["address"]) + m.Power = jsonutil.MustUint64(obj["power"]) +} + +type VotingGroup struct { + dao_interfaces.IVotingModule + + powerByAddr *havl.Tree // std.Address -> uint64 + totalPower *havl.Tree // "" -> uint64 + memberCount *havl.Tree // "" -> uint32 +} + +func NewVotingGroup() *VotingGroup { + return &VotingGroup{ + powerByAddr: havl.NewTree(), + totalPower: havl.NewTree(), + memberCount: havl.NewTree(), + } +} + +func (v *VotingGroup) Info() dao_interfaces.ModuleInfo { + return dao_interfaces.ModuleInfo{ + Kind: "gno.land/p/teritori/dao_voting_group", + Version: "0.1.0", + } +} + +func (v *VotingGroup) ConfigJSON() string { + return json.ObjectNode("", map[string]*json.Node{ + "totalPower": jsonutil.Uint64Node(v.TotalPowerAtHeight(havl.Latest, []string{})), + "members": jsonutil.Uint32Node(v.MemberCount(havl.Latest)), + }).String() +} + +func (v *VotingGroup) GetMembersJSON(start, end string, limit uint64, height int64) string { + members := v.GetMembers(start, end, limit, height) + membersJSON := make([]*json.Node, len(members)) + for i, m := range members { + membersJSON[i] = m.ToJSON() + } + return json.ArrayNode("", membersJSON).String() +} + +func (v *VotingGroup) VotingPowerAtHeight(addr std.Address, height int64, resources []string) uint64 { + _ = resources + p, ok := v.powerByAddr.Get(addr.String(), height) + if !ok { + return 0 + } + + return p.(uint64) +} + +func (v *VotingGroup) TotalPowerAtHeight(height int64, resources []string) uint64 { + _ = resources + p, ok := v.totalPower.Get("", height) + if !ok { + return 0 + } + + return p.(uint64) +} + +func (g *VotingGroup) SetMemberPower(addr std.Address, power uint64) { + if power == 0 { + g.RemoveMember(addr) + return + } + + iprevious, ok := g.powerByAddr.Get(addr.String(), havl.Latest) + if !ok { + g.memberCount.Set("", g.MemberCount(havl.Latest)+1) + } + + previous := uint64(0) + if ok { + previous = iprevious.(uint64) + } + + if power == previous { + return + } + + g.powerByAddr.Set(addr.String(), power) + + ipreviousTotal, ok := g.totalPower.Get("", havl.Latest) + previousTotal := uint64(0) + if ok { + previousTotal = ipreviousTotal.(uint64) + } + + g.totalPower.Set("", (previousTotal+power)-previous) +} + +func (g *VotingGroup) RemoveMember(addr std.Address) (uint64, bool) { + p, removed := g.powerByAddr.Remove(addr.String()) + if !removed { + return 0, false + } + + g.memberCount.Set("", g.MemberCount(havl.Latest)-1) + power := p.(uint64) + g.totalPower.Set("", g.TotalPowerAtHeight(havl.Latest, []string{})-power) + return power, true +} + +func (g *VotingGroup) UpdateMembersHandler() dao_interfaces.MessageHandler { + return &updateMembersHandler{vg: g} +} + +func (g *VotingGroup) MemberCount(height int64) uint32 { + val, ok := g.memberCount.Get("", height) + if !ok { + return 0 + } + + return val.(uint32) +} + +func (g *VotingGroup) GetMembers(start, end string, limit uint64, height int64) []Member { + var members []Member + g.powerByAddr.Iterate(start, end, height, func(k string, v interface{}) bool { + if limit > 0 && uint64(len(members)) >= limit { + return true + } + + members = append(members, Member{ + Address: std.Address(k), + Power: v.(uint64), + }) + + return false + }) + return members +} + +func (v *VotingGroup) Render(path string) string { + sb := strings.Builder{} + sb.WriteString("Member count: ") + sb.WriteString(strconv.FormatUint(uint64(v.MemberCount(havl.Latest)), 10)) + sb.WriteString("\n\n") + sb.WriteString("Total power: ") + sb.WriteString(strconv.FormatUint(v.TotalPowerAtHeight(havl.Latest, []string{}), 10)) + sb.WriteString("\n\n") + sb.WriteString("Members:\n") + v.powerByAddr.Iterate("", "", havl.Latest, func(k string, v interface{}) bool { + sb.WriteString("- ") + sb.WriteString(k) + sb.WriteString(": ") + sb.WriteString(strconv.FormatUint(v.(uint64), 10)) + sb.WriteRune('\n') + return false + }) + + sb.WriteRune('\n') + return sb.String() +} diff --git a/examples/gno.land/p/teritori/dao_voting_group/voting_group_test.gno b/examples/gno.land/p/teritori/dao_voting_group/voting_group_test.gno new file mode 100644 index 00000000000..fac489e5c33 --- /dev/null +++ b/examples/gno.land/p/teritori/dao_voting_group/voting_group_test.gno @@ -0,0 +1,29 @@ +package dao_voting_group + +import ( + "testing" + + dao_interfaces "gno.land/p/teritori/dao_interfaces" +) + +func TestVotingGroup(t *testing.T) { + v := NewVotingGroup() + var i dao_interfaces.IVotingModule + i = v + + { + got := i.TotalPowerAtHeight(0, []string{}) + expected := uint64(0) + if got != expected { + t.Fatalf("expected %s, got %s.", expected, got) + } + } + + { + conf := v.ConfigJSON() + expected := `{"totalPower":"0","members":"0"}` + if conf != expected { + t.Fatalf("expected %s, got %s.", expected, conf) + } + } +} diff --git a/examples/gno.land/p/teritori/havl/gno.mod b/examples/gno.land/p/teritori/havl/gno.mod new file mode 100644 index 00000000000..e611d513e22 --- /dev/null +++ b/examples/gno.land/p/teritori/havl/gno.mod @@ -0,0 +1 @@ +module gno.land/p/teritori/havl diff --git a/examples/gno.land/p/teritori/havl/havl.gno b/examples/gno.land/p/teritori/havl/havl.gno new file mode 100644 index 00000000000..2be4a4a0aee --- /dev/null +++ b/examples/gno.land/p/teritori/havl/havl.gno @@ -0,0 +1,133 @@ +package havl + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/avl" +) + +type Tree struct { + root avl.Tree // height -> *avl.Tree + initialHeight int64 +} + +var Latest = int64(0) + +// FIXME: this is not optimized at all, we make a full copy on write + +func NewTree() *Tree { + return &Tree{initialHeight: std.GetHeight()} +} + +func (t *Tree) Size(height int64) int { + snapshot, _ := t.GetSnapshot(height) + return snapshot.Size() +} + +func (t *Tree) Has(key string, height int64) (has bool) { + snapshot, _ := t.GetSnapshot(height) + return snapshot.Has(key) +} + +func (t *Tree) Get(key string, height int64) (value interface{}, exists bool) { + snapshot, _ := t.GetSnapshot(height) + return snapshot.Get(key) +} + +func (t *Tree) GetByIndex(index int, height int64) (key string, value interface{}) { + snapshot, _ := t.GetSnapshot(height) + return snapshot.GetByIndex(index) +} + +func (t *Tree) Set(key string, value interface{}) (updated bool) { + root := t.getOrCreateCurrentRoot() + return root.Set(key, value) +} + +func (t *Tree) Remove(key string) (value interface{}, removed bool) { + root := t.getOrCreateCurrentRoot() + return root.Remove(key) +} + +// Shortcut for TraverseInRange. +func (t *Tree) Iterate(start, end string, height int64, cb avl.IterCbFn) bool { + snapshot, _ := t.GetSnapshot(height) + return snapshot.Iterate(start, end, cb) +} + +// Shortcut for TraverseInRange. +func (t *Tree) ReverseIterate(start, end string, height int64, cb avl.IterCbFn) bool { + snapshot, _ := t.GetSnapshot(height) + return snapshot.ReverseIterate(start, end, cb) +} + +// Shortcut for TraverseByOffset. +func (t *Tree) IterateByOffset(offset int, count int, height int64, cb avl.IterCbFn) bool { + snapshot, _ := t.GetSnapshot(height) + return snapshot.IterateByOffset(offset, count, cb) +} + +// Shortcut for TraverseByOffset. +func (t *Tree) ReverseIterateByOffset(offset int, count int, height int64, cb avl.IterCbFn) bool { + snapshot, _ := t.GetSnapshot(height) + return snapshot.ReverseIterateByOffset(offset, count, cb) +} + +func (t *Tree) GetSnapshot(height int64) (*avl.Tree, int64) { + key := getPaddedKey(height) + var snapshot *avl.Tree + snapshotHeight := int(t.initialHeight) + t.root.ReverseIterate("", key, func(key string, value interface{}) bool { + snapshot = value.(*avl.Tree) + var err error + snapshotHeight, err = strconv.Atoi(key) + if err != nil { + panic("internal error: failed to unmarshal key") + } + return true + }) + + if snapshot == nil { + snapshot = avl.NewTree() + } + + return snapshot, int64(snapshotHeight) +} + +// utils + +func getPaddedKey(height int64) string { + if height <= 0 { + height = std.GetHeight() + } + + val := strconv.Itoa(int(height)) + return strings.Repeat("0", len("9223372036854775807")-len(val)) + val +} + +func clone(t *avl.Tree) *avl.Tree { + r := avl.NewTree() + t.Iterate("", "", func(key string, value interface{}) bool { + r.Set(key, value) + return false + }) + + return r +} + +func (t *Tree) getOrCreateCurrentRoot() *avl.Tree { + key := getPaddedKey(0) + iroot, ok := t.root.Get(key) + var root *avl.Tree + if ok { + root = iroot.(*avl.Tree) + } else { + snapshot, _ := t.GetSnapshot(0) + root = clone(snapshot) + t.root.Set(key, root) + } + + return root +} diff --git a/examples/gno.land/p/teritori/jsonutil/gno.mod b/examples/gno.land/p/teritori/jsonutil/gno.mod new file mode 100644 index 00000000000..69b843a2eb2 --- /dev/null +++ b/examples/gno.land/p/teritori/jsonutil/gno.mod @@ -0,0 +1 @@ +module gno.land/p/teritori/jsonutil diff --git a/examples/gno.land/p/teritori/jsonutil/jsonutil.gno b/examples/gno.land/p/teritori/jsonutil/jsonutil.gno new file mode 100644 index 00000000000..0184c53c325 --- /dev/null +++ b/examples/gno.land/p/teritori/jsonutil/jsonutil.gno @@ -0,0 +1,131 @@ +package jsonutil + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/json" + "gno.land/p/demo/users" +) + +func UnionNode(variant string, value *json.Node) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + variant: value, + }) +} + +func MustUnion(value *json.Node) (string, *json.Node) { + obj := value.MustObject() + for key, value := range obj { + return key, value + } + + panic("no variant in union") +} + +func TimeNode(value time.Time) *json.Node { + j, err := value.MarshalJSON() + if err != nil { + panic(err) + } + + return json.StringNode("", string(j[1:len(j)-1])) +} + +func MustTime(value *json.Node) time.Time { + t := time.Time{} + err := t.UnmarshalJSON([]byte(value.String())) + if err != nil { + panic(err) + } + + return t +} + +func DurationNode(value time.Duration) *json.Node { + return Int64Node(value.Nanoseconds()) +} + +func EmptyObjectNode() *json.Node { + return json.ObjectNode("", nil) +} + +// int is always 64 bits in gno so we need a string to represent it without loss of precision in a lot of javascript environment, I wish bigint in json was more widely supported +func IntNode(value int) *json.Node { + return json.StringNode("", strconv.Itoa(value)) +} + +func MustInt(value *json.Node) int { + i, err := strconv.Atoi(value.MustString()) + if err != nil { + panic(err) + } + + return i +} + +func Uint32Node(value uint32) *json.Node { + return json.StringNode("", strconv.FormatUint(uint64(value), 10)) +} + +func MustUint32(value *json.Node) uint32 { + return uint32(MustInt(value)) +} + +func Int64Node(value int64) *json.Node { + return json.StringNode("", strconv.FormatInt(value, 10)) +} + +func MustInt64(value *json.Node) int64 { + return int64(MustInt(value)) +} + +func Uint64Node(value uint64) *json.Node { + return json.StringNode("", strconv.FormatUint(value, 10)) +} + +func MustUint64(value *json.Node) uint64 { + return uint64(MustInt(value)) // FIXME: full uint64 range support (currently limited to [-2^63, 2^63-1]) +} + +func AVLTreeNode(root *avl.Tree, transform func(elem interface{}) *json.Node) *json.Node { + if root == nil { + return EmptyObjectNode() + } + + fields := make(map[string]*json.Node) + root.Iterate("", "", func(key string, val interface{}) bool { + fields[key] = transform(val) + return false + }) + + return json.ObjectNode("", fields) +} + +func AddressNode(addr std.Address) *json.Node { + return json.StringNode("", addr.String()) +} + +func MustAddress(value *json.Node) std.Address { + addr := std.Address(value.MustString()) + if !addr.IsValid() { + panic("invalid address") + } + + return addr +} + +func AddressOrNameNode(aon users.AddressOrName) *json.Node { + return json.StringNode("", string(aon)) +} + +func MustAddressOrName(value *json.Node) users.AddressOrName { + aon := users.AddressOrName(value.MustString()) + if !aon.IsValid() { + panic("invalid address or name") + } + + return aon +} 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..25636fcda78 100644 --- a/examples/gno.land/r/demo/bar20/bar20.gno +++ b/examples/gno.land/r/demo/bar20/bar20.gno @@ -9,21 +9,22 @@ 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() + getter := func() *grc20.Token { return Token } + grc20reg.Register(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 +36,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..c6cf6397b3a 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"] 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/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..5c7d7f12b99 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,51 @@ 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) + getter := func() *grc20.Token { return Token } + grc20reg.Register(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 +62,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 +83,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 index 7a4fc173d3d..e6a428090a9 100644 --- a/examples/gno.land/r/demo/games/shifumi/gno.mod +++ b/examples/gno.land/r/demo/games/shifumi/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/games/shifumi - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/seqid v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest -) diff --git a/examples/gno.land/r/demo/games/shifumi/shifumi.gno b/examples/gno.land/r/demo/games/shifumi/shifumi.gno index 9094cb8fd69..3de09196da1 100644 --- a/examples/gno.land/r/demo/games/shifumi/shifumi.gno +++ b/examples/gno.land/r/demo/games/shifumi/shifumi.gno @@ -86,8 +86,8 @@ func Render(path string) string { output := `# 👊 ✋ ✌️ Shifumi Actions: -* [NewGame](shifumi?help&__func=NewGame) opponentAddress -* [Play](shifumi?help&__func=Play) gameIndex move (1=rock, 2=paper, 3=scissors) +* [NewGame](shifumi$help&func=NewGame) opponentAddress +* [Play](shifumi$help&func=Play) gameIndex move (1=rock, 2=paper, 3=scissors) game | player1 | | player2 | | win --- | --- | --- | --- | --- | --- 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..58874409d7f 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,69 @@ 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) + getter := func() *grc20.Token { return token } + grc20reg.Register(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 +105,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 +141,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 +163,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/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/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/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/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 61b11c09b80..4d7fd15d1cd 100644 --- a/examples/gno.land/r/demo/users/gno.mod +++ b/examples/gno.land/r/demo/users/gno.mod @@ -1,7 +1 @@ module gno.land/r/demo/users - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/avlhelpers 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 4a0b9c1caf7..8547a6e60e0 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -7,6 +7,7 @@ import ( "strings" "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" "gno.land/p/demo/avlhelpers" "gno.land/p/demo/users" ) @@ -15,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 @@ -301,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 @@ -323,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_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..09538b860ca 100644 --- a/examples/gno.land/r/demo/wugnot/wugnot.gno +++ b/examples/gno.land/r/demo/wugnot/wugnot.gno @@ -7,26 +7,30 @@ 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() { + getter := func() *grc20.Token { return Token } + grc20reg.Register(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 +45,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 +54,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 +79,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 index 3cdef10d9dc..0855dc327cd 100644 --- a/examples/gno.land/r/gnoland/pages/page_contribute.gno +++ b/examples/gno.land/r/gnoland/pages/page_contribute.gno @@ -45,7 +45,7 @@ Don't fear your work being "stolen": if a submission is the result of multiple p - 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/1aXrZ6japdAykB5FLmHCCeBZTo-2tbZQHSQi79ITaTK0). +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 @@ -80,7 +80,7 @@ _[3XL]_ \* | $ 32000 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 diff --git a/examples/gno.land/r/gnoland/pages/page_ecosystem.gno b/examples/gno.land/r/gnoland/pages/page_ecosystem.gno index c6e7c22ae48..514ea7b2a98 100644 --- a/examples/gno.land/r/gnoland/pages/page_ecosystem.gno +++ b/examples/gno.land/r/gnoland/pages/page_ecosystem.gno @@ -3,48 +3,48 @@ 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. +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. 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. Play the game at [Flippando](https://gno.flippando.xyz/flip) 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_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 074e80e1892..16984a1c7ff 100644 --- a/examples/gno.land/r/gnoland/pages/pages_test.gno +++ b/examples/gno.land/r/gnoland/pages/pages_test.gno @@ -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 index cbc1e537e3f..bc800ec8263 100644 --- a/examples/gno.land/r/leon/config/config.gno +++ b/examples/gno.land/r/leon/config/config.gno @@ -8,6 +8,9 @@ import ( 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() { @@ -24,7 +27,7 @@ func Backup() std.Address { func SetAddress(a std.Address) error { if !a.IsValid() { - return errors.New("config: invalid address") + return ErrInvalidAddr } if err := checkAuthorized(); err != nil { @@ -37,7 +40,7 @@ func SetAddress(a std.Address) error { func SetBackup(a std.Address) error { if !a.IsValid() { - return errors.New("config: invalid address") + return ErrInvalidAddr } if err := checkAuthorized(); err != nil { @@ -50,16 +53,11 @@ func SetBackup(a std.Address) error { func checkAuthorized() error { caller := std.PrevRealm().Addr() - if caller != main || caller != backup { - return errors.New("config: unauthorized") + isAuthorized := caller == main || caller == backup + + if !isAuthorized { + return ErrUnauthorized } return nil } - -func AssertAuthorized() { - caller := std.PrevRealm().Addr() - if caller != main || caller != backup { - panic("config: unauthorized") - } -} 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 index 48cf64a9d0a..56fea265e29 100644 --- a/examples/gno.land/r/leon/home/gno.mod +++ b/examples/gno.land/r/leon/home/gno.mod @@ -1,8 +1 @@ module gno.land/r/leon/home - -require ( - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/r/demo/art/gnoface v0.0.0-latest - gno.land/r/demo/art/millipede v0.0.0-latest - gno.land/r/leon/config v0.0.0-latest -) diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno index 1f6a07e8959..cf33260cc6b 100644 --- a/examples/gno.land/r/leon/home/home.gno +++ b/examples/gno.land/r/leon/home/home.gno @@ -8,7 +8,9 @@ import ( "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 ( @@ -31,16 +33,25 @@ My contributions to gno.land can mainly be found TODO import r/gh `, } + + hof.Register() + mirror.Register(std.CurrentRealm().PkgPath(), Render) } func UpdatePFP(url, caption string) { - config.AssertAuthorized() + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + pfp = url pfpCaption = caption } func UpdateAboutMe(col1, col2 string) { - config.AssertAuthorized() + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + abtMe[0] = col1 abtMe[1] = col2 } @@ -119,3 +130,7 @@ func renderMillipede() string { 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/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/gno.mod b/examples/gno.land/r/morgan/guestbook/gno.mod index 2591643d33d..ac63a4cf8cd 100644 --- a/examples/gno.land/r/morgan/guestbook/gno.mod +++ b/examples/gno.land/r/morgan/guestbook/gno.mod @@ -1,7 +1 @@ module gno.land/r/morgan/guestbook - -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 -) diff --git a/examples/gno.land/r/morgan/guestbook/guestbook.gno b/examples/gno.land/r/morgan/guestbook/guestbook.gno index b3a56d88397..be9e9db6133 100644 --- a/examples/gno.land/r/morgan/guestbook/guestbook.gno +++ b/examples/gno.land/r/morgan/guestbook/guestbook.gno @@ -83,7 +83,7 @@ func validateMessage(msg string) string { 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") + bld.WriteString("# Guestbook 📝\n\n[Come sign the guestbook!](./guestbook$help&func=Sign)\n\n---\n\n") var maxIDBinary string if maxID != "" { diff --git a/examples/gno.land/r/morgan/home/home.gno b/examples/gno.land/r/morgan/home/home.gno index 33d7e0b2df7..20b66b895e3 100644 --- a/examples/gno.land/r/morgan/home/home.gno +++ b/examples/gno.land/r/morgan/home/home.gno @@ -1,10 +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/manfred/present/admin.gno b/examples/gno.land/r/moul/present/admin.gno similarity index 97% rename from examples/gno.land/r/manfred/present/admin.gno rename to examples/gno.land/r/moul/present/admin.gno index 60af578b54f..ab99b1725c5 100644 --- a/examples/gno.land/r/manfred/present/admin.gno +++ b/examples/gno.land/r/moul/present/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" } func AdminSetAdminAddr(addr std.Address) { 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/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..9586f377311 --- /dev/null +++ b/examples/gno.land/r/stefann/home/home.gno @@ -0,0 +1,303 @@ +package home + +import ( + "sort" + "std" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" + + "gno.land/r/stefann/registry" +) + +type City struct { + Name string + URL string +} + +type Sponsor struct { + Address std.Address + Amount std.Coins +} + +type Profile struct { + pfp string + 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()) + + profile = Profile{ + pfp: "https://i.ibb.co/Bc5YNCx/DSC-0095a.jpg", + 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: 5, + 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 UpdatePFP(url string) { + owner.AssertCallerIsOwner() + profile.pfp = url +} + +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 += "
\n\n" + + out += ufmt.Sprintf("
\n\n", travel.cities[travel.currentCityIndex%len(travel.cities)].URL) + + out += ufmt.Sprintf("my profile pic\n\n", profile.pfp) + + out += "
\n\n" + + for _, rows := range profile.aboutMe { + out += "
\n\n" + out += rows + "\n\n" + out += "
\n\n" + } + + out += "
\n\n" + + return out +} + +func renderTips() string { + out := `
` + "\n\n" + + out += `
` + "\n" + + out += `

Help Me Travel The World

` + "\n\n" + + out += renderTipsJar() + "\n" + + out += ufmt.Sprintf(`I am currently in %s,
tip the jar to send me somewhere else!
`, 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 += renderSponsors() + + out += `
` + "\n\n" + + out += `
` + "\n" + + return out +} + +func formatAddress(address string) string { + if len(address) <= 8 { + return address + } + return address[:4] + "..." + address[len(address)-4:] +} + +func renderSponsors() string { + out := `

Sponsor Leaderboard

` + "\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 + } + + out += `
    ` + "\n" + + for i := 0; i < numSponsors; i++ { + sponsor := topSponsors[i] + isLastItem := (i == numSponsors-1) + + padding := "10px 5px" + border := "border-bottom: 1px solid #ddd;" + + if isLastItem { + padding = "8px 5px" + border = "" + } + + out += ufmt.Sprintf( + `
  • + %d. %s + %s +
  • `, + padding, border, i+1, formatAddress(sponsor.Address.String()), sponsor.Amount.String(), + ) + } + + return out +} + +func renderTipsJar() string { + out := ufmt.Sprintf(``, travel.jarLink) + "\n" + + out += `Tips Jar` + "\n" + + out += `` + "\n" + + return out +} 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..ca146b9eb13 --- /dev/null +++ b/examples/gno.land/r/stefann/home/home_test.gno @@ -0,0 +1,291 @@ +package home + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/testutils" +) + +func TestUpdatePFP(t *testing.T) { + var owner = std.Address("g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8") + std.TestSetOrigCaller(owner) + + profile.pfp = "" + + UpdatePFP("https://example.com/pic.png") + + if profile.pfp != "https://example.com/pic.png" { + t.Fatalf("expected pfp to be https://example.com/pic.png, got %s", profile.pfp) + } +} + +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/teritori/dao_realm/dao_realm.gno b/examples/gno.land/r/teritori/dao_realm/dao_realm.gno new file mode 100644 index 00000000000..c09ff4f790d --- /dev/null +++ b/examples/gno.land/r/teritori/dao_realm/dao_realm.gno @@ -0,0 +1,132 @@ +package dao_realm + +import ( + "std" + "time" + + "gno.land/r/teritori/dao_registry" + "gno.land/r/teritori/tori" + + dao_core "gno.land/p/teritori/dao_core" + dao_interfaces "gno.land/p/teritori/dao_interfaces" + proposal_single "gno.land/p/teritori/dao_proposal_single" + "gno.land/p/teritori/dao_utils" + voting_group "gno.land/p/teritori/dao_voting_group" +) + +// Example DAO realm + +var ( + daoCore dao_interfaces.IDAOCore + group *voting_group.VotingGroup + registered bool +) + +func init() { + votingModuleFactory := func(core dao_interfaces.IDAOCore) dao_interfaces.IVotingModule { + group = voting_group.NewVotingGroup() + group.SetMemberPower("g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c", 1) + group.SetMemberPower("g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv", 1) + group.SetMemberPower("g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a", 1) + group.SetMemberPower("g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym", 1) + group.SetMemberPower(std.GetOrigCaller(), 1) + return group + } + + // TODO: consider using factories that return multiple modules and handlers + + proposalModulesFactories := []dao_interfaces.ProposalModuleFactory{ + func(core dao_interfaces.IDAOCore) dao_interfaces.IProposalModule { + tt := proposal_single.PercentageThresholdPercent(100) + tq := proposal_single.PercentageThresholdPercent(100) + return proposal_single.NewDAOProposalSingle(core, &proposal_single.DAOProposalSingleOpts{ + MaxVotingPeriod: dao_utils.DurationTime(time.Hour * 24 * 42), + Threshold: &proposal_single.ThresholdThresholdQuorum{ + Threshold: &tt, // 1% + Quorum: &tq, // 1% + }, + }) + }, + } + + messageHandlersFactories := []dao_interfaces.MessageHandlerFactory{ + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return group.UpdateMembersHandler() + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + // TODO: add a router to support multiple proposal modules + propMod := core.ProposalModules()[0] + return proposal_single.NewUpdateSettingsHandler(propMod.Module.(*proposal_single.DAOProposalSingle)) + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return tori.NewMintToriHandler() + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return tori.NewBurnToriHandler() + }, + func(core dao_interfaces.IDAOCore) dao_interfaces.MessageHandler { + return tori.NewChangeAdminHandler() + }, + } + + daoCore = dao_core.NewDAOCore(votingModuleFactory, proposalModulesFactories, messageHandlersFactories) + + // dao_registry.Register(func() dao_interfaces.IDAOCore { return daoCore }, "DAO Realm", "Default testing DAO", "https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1080&fit=max") +} + +// FIXME: the registry is currently broken in 'gno test', see https://github.com/gnolang/gno/issues/1852 +// so we're exposing a way to register after DAO instantiation +func RegisterSelf() { + if registered { + panic("already registered") + } + + dao_registry.Register(func() dao_interfaces.IDAOCore { return daoCore }, "DAO Realm", "Default testing DAO", "https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1080&fit=max") + registered = true +} + +func Render(path string) string { + return daoCore.Render(path) +} + +func VoteJSON(moduleIndex int, proposalID int, voteJSON string) { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + module.Module.VoteJSON(proposalID, voteJSON) +} + +func Execute(moduleIndex int, proposalID int) { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + module.Module.Execute(proposalID) +} + +func ProposeJSON(moduleIndex int, proposalJSON string) int { + // move check in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + if !module.Enabled { + panic("proposal module is not enabled") + } + + return module.Module.ProposeJSON(proposalJSON) +} + +func getProposalsJSON(moduleIndex int, limit int, startAfter string, reverse bool) string { + // move logic in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + return module.Module.ProposalsJSON(limit, startAfter, reverse) +} + +func getProposalJSON(moduleIndex int, proposalIndex int) string { + // move logic in dao core + module := dao_core.GetProposalModule(daoCore, moduleIndex) + return module.Module.ProposalJSON(proposalIndex) +} diff --git a/examples/gno.land/r/teritori/dao_realm/dao_realm_test.gno b/examples/gno.land/r/teritori/dao_realm/dao_realm_test.gno new file mode 100644 index 00000000000..414b82ea6ba --- /dev/null +++ b/examples/gno.land/r/teritori/dao_realm/dao_realm_test.gno @@ -0,0 +1,117 @@ +package dao_realm + +import ( + "fmt" + "testing" + + "gno.land/p/demo/json" + "gno.land/p/teritori/dao_voting_group" + "gno.land/p/teritori/havl" +) + +func TestInit(t *testing.T) { + { + proposalsJSON := getProposalsJSON(0, 42, "TODO", false) + expected := `[]` + if proposalsJSON != expected { + t.Fatalf("Expected %s, got %s", expected, proposalsJSON) + } + } + + { + members := group.GetMembers("", "", 0, havl.Latest) + iSlice := make([]*json.Node, len(members)) + for i, v := range members { + iSlice[i] = v.ToJSON() + } + + membersJSON := json.ArrayNode("", iSlice).String() + expected := `[{"address":"g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv","power":"1"},{"address":"g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a","power":"1"},{"address":"g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c","power":"1"},{"address":"g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym","power":"1"},{"address":"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm","power":"1"}]` + if membersJSON != expected { + t.Fatalf("Expected:\n%s\nGot:\n%s", expected, membersJSON) + } + } +} + +func TestUpdateMembers(t *testing.T) { + var membersJSON string + + { + id := ProposeJSON(0, fmt.Sprintf(`{"title": "Test prop", "description": "A description", "messages": [{"type": "gno.land/p/teritori/dao_voting_group.UpdateMembers", "payload": [{"power": "2", "address": "g18syxa0vh0vmne90mwhtynjet0zgeqf6prh3ryy"}]}]}`)) + VoteJSON(0, id, `{"vote": "Yes", "rationale": "testing"}`) + Execute(0, id) + members := group.GetMembers("", "", 0, havl.Latest) + iSlice := make([]*json.Node, len(members)) + for i, v := range members { + iSlice[i] = v.ToJSON() + } + + membersJSON = json.ArrayNode("", iSlice).String() + expected := fmt.Sprintf(`[{"address":"g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv","power":"1"},{"address":"g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a","power":"1"},{"address":"g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c","power":"1"},{"address":"g18syxa0vh0vmne90mwhtynjet0zgeqf6prh3ryy","power":"2"},{"address":"g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym","power":"1"},{"address":"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm","power":"1"}]`) + if membersJSON != expected { + t.Errorf("Expected:\n%s\nGot:\n%s", expected, membersJSON) + } + + totalPower := group.TotalPowerAtHeight(havl.Latest, nil) + if totalPower != 7 { + t.Errorf("Expected total power to be 6, got %d", totalPower) + } + } + + { + children := json.Must(json.Unmarshal([]byte(membersJSON))).MustArray() + if len(children) != 6 { + t.Errorf("Expected 6 members, got %d", len(children)) + } + + var member dao_voting_group.Member + member.FromJSON(children[0]) + + id := ProposeJSON(0, fmt.Sprintf(`{"title": "Test prop 2", "description": "A description", "messages": [{"type": "gno.land/p/teritori/dao_voting_group.UpdateMembers", "payload": [{"address": "%s", "power": "0"}]}]}`, member.Address.String())) + VoteJSON(0, id, `{"vote": "Yes", "rationale": "testing"}`) + Execute(0, id) + members := group.GetMembers("", "", 0, havl.Latest) + iSlice := make([]*json.Node, len(members)) + for i, v := range members { + iSlice[i] = v.ToJSON() + } + + membersJSON := json.ArrayNode("", iSlice).String() + expected := `[{"address":"g14u5eaheavy0ux4dmpykg2gvxpvqvexm9cyg58a","power":"1"},{"address":"g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c","power":"1"},{"address":"g18syxa0vh0vmne90mwhtynjet0zgeqf6prh3ryy","power":"2"},{"address":"g1ckn395mpttp0vupgtratyufdaakgh8jgkmr3ym","power":"1"},{"address":"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm","power":"1"}]` + if membersJSON != expected { + t.Errorf("Expected:\n%s\nGot:\n%s", expected, membersJSON) + } + + totalPower := group.TotalPowerAtHeight(havl.Latest, nil) + if totalPower != 6 { + t.Errorf("Expected total power to be 6, got %d", totalPower) + } + } +} + +func TestUpdateSettings(t *testing.T) { + // not sure why but in this test the proposal ids start at 3 and the voting power is 5 when all tests are run, shouldn't tests be isolated? TODO: investigate + + { + id := ProposeJSON(0, fmt.Sprintf(`{"title": "Test prop", "description": "A description", "messages": [{"type": "gno.land/p/teritori/dao_proposal_single.UpdateSettings", "payload": {"threshold": {"thresholdQuorum": {"threshold": {"percent": 200}, "quorum": {"percent": 200}}}}}]}`)) + VoteJSON(0, id, `{"vote": "Yes", "rationale": "testing"}`) + Execute(0, id) + proposalJSON := getProposalJSON(0, id) + expected := fmt.Sprintf(`{"id":"%d","title":"Test prop","description":"A description","proposer":"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm","startHeight":"123","totalPower":"6","messages":[{"type":"gno.land/p/teritori/dao_proposal_single.UpdateSettings","payload":{"threshold":{"thresholdQuorum":{"threshold":{"percent":200},"quorum":{"percent":200}}}}}],"status":"Executed","votes":{"yes":"1","no":"0","abstain":"0"},"allowRevoting":false,"ballots":{"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm":{"power":"1","vote":"Yes","rationale":"testing"}},"expiration":{"atTime":"2009-03-27T23:31:30Z"},"threshold":{"thresholdQuorum":{"threshold":{"percent":100},"quorum":{"percent":100}}}}`, id) + if proposalJSON != expected { + t.Fatalf("Expected:\n%s\nGot:\n%s", expected, proposalJSON) + } + } + + { + // make sentiment proposal + id := ProposeJSON(0, fmt.Sprintf(`{"title": "Test prop", "description": "A description", "messages": []}`)) + VoteJSON(0, id, `{"vote": "Yes", "rationale": "testing"}`) + Execute(0, id) + proposalJSON := getProposalJSON(0, id) + expected := fmt.Sprintf(`{"id":"%d","title":"Test prop","description":"A description","proposer":"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm","startHeight":"123","totalPower":"6","messages":[],"status":"Executed","votes":{"yes":"1","no":"0","abstain":"0"},"allowRevoting":false,"ballots":{"g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm":{"power":"1","vote":"Yes","rationale":"testing"}},"expiration":{"atTime":"2009-03-27T23:31:30Z"},"threshold":{"thresholdQuorum":{"threshold":{"percent":200},"quorum":{"percent":200}}}}`, id) + if proposalJSON != expected { + t.Fatalf("Expected:\n%s\nGot:\n%s", expected, proposalJSON) + } + } +} diff --git a/examples/gno.land/r/teritori/dao_realm/gno.mod b/examples/gno.land/r/teritori/dao_realm/gno.mod new file mode 100644 index 00000000000..e918be4606e --- /dev/null +++ b/examples/gno.land/r/teritori/dao_realm/gno.mod @@ -0,0 +1 @@ +module gno.land/r/teritori/dao_realm diff --git a/examples/gno.land/r/teritori/dao_registry/dao_registry.gno b/examples/gno.land/r/teritori/dao_registry/dao_registry.gno new file mode 100644 index 00000000000..9b1b7c20f32 --- /dev/null +++ b/examples/gno.land/r/teritori/dao_registry/dao_registry.gno @@ -0,0 +1,123 @@ +package dao_registry + +import ( + "encoding/binary" + "std" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/json" + "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/jsonutil" +) + +type Registration struct { + PkgPath string `json:"pkgPath"` + Addr std.Address `json:"addr"` + CreatedAt time.Time `json:"createdAt"` + Name string `json:"name"` + Description string `json:"description"` + ImageURI string `json:"imageURI"` + Getter DAOGetter `json:"-"` +} + +var ( + byCreatedAt = avl.NewTree() + byPkgPath = avl.NewTree() +) + +// TODO: use profile realm for info + +type DAOGetter func() dao_interfaces.IDAOCore + +func Register(getter DAOGetter, name string, description string, imageURI string) { + realm := std.PrevRealm() + pkgPath := realm.PkgPath() + if pkgPath == "" { + panic("caller is not a realm") + } + + doRegister(getter, pkgPath, realm.Addr(), name, description, imageURI) +} + +// splitted for tests +func doRegister(getter DAOGetter, pkgPath string, addr std.Address, name string, description string, imageURI string) { + if byPkgPath.Has(pkgPath) { + panic("already registered") + } + + registration := &Registration{ + PkgPath: pkgPath, + Addr: addr, + CreatedAt: time.Now(), + Name: name, + Description: description, + ImageURI: imageURI, + Getter: getter, + } + byPkgPath.Set(pkgPath, registration) + + keyBytes := []byte{} + keyBytes = binary.BigEndian.AppendUint64(keyBytes, uint64(registration.CreatedAt.UnixMilli())) // won't be before unix epoch + keyBytes = append(keyBytes, []byte(pkgPath)...) + key := string(keyBytes) + byCreatedAt.Set(key, registration) +} + +func GetCore(pkgPath string) dao_interfaces.IDAOCore { + r, ok := byPkgPath.Get(pkgPath) + if !ok { + panic("not found") + } + + return r.(*Registration).Getter() +} + +func GetJSON(pkgPath string) string { + r, ok := byPkgPath.Get(pkgPath) + if !ok { + panic("not found") + } + + return r.(*Registration).ToJSON().String() +} + +func List(start string, end string, limit int, reverse bool) []Registration { + regs := []Registration{} + cb := func(key string, value interface{}) bool { + regs = append(regs, *value.(*Registration)) + if limit == 0 { + return false + } + return len(regs) >= limit + } + + if reverse { + byCreatedAt.ReverseIterate(start, end, cb) + } else { + byCreatedAt.Iterate(start, end, cb) + } + + return regs +} + +func ListJSON(start string, end string, limit int, reverse bool) string { + regs := List(start, end, limit, reverse) + iSlice := make([]*json.Node, len(regs)) + for i, v := range regs { + iSlice[i] = v.ToJSON() + } + + return json.ArrayNode("", iSlice).String() +} + +func (r Registration) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "pkgPath": json.StringNode("", r.PkgPath), + "addr": jsonutil.AddressNode(r.Addr), + "createdAt": jsonutil.TimeNode(r.CreatedAt), + "name": json.StringNode("", r.Name), + "description": json.StringNode("", r.Description), + "imageURI": json.StringNode("", r.ImageURI), + }) +} diff --git a/examples/gno.land/r/teritori/dao_registry/dao_registry_test.gno b/examples/gno.land/r/teritori/dao_registry/dao_registry_test.gno new file mode 100644 index 00000000000..73ad99b22ff --- /dev/null +++ b/examples/gno.land/r/teritori/dao_registry/dao_registry_test.gno @@ -0,0 +1,58 @@ +package dao_registry + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/ufmt" + "gno.land/p/teritori/dao_interfaces" +) + +func TestRegistry(t *testing.T) { + registrations := List("", "", 0, false) + if len(registrations) != 0 { + t.Fatal("expected 0 registrations") + } + + registrationsJSON := ListJSON("", "", 0, false) + if registrationsJSON != "[]" { + t.Fatal("expected empty JSON array") + } + + pkgPath := "gno.land/r/demo/test_dao" + addr := std.DerivePkgAddr(pkgPath) + createdAt := time.Now() + core := dao_interfaces.NewDummyCore() + doRegister(func() dao_interfaces.IDAOCore { return core }, pkgPath, addr, "Test DAO", `This is a test DAO"\n\r\t\`+"\n\r\t", "https://example.com/image.png") + + registrations = List("", "", 0, false) + if len(registrations) != 1 { + t.Fatal("expected 1 registration") + } + + reg := registrations[0] + if reg.PkgPath != pkgPath { + t.Fatal("expected pkgPath to match") + } + + if reg.Addr != addr { + t.Fatal("expected addr to match") + } + + if !reg.CreatedAt.Equal(createdAt) { + t.Fatal("expected createdAt to match") + } + + registrationsJSON = ListJSON("", "", 0, false) + expected := ufmt.Sprintf(`[{"pkgPath":"gno.land/r/demo/test_dao","addr":"g1m56y6xlx95sykjchn9h9q4e7k5u2zte4as9rdr","createdAt":"2009-02-13T23:31:30Z","name":"Test DAO","description":"This is a test DAO\"\\n\\r\\t\\\n\r\t","imageURI":"https://example.com/image.png"}]`) + if registrationsJSON != expected { + t.Fatal("expected JSON to match, got:\n" + registrationsJSON + "\nexpected:\n" + expected) + } + + registrationJSON := GetJSON(pkgPath) + expected = ufmt.Sprintf(`{"pkgPath":"gno.land/r/demo/test_dao","addr":"g1m56y6xlx95sykjchn9h9q4e7k5u2zte4as9rdr","createdAt":"2009-02-13T23:31:30Z","name":"Test DAO","description":"This is a test DAO\"\\n\\r\\t\\\n\r\t","imageURI":"https://example.com/image.png"}`) + if registrationJSON != expected { + t.Fatal("expected JSON to match, got:\n" + registrationJSON + "\nexpected:\n" + expected) + } +} diff --git a/examples/gno.land/r/teritori/dao_registry/gno.mod b/examples/gno.land/r/teritori/dao_registry/gno.mod new file mode 100644 index 00000000000..20c310adff8 --- /dev/null +++ b/examples/gno.land/r/teritori/dao_registry/gno.mod @@ -0,0 +1 @@ +module gno.land/r/teritori/dao_registry diff --git a/examples/gno.land/r/teritori/tori/gno.mod b/examples/gno.land/r/teritori/tori/gno.mod new file mode 100644 index 00000000000..213ef54615f --- /dev/null +++ b/examples/gno.land/r/teritori/tori/gno.mod @@ -0,0 +1 @@ +module gno.land/r/teritori/tori diff --git a/examples/gno.land/r/teritori/tori/messages.gno b/examples/gno.land/r/teritori/tori/messages.gno new file mode 100644 index 00000000000..30adafc73af --- /dev/null +++ b/examples/gno.land/r/teritori/tori/messages.gno @@ -0,0 +1,190 @@ +package tori + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/json" + "gno.land/p/demo/users" + "gno.land/p/teritori/dao_interfaces" + "gno.land/p/teritori/jsonutil" +) + +// TODO: move this file in a generic package to administrate grc20s via daos + +type ExecutableMessageMintTori struct { + dao_interfaces.ExecutableMessage + + Recipient std.Address + Amount uint64 +} + +var _ dao_interfaces.ExecutableMessage = &ExecutableMessageMintTori{} + +func (msg ExecutableMessageMintTori) Type() string { + return "gno.land/r/teritori/tori.MintTori" +} + +func (msg *ExecutableMessageMintTori) String() string { + sb := strings.Builder{} + sb.WriteString(msg.Type()) + sb.WriteString("\n---\n") + sb.WriteString("Recipient: ") + sb.WriteString(string(msg.Recipient)) + sb.WriteRune('\n') + sb.WriteString("Amount: ") + sb.WriteString(strconv.FormatUint(msg.Amount, 10)) + return sb.String() +} + +func (msg *ExecutableMessageMintTori) FromJSON(ast *json.Node) { + obj := ast.MustObject() + msg.Recipient = jsonutil.MustAddress(obj["recipient"]) + msg.Amount = jsonutil.MustUint64(obj["amount"]) +} + +func (msg *ExecutableMessageMintTori) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "recipient": jsonutil.AddressNode(msg.Recipient), + "amount": jsonutil.Uint64Node(msg.Amount), + }) +} + +type MintToriHandler struct { + dao_interfaces.MessageHandler +} + +var _ dao_interfaces.MessageHandler = &MintToriHandler{} + +func NewMintToriHandler() *MintToriHandler { + return &MintToriHandler{} +} + +func (h *MintToriHandler) Execute(imsg dao_interfaces.ExecutableMessage) { + msg := imsg.(*ExecutableMessageMintTori) + Mint(users.AddressOrName(msg.Recipient), msg.Amount) +} + +func (h MintToriHandler) Type() string { + return ExecutableMessageMintTori{}.Type() +} + +func (h *MintToriHandler) Instantiate() dao_interfaces.ExecutableMessage { + return &ExecutableMessageMintTori{} +} + +type ExecutableMessageBurnTori struct { + dao_interfaces.ExecutableMessage + + Target users.AddressOrName + Amount uint64 +} + +var _ dao_interfaces.ExecutableMessage = &ExecutableMessageBurnTori{} + +func (msg ExecutableMessageBurnTori) Type() string { + return "gno.land/r/teritori/tori.BurnTori" +} + +func (msg *ExecutableMessageBurnTori) String() string { + sb := strings.Builder{} + sb.WriteString(msg.Type()) + sb.WriteString("\n---\n") + sb.WriteString("Target: ") + sb.WriteString(string(msg.Target)) + sb.WriteRune('\n') + sb.WriteString("Amount: ") + sb.WriteString(strconv.FormatUint(msg.Amount, 10)) + return sb.String() +} + +func (msg *ExecutableMessageBurnTori) FromJSON(ast *json.Node) { + obj := ast.MustObject() + msg.Target = jsonutil.MustAddressOrName(obj["target"]) + msg.Amount = jsonutil.MustUint64(obj["amount"]) +} + +func (msg *ExecutableMessageBurnTori) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "target": jsonutil.AddressOrNameNode(msg.Target), + "amount": jsonutil.Uint64Node(msg.Amount), + }) +} + +type BurnToriHandler struct { + dao_interfaces.MessageHandler +} + +var _ dao_interfaces.MessageHandler = &BurnToriHandler{} + +func NewBurnToriHandler() *BurnToriHandler { + return &BurnToriHandler{} +} + +func (h *BurnToriHandler) Execute(imsg dao_interfaces.ExecutableMessage) { + msg := imsg.(*ExecutableMessageBurnTori) + Burn(msg.Target, msg.Amount) +} + +func (h BurnToriHandler) Type() string { + return ExecutableMessageBurnTori{}.Type() +} + +func (h *BurnToriHandler) Instantiate() dao_interfaces.ExecutableMessage { + return &ExecutableMessageBurnTori{} +} + +type ExecutableMessageChangeAdmin struct { + dao_interfaces.ExecutableMessage + + NewAdmin std.Address +} + +var _ dao_interfaces.ExecutableMessage = &ExecutableMessageChangeAdmin{} + +func (msg ExecutableMessageChangeAdmin) Type() string { + return "gno.land/r/teritori/tori.ChangeAdmin" +} + +func (msg *ExecutableMessageChangeAdmin) String() string { + var ss []string + ss = append(ss, msg.Type()) + s := "New admin: " + string(msg.NewAdmin) + ss = append(ss, s) + return strings.Join(ss, "\n---\n") +} + +func (msg *ExecutableMessageChangeAdmin) FromJSON(ast *json.Node) { + obj := ast.MustObject() + msg.NewAdmin = jsonutil.MustAddress(obj["newAdmin"]) +} + +func (msg *ExecutableMessageChangeAdmin) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "newAdmin": jsonutil.AddressNode(msg.NewAdmin), + }) +} + +type ChangeAdminHandler struct { + dao_interfaces.MessageHandler +} + +var _ dao_interfaces.MessageHandler = &ChangeAdminHandler{} + +func NewChangeAdminHandler() *ChangeAdminHandler { + return &ChangeAdminHandler{} +} + +func (h *ChangeAdminHandler) Execute(imsg dao_interfaces.ExecutableMessage) { + msg := imsg.(*ExecutableMessageChangeAdmin) + owner.TransferOwnership(msg.NewAdmin) +} + +func (h ChangeAdminHandler) Type() string { + return ExecutableMessageChangeAdmin{}.Type() +} + +func (h *ChangeAdminHandler) Instantiate() dao_interfaces.ExecutableMessage { + return &ExecutableMessageChangeAdmin{} +} diff --git a/examples/gno.land/r/teritori/tori/tori.gno b/examples/gno.land/r/teritori/tori/tori.gno new file mode 100644 index 00000000000..0fe2dadd0e8 --- /dev/null +++ b/examples/gno.land/r/teritori/tori/tori.gno @@ -0,0 +1,100 @@ +// tori is a copy of foo20 that can be administred by a dao +package tori + +import ( + "std" + "strings" + + "gno.land/p/demo/grc/grc20" + "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 ( + Token, privateLedger = grc20.NewToken("Tori", "TORI", 4) + UserTeller = Token.CallerTeller() + owner = ownable.NewWithAddress(std.DerivePkgAddr("gno.land/r/teritori/dao_realm")) +) + +func init() { + privateLedger.Mint(owner.Owner(), 1_000_000*10_000) + getter := func() *grc20.Token { return Token } + grc20reg.Register(getter, "") +} + +func TotalSupply() uint64 { + return UserTeller.TotalSupply() +} + +func BalanceOf(owner pusers.AddressOrName) uint64 { + ownerAddr := users.Resolve(owner) + return UserTeller.BalanceOf(ownerAddr) +} + +func Allowance(owner, spender pusers.AddressOrName) uint64 { + ownerAddr := users.Resolve(owner) + spenderAddr := users.Resolve(spender) + return UserTeller.Allowance(ownerAddr, spenderAddr) +} + +func Transfer(to pusers.AddressOrName, amount uint64) { + toAddr := users.Resolve(to) + checkErr(UserTeller.Transfer(toAddr, amount)) +} + +func Approve(spender pusers.AddressOrName, amount uint64) { + spenderAddr := users.Resolve(spender) + checkErr(UserTeller.Approve(spenderAddr, amount)) +} + +func TransferFrom(from, to pusers.AddressOrName, amount uint64) { + fromAddr := users.Resolve(from) + toAddr := users.Resolve(to) + checkErr(UserTeller.TransferFrom(fromAddr, toAddr, amount)) +} + +// Faucet is distributing tori tokens without restriction (unsafe). +// For a real token faucet, you should take care of setting limits are asking payment. +func Faucet() { + caller := std.PrevRealm().Addr() + amount := uint64(1_000 * 10_000) // 1k + checkErr(privateLedger.Mint(caller, amount)) +} + +func Mint(to pusers.AddressOrName, amount uint64) { + owner.AssertCallerIsOwner() + toAddr := users.Resolve(to) + checkErr(privateLedger.Mint(toAddr, amount)) +} + +func Burn(from pusers.AddressOrName, amount uint64) { + owner.AssertCallerIsOwner() + fromAddr := users.Resolve(from) + checkErr(privateLedger.Burn(fromAddr, amount)) +} + +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return Token.RenderHome() + case c == 2 && parts[0] == "balance": + owner := pusers.AddressOrName(parts[1]) + ownerAddr := users.Resolve(owner) + balance := UserTeller.BalanceOf(ownerAddr) + return ufmt.Sprintf("%d\n", balance) + default: + return "404\n" + } +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} 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/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/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/start.go b/gno.land/cmd/gnoland/start.go index 21f0cb4b1a6..a420e652810 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,7 @@ 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,6 +44,12 @@ var startGraphic = strings.ReplaceAll(` /___/ `, "'", "`") +var ( + // Keep in sync with contribs/gnogenesis/internal/txs/txs_add_packages.go + genesisDeployAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 + 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 @@ -51,8 +59,6 @@ type startCfg struct { genesisFile string chainID string dataDir string - genesisMaxVMCycles int64 - config string lazyInit bool logLevel string @@ -137,20 +143,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", @@ -418,10 +410,10 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro genesisTxs = append(pkgsTxs, genesisTxs...) // Construct genesis AppState. - gen.AppState = gnoland.GnoGenesisState{ - Balances: balances, - Txs: genesisTxs, - } + defaultGenState := gnoland.DefaultGenState() + defaultGenState.Balances = balances + defaultGenState.Txs = genesisTxs + gen.AppState = defaultGenState // Write genesis state if err := gen.SaveAs(genesisFile); err != nil { diff --git a/gno.land/cmd/gnoland/testdata/addpkg_domain.txtar b/gno.land/cmd/gnoland/testdata/addpkg_domain.txtar new file mode 100644 index 00000000000..25e4fe0d3a3 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg_domain.txtar @@ -0,0 +1,15 @@ +gnoland start + +# addpkg with anotherdomain.land +! gnokey maketx addpkg -pkgdir $WORK -pkgpath anotherdomain.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout 'TX HASH:' +stderr 'invalid package path' +stderr 'invalid domain: anotherdomain.land/r/foobar/bar' + +# addpkg with gno.land +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout 'OK!' + +-- bar.gno -- +package bar +func Render(path string) string { return "hello" } diff --git a/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar b/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar index 5a88fd6d603..89da8a51820 100644 --- a/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar +++ b/gno.land/cmd/gnoland/testdata/addpkg_namespace.txtar @@ -4,7 +4,7 @@ loadpkg gno.land/r/sys/users adduser admin adduser gui -patchpkg "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" $USER_ADDR_admin # use our custom admin +patchpkg "g1manfred47kzduec920z88wfr64ylksmdcedlf5" $USER_ADDR_admin # use our custom admin gnoland start @@ -14,70 +14,72 @@ gnoland start # 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 +gnokey maketx call -pkgpath gno.land/r/sys/users -func IsEnabled -gas-fee 100000ugnot -gas-wanted 200000 -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 +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_test1/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 400000 -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 +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/randomname/mysuperpkg -gas-fee 1000000ugnot -gas-wanted 350000 -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 +gnokey maketx call -pkgpath gno.land/r/sys/users -func AdminEnable -gas-fee 100000ugnot -gas-wanted 1000000 -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 +gnokey maketx call -pkgpath gno.land/r/sys/users -func IsEnabled -gas-fee 100000ugnot -gas-wanted 200000 -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 +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_test1/one -gas-fee 1000000ugnot -gas-wanted 1000000 -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 +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/$USER_ADDR_gui/one -gas-fee 1000000ugnot -gas-wanted 1000000 -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 +# This is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1. +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/one -gas-fee 1000000ugnot -gas-wanted 1 -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 +gnokey maketx call -pkgpath gno.land/r/demo/users -func Invite -gas-fee 1000000ugnot -gas-wanted 2500000 -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 +gnokey maketx call -pkgpath gno.land/r/demo/users -func Register -gas-fee 1000000ugnot -gas-wanted 2500000 -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 +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/one -gas-fee 1000000ugnot -gas-wanted 1700000 -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 +# This is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1. +! gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/guiland/two -gas-fee 1000000ugnot -gas-wanted 1 -broadcast -chainid=tendermint_test admin stderr 'unauthorized user' -- one.gno -- diff --git a/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar b/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar new file mode 100644 index 00000000000..56050f4733b --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar @@ -0,0 +1,57 @@ +# ensure users get proper out of gas errors when they add packages + +# start a new node +gnoland start + +# add foo package +gnokey maketx addpkg -pkgdir $WORK/foo -pkgpath gno.land/r/foo -gas-fee 1000000ugnot -gas-wanted 220000 -broadcast -chainid=tendermint_test test1 + + +# add bar package +# out of gas at store.GetPackage() with gas 60000 + +! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 60000 -broadcast -chainid=tendermint_test test1 + +# Out of gas error + +stderr '--= Error =--' +stderr 'Data: out of gas error' +stderr 'Msg Traces:' +stderr 'out of gas.*?in preprocess' +stderr '--= /Error =--' + + + +# out of gas at store.store.GetTypeSafe() with gas 63000 + +! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 63000 -broadcast -chainid=tendermint_test test1 + +stderr '--= Error =--' +stderr 'Data: out of gas error' +stderr 'Msg Traces:' +stderr 'out of gas.*?in preprocess' +stderr '--= /Error =--' + + +-- foo/foo.gno -- +package foo + +type Counter int + +func Inc(i Counter) Counter{ + i = i+1 + return i +} + +-- bar/bar.gno -- +package bar + +import "gno.land/r/foo" + +type NewCounter foo.Counter + +func Add2(i NewCounter) NewCounter{ + i=i+2 + + return i +} diff --git a/gno.land/cmd/gnoland/testdata/append.txtar b/gno.land/cmd/gnoland/testdata/append.txtar index 46b66f9524b..c5c5272d3be 100644 --- a/gno.land/cmd/gnoland/testdata/append.txtar +++ b/gno.land/cmd/gnoland/testdata/append.txtar @@ -3,69 +3,69 @@ 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 +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/append -gas-fee 1000000ugnot -gas-wanted 9000000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 300000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func AppendNil -gas-fee 1000000ugnot -gas-wanted 300000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 300000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 300000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Pop -gas-fee 1000000ugnot -gas-wanted 300000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Append -gas-fee 1000000ugnot -gas-wanted 300000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func CopyAppend -gas-fee 1000000ugnot -gas-wanted 300000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func PopB -gas-fee 1000000ugnot -gas-wanted 350000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func AppendMoreAndC -gas-fee 1000000ugnot -gas-wanted 350000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func ReassignC -gas-fee 1000000ugnot -gas-wanted 350000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -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 +gnokey maketx call -pkgpath gno.land/r/append -func Render -gas-fee 1000000ugnot -gas-wanted 1500000 -args 'd' -broadcast -chainid=tendermint_test test1 stdout '("1-" string)' stdout OK! diff --git a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar index e3cd1be744a..2c4a27f9d06 100644 --- a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar +++ b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar @@ -9,18 +9,18 @@ # | 4 | | through /r/foo | myrealm.A() | PANIC | # | 5 | | | myrealm.B() | pass | # | 6 | | | myrealm.C() | PANIC | -# | 7 | | through /p/demo/bar | myrealm.A() | PANIC | -# | 8 | | | myrealm.B() | pass | -# | 9 | | | myrealm.C() | PANIC | +# | 7 | | through /p/demo/bar | bar.A() | PANIC | +# | 8 | | | bar.B() | pass | +# | 9 | | | bar.C() | PANIC | # | 10 | MsgRun | wallet direct | myrealm.A() | PANIC | # | 11 | | | myrealm.B() | pass | # | 12 | | | myrealm.C() | PANIC | # | 13 | | through /r/foo | myrealm.A() | PANIC | # | 14 | | | myrealm.B() | pass | # | 15 | | | myrealm.C() | PANIC | -# | 16 | | through /p/demo/bar | myrealm.A() | PANIC | -# | 17 | | | myrealm.B() | pass | -# | 18 | | | myrealm.C() | PANIC | +# | 16 | | through /p/demo/bar | bar.A() | PANIC | +# | 17 | | | bar.B() | pass | +# | 18 | | | bar.C() | PANIC | # | 19 | MsgCall | wallet direct | std.AssertOriginCall() | pass | # | 20 | MsgRun | wallet direct | std.AssertOriginCall() | PANIC | @@ -31,87 +31,89 @@ loadpkg gno.land/r/foo $WORK/r/foo loadpkg gno.land/p/demo/bar $WORK/p/demo/bar gnoland start +# The PANIC is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1. + # Test cases ## 1. MsgCall -> myrlm.A: PANIC -! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test test1 +! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 1 -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 +gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 150000 -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 +gnokey maketx call -pkgpath gno.land/r/myrlm -func C -gas-fee 100000ugnot -gas-wanted 700000 -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 +! gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 1 -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 +gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 200000 -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 +! gnokey maketx call -pkgpath gno.land/r/foo -func C -gas-fee 100000ugnot -gas-wanted 10000000 -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 +## 7. MsgCall -> p/demo/bar.A: PANIC +## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 5000000 -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 +## 8. MsgCall -> p/demo/bar.B: PASS +## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 5000000 -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 +## 9. MsgCall -> p/demo/bar.C: PANIC +## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 5000000 -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 +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 10000000 -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 +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -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 +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 10000000 -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 +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -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 +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 10000000 -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 +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5500000 -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 +## gnokey maketx call -pkgpath std -func AssertOriginCall -gas-fee 100000ugnot -gas-wanted 10000000 -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 +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 10000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno stderr 'invalid non-origin call' @@ -152,18 +154,19 @@ func C() { -- p/demo/bar/bar.gno -- package bar -import "gno.land/r/myrlm" +import "std" func A() { - myrlm.A() + C() } func B() { - myrlm.B() + if false { + C() + } } - func C() { - myrlm.C() + std.AssertOriginCall() } -- run/myrlmA.gno -- package main diff --git a/gno.land/cmd/gnoland/testdata/genesis_params.txtar b/gno.land/cmd/gnoland/testdata/genesis_params.txtar new file mode 100644 index 00000000000..d09ededf78a --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/genesis_params.txtar @@ -0,0 +1,28 @@ +# Test for #3003, #2911. + +gnoland start + +# Query and validate official parameters. +# These parameters should ideally be tested in a txtar format to ensure that a +# default initialization of "gnoland" provides the expected default values. + +# Verify the default chain domain parameter for Gno.land +gnokey query params/vm/gno.land/r/sys/params.vm.chain_domain.string +stdout 'data: "gno.land"$' + +# Test custom parameters to confirm they return the expected values and types. + +gnokey query params/vm/gno.land/r/sys/params.test.foo.string +stdout 'data: "bar"$' + +gnokey query params/vm/gno.land/r/sys/params.test.foo.int64 +stdout 'data: "-1337"' + +gnokey query params/vm/gno.land/r/sys/params.test.foo.uint64 +stdout 'data: "42"' + +gnokey query params/vm/gno.land/r/sys/params.test.foo.bool +stdout 'data: true' + +# TODO: Consider adding a test case for a byte array parameter + diff --git a/gno.land/cmd/gnoland/testdata/ghverify.txtar b/gno.land/cmd/gnoland/testdata/ghverify.txtar index f8cd05c762f..b53849e85b5 100644 --- a/gno.land/cmd/gnoland/testdata/ghverify.txtar +++ b/gno.land/cmd/gnoland/testdata/ghverify.txtar @@ -4,36 +4,37 @@ loadpkg gno.land/r/gnoland/ghverify 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 +gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func RequestVerification -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 3500000 -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 +gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'request' -gas-fee 1000000ugnot -gas-wanted 6000000 -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 +gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetHandleByAddress -args 'g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5' -gas-fee 1000000ugnot -gas-wanted 700000 -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 +gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetAddressByHandle -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 700000 -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 +# This is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1. +! gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'ingest,a' -gas-fee 1000000ugnot -gas-wanted 1 -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 +gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GnorkleEntrypoint -args 'ingest,g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5,OK' -gas-fee 1000000ugnot -gas-wanted 5000000 -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 +gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetHandleByAddress -args 'g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5' -gas-fee 1000000ugnot -gas-wanted 700000 -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 +gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func GetAddressByHandle -args 'deelawn' -gas-fee 1000000ugnot -gas-wanted 700000 -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 +gnokey maketx call -pkgpath gno.land/r/gnoland/ghverify -func Render -args '' -gas-fee 1000000ugnot -gas-wanted 700000 -broadcast -chainid=tendermint_test test1 +stdout '\("\{\\"deelawn\\": \\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\\"\}" string\)' diff --git a/gno.land/cmd/gnoland/testdata/grc20_invalid_address.txtar b/gno.land/cmd/gnoland/testdata/grc20_invalid_address.txtar index da903315333..d3dcc86725c 100644 --- a/gno.land/cmd/gnoland/testdata/grc20_invalid_address.txtar +++ b/gno.land/cmd/gnoland/testdata/grc20_invalid_address.txtar @@ -4,9 +4,10 @@ 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 +gnokey maketx call -pkgpath gno.land/r/demo/foo20 -func Faucet -gas-fee 1000000ugnot -gas-wanted 4000000 -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 +# This is expected to fail at the transaction simulation stage, which is why we set gas-wanted to 1. +! gnokey maketx call -pkgpath gno.land/r/demo/foo20 -func Transfer -args g1ubwj0apf60hd90txhnh855fkac34rxlsvua0aa -args 1 -gas-fee 1000000ugnot -gas-wanted 1 -broadcast -chainid=tendermint_test test1 +stderr '"gnokey" error: --= Error =--\nData: invalid address' diff --git a/gno.land/cmd/gnoland/testdata/grc20_registry.txtar b/gno.land/cmd/gnoland/testdata/grc20_registry.txtar index 20e78f7ba6e..df11e92f8db 100644 --- a/gno.land/cmd/gnoland/testdata/grc20_registry.txtar +++ b/gno.land/cmd/gnoland/testdata/grc20_registry.txtar @@ -6,15 +6,15 @@ loadpkg gno.land/r/registry $WORK/registry gnoland start # we call Transfer with foo20, before it's registered -gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 150000 -broadcast -chainid=tendermint_test test1 stdout 'not found' # add foo20, and foo20wrapper -gnokey maketx addpkg -pkgdir $WORK/foo20 -pkgpath gno.land/r/foo20 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 -gnokey maketx addpkg -pkgdir $WORK/foo20wrapper -pkgpath gno.land/r/foo20wrapper -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/foo20 -pkgpath gno.land/r/foo20 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/foo20wrapper -pkgpath gno.land/r/foo20wrapper -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 # we call Transfer with foo20, after it's registered -gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 800000 -broadcast -chainid=tendermint_test test1 stdout 'same address, success!' -- registry/registry.gno -- @@ -49,7 +49,7 @@ import "gno.land/r/registry" import "gno.land/r/foo20" func init() { - registry.Register("foo20", foo20.Transfer) + registry.Register("foo20", foo20.Transfer) } -- foo20/foo20.gno -- diff --git a/gno.land/cmd/gnoland/testdata/grc721_emit.txtar b/gno.land/cmd/gnoland/testdata/grc721_emit.txtar new file mode 100644 index 00000000000..6b4770e37c6 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/grc721_emit.txtar @@ -0,0 +1,95 @@ +# Test for https://github.com/gnolang/gno/pull/3102 +loadpkg gno.land/p/demo/grc/grc721 +loadpkg gno.land/r/demo/users +loadpkg gno.land/r/foo721 $WORK/foo721 + +gnoland start + +# Mint +gnokey maketx call -pkgpath gno.land/r/foo721 -func Mint -args g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -args 1 -gas-fee 1000000ugnot -gas-wanted 3500000 -broadcast -chainid=tendermint_test test1 +stdout '\[{\"type\":\"Mint\",\"attrs\":\[{\"key\":\"to\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno.land\/r\/foo721\",\"func\":\"mint\"}\]' + +# Approve +gnokey maketx call -pkgpath gno.land/r/foo721 -func Approve -args g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj -args 1 -gas-fee 1000000ugnot -gas-wanted 3500000 -broadcast -chainid=tendermint_test test1 +stdout '\[{\"type\":\"Approval\",\"attrs\":\[{\"key\":\"owner\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"to\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno.land\/r\/foo721\",\"func\":\"Approve\"}\]' + +# SetApprovalForAll +gnokey maketx call -pkgpath gno.land/r/foo721 -func SetApprovalForAll -args g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj -args false -gas-fee 1000000ugnot -gas-wanted 3500000 -broadcast -chainid=tendermint_test test1 +stdout '\[{\"type\":\"ApprovalForAll\",\"attrs\":\[{\"key\":\"owner\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"to\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"approved\",\"value\":\"false\"}],\"pkg_path\":\"gno\.land/r/foo721\",\"func\":\"setApprovalForAll\"}\]' + +# TransferFrom +gnokey maketx call -pkgpath gno.land/r/foo721 -func TransferFrom -args g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -args g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj -args 1 -gas-fee 1000000ugnot -gas-wanted 3500000 -broadcast -chainid=tendermint_test test1 +stdout '\[{\"type\":\"Transfer\",\"attrs\":\[{\"key\":\"from\",\"value\":\"g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5\"},{\"key\":\"to\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno\.land/r/foo721\",\"func\":\"transfer\"}\]' + +# Burn +gnokey maketx call -pkgpath gno.land/r/foo721 -func Burn -args 1 -gas-fee 1000000ugnot -gas-wanted 3500000 -broadcast -chainid=tendermint_test test1 +stdout '\[{\"type\":\"Burn\",\"attrs\":\[{\"key\":\"from\",\"value\":\"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj\"},{\"key\":\"tokenId\",\"value\":\"1\"}],\"pkg_path\":\"gno\.land/r/foo721\",\"func\":\"Burn\"}\]' + + +-- foo721/foo721.gno -- +package foo721 + +import ( + "std" + + "gno.land/p/demo/grc/grc721" + "gno.land/r/demo/users" + + pusers "gno.land/p/demo/users" +) + +var ( + admin std.Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + foo = grc721.NewBasicNFT("FooNFT", "FNFT") +) + +// Setters + +func Approve(user pusers.AddressOrName, tid grc721.TokenID) { + err := foo.Approve(users.Resolve(user), tid) + if err != nil { + panic(err) + } +} + +func SetApprovalForAll(user pusers.AddressOrName, approved bool) { + err := foo.SetApprovalForAll(users.Resolve(user), approved) + if err != nil { + panic(err) + } +} + +func TransferFrom(from, to pusers.AddressOrName, tid grc721.TokenID) { + err := foo.TransferFrom(users.Resolve(from), users.Resolve(to), tid) + if err != nil { + panic(err) + } +} + +// Admin + +func Mint(to pusers.AddressOrName, tid grc721.TokenID) { + caller := std.PrevRealm().Addr() + assertIsAdmin(caller) + err := foo.Mint(users.Resolve(to), tid) + if err != nil { + panic(err) + } +} + +func Burn(tid grc721.TokenID) { + caller := std.PrevRealm().Addr() + assertIsAdmin(caller) + err := foo.Burn(tid) + if err != nil { + panic(err) + } +} + +// Util + +func assertIsAdmin(address std.Address) { + if address != admin { + panic("restricted access") + } +} diff --git a/gno.land/cmd/gnoland/testdata/issue_1167.txtar b/gno.land/cmd/gnoland/testdata/issue_1167.txtar index c43f7a45bd5..7e33d61e9cd 100644 --- a/gno.land/cmd/gnoland/testdata/issue_1167.txtar +++ b/gno.land/cmd/gnoland/testdata/issue_1167.txtar @@ -4,11 +4,11 @@ loadpkg gno.land/p/demo/avl gnoland start # add contract -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/xx -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/xx -gas-fee 1000000ugnot -gas-wanted 8000000 -broadcast -chainid=tendermint_test test1 stdout OK! # execute New -gnokey maketx call -pkgpath gno.land/r/demo/xx -func New -args X -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/demo/xx -func New -args X -gas-fee 1000000ugnot -gas-wanted 700000 -broadcast -chainid=tendermint_test test1 stdout OK! # execute Delta for the first time diff --git a/gno.land/cmd/gnoland/testdata/issue_1786.txtar b/gno.land/cmd/gnoland/testdata/issue_1786.txtar index 7c92e81dfb6..0e66a882a6d 100644 --- a/gno.land/cmd/gnoland/testdata/issue_1786.txtar +++ b/gno.land/cmd/gnoland/testdata/issue_1786.txtar @@ -5,24 +5,24 @@ loadpkg gno.land/r/demo/wugnot gnoland start # add contract -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/proxywugnot -gas-fee 1000000ugnot -gas-wanted 6000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/demo/proxywugnot -gas-fee 1000000ugnot -gas-wanted 16000000 -broadcast -chainid=tendermint_test test1 stdout OK! # approve wugnot to `proxywugnot ≈ g1fndyg0we60rdfchyy5dwxzkfmhl5u34j932rg3` -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args "g1fndyg0we60rdfchyy5dwxzkfmhl5u34j932rg3" -args 10000 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args "g1fndyg0we60rdfchyy5dwxzkfmhl5u34j932rg3" -args 10000 -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 stdout OK! # send 10000ugnot to `proxywugnot` to wrap it -gnokey maketx call -pkgpath gno.land/r/demo/proxywugnot --send "10000ugnot" -func ProxyWrap -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/demo/proxywugnot --send "10000ugnot" -func ProxyWrap -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 stdout OK! # check user's wugnot balance -gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func BalanceOf -args "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func BalanceOf -args "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 stdout OK! stdout '10000 uint64' # unwrap 500 wugnot -gnokey maketx call -pkgpath gno.land/r/demo/proxywugnot -func ProxyUnwrap -args 500 -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/demo/proxywugnot -func ProxyUnwrap -args 500 -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 # XXX without patching anything it will panic # panic msg: insufficient coins error diff --git a/gno.land/cmd/gnoland/testdata/params.txtar b/gno.land/cmd/gnoland/testdata/params.txtar new file mode 100644 index 00000000000..30363aa6369 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/params.txtar @@ -0,0 +1,65 @@ +# test for https://github.com/gnolang/gno/pull/2920 + +gnoland start + +# query before adding the package +gnokey query params/vm/gno.land/r/sys/setter.foo.string +stdout 'data: $' +gnokey query params/vm/gno.land/r/sys/setter.bar.bool +stdout 'data: $' +gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +stdout 'data: $' + +gnokey maketx addpkg -pkgdir $WORK/setter -pkgpath gno.land/r/sys/setter -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 + +# query after adding the package, but before setting values +gnokey query params/vm/gno.land/r/sys/setter.foo.string +stdout 'data: $' +gnokey query params/vm/gno.land/r/sys/setter.bar.bool +stdout 'data: $' +gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +stdout 'data: $' + + +# set foo (string) +gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetFoo -args foo1 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/setter.foo.string +stdout 'data: "foo1"' + +# override foo +gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetFoo -args foo2 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/setter.foo.string +stdout 'data: "foo2"' + + +# set bar (bool) +gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBar -args true -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/setter.bar.bool +stdout 'data: true' + +# override bar +gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBar -args false -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/setter.bar.bool +stdout 'data: false' + + +# set baz (bool) +gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBaz -args 1337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +stdout 'data: "1337"' + +# override baz +gnokey maketx call -pkgpath gno.land/r/sys/setter -func SetBaz -args 31337 -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey query params/vm/gno.land/r/sys/setter.baz.int64 +stdout 'data: "31337"' + +-- setter/setter.gno -- +package setter + +import ( + "std" +) + +func SetFoo(newFoo string) { std.SetParamString("foo.string", newFoo) } +func SetBar(newBar bool) { std.SetParamBool("bar.bool", newBar) } +func SetBaz(newBaz int64) { std.SetParamInt64("baz.int64", newBaz) } diff --git a/gno.land/cmd/gnoland/testdata/prevrealm.txtar b/gno.land/cmd/gnoland/testdata/prevrealm.txtar index 72a207fae22..20317d87345 100644 --- a/gno.land/cmd/gnoland/testdata/prevrealm.txtar +++ b/gno.land/cmd/gnoland/testdata/prevrealm.txtar @@ -8,14 +8,14 @@ # | 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 | +# | 5 | | through /p/demo/bar | bar.A() | user address | +# | 6 | | | bar.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 | +# | 11 | | through /p/demo/bar | bar.A() | user address | +# | 12 | | | bar.B() | user address | # | 13 | MsgCall | wallet direct | std.PrevRealm() | user address | # | 14 | MsgRun | wallet direct | std.PrevRealm() | user address | @@ -34,60 +34,60 @@ 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 +gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 700000 -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 +gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 800000 -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 +gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 800000 -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 +gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 800000 -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 +## 5. MsgCall -> p/demo/bar.A: user address +## gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 4000000 -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 +## 6. MsgCall -> p/demo/bar.B: user address +## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 4000000 -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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -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 +## gnokey maketx call -pkgpath std -func PrevRealm -gas-fee 100000ugnot -gas-wanted 4000000 -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 +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 12000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno stdout ${USER_ADDR_test1} -- r/myrlm/myrlm.gno -- @@ -117,14 +117,14 @@ func B() string { -- p/demo/bar/bar.gno -- package bar -import "gno.land/r/myrlm" +import "std" func A() string { - return myrlm.A() + return std.PrevRealm().Addr().String() } func B() string { - return myrlm.B() + return A() } -- run/myrlmA.gno -- package main 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 index 71ef6400471..be9a686bac6 100644 --- a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar +++ b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar @@ -12,6 +12,9 @@ gnokey maketx addpkg -pkgdir $WORK/short -pkgpath gno.land/r/test/realm_banker - ## 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 +## add invalid realm_denom +gnokey maketx addpkg -pkgdir $WORK/invalid_realm_denom -pkgpath gno.land/r/test/invalid_realm_denom -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 @@ -52,6 +55,22 @@ gnokey maketx call -pkgpath gno.land/r/test/package89_123456789_123456789_123456 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"' +## mint invalid base denom +! gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "2gnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits' + +## burn invalid base denom +! gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Burn -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "2gnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits' + +## mint invalid realm denom +! gnokey maketx call -pkgpath gno.land/r/test/invalid_realm_denom -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid denom, can only issue/remove coins with the realm.s prefix' + +## burn invalid realm denom +! gnokey maketx call -pkgpath gno.land/r/test/invalid_realm_denom -func Burn -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid denom, can only issue/remove coins with the realm.s prefix' + -- short/realm_banker.gno -- package realm_banker @@ -61,12 +80,12 @@ import ( func Mint(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.IssueCoin(addr, denom, amount) + banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } func Burn(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.RemoveCoin(addr, denom, amount) + banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } -- long/realm_banker.gno -- @@ -77,6 +96,23 @@ import ( "std" ) +func Mint(addr std.Address, denom string, amount int64) { + banker := std.GetBanker(std.BankerTypeRealmIssue) + banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) +} + +func Burn(addr std.Address, denom string, amount int64) { + banker := std.GetBanker(std.BankerTypeRealmIssue) + banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) +} + +-- invalid_realm_denom/realm_banker.gno -- +package invalid_realm_denom + +import ( + "std" +) + func Mint(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) banker.IssueCoin(addr, denom, amount) diff --git a/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar b/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar index 7592693eeff..b02acc16d96 100644 --- a/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar +++ b/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar @@ -87,7 +87,7 @@ gnoland restart } ], "fee": { - "gas_wanted": "1000000", + "gas_wanted": "30000000", "gas_fee": "1000000ugnot" }, "signatures": [], @@ -162,7 +162,7 @@ gnoland restart } ], "fee": { - "gas_wanted": "15000000", + "gas_wanted": "35000000", "gas_fee": "1000000ugnot" }, "signatures": [], @@ -193,10 +193,9 @@ gnoland restart } ], "fee": { - "gas_wanted": "15000000", + "gas_wanted": "30000000", "gas_fee": "1000000ugnot" }, "signatures": [], "memo": "" } - diff --git a/gno.land/cmd/gnoland/testdata/simulate_gas.txtar b/gno.land/cmd/gnoland/testdata/simulate_gas.txtar new file mode 100644 index 00000000000..8550419f205 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/simulate_gas.txtar @@ -0,0 +1,28 @@ +# load the package +loadpkg gno.land/r/simulate $WORK/simulate + +# start a new node +gnoland start + +# simulate only +gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate only test1 +stdout 'GAS USED: 96411' + +# simulate skip +gnokey maketx call -pkgpath gno.land/r/simulate -func Hello -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -simulate skip test1 +stdout 'GAS USED: 96411' # same as simulate only + + +-- package/package.gno -- +package call_package + +func Render() string { + return "notok" +} + +-- simulate/simulate.gno -- +package simulate + +func Hello() string { + return "Hello" +} diff --git a/gno.land/cmd/gnoland/testdata/time_simple.txtar b/gno.land/cmd/gnoland/testdata/time_simple.txtar index 932a5721695..ace34fa00a5 100644 --- a/gno.land/cmd/gnoland/testdata/time_simple.txtar +++ b/gno.land/cmd/gnoland/testdata/time_simple.txtar @@ -3,7 +3,7 @@ gnoland start -gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/time_simple -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/time_simple -gas-fee 1ugnot -gas-wanted 15000000 -broadcast -chainid=tendermint_test test1 stdout OK! -- time_simple.gno -- diff --git a/gno.land/cmd/gnoland/testdata/wugnot.txtar b/gno.land/cmd/gnoland/testdata/wugnot.txtar index 1640909fdb9..5fa7dab2945 100644 --- a/gno.land/cmd/gnoland/testdata/wugnot.txtar +++ b/gno.land/cmd/gnoland/testdata/wugnot.txtar @@ -2,44 +2,44 @@ 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 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 5000000 -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 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send 12345678ugnot -gas-fee 1000000ugnot -gas-wanted 5000000 -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 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 5000000 -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 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send 12345678ugnot -gas-fee 1000000ugnot -gas-wanted 5000000 -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 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 5000000 -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 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Transfer -gas-fee 1000000ugnot -gas-wanted 5000000 -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 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 5000000 -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 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Withdraw -args 10000000 -gas-fee 1000000ugnot -gas-wanted 5000000 -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 +gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Render -gas-fee 1000000ugnot -gas-wanted 5000000 -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..6500e44fcc4 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 (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 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 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) + appcfg := gnoweb.NewDefaultAppConfig() + appcfg.ChainID = cfg.chainid + appcfg.NodeRemote = cfg.remote + appcfg.RemoteHelp = cfg.remoteHelp + appcfg.Analytics = cfg.analytics + appcfg.UnsafeHTML = cfg.html + appcfg.FaucetURL = cfg.faucetURL + appcfg.AssetsDir = cfg.assetsDir + if appcfg.RemoteHelp == "" { + appcfg.RemoteHelp = appcfg.NodeRemote + } + + app, err := gnoweb.NewRouter(logger, appcfg) + if err != nil { + return nil, fmt.Errorf("unable to start gnoweb app: %w", err) + } + + bindaddr, err := net.ResolveTCPAddr("tcp", cfg.bind) + if err != nil { + return nil, fmt.Errorf("unable to resolve listener %q: %w", cfg.bind, err) + } + + logger.Info("Running", "listener", bindaddr.String()) + server := &http.Server{ - Addr: bindAddress, + Handler: app, + Addr: bindaddr.String(), ReadHeaderTimeout: 60 * time.Second, - Handler: gnoweb.MakeApp(logger, cfg).Router, } - if err := server.ListenAndServe(); err != nil { - logger.Error("HTTP server stopped", " error:", err) - } + return func() error { + if err := server.ListenAndServe(); err != nil { + logger.Error("HTTP server stopped", " error:", err) + return commands.ExitCodeError(1) + } - return zapLogger.Sync() + 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 b7eb21837a7..1f8563d34fe 100644 --- a/gno.land/pkg/gnoclient/client_test.go +++ b/gno.land/pkg/gnoclient/client_test.go @@ -8,6 +8,7 @@ import ( "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 +18,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() @@ -652,8 +653,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, @@ -729,8 +730,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, @@ -742,8 +743,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, @@ -794,10 +795,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: "", @@ -841,10 +842,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: "", @@ -872,10 +873,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: "", @@ -903,10 +904,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: "", @@ -943,7 +944,7 @@ func TestRunErrors(t *testing.T) { msgs: []vm.MsgRun{ { Caller: mockAddress, - Package: &std.MemPackage{Name: "", Path: " "}, + Package: &gnovm.MemPackage{Name: "", Path: " "}, Send: nil, }, }, @@ -993,10 +994,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: "", @@ -1040,10 +1041,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: "", @@ -1071,10 +1072,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: "", @@ -1102,10 +1103,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: "", @@ -1142,7 +1143,7 @@ func TestAddPackageErrors(t *testing.T) { msgs: []vm.MsgAddPackage{ { Creator: mockAddress, - Package: &std.MemPackage{Name: "", Path: ""}, + Package: &gnovm.MemPackage{Name: "", Path: ""}, Deposit: nil, }, }, diff --git a/gno.land/pkg/gnoclient/client_txs.go b/gno.land/pkg/gnoclient/client_txs.go index 9d3dbde22ae..d7f6f053242 100644 --- a/gno.land/pkg/gnoclient/client_txs.go +++ b/gno.land/pkg/gnoclient/client_txs.go @@ -283,10 +283,10 @@ func (c *Client) BroadcastTxCommit(signedTx *std.Tx) (*ctypes.ResultBroadcastTxC } 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 diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index ea068e0680b..4b70fb60c49 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -5,17 +5,17 @@ import ( "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/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" ) @@ -39,8 +39,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: "", @@ -92,8 +92,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: "", @@ -154,8 +154,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: "", @@ -218,8 +218,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: "", @@ -290,8 +290,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: "", @@ -316,9 +316,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, @@ -358,8 +358,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: "", @@ -393,9 +393,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, @@ -406,9 +406,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, @@ -451,8 +451,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: "", @@ -474,10 +474,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, @@ -536,8 +536,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: "", @@ -556,7 +556,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() @@ -564,10 +564,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, @@ -579,10 +579,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", diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 2380658c6e9..9e8f2163441 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -3,6 +3,7 @@ package gnoland import ( "fmt" + "io" "log/slog" "path/filepath" "strconv" @@ -12,6 +13,7 @@ import ( "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" @@ -19,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" @@ -34,11 +37,11 @@ type AppOptions struct { DB dbm.DB // required Logger *slog.Logger // required EventSwitch events.EventSwitch // required - MaxCycles int64 // hard limit for cycles in GnoVM + VMOutput io.Writer // optional InitChainerConfig // options related to InitChainer } -// DefaultAppOptions provides a "ready" default [AppOptions] for use with +// TestAppOptions provides a "ready" default [AppOptions] for use with // [NewAppWithOptions], using the provided db. func TestAppOptions(db dbm.DB) *AppOptions { return &AppOptions{ @@ -86,14 +89,18 @@ 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) - vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, cfg.MaxCycles) + + vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, paramsKpr) + vmk.Output = cfg.VMOutput // Set InitChainer icc := cfg.InitChainerConfig icc.baseApp = baseApp - icc.acctKpr, icc.bankKpr, icc.vmKpr = acctKpr, bankKpr, vmk + icc.acctKpr, icc.bankKpr, icc.vmKpr, icc.paramsKpr, icc.gpKpr = acctKpr, bankKpr, vmk, paramsKpr, gpKpr baseApp.SetInitChainer(icc.InitChainer) // Set AnteHandler @@ -107,9 +114,11 @@ 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 @@ -140,6 +149,8 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { baseApp.SetEndBlocker( EndBlocker( c, + acctKpr, + gpKpr, vmk, baseApp, ), @@ -148,6 +159,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. @@ -225,10 +237,12 @@ type InitChainerConfig struct { // 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 + 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]. @@ -286,8 +300,12 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci 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) - // Parse and set genesis state balances + // Apply genesis balances. for _, bal := range state.Balances { acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) cfg.acctKpr.SetAccount(ctx, acc) @@ -297,10 +315,38 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci } } + // 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 { - res := cfg.baseApp.Deliver(tx) + 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) + } + } + + res := cfg.baseApp.Deliver(stdTx, ctxFn) if res.IsErr() { ctx.Logger().Error( "Unable to deliver genesis tx", @@ -316,7 +362,7 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci GasUsed: res.GasUsed, }) - cfg.GenesisTxResultHandler(ctx, tx, res) + cfg.GenesisTxResultHandler(ctx, stdTx, res) } return txResponses, nil } @@ -335,6 +381,8 @@ type endBlockerApp interface { // validator set changes func EndBlocker( collector *collector[validatorUpdate], + acctKpr auth.AccountKeeperI, + gpKpr auth.GasPriceKeeperI, vmk vm.VMKeeperI, app endBlockerApp, ) func( @@ -342,6 +390,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 193ff0b0b14..375602cfa4a 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -9,7 +9,8 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - gnostd "github.com/gnolang/gno/gnovm/stdlibs/std" + "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" bft "github.com/gnolang/gno/tm2/pkg/bft/types" @@ -18,6 +19,10 @@ import ( "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" @@ -37,6 +42,36 @@ func TestNewAppWithOptions(t *testing.T) { 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", @@ -44,26 +79,7 @@ func TestNewAppWithOptions(t *testing.T) { Block: defaultBlockParams(), }, Validators: []abci.ValidatorUpdate{}, - AppState: GnoGenesisState{ - Balances: []Balance{ - { - Address: addr, - Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, - }, - }, - Txs: []std.Tx{ - { - Msgs: []std.Msg{vm.NewMsgAddPackage(addr, "gno.land/r/demo", []*std.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: appState, }) require.True(t, resp.IsOK(), "InitChain response: %v", resp) @@ -84,6 +100,27 @@ func TestNewAppWithOptions(t *testing.T) { 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) { @@ -111,7 +148,7 @@ func TestNewApp(t *testing.T) { }, }, Validators: []abci.ValidatorUpdate{}, - AppState: GnoGenesisState{}, + AppState: DefaultGenState(), }) assert.True(t, resp.IsOK(), "resp is not OK: %v", resp) } @@ -181,8 +218,12 @@ func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper 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: GnoGenesisState{}, + AppState: DefaultGenState(), }) // assert number of calls @@ -205,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(), @@ -219,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() @@ -271,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{}) @@ -308,10 +532,10 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](mockEventSwitch, noFilter) // Fire a GnoVM event - mockEventSwitch.FireEvent(gnostd.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{}) @@ -351,10 +575,10 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](mockEventSwitch, noFilter) // Fire a GnoVM event - mockEventSwitch.FireEvent(gnostd.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{}) @@ -390,7 +614,7 @@ func TestEndBlocker(t *testing.T) { // Construct the GnoVM events vmEvents := make([]abci.Event, 0, len(changes)) for index := range changes { - event := gnostd.GnoEvent{ + event := gnostdlibs.GnoEvent{ Type: validatorAddedEvent, PkgPath: valRealm, } @@ -399,7 +623,7 @@ func TestEndBlocker(t *testing.T) { if index%2 == 0 { changes[index].Power = 0 - event = gnostd.GnoEvent{ + event = gnostdlibs.GnoEvent{ Type: validatorRemovedEvent, PkgPath: valRealm, } @@ -422,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{}) @@ -437,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..ccc3369766d 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -12,13 +12,20 @@ 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 = "10ugnot/100gas" + // LoadGenesisBalancesFile loads genesis balances from the provided file path. func LoadGenesisBalancesFile(path string) ([]Balance, 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)) @@ -58,11 +65,54 @@ func LoadGenesisBalancesFile(path string) ([]Balance, error) { 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 +123,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 +136,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 +151,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 +171,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 +190,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/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index d168c955607..f42166411c8 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -2,6 +2,7 @@ package gnoland import ( "fmt" + "io" "log/slog" "path/filepath" "time" @@ -16,15 +17,14 @@ import ( "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" ) type InMemoryNodeConfig struct { - PrivValidator bft.PrivValidator // identity of the validator - Genesis *bft.GenesisDoc - TMConfig *tmcfg.Config - GenesisMaxVMCycles int64 - DB *memdb.MemDB // will be initialized if nil + PrivValidator bft.PrivValidator // identity of the validator + Genesis *bft.GenesisDoc + TMConfig *tmcfg.Config + DB *memdb.MemDB // will be initialized if nil + VMOutput io.Writer // optional // If StdlibDir not set, then it's filepath.Join(TMConfig.RootDir, "gnovm", "stdlibs") InitChainerConfig @@ -36,7 +36,11 @@ 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, @@ -45,7 +49,10 @@ func NewDefaultGenesisConfig(chainid string) *bft.GenesisDoc { }, AppState: &GnoGenesisState{ Balances: []Balance{}, - Txs: []std.Tx{}, + Txs: []TxWithMetadata{}, + Params: []Param{ + domainParam, + }, }, } } @@ -106,10 +113,10 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, // Initialize the application with the provided options gnoApp, err := NewAppWithOptions(&AppOptions{ Logger: logger, - MaxCycles: cfg.GenesisMaxVMCycles, DB: cfg.DB, EventSwitch: evsw, InitChainerConfig: cfg.InitChainerConfig, + VMOutput: cfg.VMOutput, }) if err != nil { return nil, fmt.Errorf("error initializing new app: %w", err) 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..ed35c4141f4 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -1,8 +1,14 @@ package gnoland import ( + "bufio" + "context" "errors" + "fmt" + "os" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/sdk/auth" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -20,6 +26,63 @@ 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 } diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go new file mode 100644 index 00000000000..b4625d6d7d6 --- /dev/null +++ b/gno.land/pkg/gnoland/types_test.go @@ -0,0 +1,131 @@ +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/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) + } + }) +} 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..61397fef54f --- /dev/null +++ b/gno.land/pkg/gnoweb/Makefile @@ -0,0 +1,100 @@ +# 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 + +# 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 + +# Generate process +generate: css ts static + +css: $(output_css) +$(output_css): $(input_css) + npx -y tailwindcss@$(tw_version) -c $(tw_config_path) -i $< -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 7fb28d5cbc3..06bb3941e41 100644 --- a/gno.land/pkg/gnoweb/alias.go +++ b/gno.land/pkg/gnoweb/alias.go @@ -1,6 +1,12 @@ 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", @@ -14,7 +20,7 @@ var Aliases = map[string]string{ "/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", @@ -23,5 +29,29 @@ var Redirects = map[string]string{ "/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..dc13253468e --- /dev/null +++ b/gno.land/pkg/gnoweb/app.go @@ -0,0 +1,152 @@ +package gnoweb + +import ( + "fmt" + "log/slog" + "net/http" + "path" + "strings" + + "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/gno.land/pkg/gnoweb/markdown" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/yuin/goldmark" + 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 +} + +// 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{ + // same as Remote by default + NodeRemote: defaultRemote, + RemoteHelp: defaultRemote, + ChainID: "dev", + AssetsPath: "/public/", + } +} + +var chromaStyle = 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 given logger and config. +func NewRouter(logger *slog.Logger, cfg *AppConfig) (http.Handler, error) { + chromaOptions := []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + chromahtml.WithLinkableLineNumbers(true, "L"), + chromahtml.WithClasses(true), + chromahtml.ClassPrefix("chroma-"), + } + + mdopts := []goldmark.Option{ + goldmark.WithExtensions( + markdown.NewHighlighting( + markdown.WithFormatOptions(chromaOptions...), + ), + ), + } + if cfg.UnsafeHTML { + mdopts = append(mdopts, goldmark.WithRendererOptions(mdhtml.WithXHTML(), mdhtml.WithUnsafe())) + } + + md := goldmark.New(mdopts...) + + client, err := client.NewHTTPClient(cfg.NodeRemote) + if err != nil { + return nil, fmt.Errorf("unable to create http client: %w", err) + } + webcli := NewWebClient(logger, client, md) + + formatter := chromahtml.New(chromaOptions...) + chromaStylePath := path.Join(cfg.AssetsPath, "_chroma", "style.css") + + var webConfig WebHandlerConfig + + webConfig.RenderClient = webcli + webConfig.Formatter = newFormatterWithStyle(formatter, chromaStyle) + + // Static meta + webConfig.Meta.AssetsPath = cfg.AssetsPath + webConfig.Meta.ChromaPath = chromaStylePath + webConfig.Meta.RemoteHelp = cfg.RemoteHelp + webConfig.Meta.ChainId = cfg.ChainID + webConfig.Meta.Analytics = cfg.Analytics + + // Setup main handler + webhandler := NewWebHandler(logger, webConfig) + + mux := http.NewServeMux() + + // Setup Webahndler along 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, + }) + })) + } + + // setup assets + mux.Handle(chromaStylePath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Setup Formatter + w.Header().Set("Content-Type", "text/css") + if err := formatter.WriteCSS(w, chromaStyle); err != nil { + logger.Error("unable to write css", "err", err) + http.NotFound(w, r) + } + })) + + // Normalize assets path + assetsBase := "/" + strings.Trim(cfg.AssetsPath, "/") + "/" + + // Handle assets path + if cfg.AssetsDir != "" { + logger.Debug("using assets dir instead of embed 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..78fe197a134 --- /dev/null +++ b/gno.land/pkg/gnoweb/app_test.go @@ -0,0 +1,150 @@ +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"}, // assert / gives 200 (OK). assert / contains "Welcome". + {"/about", ok, "blockchain"}, + {"/r/gnoland/blog", ok, ""}, // whatever 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"}, + {"/404/not/found/", notFound, ""}, + {"/아스키문자가아닌경로", 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 := NewDefaultAppConfig() + cfg.NodeRemote = remoteAddr + + logger := log.NewTestingLogger(t) + + // set the `remoteAddr` of the client to the listening address of the + // node, which is randomly assigned. + 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) { + 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) + fmt.Println("HELLO:", response.Body.String()) + 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..9e7a97b2fae --- /dev/null +++ b/gno.land/pkg/gnoweb/components/breadcrumb.go @@ -0,0 +1,18 @@ +package components + +import ( + "io" +) + +type BreadcrumbPart struct { + Name string + Path string +} + +type BreadcrumbData struct { + Parts []BreadcrumbPart +} + +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..a3301cb037e --- /dev/null +++ b/gno.land/pkg/gnoweb/components/breadcrumb.gohtml @@ -0,0 +1,12 @@ +{{ define "breadcrumb" }} +
      + {{- range $index, $part := .Parts }} + {{- if $index }} +
    1. + {{- else }} +
    2. + {{- end }} + {{ $part.Name }}
    3. + {{- end }} +
    +{{ end }} \ No newline at end of file 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..4cdeff12a38 --- /dev/null +++ b/gno.land/pkg/gnoweb/components/directory.gohtml @@ -0,0 +1,39 @@ +{{ 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..d3ca9dea81f --- /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..22de0fd2968 --- /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..20e710ca29b --- /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/formatter.go b/gno.land/pkg/gnoweb/formatter.go new file mode 100644 index 00000000000..e172afe9e21 --- /dev/null +++ b/gno.land/pkg/gnoweb/formatter.go @@ -0,0 +1,25 @@ +package gnoweb + +import ( + "io" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/formatters/html" +) + +type Formatter interface { + Format(w io.Writer, iterator chroma.Iterator) error +} + +type formatterWithStyle struct { + *html.Formatter + style *chroma.Style +} + +func newFormatterWithStyle(formater *html.Formatter, style *chroma.Style) Formatter { + return &formatterWithStyle{Formatter: formater, style: style} +} + +func (f *formatterWithStyle) Format(w io.Writer, iterator chroma.Iterator) error { + return f.Formatter.Format(w, f.style, iterator) +} 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..d54d30123b7 --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/css/input.css @@ -0,0 +1,346 @@ +@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-100 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 w-full border-collapse my-8; + } + + .realm-content th, + .realm-content td { + @apply border border-gray-300 px-4 py-2; + } + + .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 table th:first-child, + .realm-content td:first-child { + @apply pl-0; + } + + .realm-content table th:last-child, + .realm-content td:last-child { + @apply pr-0; + } + + .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..980e9625875 --- /dev/null +++ b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts @@ -0,0 +1,125 @@ +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); + + console.log(this.DOM); + this.funcList = this.DOM.funcs.map((funcEl) => new HelpFunc(funcEl)); + + this.bindEvents(); + } + + private bindEvents(): void { + const { addressInput, cmdModeSelect } = this.DOM; + + addressInput?.addEventListener("input", () => { + this.funcList.forEach((func) => func.updateAddr(addressInput.value)); + }); + + 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[]; + }; + + 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)), + }; + + this.funcName = el.dataset.func || null; + + this.bindEvents(); + } + + private bindEvents(): void { + this.DOM.el.addEventListener("input", (e) => { + const target = e.target as HTMLInputElement; + if (target.dataset.role === "help-param-input") { + this.updateArg(target.dataset.param || "", target.value); + } + }); + } + + public updateArg(paramName: string, paramValue: string): void { + this.DOM.args + .filter((arg) => arg.dataset.arg === paramName) + .forEach((arg) => { + arg.textContent = paramValue.trim() || ""; + }); + } + + 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.className = isVisible ? "inline" : "hidden"; + 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/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 18df5ec2356..00000000000 --- a/gno.land/pkg/gnoweb/gnoweb_test.go +++ /dev/null @@ -1,139 +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("}, - {"/contribute", ok, "Game of Realms"}, - {"/game-of-realms", found, "/contribute"}, - {"/gor", found, "/contribute"}, - {"/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..b3a9fcd143c --- /dev/null +++ b/gno.land/pkg/gnoweb/handler.go @@ -0,0 +1,381 @@ +package gnoweb + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "io" + "log/slog" + "net/http" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // for error types +) + +const DefaultChainDomain = "gno.land" + +type StaticMetadata struct { + AssetsPath string + ChromaPath string + RemoteHelp string + ChainId string + Analytics bool +} + +type WebHandlerConfig struct { + Meta StaticMetadata + RenderClient *WebClient + Formatter Formatter +} + +type WebHandler struct { + formatter Formatter + + logger *slog.Logger + static StaticMetadata + webcli *WebClient +} + +func NewWebHandler(logger *slog.Logger, cfg WebHandlerConfig) *WebHandler { + if cfg.RenderClient == nil { + logger.Error("no renderer has been defined") + } + + return &WebHandler{ + formatter: cfg.Formatter, + webcli: cfg.RenderClient, + logger: logger, + static: cfg.Meta, + } +} + +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) +} + +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()) + }() + + var indexData components.IndexData + indexData.HeadData.AssetsPath = h.static.AssetsPath + indexData.HeadData.ChromaPath = h.static.ChromaPath + indexData.FooterData.Analytics = h.static.Analytics + indexData.FooterData.AssetsPath = h.static.AssetsPath + + // Render the page body into the buffer + var status int + gnourl, err := ParseGnoURL(r.URL) + if err != nil { + h.logger.Warn("page not found", "path", r.URL.Path, "err", err) + status, err = http.StatusNotFound, components.RenderStatusComponent(&body, "page not found") + } else { + // TODO: real data (title & description) + indexData.HeadData.Title = "gno.land - " + gnourl.Path + + // Header + indexData.HeaderData.RealmPath = gnourl.Path + indexData.HeaderData.Breadcrumb.Parts = generateBreadcrumbPaths(gnourl.Path) + indexData.HeaderData.WebQuery = gnourl.WebQuery + + // Render + switch gnourl.Kind() { + case KindRealm, KindPure: + status, err = h.renderPackage(&body, gnourl) + default: + h.logger.Debug("invalid page kind", "kind", gnourl.Kind) + status, err = http.StatusNotFound, components.RenderStatusComponent(&body, "page not found") + } + } + + 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) + } + + return +} + +func (h *WebHandler) renderPackage(w io.Writer, gnourl *GnoURL) (status int, err error) { + h.logger.Info("component render", "path", gnourl.Path, "args", gnourl.Args) + + kind := gnourl.Kind() + + // Display realm help page? + if kind == KindRealm && gnourl.WebQuery.Has("help") { + return h.renderRealmHelp(w, gnourl) + } + + // Display package source page? + switch { + case gnourl.WebQuery.Has("source"): + return h.renderRealmSource(w, gnourl) + case kind == KindPure, + strings.HasSuffix(gnourl.Path, "/"), + isFile(gnourl.Path): + i := strings.LastIndexByte(gnourl.Path, '/') + if i < 0 { + return http.StatusInternalServerError, fmt.Errorf("unable to get ending slash for %q", gnourl.Path) + } + + // Fill webquery with file infos + gnourl.WebQuery.Set("source", "") // set source + + file := gnourl.Path[i+1:] + if file == "" { + return h.renderRealmDirectory(w, gnourl) + } + + gnourl.WebQuery.Set("file", file) + gnourl.Path = gnourl.Path[:i] + + return h.renderRealmSource(w, gnourl) + } + + // Render content into the content buffer + var content bytes.Buffer + meta, err := h.webcli.Render(&content, gnourl.Path, gnourl.EncodeArgs()) + if err != nil { + if errors.Is(err, vm.InvalidPkgPathError{}) { + return http.StatusNotFound, components.RenderStatusComponent(w, "not found") + } + + h.logger.Error("unable to render markdown", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + err = components.RenderRealmComponent(w, components.RealmData{ + TocItems: &components.RealmTOCData{ + Items: meta.Items, + }, + // NOTE: `content` should have already been escaped by + 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") + } + + // Write the rendered content to the response writer + return http.StatusOK, nil +} + +func (h *WebHandler) renderRealmHelp(w io.Writer, gnourl *GnoURL) (status int, err error) { + fsigs, err := h.webcli.Functions(gnourl.Path) + if err != nil { + h.logger.Error("unable to fetch path functions", "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + var selArgs map[string]string + var selFn string + if selFn = gnourl.WebQuery.Get("func"); selFn != "" { + for _, fn := range fsigs { + if selFn != fn.FuncName { + continue + } + + selArgs = make(map[string]string) + for _, param := range fn.Params { + selArgs[param.Name] = gnourl.WebQuery.Get(param.Name) + } + + fsigs = []vm.FunctionSignature{fn} + break + } + } + + // Catch last name of the path + // XXX: we should probably add a helper within the template + 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(DefaultChainDomain, 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 +} + +func (h *WebHandler) renderRealmSource(w io.Writer, gnourl *GnoURL) (status int, err error) { + pkgPath := gnourl.Path + + files, err := h.webcli.Sources(pkgPath) + if err != nil { + h.logger.Error("unable to list sources file", "path", gnourl.Path, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + 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 + file := gnourl.WebQuery.Get("file") + if file == "" { + fileName = files[0] + } else if slices.Contains(files, file) { + fileName = file + } else { + h.logger.Error("unable to render source", "file", file, "err", "file does not exist") + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + source, err := h.webcli.SourceFile(pkgPath, fileName) + if err != nil { + h.logger.Error("unable to get source file", "file", fileName, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + // XXX: we should either do this on the front or in the markdown parsing side + fileLines := strings.Count(string(source), "\n") + fileSizeKb := float64(len(source)) / 1024.0 + fileSizeStr := fmt.Sprintf("%.2f Kb", fileSizeKb) + + // Highlight code source + hsource, err := h.highlightSource(fileName, source) + if err != nil { + h.logger.Error("unable to highlight source file", "file", fileName, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + err = components.RenderSourceComponent(w, components.SourceData{ + PkgPath: gnourl.Path, + Files: files, + FileName: fileName, + FileCounter: len(files), + FileLines: fileLines, + FileSize: fileSizeStr, + FileSource: template.HTML(hsource), //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 +} + +func (h *WebHandler) renderRealmDirectory(w io.Writer, gnourl *GnoURL) (status int, err error) { + pkgPath := gnourl.Path + + files, err := h.webcli.Sources(pkgPath) + if err != nil { + h.logger.Error("unable to list sources file", "path", gnourl.Path, "err", err) + return http.StatusInternalServerError, components.RenderStatusComponent(w, "internal error") + } + + 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, "internal error") + } + + return http.StatusOK, nil +} + +func (h *WebHandler) highlightSource(fileName string, src []byte) ([]byte, error) { + var lexer chroma.Lexer + + 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") // file kind not supported, fallback on `.txt` + } + + if lexer == nil { + return nil, fmt.Errorf("unsupported lexer for file %q", fileName) + } + + iterator, err := lexer.Tokenise(nil, string(src)) + if err != nil { + h.logger.Error("unable to ", "fileName", fileName, "err", err) + } + + var buff bytes.Buffer + if err := h.formatter.Format(&buff, iterator); err != nil { + return nil, fmt.Errorf("unable to format source file %q: %w", fileName, err) + } + + return buff.Bytes(), nil +} + +func generateBreadcrumbPaths(path string) []components.BreadcrumbPart { + split := strings.Split(path, "/") + parts := []components.BreadcrumbPart{} + + var name string + for i := range split { + if name = split[i]; name == "" { + continue + } + + parts = append(parts, components.BreadcrumbPart{ + Name: name, + Path: strings.Join(split[:i+1], "/"), + }) + } + + return parts +} + +// IsFile checks if the last element of the path is a file (has an extension) +func isFile(path string) bool { + base := filepath.Base(path) + ext := filepath.Ext(base) + return ext != "" +} diff --git a/gno.land/pkg/gnoweb/markdown/highlighting.go b/gno.land/pkg/gnoweb/markdown/highlighting.go new file mode 100644 index 00000000000..51c66674df1 --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/highlighting.go @@ -0,0 +1,588 @@ +// This file was copied from https://github.com/yuin/goldmark-highlighting +// +// package highlighting is an extension for the goldmark(http://github.com/yuin/goldmark). +// +// This extension adds syntax-highlighting to the fenced code blocks using +// chroma(https://github.com/alecthomas/chroma). +package markdown + +import ( + "bytes" + "io" + "strconv" + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" + + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/alecthomas/chroma/v2/styles" +) + +// ImmutableAttributes is a read-only interface for ast.Attributes. +type ImmutableAttributes interface { + // Get returns (value, true) if an attribute associated with given + // name exists, otherwise (nil, false) + Get(name []byte) (interface{}, bool) + + // GetString returns (value, true) if an attribute associated with given + // name exists, otherwise (nil, false) + GetString(name string) (interface{}, bool) + + // All returns all attributes. + All() []ast.Attribute +} + +type immutableAttributes struct { + n ast.Node +} + +func (a *immutableAttributes) Get(name []byte) (interface{}, bool) { + return a.n.Attribute(name) +} + +func (a *immutableAttributes) GetString(name string) (interface{}, bool) { + return a.n.AttributeString(name) +} + +func (a *immutableAttributes) All() []ast.Attribute { + if a.n.Attributes() == nil { + return []ast.Attribute{} + } + return a.n.Attributes() +} + +// CodeBlockContext holds contextual information of code highlighting. +type CodeBlockContext interface { + // Language returns (language, true) if specified, otherwise (nil, false). + Language() ([]byte, bool) + + // Highlighted returns true if this code block can be highlighted, otherwise false. + Highlighted() bool + + // Attributes return attributes of the code block. + Attributes() ImmutableAttributes +} + +type codeBlockContext struct { + language []byte + highlighted bool + attributes ImmutableAttributes +} + +func newCodeBlockContext(language []byte, highlighted bool, attrs ImmutableAttributes) CodeBlockContext { + return &codeBlockContext{ + language: language, + highlighted: highlighted, + attributes: attrs, + } +} + +func (c *codeBlockContext) Language() ([]byte, bool) { + if c.language != nil { + return c.language, true + } + return nil, false +} + +func (c *codeBlockContext) Highlighted() bool { + return c.highlighted +} + +func (c *codeBlockContext) Attributes() ImmutableAttributes { + return c.attributes +} + +// WrapperRenderer renders wrapper elements like div, pre, etc. +type WrapperRenderer func(w util.BufWriter, context CodeBlockContext, entering bool) + +// CodeBlockOptions creates Chroma options per code block. +type CodeBlockOptions func(ctx CodeBlockContext) []chromahtml.Option + +// Config struct holds options for the extension. +type Config struct { + html.Config + + // Style is a highlighting style. + // Supported styles are defined under https://github.com/alecthomas/chroma/tree/master/formatters. + Style string + + // Pass in a custom Chroma style. If this is not nil, the Style string will be ignored + CustomStyle *chroma.Style + + // If set, will try to guess language if none provided. + // If the guessing fails, we will fall back to a text lexer. + // Note that while Chroma's API supports language guessing, the implementation + // is not there yet, so you will currently always get the basic text lexer. + GuessLanguage bool + + // FormatOptions is a option related to output formats. + // See https://github.com/alecthomas/chroma#the-html-formatter for details. + FormatOptions []chromahtml.Option + + // CSSWriter is an io.Writer that will be used as CSS data output buffer. + // If WithClasses() is enabled, you can get CSS data corresponds to the style. + CSSWriter io.Writer + + // CodeBlockOptions allows set Chroma options per code block. + CodeBlockOptions CodeBlockOptions + + // WrapperRenderer allows you to change wrapper elements. + WrapperRenderer WrapperRenderer +} + +// NewConfig returns a new Config with defaults. +func NewConfig() Config { + return Config{ + Config: html.NewConfig(), + Style: "github", + FormatOptions: []chromahtml.Option{}, + CSSWriter: nil, + WrapperRenderer: nil, + CodeBlockOptions: nil, + } +} + +// SetOption implements renderer.SetOptioner. +func (c *Config) SetOption(name renderer.OptionName, value interface{}) { + switch name { + case optStyle: + c.Style = value.(string) + case optCustomStyle: + c.CustomStyle = value.(*chroma.Style) + case optFormatOptions: + if value != nil { + c.FormatOptions = value.([]chromahtml.Option) + } + case optCSSWriter: + c.CSSWriter = value.(io.Writer) + case optWrapperRenderer: + c.WrapperRenderer = value.(WrapperRenderer) + case optCodeBlockOptions: + c.CodeBlockOptions = value.(CodeBlockOptions) + case optGuessLanguage: + c.GuessLanguage = value.(bool) + default: + c.Config.SetOption(name, value) + } +} + +// Option interface is a functional option interface for the extension. +type Option interface { + renderer.Option + // SetHighlightingOption sets given option to the extension. + SetHighlightingOption(*Config) +} + +type withHTMLOptions struct { + value []html.Option +} + +func (o *withHTMLOptions) SetConfig(c *renderer.Config) { + if o.value != nil { + for _, v := range o.value { + v.(renderer.Option).SetConfig(c) + } + } +} + +func (o *withHTMLOptions) SetHighlightingOption(c *Config) { + if o.value != nil { + for _, v := range o.value { + v.SetHTMLOption(&c.Config) + } + } +} + +// WithHTMLOptions is functional option that wraps goldmark HTMLRenderer options. +func WithHTMLOptions(opts ...html.Option) Option { + return &withHTMLOptions{opts} +} + +const ( + optStyle renderer.OptionName = "HighlightingStyle" + optCustomStyle renderer.OptionName = "HighlightingCustomStyle" +) + +var highlightLinesAttrName = []byte("hl_lines") + +var ( + styleAttrName = []byte("hl_style") + nohlAttrName = []byte("nohl") + linenosAttrName = []byte("linenos") + linenosTableAttrValue = []byte("table") + linenosInlineAttrValue = []byte("inline") + linenostartAttrName = []byte("linenostart") +) + +type withStyle struct { + value string +} + +func (o *withStyle) SetConfig(c *renderer.Config) { + c.Options[optStyle] = o.value +} + +func (o *withStyle) SetHighlightingOption(c *Config) { + c.Style = o.value +} + +// WithStyle is a functional option that changes highlighting style. +func WithStyle(style string) Option { + return &withStyle{style} +} + +type withCustomStyle struct { + value *chroma.Style +} + +func (o *withCustomStyle) SetConfig(c *renderer.Config) { + c.Options[optCustomStyle] = o.value +} + +func (o *withCustomStyle) SetHighlightingOption(c *Config) { + c.CustomStyle = o.value +} + +// WithStyle is a functional option that changes highlighting style. +func WithCustomStyle(style *chroma.Style) Option { + return &withCustomStyle{style} +} + +const optCSSWriter renderer.OptionName = "HighlightingCSSWriter" + +type withCSSWriter struct { + value io.Writer +} + +func (o *withCSSWriter) SetConfig(c *renderer.Config) { + c.Options[optCSSWriter] = o.value +} + +func (o *withCSSWriter) SetHighlightingOption(c *Config) { + c.CSSWriter = o.value +} + +// WithCSSWriter is a functional option that sets io.Writer for CSS data. +func WithCSSWriter(w io.Writer) Option { + return &withCSSWriter{w} +} + +const optGuessLanguage renderer.OptionName = "HighlightingGuessLanguage" + +type withGuessLanguage struct { + value bool +} + +func (o *withGuessLanguage) SetConfig(c *renderer.Config) { + c.Options[optGuessLanguage] = o.value +} + +func (o *withGuessLanguage) SetHighlightingOption(c *Config) { + c.GuessLanguage = o.value +} + +// WithGuessLanguage is a functional option that toggles language guessing +// if none provided. +func WithGuessLanguage(b bool) Option { + return &withGuessLanguage{value: b} +} + +const optWrapperRenderer renderer.OptionName = "HighlightingWrapperRenderer" + +type withWrapperRenderer struct { + value WrapperRenderer +} + +func (o *withWrapperRenderer) SetConfig(c *renderer.Config) { + c.Options[optWrapperRenderer] = o.value +} + +func (o *withWrapperRenderer) SetHighlightingOption(c *Config) { + c.WrapperRenderer = o.value +} + +// WithWrapperRenderer is a functional option that sets WrapperRenderer that +// renders wrapper elements like div, pre, etc. +func WithWrapperRenderer(w WrapperRenderer) Option { + return &withWrapperRenderer{w} +} + +const optCodeBlockOptions renderer.OptionName = "HighlightingCodeBlockOptions" + +type withCodeBlockOptions struct { + value CodeBlockOptions +} + +func (o *withCodeBlockOptions) SetConfig(c *renderer.Config) { + c.Options[optCodeBlockOptions] = o.value +} + +func (o *withCodeBlockOptions) SetHighlightingOption(c *Config) { + c.CodeBlockOptions = o.value +} + +// WithCodeBlockOptions is a functional option that sets CodeBlockOptions that +// allows setting Chroma options per code block. +func WithCodeBlockOptions(c CodeBlockOptions) Option { + return &withCodeBlockOptions{value: c} +} + +const optFormatOptions renderer.OptionName = "HighlightingFormatOptions" + +type withFormatOptions struct { + value []chromahtml.Option +} + +func (o *withFormatOptions) SetConfig(c *renderer.Config) { + if _, ok := c.Options[optFormatOptions]; !ok { + c.Options[optFormatOptions] = []chromahtml.Option{} + } + c.Options[optFormatOptions] = append(c.Options[optFormatOptions].([]chromahtml.Option), o.value...) +} + +func (o *withFormatOptions) SetHighlightingOption(c *Config) { + c.FormatOptions = append(c.FormatOptions, o.value...) +} + +// WithFormatOptions is a functional option that wraps chroma HTML formatter options. +func WithFormatOptions(opts ...chromahtml.Option) Option { + return &withFormatOptions{opts} +} + +// HTMLRenderer struct is a renderer.NodeRenderer implementation for the extension. +type HTMLRenderer struct { + Config +} + +// NewHTMLRenderer builds a new HTMLRenderer with given options and returns it. +func NewHTMLRenderer(opts ...Option) renderer.NodeRenderer { + r := &HTMLRenderer{ + Config: NewConfig(), + } + for _, opt := range opts { + opt.SetHighlightingOption(&r.Config) + } + return r +} + +// RegisterFuncs implements NodeRenderer.RegisterFuncs. +func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock) +} + +func getAttributes(node *ast.FencedCodeBlock, infostr []byte) ImmutableAttributes { + if node.Attributes() != nil { + return &immutableAttributes{node} + } + if infostr != nil { + attrStartIdx := -1 + + for idx, char := range infostr { + if char == '{' { + attrStartIdx = idx + break + } + } + if attrStartIdx > 0 { + n := ast.NewTextBlock() // dummy node for storing attributes + attrStr := infostr[attrStartIdx:] + if attrs, hasAttr := parser.ParseAttributes(text.NewReader(attrStr)); hasAttr { + for _, attr := range attrs { + n.SetAttribute(attr.Name, attr.Value) + } + return &immutableAttributes{n} + } + } + } + return nil +} + +func (r *HTMLRenderer) renderFencedCodeBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.FencedCodeBlock) + if !entering { + return ast.WalkContinue, nil + } + language := n.Language(source) + + chromaFormatterOptions := make([]chromahtml.Option, 0, len(r.FormatOptions)) + for _, opt := range r.FormatOptions { + chromaFormatterOptions = append(chromaFormatterOptions, opt) + } + + style := r.CustomStyle + if style == nil { + style = styles.Get(r.Style) + } + nohl := false + + var info []byte + if n.Info != nil { + info = n.Info.Segment.Value(source) + } + attrs := getAttributes(n, info) + if attrs != nil { + baseLineNumber := 1 + if linenostartAttr, ok := attrs.Get(linenostartAttrName); ok { + if linenostart, ok := linenostartAttr.(float64); ok { + baseLineNumber = int(linenostart) + chromaFormatterOptions = append( + chromaFormatterOptions, chromahtml.BaseLineNumber(baseLineNumber), + ) + } + } + if linesAttr, hasLinesAttr := attrs.Get(highlightLinesAttrName); hasLinesAttr { + if lines, ok := linesAttr.([]interface{}); ok { + var hlRanges [][2]int + for _, l := range lines { + if ln, ok := l.(float64); ok { + hlRanges = append(hlRanges, [2]int{int(ln) + baseLineNumber - 1, int(ln) + baseLineNumber - 1}) + } + if rng, ok := l.([]uint8); ok { + slices := strings.Split(string(rng), "-") + lhs, err := strconv.Atoi(slices[0]) + if err != nil { + continue + } + rhs := lhs + if len(slices) > 1 { + rhs, err = strconv.Atoi(slices[1]) + if err != nil { + continue + } + } + hlRanges = append(hlRanges, [2]int{lhs + baseLineNumber - 1, rhs + baseLineNumber - 1}) + } + } + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.HighlightLines(hlRanges)) + } + } + if styleAttr, hasStyleAttr := attrs.Get(styleAttrName); hasStyleAttr { + if st, ok := styleAttr.([]uint8); ok { + styleStr := string(st) + style = styles.Get(styleStr) + } + } + if _, hasNohlAttr := attrs.Get(nohlAttrName); hasNohlAttr { + nohl = true + } + + if linenosAttr, ok := attrs.Get(linenosAttrName); ok { + switch v := linenosAttr.(type) { + case bool: + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(v)) + case []uint8: + if v != nil { + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.WithLineNumbers(true)) + } + if bytes.Equal(v, linenosTableAttrValue) { + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(true)) + } else if bytes.Equal(v, linenosInlineAttrValue) { + chromaFormatterOptions = append(chromaFormatterOptions, chromahtml.LineNumbersInTable(false)) + } + } + } + } + + var lexer chroma.Lexer + if language != nil { + lexer = lexers.Get(string(language)) + } + if !nohl && (lexer != nil || r.GuessLanguage) { + if style == nil { + style = styles.Fallback + } + var buffer bytes.Buffer + l := n.Lines().Len() + for i := 0; i < l; i++ { + line := n.Lines().At(i) + buffer.Write(line.Value(source)) + } + + if lexer == nil { + lexer = lexers.Analyse(buffer.String()) + if lexer == nil { + lexer = lexers.Fallback + } + language = []byte(strings.ToLower(lexer.Config().Name)) + } + lexer = chroma.Coalesce(lexer) + + iterator, err := lexer.Tokenise(nil, buffer.String()) + if err == nil { + c := newCodeBlockContext(language, true, attrs) + + if r.CodeBlockOptions != nil { + chromaFormatterOptions = append(chromaFormatterOptions, r.CodeBlockOptions(c)...) + } + formatter := chromahtml.New(chromaFormatterOptions...) + if r.WrapperRenderer != nil { + r.WrapperRenderer(w, c, true) + } + _ = formatter.Format(w, style, iterator) == nil + if r.WrapperRenderer != nil { + r.WrapperRenderer(w, c, false) + } + if r.CSSWriter != nil { + _ = formatter.WriteCSS(r.CSSWriter, style) + } + return ast.WalkContinue, nil + } + } + + var c CodeBlockContext + if r.WrapperRenderer != nil { + c = newCodeBlockContext(language, false, attrs) + r.WrapperRenderer(w, c, true) + } else { + _, _ = w.WriteString("
    ')
    +	}
    +	l := n.Lines().Len()
    +	for i := 0; i < l; i++ {
    +		line := n.Lines().At(i)
    +		r.Writer.RawWrite(w, line.Value(source))
    +	}
    +	if r.WrapperRenderer != nil {
    +		r.WrapperRenderer(w, c, false)
    +	} else {
    +		_, _ = w.WriteString("
    \n") + } + return ast.WalkContinue, nil +} + +type highlighting struct { + options []Option +} + +// Highlighting is a goldmark.Extender implementation. +var Highlighting = &highlighting{ + options: []Option{}, +} + +// NewHighlighting returns a new extension with given options. +func NewHighlighting(opts ...Option) goldmark.Extender { + return &highlighting{ + options: opts, + } +} + +// Extend implements goldmark.Extender. +func (e *highlighting) Extend(m goldmark.Markdown) { + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(NewHTMLRenderer(e.options...), 200), + )) +} diff --git a/gno.land/pkg/gnoweb/markdown/highlighting_test.go b/gno.land/pkg/gnoweb/markdown/highlighting_test.go new file mode 100644 index 00000000000..25bc4fedd61 --- /dev/null +++ b/gno.land/pkg/gnoweb/markdown/highlighting_test.go @@ -0,0 +1,568 @@ +// This file was copied from https://github.com/yuin/goldmark-highlighting + +package markdown + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/alecthomas/chroma/v2" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/testutil" + "github.com/yuin/goldmark/util" +) + +func TestHighlighting(t *testing.T) { + var css bytes.Buffer + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithStyle("monokai"), + WithCSSWriter(&css), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(false), + ), + WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) { + _, ok := c.Language() + if entering { + if !ok { + w.WriteString("
    ")
    +							return
    +						}
    +						w.WriteString(`
    `) + } else { + if !ok { + w.WriteString("
    ") + return + } + w.WriteString(`
`) + } + }), + WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option { + if language, ok := c.Language(); ok { + // Turn on line numbers for Go only. + if string(language) == "go" { + return []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + } + } + } + return nil + }), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= +`+"``` go\n"+`func main() { + fmt.Println("ok") +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
1func main() {
+2    fmt.Println("ok")
+3}
+
+`) { + t.Errorf("failed to render HTML\n%s", buffer.String()) + } + + expected := strings.TrimSpace(`/* Background */ .bg { color: #f8f8f2; background-color: #272822; } +/* PreWrapper */ .chroma { color: #f8f8f2; background-color: #272822; } +/* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #f8f8f2; background-color: #3c3d38 } +/* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #f8f8f2; background-color: #3c3d38 } +/* Error */ .chroma .err { color: #960050; background-color: #1e0010 } +/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } +/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } +/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } +/* LineHighlight */ .chroma .hl { background-color: #3c3d38 } +/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* Line */ .chroma .line { display: flex; } +/* Keyword */ .chroma .k { color: #66d9ef } +/* KeywordConstant */ .chroma .kc { color: #66d9ef } +/* KeywordDeclaration */ .chroma .kd { color: #66d9ef } +/* KeywordNamespace */ .chroma .kn { color: #f92672 } +/* KeywordPseudo */ .chroma .kp { color: #66d9ef } +/* KeywordReserved */ .chroma .kr { color: #66d9ef } +/* KeywordType */ .chroma .kt { color: #66d9ef } +/* NameAttribute */ .chroma .na { color: #a6e22e } +/* NameClass */ .chroma .nc { color: #a6e22e } +/* NameConstant */ .chroma .no { color: #66d9ef } +/* NameDecorator */ .chroma .nd { color: #a6e22e } +/* NameException */ .chroma .ne { color: #a6e22e } +/* NameFunction */ .chroma .nf { color: #a6e22e } +/* NameOther */ .chroma .nx { color: #a6e22e } +/* NameTag */ .chroma .nt { color: #f92672 } +/* Literal */ .chroma .l { color: #ae81ff } +/* LiteralDate */ .chroma .ld { color: #e6db74 } +/* LiteralString */ .chroma .s { color: #e6db74 } +/* LiteralStringAffix */ .chroma .sa { color: #e6db74 } +/* LiteralStringBacktick */ .chroma .sb { color: #e6db74 } +/* LiteralStringChar */ .chroma .sc { color: #e6db74 } +/* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 } +/* LiteralStringDoc */ .chroma .sd { color: #e6db74 } +/* LiteralStringDouble */ .chroma .s2 { color: #e6db74 } +/* LiteralStringEscape */ .chroma .se { color: #ae81ff } +/* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 } +/* LiteralStringInterpol */ .chroma .si { color: #e6db74 } +/* LiteralStringOther */ .chroma .sx { color: #e6db74 } +/* LiteralStringRegex */ .chroma .sr { color: #e6db74 } +/* LiteralStringSingle */ .chroma .s1 { color: #e6db74 } +/* LiteralStringSymbol */ .chroma .ss { color: #e6db74 } +/* LiteralNumber */ .chroma .m { color: #ae81ff } +/* LiteralNumberBin */ .chroma .mb { color: #ae81ff } +/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff } +/* LiteralNumberHex */ .chroma .mh { color: #ae81ff } +/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff } +/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff } +/* LiteralNumberOct */ .chroma .mo { color: #ae81ff } +/* Operator */ .chroma .o { color: #f92672 } +/* OperatorWord */ .chroma .ow { color: #f92672 } +/* Comment */ .chroma .c { color: #75715e } +/* CommentHashbang */ .chroma .ch { color: #75715e } +/* CommentMultiline */ .chroma .cm { color: #75715e } +/* CommentSingle */ .chroma .c1 { color: #75715e } +/* CommentSpecial */ .chroma .cs { color: #75715e } +/* CommentPreproc */ .chroma .cp { color: #75715e } +/* CommentPreprocFile */ .chroma .cpf { color: #75715e } +/* GenericDeleted */ .chroma .gd { color: #f92672 } +/* GenericEmph */ .chroma .ge { font-style: italic } +/* GenericInserted */ .chroma .gi { color: #a6e22e } +/* GenericStrong */ .chroma .gs { font-weight: bold } +/* GenericSubheading */ .chroma .gu { color: #75715e }`) + + gotten := strings.TrimSpace(css.String()) + + if expected != gotten { + diff := testutil.DiffPretty([]byte(expected), []byte(gotten)) + t.Errorf("incorrect CSS.\n%s", string(diff)) + } +} + +func TestHighlighting2(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + Highlighting, + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= +`+"```"+` +func main() { + fmt.Println("ok") +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
func main() {
+    fmt.Println("ok")
+}
+
+`) { + t.Error("failed to render HTML") + } +} + +func TestHighlighting3(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + Highlighting, + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= + +`+"```"+`cpp {hl_lines=[1,2]} +#include +int main() { + std::cout<< "hello" << std::endl; +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
#include <iostream>
+int main() {
+    std::cout<< "hello" << std::endl;
+}
+
+`) { + t.Errorf("failed to render HTML:\n%s", buffer.String()) + } +} + +func TestHighlightingCustom(t *testing.T) { + custom := chroma.MustNewStyle("custom", chroma.StyleEntries{ + chroma.Background: "#cccccc bg:#1d1d1d", + chroma.Comment: "#999999", + chroma.CommentSpecial: "#cd0000", + chroma.Keyword: "#cc99cd", + chroma.KeywordDeclaration: "#cc99cd", + chroma.KeywordNamespace: "#cc99cd", + chroma.KeywordType: "#cc99cd", + chroma.Operator: "#67cdcc", + chroma.OperatorWord: "#cdcd00", + chroma.NameClass: "#f08d49", + chroma.NameBuiltin: "#f08d49", + chroma.NameFunction: "#f08d49", + chroma.NameException: "bold #666699", + chroma.NameVariable: "#00cdcd", + chroma.LiteralString: "#7ec699", + chroma.LiteralNumber: "#f08d49", + chroma.LiteralStringBoolean: "#f08d49", + chroma.GenericHeading: "bold #000080", + chroma.GenericSubheading: "bold #800080", + chroma.GenericDeleted: "#e2777a", + chroma.GenericInserted: "#cc99cd", + chroma.GenericError: "#e2777a", + chroma.GenericEmph: "italic", + chroma.GenericStrong: "bold", + chroma.GenericPrompt: "bold #000080", + chroma.GenericOutput: "#888", + chroma.GenericTraceback: "#04D", + chroma.GenericUnderline: "underline", + chroma.Error: "border:#e2777a", + }) + + var css bytes.Buffer + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithStyle("monokai"), // to make sure it is overrided even if present + WithCustomStyle(custom), + WithCSSWriter(&css), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(false), + ), + WithWrapperRenderer(func(w util.BufWriter, c CodeBlockContext, entering bool) { + _, ok := c.Language() + if entering { + if !ok { + w.WriteString("
")
+							return
+						}
+						w.WriteString(`
`) + } else { + if !ok { + w.WriteString("
") + return + } + w.WriteString(`
`) + } + }), + WithCodeBlockOptions(func(c CodeBlockContext) []chromahtml.Option { + if language, ok := c.Language(); ok { + // Turn on line numbers for Go only. + if string(language) == "go" { + return []chromahtml.Option{ + chromahtml.WithLineNumbers(true), + } + } + } + return nil + }), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte(` +Title +======= +`+"``` go\n"+`func main() { + fmt.Println("ok") +} +`+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +

Title

+
1func main() {
+2    fmt.Println("ok")
+3}
+
+`) { + t.Error("failed to render HTML", buffer.String()) + } + + expected := strings.TrimSpace(`/* Background */ .bg { color: #cccccc; background-color: #1d1d1d; } +/* PreWrapper */ .chroma { color: #cccccc; background-color: #1d1d1d; } +/* LineNumbers targeted by URL anchor */ .chroma .ln:target { color: #cccccc; background-color: #333333 } +/* LineNumbersTable targeted by URL anchor */ .chroma .lnt:target { color: #cccccc; background-color: #333333 } +/* Error */ .chroma .err { } +/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } +/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } +/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } +/* LineHighlight */ .chroma .hl { background-color: #333333 } +/* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 } +/* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #666666 } +/* Line */ .chroma .line { display: flex; } +/* Keyword */ .chroma .k { color: #cc99cd } +/* KeywordConstant */ .chroma .kc { color: #cc99cd } +/* KeywordDeclaration */ .chroma .kd { color: #cc99cd } +/* KeywordNamespace */ .chroma .kn { color: #cc99cd } +/* KeywordPseudo */ .chroma .kp { color: #cc99cd } +/* KeywordReserved */ .chroma .kr { color: #cc99cd } +/* KeywordType */ .chroma .kt { color: #cc99cd } +/* NameBuiltin */ .chroma .nb { color: #f08d49 } +/* NameClass */ .chroma .nc { color: #f08d49 } +/* NameException */ .chroma .ne { color: #666699; font-weight: bold } +/* NameFunction */ .chroma .nf { color: #f08d49 } +/* NameVariable */ .chroma .nv { color: #00cdcd } +/* LiteralString */ .chroma .s { color: #7ec699 } +/* LiteralStringAffix */ .chroma .sa { color: #7ec699 } +/* LiteralStringBacktick */ .chroma .sb { color: #7ec699 } +/* LiteralStringChar */ .chroma .sc { color: #7ec699 } +/* LiteralStringDelimiter */ .chroma .dl { color: #7ec699 } +/* LiteralStringDoc */ .chroma .sd { color: #7ec699 } +/* LiteralStringDouble */ .chroma .s2 { color: #7ec699 } +/* LiteralStringEscape */ .chroma .se { color: #7ec699 } +/* LiteralStringHeredoc */ .chroma .sh { color: #7ec699 } +/* LiteralStringInterpol */ .chroma .si { color: #7ec699 } +/* LiteralStringOther */ .chroma .sx { color: #7ec699 } +/* LiteralStringRegex */ .chroma .sr { color: #7ec699 } +/* LiteralStringSingle */ .chroma .s1 { color: #7ec699 } +/* LiteralStringSymbol */ .chroma .ss { color: #7ec699 } +/* LiteralNumber */ .chroma .m { color: #f08d49 } +/* LiteralNumberBin */ .chroma .mb { color: #f08d49 } +/* LiteralNumberFloat */ .chroma .mf { color: #f08d49 } +/* LiteralNumberHex */ .chroma .mh { color: #f08d49 } +/* LiteralNumberInteger */ .chroma .mi { color: #f08d49 } +/* LiteralNumberIntegerLong */ .chroma .il { color: #f08d49 } +/* LiteralNumberOct */ .chroma .mo { color: #f08d49 } +/* Operator */ .chroma .o { color: #67cdcc } +/* OperatorWord */ .chroma .ow { color: #cdcd00 } +/* Comment */ .chroma .c { color: #999999 } +/* CommentHashbang */ .chroma .ch { color: #999999 } +/* CommentMultiline */ .chroma .cm { color: #999999 } +/* CommentSingle */ .chroma .c1 { color: #999999 } +/* CommentSpecial */ .chroma .cs { color: #cd0000 } +/* CommentPreproc */ .chroma .cp { color: #999999 } +/* CommentPreprocFile */ .chroma .cpf { color: #999999 } +/* GenericDeleted */ .chroma .gd { color: #e2777a } +/* GenericEmph */ .chroma .ge { font-style: italic } +/* GenericError */ .chroma .gr { color: #e2777a } +/* GenericHeading */ .chroma .gh { color: #000080; font-weight: bold } +/* GenericInserted */ .chroma .gi { color: #cc99cd } +/* GenericOutput */ .chroma .go { color: #888888 } +/* GenericPrompt */ .chroma .gp { color: #000080; font-weight: bold } +/* GenericStrong */ .chroma .gs { font-weight: bold } +/* GenericSubheading */ .chroma .gu { color: #800080; font-weight: bold } +/* GenericTraceback */ .chroma .gt { color: #0044dd } +/* GenericUnderline */ .chroma .gl { text-decoration: underline }`) + + gotten := strings.TrimSpace(css.String()) + + if expected != gotten { + diff := testutil.DiffPretty([]byte(expected), []byte(gotten)) + t.Errorf("incorrect CSS.\n%s", string(diff)) + } +} + +func TestHighlightingHlLines(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithFormatOptions( + chromahtml.WithClasses(true), + ), + ), + ), + ) + + for i, test := range []struct { + attributes string + expect []int + }{ + {`hl_lines=["2"]`, []int{2}}, + {`hl_lines=["2-3",5],linenostart=5`, []int{2, 3, 5}}, + {`hl_lines=["2-3"]`, []int{2, 3}}, + {`hl_lines=["2-3",5],linenostart="5"`, []int{2, 3}}, // linenostart must be a number. string values are ignored + } { + t.Run(fmt.Sprint(i), func(t *testing.T) { + var buffer bytes.Buffer + codeBlock := fmt.Sprintf(`bash {%s} +LINE1 +LINE2 +LINE3 +LINE4 +LINE5 +LINE6 +LINE7 +LINE8 +`, test.attributes) + + if err := markdown.Convert([]byte(` +`+"```"+codeBlock+"```"+` +`), &buffer); err != nil { + t.Fatal(err) + } + + for _, line := range test.expect { + expectStr := fmt.Sprintf("LINE%d\n", line) + if !strings.Contains(buffer.String(), expectStr) { + t.Fatal("got\n", buffer.String(), "\nexpected\n", expectStr) + } + } + }) + } +} + +type nopPreWrapper struct{} + +// Start is called to write a start
 element.
+func (nopPreWrapper) Start(code bool, styleAttr string) string { return "" }
+
+// End is called to write the end 
element. +func (nopPreWrapper) End(code bool) string { return "" } + +func TestHighlightingLinenos(t *testing.T) { + outputLineNumbersInTable := `
+ +
+1 + +LINE1 +
+
` + + for i, test := range []struct { + attributes string + lineNumbers bool + lineNumbersInTable bool + expect string + }{ + {`linenos=true`, false, false, `1LINE1 +`}, + {`linenos=false`, false, false, `LINE1 +`}, + {``, true, false, `1LINE1 +`}, + {``, true, true, outputLineNumbersInTable}, + {`linenos=inline`, true, true, `1LINE1 +`}, + {`linenos=foo`, false, false, `1LINE1 +`}, + {`linenos=table`, false, false, outputLineNumbersInTable}, + } { + t.Run(fmt.Sprint(i), func(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithFormatOptions( + chromahtml.WithLineNumbers(test.lineNumbers), + chromahtml.LineNumbersInTable(test.lineNumbersInTable), + chromahtml.WithPreWrapper(nopPreWrapper{}), + chromahtml.WithClasses(true), + ), + ), + ), + ) + + var buffer bytes.Buffer + codeBlock := fmt.Sprintf(`bash {%s} +LINE1 +`, test.attributes) + + content := "```" + codeBlock + "```" + + if err := markdown.Convert([]byte(content), &buffer); err != nil { + t.Fatal(err) + } + + s := strings.TrimSpace(buffer.String()) + + if s != test.expect { + t.Fatal("got\n", s, "\nexpected\n", test.expect) + } + }) + } +} + +func TestHighlightingGuessLanguage(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + WithGuessLanguage(true), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(true), + ), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte("```"+` +LINE +`+"```"), &buffer); err != nil { + t.Fatal(err) + } + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +
1LINE
+
+`) { + t.Errorf("render mismatch, got\n%s", buffer.String()) + } +} + +func TestCoalesceNeeded(t *testing.T) { + markdown := goldmark.New( + goldmark.WithExtensions( + NewHighlighting( + // WithGuessLanguage(true), + WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(true), + ), + ), + ), + ) + var buffer bytes.Buffer + if err := markdown.Convert([]byte("```http"+` +GET /foo HTTP/1.1 +Content-Type: application/json +User-Agent: foo + +{ + "hello": "world" +} +`+"```"), &buffer); err != nil { + t.Fatal(err) + } + if strings.TrimSpace(buffer.String()) != strings.TrimSpace(` +
1GET /foo HTTP/1.1
+2Content-Type: application/json
+3User-Agent: foo
+4
+5{
+6  "hello": "world"
+7}
+
+`) { + t.Errorf("render mismatch, got\n%s", buffer.String()) + } +} diff --git a/gno.land/pkg/gnoweb/markdown/toc.go b/gno.land/pkg/gnoweb/markdown/toc.go new file mode 100644 index 00000000000..59d4941fabf --- /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..9b045061a00 --- /dev/null +++ b/gno.land/pkg/gnoweb/public/js/realmhelp.js @@ -0,0 +1 @@ +var s=class a{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(a.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(a.SELECTORS.func)),this.DOM.addressInput=e.querySelector(a.SELECTORS.addressInput),this.DOM.cmdModeSelect=e.querySelector(a.SELECTORS.cmdModeSelect),console.log(this.DOM),this.funcList=this.DOM.funcs.map(t=>new r(t)),this.bindEvents())}bindEvents(){let{addressInput:e,cmdModeSelect:t}=this.DOM;e?.addEventListener("input",()=>{this.funcList.forEach(n=>n.updateAddr(e.value))}),t?.addEventListener("change",n=>{let d=n.target;this.funcList.forEach(l=>l.updateMode(d.value))})}},r=class a{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(a.SELECTORS.address)),args:Array.from(e.querySelectorAll(a.SELECTORS.args)),modes:Array.from(e.querySelectorAll(a.SELECTORS.mode))},this.funcName=e.dataset.func||null,this.bindEvents()}bindEvents(){this.DOM.el.addEventListener("input",e=>{let t=e.target;t.dataset.role==="help-param-input"&&this.updateArg(t.dataset.param||"",t.value)})}updateArg(e,t){this.DOM.args.filter(n=>n.dataset.arg===e).forEach(n=>{n.textContent=t.trim()||""})}updateAddr(e){this.DOM.addrs.forEach(t=>{t.textContent=e.trim()||"ADDRESS"})}updateMode(e){this.DOM.modes.forEach(t=>{let n=t.dataset.codeMode===e;t.className=n?"inline":"hidden",t.dataset.copyContent=n?`help-cmd-${this.funcName}`:""})}},i=()=>new s;export{i 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/styles.css b/gno.land/pkg/gnoweb/public/styles.css new file mode 100644 index 00000000000..9d79989f1f8 --- /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:.875rem}.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;width:100%;border-collapse:collapse}.realm-content td,.realm-content th{border-width:1px;--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity));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 table th:first-child,.realm-content td:first-child{padding-left:0}.realm-content table th:last-child,.realm-content td:last-child{padding-right:0}.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-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-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))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(124 124 124/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/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))}.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-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.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\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.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-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\: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