diff --git a/.github/codecov.yml b/.github/codecov.yml index f0cb9583cf2..d973bcce859 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -10,19 +10,10 @@ coverage: round: down precision: 2 status: - project: + patch: # new lines default: - target: auto - threshold: 5 # Let's decrease this later. - base: parent - if_no_uploads: error - if_not_found: success - if_ci_failed: error - only_pulls: false - patch: - default: - target: auto - threshold: 5 # Let's decrease this later. + target: 80 + threshold: 10 base: auto if_no_uploads: error if_not_found: success diff --git a/.github/goreleaser.yaml b/.github/goreleaser.yaml index 71a8ba98745..95f7fdaab4c 100644 --- a/.github/goreleaser.yaml +++ b/.github/goreleaser.yaml @@ -23,10 +23,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" - id: gnoland main: ./gno.land/cmd/gnoland binary: gnoland @@ -38,10 +34,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" - id: gnokey main: ./gno.land/cmd/gnokey binary: gnokey @@ -53,10 +45,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" - id: gnoweb main: ./gno.land/cmd/gnoweb binary: gnoweb @@ -68,10 +56,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" - id: gnofaucet dir: ./contribs/gnofaucet binary: gnofaucet @@ -83,10 +67,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" # Gno Contribs # NOTE: Contribs binary will be added in a single docker image below: gnocontribs - id: gnobro @@ -100,10 +80,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" - id: gnogenesis dir: ./contribs/gnogenesis binary: gnogenesis @@ -115,10 +91,6 @@ builds: goarch: - amd64 - arm64 - - arm - goarm: - - "6" - - "7" gomod: proxy: true @@ -188,48 +160,6 @@ dockers: - 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 }}:{{ .Env.TAG_VERSION }}-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 }}:{{ .Env.TAG_VERSION }}-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 @@ -274,51 +204,7 @@ dockers: - 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:{{ .Env.TAG_VERSION }}-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:{{ .Env.TAG_VERSION }}-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 @@ -352,40 +238,6 @@ dockers: - "--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:{{ .Env.TAG_VERSION }}-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:{{ .Env.TAG_VERSION }}-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 @@ -420,40 +272,6 @@ dockers: - "--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:{{ .Env.TAG_VERSION }}-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:{{ .Env.TAG_VERSION }}-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 # gnofaucet - use: buildx @@ -488,40 +306,6 @@ dockers: - "--label=org.opencontainers.image.version={{.Version}}" ids: - gnofaucet - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 6 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-armv6" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv6" - build_flag_templates: - - "--target=gnofaucet" - - "--platform=linux/arm/v6" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnofaucet" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnofaucet - - use: buildx - dockerfile: Dockerfile.release - goos: linux - goarch: arm - goarm: 7 - image_templates: - - "ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-armv7" - - "ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv7" - build_flag_templates: - - "--target=gnofaucet" - - "--platform=linux/arm/v7" - - "--label=org.opencontainers.image.created={{.Date}}" - - "--label=org.opencontainers.image.title={{.ProjectName}}/gnofaucet" - - "--label=org.opencontainers.image.revision={{.FullCommit}}" - - "--label=org.opencontainers.image.version={{.Version}}" - ids: - - gnofaucet # gnocontribs - use: buildx @@ -568,52 +352,6 @@ dockers: - 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/ @@ -623,84 +361,60 @@ docker_manifests: 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 }}:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}:{{ .Env.TAG_VERSION }}-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:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoland:{{ .Env.TAG_VERSION }}-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:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnokey:{{ .Env.TAG_VERSION }}-amd64 - 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: - 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:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnoweb:{{ .Env.TAG_VERSION }}-armv7 # gnofaucet - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-arm64v8 - - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-armv6 - - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Version }}-armv7 - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }} image_templates: - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-amd64 - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-arm64v8 - - 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 diff --git a/.github/workflows/build_template.yml b/.github/workflows/build_template.yml index a2c96f2d37e..00c14a66cb2 100644 --- a/.github/workflows/build_template.yml +++ b/.github/workflows/build_template.yml @@ -23,9 +23,14 @@ jobs: - name: Check generated files are up to date working-directory: ${{ inputs.modulepath }} run: | - go generate -x ./... - if [ "$(git status -s)" != "" ]; then - echo "command 'go generate' creates file that differ from git tree, please run 'go generate' and commit:" - git status -s - exit 1 + if make -qp | grep -q '^generate:'; then + make generate + + if [ "$(git status -s)" != "" ]; then + echo "command 'make generate' creates files that differ from the git tree, please run 'make generate' and commit:" + git status -s + exit 1 + fi + else + echo "'make generate' rule not found, skipping." fi diff --git a/.github/workflows/genesis-verify.yml b/.github/workflows/genesis-verify.yml index acc41cc99ad..a9e9a43e55a 100644 --- a/.github/workflows/genesis-verify.yml +++ b/.github/workflows/genesis-verify.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - testnet: [ "test5.gno.land" ] + testnet: [ ] # Currently, all active testnet deployment genesis.json are legacy runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index a293469bb5d..2b27a2537e1 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -1,11 +1,14 @@ # generate Go docs and publish on gh-pages branch # Live at: https://gnolang.github.io/gno -name: Go Reference Docs Deployment +name: GitHub pages (godoc & stdlib_diff) build and deploy on: push: branches: - master + pull_request: + branches: + - master workflow_dispatch: permissions: @@ -19,29 +22,39 @@ concurrency: jobs: build: - if: ${{ github.repository == 'gnolang/gno' }} # Alternatively, validate based on provided tokens and permissions. + if: github.repository == 'gnolang/gno' # Alternatively, validate based on provided tokens and permissions. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - - run: echo "GOROOT=$(go env GOROOT)" >> $GITHUB_ENV - - run: echo $GOROOT + # Use the goroot at the top of the project to compare with the GnoVM + # stdlib, rather than the one in stdlib_diff (which may have a go.mod with + # a different toolchain version). + - run: echo "GOROOT_SAVE=$(go env GOROOT)" >> $GITHUB_ENV - run: "cd misc/stdlib_diff && make gen" - run: "cd misc/gendocs && make install gen" - run: "mkdir -p pages_output/stdlib_diff" - run: | cp -r misc/gendocs/godoc/* pages_output/ cp -r misc/stdlib_diff/stdlib_diff/* pages_output/stdlib_diff/ + + # These two last steps will be skipped on pull requests - uses: actions/configure-pages@v5 id: pages + if: github.event_name != 'pull_request' + - uses: actions/upload-pages-artifact@v3 + if: github.event_name != 'pull_request' with: path: ./pages_output deploy: - if: ${{ github.repository == 'gnolang/gno' }} # Alternatively, validate based on provided tokens and permissions. + if: > + github.repository == 'gnolang/gno' && + github.ref == 'refs/heads/master' && + github.event_name == 'push' runs-on: ubuntu-latest environment: name: github-pages diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index b02e7b364e6..c4bc26a45fc 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -14,6 +14,9 @@ on: # Changes to examples/ can create failures in gno.land, eg. txtars, # see: https://github.com/gnolang/gno/pull/3590 - examples/** + # We trigger the testing workflow for changes to the main go.mod, + # since this can affect test results + - go.mod workflow_dispatch: jobs: diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 7a015b74e09..08b0b66c4e8 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -8,6 +8,9 @@ on: paths: - gnovm/** - tm2/** # GnoVM has a dependency on TM2 types + # We trigger the testing workflow for changes to the main go.mod, + # since this can affect test results + - go.mod workflow_dispatch: jobs: diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml index ddf51ac6683..5b6f76a9135 100644 --- a/.github/workflows/releaser-master.yml +++ b/.github/workflows/releaser-master.yml @@ -29,8 +29,8 @@ jobs: go-version-file: go.mod cache: true - - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.9 + - uses: sigstore/cosign-installer@v3.8.0 + - uses: anchore/sbom-action/download-syft@v0.18.0 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/releaser-nightly.yml b/.github/workflows/releaser-nightly.yml index 47b6cabb223..62066db24a1 100644 --- a/.github/workflows/releaser-nightly.yml +++ b/.github/workflows/releaser-nightly.yml @@ -23,8 +23,8 @@ jobs: go-version-file: go.mod cache: true - - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.9 + - uses: sigstore/cosign-installer@v3.8.0 + - uses: anchore/sbom-action/download-syft@v0.18.0 - uses: docker/login-action@v3 with: diff --git a/.github/workflows/tm2.yml b/.github/workflows/tm2.yml index 757391eab8c..d2157eb8828 100644 --- a/.github/workflows/tm2.yml +++ b/.github/workflows/tm2.yml @@ -7,6 +7,9 @@ on: pull_request: paths: - tm2/** + # We trigger the testing workflow for changes to the main go.mod, + # since this can affect test results + - go.mod workflow_dispatch: jobs: diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index 43a505ad15a..5db91a73413 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -33,12 +33,18 @@ 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(), + If: c.And( + c.BaseBranch("^master$"), + c.CreatedFromFork(), + ), + Then: r.MaintainerCanModify(), }, { Description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff", - If: c.FileChanged(gh, "^docs/"), + If: c.And( + c.BaseBranch("^master$"), + c.FileChanged(gh, "^docs/"), + ), Then: r.And( r.Or( r.AuthorInTeam(gh, "tech-staff"), @@ -57,7 +63,10 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { }, { Description: "Pending initial approval by a review team member, or review from tech-staff", - If: c.Not(c.AuthorInTeam(gh, "tech-staff")), + If: c.And( + c.BaseBranch("^master$"), + c.Not(c.AuthorInTeam(gh, "tech-staff")), + ), Then: r. If(r.Or( r.ReviewByOrgMembers(gh).WithDesiredState(utils.ReviewStateApproved), @@ -91,7 +100,7 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { { Description: "Determine if infra needs to be updated before merging", If: c.And( - c.BaseBranch("master"), + c.BaseBranch("^master$"), c.Or( c.FileChanged(gh, `Dockerfile`), c.FileChanged(gh, `^misc/deployments`), diff --git a/contribs/github-bot/internal/requirements/reviewer.go b/contribs/github-bot/internal/requirements/reviewer.go index 3e91c394fb7..36216b0b75c 100644 --- a/contribs/github-bot/internal/requirements/reviewer.go +++ b/contribs/github-bot/internal/requirements/reviewer.go @@ -11,6 +11,38 @@ import ( "github.com/xlab/treeprint" ) +// deduplicateReviews returns a list of reviews with at most 1 review per +// author, where approval/changes requested reviews are preferred over comments +// and later reviews are preferred over earlier ones. +func deduplicateReviews(reviews []*github.PullRequestReview) []*github.PullRequestReview { + added := make(map[string]int) + result := make([]*github.PullRequestReview, 0, len(reviews)) + for _, rev := range reviews { + idx, ok := added[rev.User.GetLogin()] + switch utils.ReviewState(rev.GetState()) { + case utils.ReviewStateApproved, utils.ReviewStateChangesRequested: + // this review changes the "approval state", and is more relevant, + // so substitute it with the previous one if it exists. + if ok { + result[idx] = rev + } else { + result = append(result, rev) + added[rev.User.GetLogin()] = len(result) - 1 + } + case utils.ReviewStateCommented: + // this review does not change the "approval state", so only append + // it if a previous review doesn't exist. + if !ok { + result = append(result, rev) + added[rev.User.GetLogin()] = len(result) - 1 + } + default: + panic(fmt.Sprintf("invalid review state %q", rev.GetState())) + } + } + return result +} + // ReviewByUserRequirement asserts that there is a review by the given user, // and if given that the review matches the desiredState. type ReviewByUserRequirement struct { @@ -28,6 +60,23 @@ func (r *ReviewByUserRequirement) IsSatisfied(pr *github.PullRequest, details tr detail += fmt.Sprintf(" (with state %q)", r.desiredState) } + // 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) + } + reviews = deduplicateReviews(reviews) + + 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()) + result := r.desiredState == "" || review.GetState() == r.desiredState + return utils.AddStatusNode(result, detail, details) + } + } + r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber()) + // If not a dry run, make the user a reviewer if he's not already. if !r.gh.DryRun { requested := false @@ -62,22 +111,6 @@ func (r *ReviewByUserRequirement) IsSatisfied(pr *github.PullRequest, details tr } } - // 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()) - result := r.desiredState == "" || review.GetState() == r.desiredState - return utils.AddStatusNode(result, detail, details) - } - } - r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber()) - return utils.AddStatusNode(false, detail, details) } @@ -123,6 +156,14 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det return utils.AddStatusNode(false, detail, details) } + reviews, err := r.gh.ListPRReviews(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to fetch existing reviews of pr %d: %v", pr.GetNumber(), err) + return utils.AddStatusNode(false, detail, details) + } + + reviews = deduplicateReviews(reviews) + // If not a dry run, request a team review if no member has reviewed yet, // and the team review has not been requested. if !r.gh.DryRun { @@ -144,12 +185,18 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det if !teamRequested { for _, user := range reviewers.Users { - if slices.ContainsFunc(teamMembers, func(memb *github.User) bool { - return memb.GetID() == user.GetID() - }) { + if containsUserWithLogin(teamMembers, user.GetLogin()) { usersRequested = append(usersRequested, user.GetLogin()) } } + + for _, rev := range reviews { + // if not already requested and user is a team member... + if !slices.Contains(usersRequested, rev.User.GetLogin()) && + containsUserWithLogin(teamMembers, rev.User.GetLogin()) { + usersRequested = append(usersRequested, rev.User.GetLogin()) + } + } } switch { @@ -176,33 +223,29 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det // Check how many members of this team already reviewed this PR. reviewCount := 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 reviewed this PR: %v", r.team, err) - return utils.AddStatusNode(false, detail, details) - } - stateStr := "" - if r.desiredState != "" { - stateStr = fmt.Sprintf("%q ", r.desiredState) - } for _, review := range reviews { - for _, member := range teamMembers { - if review.GetUser().GetLogin() == member.GetLogin() { - if desired := r.desiredState; desired == "" || desired == review.GetState() { - reviewCount += 1 - } - r.gh.Logger.Debugf( - "Member %s from team %s already reviewed PR %d with state %s (%d/%d required %sreview(s))", - member.GetLogin(), r.team, pr.GetNumber(), review.GetState(), reviewCount, r.count, stateStr, - ) + login := review.GetUser().GetLogin() + if containsUserWithLogin(teamMembers, login) { + if desired := r.desiredState; desired == "" || desired == review.GetState() { + reviewCount += 1 } + r.gh.Logger.Debugf( + "Member %s from team %s already reviewed PR %d with state %s (%d/%d required review(s) with state %q)", + login, r.team, pr.GetNumber(), review.GetState(), reviewCount, r.count, r.desiredState, + ) } } return utils.AddStatusNode(reviewCount >= r.count, detail, details) } +func containsUserWithLogin(users []*github.User, login string) bool { + return slices.ContainsFunc(users, func(u *github.User) bool { + return u.GetLogin() == login + }) +} + // WithCount specifies the number of required reviews. // By default, this is 1. func (r *ReviewByTeamMembersRequirement) WithCount(n uint) *ReviewByTeamMembersRequirement { @@ -262,20 +305,17 @@ func (r *ReviewByOrgMembersRequirement) IsSatisfied(pr *github.PullRequest, deta r.gh.Logger.Errorf("unable to check number of reviews on this PR: %v", err) return utils.AddStatusNode(false, detail, details) } + reviews = deduplicateReviews(reviews) - stateStr := "" - if r.desiredState != "" { - stateStr = fmt.Sprintf("%q ", r.desiredState) - } for _, review := range reviews { if review.GetAuthorAssociation() == "MEMBER" { if r.desiredState == "" || review.GetState() == r.desiredState { reviewed++ } r.gh.Logger.Debugf( - "Member %s already reviewed PR %d with state %s (%d/%d required %sreviews)", + "Member %s already reviewed PR %d with state %s (%d/%d required reviews with state %q)", review.GetUser().GetLogin(), pr.GetNumber(), review.GetState(), - reviewed, r.count, stateStr, + reviewed, r.count, r.desiredState, ) } } diff --git a/contribs/github-bot/internal/requirements/reviewer_test.go b/contribs/github-bot/internal/requirements/reviewer_test.go index 235dca14034..b952294a338 100644 --- a/contribs/github-bot/internal/requirements/reviewer_test.go +++ b/contribs/github-bot/internal/requirements/reviewer_test.go @@ -16,6 +16,68 @@ import ( "github.com/xlab/treeprint" ) +func Test_deduplicateReviews(t *testing.T) { + tests := []struct { + name string + reviews []*github.PullRequestReview + expected []*github.PullRequestReview + }{ + { + name: "three different authors", + reviews: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED")}, + {User: &github.User{Login: github.String("user2")}, State: github.String("CHANGES_REQUESTED")}, + {User: &github.User{Login: github.String("user3")}, State: github.String("COMMENTED")}, + }, + expected: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED")}, + {User: &github.User{Login: github.String("user2")}, State: github.String("CHANGES_REQUESTED")}, + {User: &github.User{Login: github.String("user3")}, State: github.String("COMMENTED")}, + }, + }, + { + name: "single author - approval then comment", + reviews: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED")}, + {User: &github.User{Login: github.String("user1")}, State: github.String("COMMENTED")}, + }, + expected: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED")}, + }, + }, + { + name: "single author - approval then changes requested", + reviews: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED")}, + {User: &github.User{Login: github.String("user1")}, State: github.String("CHANGES_REQUESTED")}, + }, + expected: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("user1")}, State: github.String("CHANGES_REQUESTED")}, + }, + }, + { + name: "two authors - mixed reviews", + reviews: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("userA")}, State: github.String("APPROVED")}, + {User: &github.User{Login: github.String("userB")}, State: github.String("CHANGES_REQUESTED")}, + {User: &github.User{Login: github.String("userA")}, State: github.String("CHANGES_REQUESTED")}, + {User: &github.User{Login: github.String("userB")}, State: github.String("COMMENTED")}, + }, + expected: []*github.PullRequestReview{ + {User: &github.User{Login: github.String("userA")}, State: github.String("CHANGES_REQUESTED")}, + {User: &github.User{Login: github.String("userB")}, State: github.String("CHANGES_REQUESTED")}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := deduplicateReviews(tt.reviews) + assert.Equal(t, tt.expected, result) + }) + } +} + func TestReviewByUser(t *testing.T) { t.Parallel() @@ -34,6 +96,10 @@ func TestReviewByUser(t *testing.T) { }, { User: &github.User{Login: github.String("user")}, State: github.String("APPROVED"), + }, { + // Should be ignored in favour of the following one + User: &github.User{Login: github.String("anotherOne")}, + State: github.String("APPROVED"), }, { User: &github.User{Login: github.String("anotherOne")}, State: github.String("CHANGES_REQUESTED"), @@ -138,6 +204,10 @@ func TestReviewByTeamMembers(t *testing.T) { reviews := []*github.PullRequestReview{ { + // only later review should be counted. + User: &github.User{Login: github.String("user1")}, + State: github.String("CHANGES_REQUESTED"), + }, { User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED"), }, { @@ -149,6 +219,10 @@ func TestReviewByTeamMembers(t *testing.T) { }, { User: &github.User{Login: github.String("user4")}, State: github.String("CHANGES_REQUESTED"), + }, { + // only later review should be counted. + User: &github.User{Login: github.String("user5")}, + State: github.String("APPROVED"), }, { User: &github.User{Login: github.String("user5")}, State: github.String("CHANGES_REQUESTED"), @@ -163,22 +237,113 @@ func TestReviewByTeamMembers(t *testing.T) { for _, testCase := range []struct { name string - team string - count uint - state utils.ReviewState + req *ReviewByTeamMembersRequirement + reviews []*github.PullRequestReview reviewers github.Reviewers expectedResult byte }{ - {"3/3 team members approved", "team1", 3, utils.ReviewStateApproved, reviewers, satisfied}, - {"3/3 team members approved (with user reviewers)", "team1", 3, utils.ReviewStateApproved, userReviewers, satisfied}, - {"1/1 team member approved", "team2", 1, utils.ReviewStateApproved, reviewers, satisfied}, - {"1/2 team member approved", "team2", 2, utils.ReviewStateApproved, reviewers, notSatisfied}, - {"0/1 team member approved", "team3", 1, utils.ReviewStateApproved, reviewers, notSatisfied}, - {"0/1 team member approved with request", "team3", 1, utils.ReviewStateApproved, noReviewers, notSatisfied | withRequest}, - {"team doesn't exist with request", "team4", 1, utils.ReviewStateApproved, noReviewers, notSatisfied | withRequest}, - {"3/3 team members reviewed", "team2", 3, "", reviewers, satisfied}, - {"2/2 team members rejected", "team2", 2, utils.ReviewStateChangesRequested, reviewers, satisfied}, - {"1/3 team members approved", "team2", 3, utils.ReviewStateApproved, reviewers, notSatisfied}, + { + name: "3/3 team members approved", + req: ReviewByTeamMembers(nil, "team1"). + WithCount(3). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: reviewers, + expectedResult: satisfied, + }, + { + name: "3/3 team members approved (with user reviewers)", + req: ReviewByTeamMembers(nil, "team1"). + WithCount(3). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: userReviewers, + expectedResult: satisfied, + }, + { + name: "1/1 team member approved", + req: ReviewByTeamMembers(nil, "team2"). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: reviewers, + expectedResult: satisfied, + }, + { + name: "1/2 team member approved", + req: ReviewByTeamMembers(nil, "team2"). + WithCount(2). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: reviewers, + expectedResult: notSatisfied, + }, + { + name: "0/1 team member approved", + req: ReviewByTeamMembers(nil, "team3"). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: reviewers, + expectedResult: notSatisfied, + }, + { + name: "0/1 team member reviewed with request", + req: ReviewByTeamMembers(nil, "team3"), + // Show there are no current reviews, so we actually perform the request. + reviewers: noReviewers, + expectedResult: notSatisfied | withRequest, + }, + { + name: "3/3 team member approved from review list", + req: ReviewByTeamMembers(nil, "team1"). + WithDesiredState(utils.ReviewStateApproved). + WithCount(3), + reviews: reviews, + reviewers: noReviewers, + expectedResult: satisfied, + }, + { + name: "1/2 team member approved from review list", + req: ReviewByTeamMembers(nil, "team3"). + WithDesiredState(utils.ReviewStateApproved). + WithCount(2), + reviews: reviews, + reviewers: noReviewers, + expectedResult: notSatisfied, + }, + { + name: "team doesn't exist with request", + req: ReviewByTeamMembers(nil, "team4"). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: noReviewers, + expectedResult: notSatisfied | withRequest, + }, + { + name: "3/3 team members reviewed", + req: ReviewByTeamMembers(nil, "team2"). + WithCount(3), + reviews: reviews, + reviewers: reviewers, + expectedResult: satisfied, + }, + { + name: "2/2 team members rejected", + req: ReviewByTeamMembers(nil, "team2"). + WithCount(2). + WithDesiredState(utils.ReviewStateChangesRequested), + reviews: reviews, + reviewers: reviewers, + expectedResult: satisfied, + }, + { + name: "1/3 team members approved", + req: ReviewByTeamMembers(nil, "team2"). + WithCount(3). + WithDesiredState(utils.ReviewStateApproved), + reviews: reviews, + reviewers: reviewers, + expectedResult: notSatisfied, + }, } { t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -202,17 +367,17 @@ func TestReviewByTeamMembers(t *testing.T) { ), mock.WithRequestMatchPages( mock.EndpointPattern{ - Pattern: fmt.Sprintf("/orgs/teams/%s/members", testCase.team), + Pattern: fmt.Sprintf("/orgs/teams/%s/members", testCase.req.team), Method: "GET", }, - members[testCase.team], + members[testCase.req.team], ), mock.WithRequestMatchPages( mock.EndpointPattern{ Pattern: "/repos/pulls/0/reviews", Method: "GET", }, - reviews, + testCase.reviews, ), ) @@ -224,13 +389,13 @@ func TestReviewByTeamMembers(t *testing.T) { pr := &github.PullRequest{} details := treeprint.New() - requirement := ReviewByTeamMembers(gh, testCase.team). - WithCount(testCase.count). - WithDesiredState(testCase.state) + req := new(ReviewByTeamMembersRequirement) + *req = *testCase.req + req.gh = gh - expSatisfied := testCase.expectedResult&satisfied != 0 + expSatisfied := testCase.expectedResult&satisfied > 0 expRequested := testCase.expectedResult&withRequest > 0 - assert.Equal(t, expSatisfied, requirement.IsSatisfied(pr, details), + assert.Equal(t, expSatisfied, req.IsSatisfied(pr, details), "requirement should have a satisfied status: %t", expSatisfied) assert.True(t, utils.TestLastNodeStatus(t, expSatisfied, details), "requirement details should have a status: %t", expSatisfied) @@ -248,6 +413,11 @@ func TestReviewByOrgMembers(t *testing.T) { User: &github.User{Login: github.String("user1")}, State: github.String("APPROVED"), AuthorAssociation: github.String("MEMBER"), + }, { + // should be ignored in favour of the following one. + User: &github.User{Login: github.String("user2")}, + State: github.String("CHANGES_REQUESTED"), + AuthorAssociation: github.String("COLLABORATOR"), }, { User: &github.User{Login: github.String("user2")}, State: github.String("APPROVED"), diff --git a/contribs/gnodev/cmd/gnodev/accounts.go b/contribs/gnodev/cmd/gnodev/accounts.go index 95c2c3efffc..e148f4827c1 100644 --- a/contribs/gnodev/cmd/gnodev/accounts.go +++ b/contribs/gnodev/cmd/gnodev/accounts.go @@ -49,7 +49,7 @@ func (va varPremineAccounts) String() string { return strings.Join(accs, ",") } -func generateBalances(bk *address.Book, cfg *devCfg) (gnoland.Balances, error) { +func generateBalances(bk *address.Book, cfg *AppConfig) (gnoland.Balances, error) { bls := gnoland.NewBalances() premineBalance := std.Coins{std.NewCoin(ugnot.Denom, 10e12)} diff --git a/contribs/gnodev/cmd/gnodev/app.go b/contribs/gnodev/cmd/gnodev/app.go new file mode 100644 index 00000000000..5744be8d0b4 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/app.go @@ -0,0 +1,582 @@ +package main + +import ( + "context" + "fmt" + "io" + "log/slog" + "net/http" + "os" + "path/filepath" + "slices" + "strings" + "time" + + "github.com/gnolang/gno/contribs/gnodev/pkg/address" + gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/contribs/gnodev/pkg/proxy" + "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" + "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + osm "github.com/gnolang/gno/tm2/pkg/os" +) + +const ( + DefaultDeployerName = integration.DefaultAccount_Name + DefaultDeployerSeed = integration.DefaultAccount_Seed +) + +var defaultDeployerAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) + +const ( + NodeLogName = "Node" + WebLogName = "GnoWeb" + KeyPressLogName = "KeyPress" + EventServerLogName = "Event" + AccountsLogName = "Accounts" + LoaderLogName = "Loader" + ProxyLogName = "Proxy" +) + +type App struct { + io commands.IO + start time.Time // Time when the server started + cfg *AppConfig + logger *slog.Logger + pathManager *pathManager + // Contains all the deferred functions of the app. + // Will be triggered on close for cleanup. + deferred func() + + webHomePath string + paths []string + devNode *gnodev.Node + emitterServer *emitter.Server + watcher *watcher.PackageWatcher + loader packages.Loader + book *address.Book + exportPath string + proxy *proxy.PathInterceptor + + // XXX: move this + exported uint +} + +func runApp(cfg *AppConfig, cio commands.IO, dirs ...string) (err error) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var rt *rawterm.RawTerm + var out io.Writer + if cfg.interactive { + var restore func() error + rt, restore, err = setupRawTerm(cfg, cio) + if err != nil { + return fmt.Errorf("unable to init raw term: %w", err) + } + defer restore() + + osm.TrapSignal(func() { + cancel() + restore() + }) + + out = rt + } else { + osm.TrapSignal(cancel) + out = cio.Out() + } + + logger, err := setuplogger(cfg, out) + if err != nil { + return fmt.Errorf("unable to setup logger: %w", err) + } + + app := NewApp(logger, cfg, cio) + if err := app.Setup(ctx, dirs...); err != nil { + return err + } + defer app.Close() + + if rt != nil { + go func() { + app.RunInteractive(ctx, rt) + cancel() + }() + } + + return app.RunServer(ctx, rt) +} + +func NewApp(logger *slog.Logger, cfg *AppConfig, io commands.IO) *App { + return &App{ + start: time.Now(), + deferred: func() {}, + logger: logger, + cfg: cfg, + io: io, + pathManager: newPathManager(), + } +} + +func (ds *App) Defer(fn func()) { + old := ds.deferred + ds.deferred = func() { + defer old() + fn() + } +} + +func (ds *App) DeferClose(fn func() error) { + ds.Defer(func() { + if err := fn(); err != nil { + ds.logger.Debug("close", "error", err.Error()) + } + }) +} + +func (ds *App) Close() { + ds.deferred() +} + +func (ds *App) Setup(ctx context.Context, dirs ...string) (err error) { + if err := ds.cfg.validateConfigFlags(); err != nil { + return fmt.Errorf("validate error: %w", err) + } + + loggerEvents := ds.logger.WithGroup(EventServerLogName) + ds.emitterServer = emitter.NewServer(loggerEvents) + + // XXX: it would be nice to not have this hardcoded + examplesDir := filepath.Join(ds.cfg.root, "examples") + + // Setup loader and resolver + loaderLogger := ds.logger.WithGroup(LoaderLogName) + resolver, localPaths := setupPackagesResolver(loaderLogger, ds.cfg, dirs...) + ds.loader = packages.NewGlobLoader(examplesDir, resolver) + + // Get user's address book from local keybase + accountLogger := ds.logger.WithGroup(AccountsLogName) + ds.book, err = setupAddressBook(accountLogger, ds.cfg) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + + // Generate user's paths using a comma as the delimiter + qpaths := strings.Split(ds.cfg.paths, ",") + + // Set up the packages modifier and extract paths from queries + // XXX: This should probably be moved into the setup node configuration + modifiers, paths, err := resolvePackagesModifier(ds.cfg, ds.book, qpaths) + if err != nil { + return fmt.Errorf("unable to resolve paths %v: %w", paths, err) + } + + // Add the user's paths to the pre-loaded paths + // Modifiers will be added later to the node config bellow + ds.paths = append(paths, localPaths...) + + // Setup default web home realm, fallback on first local path + switch webHome := ds.cfg.webHome; webHome { + case "": + if len(ds.paths) > 0 { + ds.webHomePath = strings.TrimPrefix(ds.paths[0], ds.cfg.chainDomain) + ds.logger.WithGroup(WebLogName).Info("using default package", "path", ds.paths[0]) + } + case "/", ":none:": // skip + default: + ds.webHomePath = webHome + } + + balances, err := generateBalances(ds.book, ds.cfg) + if err != nil { + return fmt.Errorf("unable to generate balances: %w", err) + } + ds.logger.Debug("balances loaded", "list", balances.List()) + + nodeLogger := ds.logger.WithGroup(NodeLogName) + nodeCfg := setupDevNodeConfig(ds.cfg, nodeLogger, ds.emitterServer, balances, ds.loader) + nodeCfg.PackagesModifier = modifiers // add modifiers + + address := resolveUnixOrTCPAddr(nodeCfg.TMConfig.RPC.ListenAddress) + + // Setup lazy proxy + if ds.cfg.lazyLoader { + proxyLogger := ds.logger.WithGroup(ProxyLogName) + ds.proxy, err = proxy.NewPathInterceptor(proxyLogger, address) + if err != nil { + return fmt.Errorf("unable to setup proxy: %w", err) + } + ds.DeferClose(ds.proxy.Close) + + // Override current rpc listener + nodeCfg.TMConfig.RPC.ListenAddress = ds.proxy.ProxyAddress() + proxyLogger.Debug("proxy started", + "proxy_addr", ds.proxy.ProxyAddress(), + "target_addr", ds.proxy.TargetAddress(), + ) + + proxyLogger.Info("lazy loading is enabled. packages will be loaded only upon a request via a query or transaction.", "loader", ds.loader.Name()) + } else { + nodeCfg.TMConfig.RPC.ListenAddress = fmt.Sprintf("%s://%s", address.Network(), address.String()) + } + + ds.devNode, err = setupDevNode(ctx, ds.cfg, nodeCfg, ds.paths...) + if err != nil { + return err + } + ds.DeferClose(ds.devNode.Close) + + ds.watcher, err = watcher.NewPackageWatcher(loggerEvents, ds.emitterServer) + if err != nil { + return fmt.Errorf("unable to setup packages watcher: %w", err) + } + + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) + + return nil +} + +func (ds *App) setupHandlers(ctx context.Context) (http.Handler, error) { + mux := http.NewServeMux() + remote := ds.devNode.GetRemoteAddress() + + if ds.proxy != nil { + proxyLogger := ds.logger.WithGroup(ProxyLogName) + remote = ds.proxy.TargetAddress() // update remote address with proxy target address + + // Generate initial paths + initPaths := map[string]struct{}{} + for _, pkg := range ds.devNode.ListPkgs() { + initPaths[pkg.Path] = struct{}{} + } + + ds.proxy.HandlePath(func(paths ...string) { + newPath := false + for _, path := range paths { + // Check if the path is an initial path. + if _, ok := initPaths[path]; ok { + continue + } + + // Try to resolve the path first. + // If we are unable to resolve it, ignore and continue + + if _, err := ds.loader.Resolve(path); err != nil { + proxyLogger.Debug("unable to resolve path", + "error", err, + "path", path) + continue + } + + // If we already know this path, continue. + if exist := ds.pathManager.Save(path); exist { + continue + } + + proxyLogger.Info("new monitored path", + "path", path) + + newPath = true + } + + if !newPath { + return + } + + ds.emitterServer.LockEmit() + defer ds.emitterServer.UnlockEmit() + + ds.devNode.SetPackagePaths(ds.paths...) + ds.devNode.AddPackagePaths(ds.pathManager.List()...) + + // Check if the node needs to be reloaded + // XXX: This part can likely be optimized if we believe + // it significantly impacts performance. + for _, path := range paths { + if ds.devNode.HasPackageLoaded(path) { + continue + } + + ds.logger.WithGroup(NodeLogName).Debug("some paths aren't loaded yet", "path", path) + + // If the package isn't loaded, attempt to reload the node + if err := ds.devNode.Reload(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + + // Update the watcher list with the currently loaded packages + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) + + // Reloading the node once is sufficient, so exit the loop + return + } + + ds.logger.WithGroup(NodeLogName).Debug("paths already loaded, skipping reload", "paths", paths) + }) + } + + // Setup gnoweb + webhandler, err := setupGnoWebServer(ds.logger.WithGroup(WebLogName), ds.cfg, remote) + if err != nil { + return nil, fmt.Errorf("unable to setup gnoweb server: %w", err) + } + + if ds.webHomePath != "" { + serveWeb := webhandler.ServeHTTP + webhandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "" || r.URL.Path == "/" { + http.Redirect(w, r, ds.webHomePath, http.StatusFound) + } else { + serveWeb(w, r) + } + }) + } + + // Setup unsafe API + if ds.cfg.unsafeAPI { + mux.HandleFunc("/reset", func(res http.ResponseWriter, req *http.Request) { + if err := ds.devNode.Reset(req.Context()); err != nil { + ds.logger.Error("failed to reset", slog.Any("err", err)) + res.WriteHeader(http.StatusInternalServerError) + } + }) + + mux.HandleFunc("/reload", func(res http.ResponseWriter, req *http.Request) { + if err := ds.devNode.Reload(req.Context()); err != nil { + ds.logger.Error("failed to reload", slog.Any("err", err)) + res.WriteHeader(http.StatusInternalServerError) + } + }) + } + + if !ds.cfg.noWatch { + evtstarget := fmt.Sprintf("%s/_events", ds.cfg.webListenerAddr) + mux.Handle("/_events", ds.emitterServer) + mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) + } else { + mux.Handle("/", webhandler) + } + + return mux, nil +} + +func (ds *App) RunServer(ctx context.Context, term *rawterm.RawTerm) error { + ctx, cancelWith := context.WithCancelCause(ctx) + defer cancelWith(nil) + + addr := ds.cfg.webListenerAddr + handlers, err := ds.setupHandlers(ctx) + if err != nil { + return fmt.Errorf("unable to setup handlers: %w", err) + } + + server := &http.Server{ + Handler: handlers, + Addr: addr, + ReadHeaderTimeout: 60 * time.Second, + } + + // Serve gnoweb + if !ds.cfg.noWeb { + go func() { + err := server.ListenAndServe() + cancelWith(err) + }() + + ds.logger.WithGroup(WebLogName).Info("gnoweb started", + "lisn", fmt.Sprintf("http://%s", addr)) + } + + if ds.cfg.interactive { + ds.logger.WithGroup("--- READY").Info("for commands and help, press `h`", "took", time.Since(ds.start)) + } else { + ds.logger.Info("node is ready", "took", time.Since(ds.start)) + } + + for { + select { + case <-ctx.Done(): + return context.Cause(ctx) + case _, ok := <-ds.watcher.PackagesUpdate: + if !ok { + return nil + } + + ds.logger.WithGroup(NodeLogName).Info("reloading...") + if err := ds.devNode.Reload(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + ds.watcher.UpdatePackagesWatch(ds.devNode.ListPkgs()...) + } + } +} + +func (ds *App) RunInteractive(ctx context.Context, term *rawterm.RawTerm) { + ds.logger.WithGroup(KeyPressLogName).Debug("starting interactive mode") + var keyPressCh <-chan rawterm.KeyPress + if ds.cfg.interactive { + keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) + } + + for { + select { + case <-ctx.Done(): + return + case key, ok := <-keyPressCh: + ds.logger.WithGroup(KeyPressLogName).Debug("pressed", "key", key.String()) + if !ok { + return + } + + if key == rawterm.KeyCtrlC { + return + } + + ds.handleKeyPress(ctx, key) + keyPressCh = listenForKeyPress(ds.logger.WithGroup(KeyPressLogName), term) + } + } +} + +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 +N Next TX - Go to the next tx +E Export - Export the current state as genesis doc +A Accounts - Display known accounts and balances +H Help - Display this message +R Reload - Reload all packages to take change into account. +Ctrl+S Save State - Save the current state +Ctrl+R Reset - Reset application to it's initial/save state. +Ctrl+C Exit - Exit the application +` + +func (ds *App) handleKeyPress(ctx context.Context, key rawterm.KeyPress) { + var err error + + switch key.Upper() { + case rawterm.KeyH: // Helper + ds.logger.Info("Gno Dev Helper", "helper", helper) + + case rawterm.KeyA: // Accounts + logAccounts(ds.logger.WithGroup(AccountsLogName), ds.book, ds.devNode) + + case rawterm.KeyR: // Reload + ds.logger.WithGroup(NodeLogName).Info("reloading...") + if err = ds.devNode.ReloadAll(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reload node", "err", err) + } + + case rawterm.KeyCtrlR: // Reset + ds.logger.WithGroup(NodeLogName).Info("resetting node state...") + // Reset paths + ds.pathManager.Reset() + ds.devNode.SetPackagePaths(ds.paths...) + // Reset the node + if err = ds.devNode.Reset(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to reset node state", "err", err) + } + + case rawterm.KeyCtrlS: // Save + ds.logger.WithGroup(NodeLogName).Info("saving state...") + if err := ds.devNode.SaveCurrentState(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to save node state", "err", err) + } + + case rawterm.KeyE: // Export + // Create a temporary export dir + if ds.exported == 0 { + ds.exportPath, err = os.MkdirTemp("", "gnodev-export") + if err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to create `export` directory", "err", err) + return + } + } + ds.exported++ + + ds.logger.WithGroup(NodeLogName).Info("exporting state...") + doc, err := ds.devNode.ExportStateAsGenesis(ctx) + if err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to export node state", "err", err) + return + } + + docfile := filepath.Join(ds.exportPath, fmt.Sprintf("export_%d.jsonl", ds.exported)) + if err := doc.SaveAs(docfile); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to save genesis", "err", err) + } + + ds.logger.WithGroup(NodeLogName).Info("node state exported", "file", docfile) + + case rawterm.KeyN: // Next tx + ds.logger.Info("moving forward...") + if err := ds.devNode.MoveToNextTX(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to move forward", "err", err) + } + + case rawterm.KeyP: // Previous tx + ds.logger.Info("moving backward...") + if err := ds.devNode.MoveToPreviousTX(ctx); err != nil { + ds.logger.WithGroup(NodeLogName).Error("unable to move backward", "err", err) + } + default: + } +} + +// XXX: packages modifier does not support glob yet +func resolvePackagesModifier(cfg *AppConfig, bk *address.Book, qpaths []string) ([]gnodev.QueryPath, []string, error) { + if cfg.deployKey == "" { + return nil, nil, fmt.Errorf("default deploy key cannot be empty") + } + + defaultKey, _, ok := bk.GetFromNameOrAddress(cfg.deployKey) + if !ok { + return nil, nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) + } + + modifiers := make([]gnodev.QueryPath, 0, len(qpaths)) + paths := make([]string, 0, len(qpaths)) + + for _, path := range qpaths { + if path == "" { + continue + } + + qpath, err := gnodev.ResolveQueryPath(bk, path) + if err != nil { + return nil, nil, fmt.Errorf("invalid package path/query %q: %w", path, err) + } + + // Assign a default creator if user haven't specified it. + if qpath.Creator.IsZero() { + qpath.Creator = defaultKey + } + + modifiers = append(modifiers, qpath) + paths = append(paths, qpath.Path) + } + + return slices.Clip(modifiers), slices.Clip(paths), nil +} + +func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { + cc := make(chan rawterm.KeyPress, 1) + go func() { + defer close(cc) + key, err := rt.ReadKeyPress() + if err != nil { + logger.Error("unable to read keypress", "err", err) + return + } + + cc <- key + }() + + return cc +} diff --git a/contribs/gnodev/cmd/gnodev/app_config.go b/contribs/gnodev/cmd/gnodev/app_config.go new file mode 100644 index 00000000000..07231f24f9b --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/app_config.go @@ -0,0 +1,237 @@ +package main + +import "flag" + +type AppConfig struct { + // Listeners + nodeRPCListenerAddr string + nodeP2PListenerAddr string + nodeProxyAppListenerAddr string + + // Users default + deployKey string + home string + root string + premineAccounts varPremineAccounts + + // Files + balancesFile string + genesisFile string + txsFile string + + // Web Configuration + noWeb bool + webHTML bool + webListenerAddr string + webRemoteHelperAddr string + webWithHTML bool + webHome string + + // Resolver + resolvers varResolver + + // Node Configuration + logFormat string + lazyLoader bool + verbose bool + noWatch bool + noReplay bool + maxGas int64 + chainId string + chainDomain string + unsafeAPI bool + interactive bool + paths string +} + +func (c *AppConfig) RegisterFlagsWith(fs *flag.FlagSet, defaultCfg AppConfig) { + *c = defaultCfg // Copy default config + + fs.StringVar( + &c.home, + "home", + defaultCfg.home, + "user's local directory for keys", + ) + + fs.BoolVar( + &c.interactive, + "interactive", + defaultCfg.interactive, + "enable gnodev interactive mode", + ) + + fs.StringVar( + &c.root, + "root", + defaultCfg.root, + "gno root directory", + ) + + fs.BoolVar( + &c.noWeb, + "no-web", + defaultLocalAppConfig.noWeb, + "disable gnoweb", + ) + + fs.BoolVar( + &c.webHTML, + "web-html", + defaultLocalAppConfig.webHTML, + "gnoweb: enable unsafe HTML parsing in markdown rendering", + ) + + fs.StringVar( + &c.webListenerAddr, + "web-listener", + defaultCfg.webListenerAddr, + "gnoweb: web server listener address", + ) + + fs.StringVar( + &c.webRemoteHelperAddr, + "web-help-remote", + defaultCfg.webRemoteHelperAddr, + "gnoweb: web server help page's remote addr (default to )", + ) + + fs.BoolVar( + &c.webWithHTML, + "web-with-html", + defaultCfg.webWithHTML, + "gnoweb: enable HTML parsing in markdown rendering", + ) + + fs.StringVar( + &c.webHome, + "web-home", + defaultCfg.webHome, + "gnoweb: set default home page, use `/` or `:none:` to use default web home redirect", + ) + + fs.Var( + &c.resolvers, + "resolver", + "list of additional resolvers (`root`, `local` or `remote`), will be executed in the given order", + ) + + fs.StringVar( + &c.nodeRPCListenerAddr, + "node-rpc-listener", + defaultCfg.nodeRPCListenerAddr, + "listening address for GnoLand RPC node", + ) + + fs.Var( + &c.premineAccounts, + "add-account", + "add (or set) a premine account in the form `[=]`, can be used multiple time", + ) + + fs.StringVar( + &c.balancesFile, + "balance-file", + defaultCfg.balancesFile, + "load the provided balance file (refer to the documentation for format)", + ) + + fs.StringVar( + &c.txsFile, + "txs-file", + defaultCfg.txsFile, + "load the provided transactions file (refer to the documentation for format)", + ) + + fs.StringVar( + &c.genesisFile, + "genesis", + defaultCfg.genesisFile, + "load the given genesis file", + ) + + fs.StringVar( + &c.deployKey, + "deploy-key", + defaultCfg.deployKey, + "default key name or Bech32 address for deploying packages", + ) + + fs.StringVar( + &c.chainId, + "chain-id", + defaultCfg.chainId, + "set node ChainID", + ) + + fs.StringVar( + &c.chainDomain, + "chain-domain", + defaultCfg.chainDomain, + "set node ChainDomain", + ) + + fs.BoolVar( + &c.noWatch, + "no-watch", + defaultCfg.noWatch, + "do not watch for file changes", + ) + + fs.BoolVar( + &c.noReplay, + "no-replay", + defaultCfg.noReplay, + "do not replay previous transactions upon reload", + ) + + fs.BoolVar( + &c.lazyLoader, + "lazy-loader", + defaultCfg.lazyLoader, + "enable lazy loader", + ) + + fs.Int64Var( + &c.maxGas, + "max-gas", + defaultCfg.maxGas, + "set the maximum gas per block", + ) + + fs.BoolVar( + &c.unsafeAPI, + "unsafe-api", + defaultCfg.unsafeAPI, + "enable /reset and /reload endpoints which are not safe to expose publicly", + ) + + fs.StringVar( + &c.logFormat, + "log-format", + defaultCfg.logFormat, + "log output format, can be `json` or `console`", + ) + + fs.StringVar( + &c.paths, + "paths", + defaultCfg.paths, + "additional path(s) to load, separated by comma", + ) + + fs.BoolVar( + &c.verbose, + "v", + defaultCfg.verbose, + "enable verbose output for development", + ) +} + +func (c *AppConfig) validateConfigFlags() error { + if (c.balancesFile != "" || c.txsFile != "") && c.genesisFile != "" { + return ErrConflictingFileArgs + } + + return nil +} diff --git a/contribs/gnodev/cmd/gnodev/command_local.go b/contribs/gnodev/cmd/gnodev/command_local.go new file mode 100644 index 00000000000..2a1ccfa063d --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/command_local.go @@ -0,0 +1,114 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/mattn/go-isatty" +) + +const DefaultDomain = "gno.land" + +var ErrConflictingFileArgs = errors.New("cannot specify `balances-file` or `txs-file` along with `genesis-file`") + +type LocalAppConfig struct { + AppConfig + + chdir string // directory context +} + +var defaultLocalAppConfig = AppConfig{ + chainId: "dev", + logFormat: "console", + chainDomain: DefaultDomain, + maxGas: 10_000_000_000, + webListenerAddr: "127.0.0.1:8888", + nodeRPCListenerAddr: "127.0.0.1:26657", + deployKey: defaultDeployerAddress.String(), + home: gnoenv.HomeDir(), + root: gnoenv.RootDir(), + interactive: isatty.IsTerminal(os.Stdout.Fd()), + unsafeAPI: true, + lazyLoader: true, + + // As we have no reason to configure this yet, set this to random port + // to avoid potential conflict with other app + nodeP2PListenerAddr: "tcp://127.0.0.1:0", + nodeProxyAppListenerAddr: "tcp://127.0.0.1:0", +} + +func NewLocalCmd(io commands.IO) *commands.Command { + var cfg LocalAppConfig + + return commands.NewCommand( + commands.Metadata{ + Name: "local", + ShortUsage: "gnodev local [flags] [package_dir...]", + ShortHelp: "Start gnodev in local development mode (default)", + LongHelp: "LOCAL: Local mode configure the node for local development usage", + NoParentFlags: true, + }, + &cfg, + func(_ context.Context, args []string) error { + return execLocalApp(&cfg, args, io) + }, + ) +} + +func (c *LocalAppConfig) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.chdir, + "C", + c.chdir, + "change directory context before running gnodev", + ) + + c.AppConfig.RegisterFlagsWith(fs, defaultLocalAppConfig) +} + +func execLocalApp(cfg *LocalAppConfig, args []string, cio commands.IO) error { + if cfg.chdir != "" { + if err := os.Chdir(cfg.chdir); err != nil { + return fmt.Errorf("unable to change directory: %w", err) + } + } + + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("unable to guess current dir: %w", err) + } + + // If no resolvers is defined, use gno example as root resolver + var baseResolvers []packages.Resolver + if len(cfg.resolvers) == 0 { + gnoroot, err := gnoenv.GuessRootDir() + if err != nil { + return err + } + + exampleRoot := filepath.Join(gnoroot, "examples") + baseResolvers = append(baseResolvers, packages.NewRootResolver(exampleRoot)) + } + + // Check if current directory is a valid gno package + path := guessPath(&cfg.AppConfig, dir) + resolver := packages.NewLocalResolver(path, dir) + if resolver.IsValid() { + // Add current directory as local resolver + baseResolvers = append([]packages.Resolver{resolver}, baseResolvers...) + if len(cfg.paths) > 0 { + cfg.paths += "," + } + cfg.paths += resolver.Path + } + cfg.resolvers = append(baseResolvers, cfg.resolvers...) + + return runApp(&cfg.AppConfig, cio) // else run app without any dir +} diff --git a/contribs/gnodev/cmd/gnodev/command_staging.go b/contribs/gnodev/cmd/gnodev/command_staging.go new file mode 100644 index 00000000000..7b1a0ab3f5a --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/command_staging.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "flag" + "path/filepath" + + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type StagingAppConfig struct { + AppConfig +} + +var defaultStagingOptions = AppConfig{ + chainId: "dev", + chainDomain: DefaultDomain, + logFormat: "json", + maxGas: 10_000_000_000, + webHome: ":none:", + webListenerAddr: "127.0.0.1:8888", + nodeRPCListenerAddr: "127.0.0.1:26657", + deployKey: defaultDeployerAddress.String(), + home: gnoenv.HomeDir(), + root: gnoenv.RootDir(), + interactive: false, + unsafeAPI: false, + lazyLoader: false, + paths: filepath.Join(DefaultDomain, "/**"), // Load every package under the main domain}, + + // As we have no reason to configure this yet, set this to random port + // to avoid potential conflict with other app + nodeP2PListenerAddr: "tcp://127.0.0.1:0", + nodeProxyAppListenerAddr: "tcp://127.0.0.1:0", +} + +func NewStagingCmd(io commands.IO) *commands.Command { + var cfg StagingAppConfig + + return commands.NewCommand( + commands.Metadata{ + Name: "staging", + ShortUsage: "gnodev staging [flags] [package_dir...]", + ShortHelp: "Start gnodev in staging mode", + LongHelp: "STAGING: Staging mode configure the node for server usage", + NoParentFlags: true, + }, + &cfg, + func(_ context.Context, args []string) error { + return execStagingCmd(&cfg, args, io) + }, + ) +} + +func (c *StagingAppConfig) RegisterFlags(fs *flag.FlagSet) { + c.AppConfig.RegisterFlagsWith(fs, defaultStagingOptions) +} + +func execStagingCmd(cfg *StagingAppConfig, args []string, io commands.IO) error { + // If no resolvers is defined, use gno example as root resolver + if len(cfg.AppConfig.resolvers) == 0 { + gnoroot, err := gnoenv.GuessRootDir() + if err != nil { + return err + } + + exampleRoot := filepath.Join(gnoroot, "examples") + cfg.AppConfig.resolvers = append(cfg.AppConfig.resolvers, packages.NewRootResolver(exampleRoot)) + } + + return runApp(&cfg.AppConfig, io, args...) +} diff --git a/contribs/gnodev/cmd/gnodev/logger.go b/contribs/gnodev/cmd/gnodev/logger.go index 9e69654f478..1fbcd95e953 100644 --- a/contribs/gnodev/cmd/gnodev/logger.go +++ b/contribs/gnodev/cmd/gnodev/logger.go @@ -1,35 +1,59 @@ package main import ( + "fmt" "io" "log/slog" "github.com/charmbracelet/lipgloss" "github.com/gnolang/gno/contribs/gnodev/pkg/logger" - gnolog "github.com/gnolang/gno/gno.land/pkg/log" + "github.com/gnolang/gno/gno.land/pkg/log" "github.com/muesli/termenv" + "go.uber.org/zap/zapcore" ) -func setuplogger(cfg *devCfg, out io.Writer) *slog.Logger { +func setuplogger(cfg *AppConfig, out io.Writer) (*slog.Logger, error) { level := slog.LevelInfo if cfg.verbose { level = slog.LevelDebug } - if cfg.serverMode { - zaplogger := logger.NewZapLogger(out, level) - return gnolog.ZapLoggerToSlog(zaplogger) - } + // Set up the logger + switch cfg.logFormat { + case "json": + return newJSONLogger(out, level), nil + case "console", "": + // Detect term color profile + colorProfile := termenv.DefaultOutput().Profile + + clogger := logger.NewColumnLogger(out, level, colorProfile) + + // Register well known group color with system colors + clogger.RegisterGroupColor(NodeLogName, lipgloss.Color("3")) + clogger.RegisterGroupColor(WebLogName, lipgloss.Color("4")) + clogger.RegisterGroupColor(KeyPressLogName, lipgloss.Color("5")) + clogger.RegisterGroupColor(EventServerLogName, lipgloss.Color("6")) - // Detect term color profile - colorProfile := termenv.DefaultOutput().Profile - clogger := logger.NewColumnLogger(out, level, colorProfile) + return slog.New(clogger), nil + default: + return nil, fmt.Errorf("invalid log format %q", cfg.logFormat) + } +} - // Register well known group color with system colors - clogger.RegisterGroupColor(NodeLogName, lipgloss.Color("3")) - clogger.RegisterGroupColor(WebLogName, lipgloss.Color("4")) - clogger.RegisterGroupColor(KeyPressLogName, lipgloss.Color("5")) - clogger.RegisterGroupColor(EventServerLogName, lipgloss.Color("6")) +func newJSONLogger(w io.Writer, level slog.Level) *slog.Logger { + var zaplevel zapcore.Level + switch level { + case slog.LevelDebug: + zaplevel = zapcore.DebugLevel + case slog.LevelInfo: + zaplevel = zapcore.InfoLevel + case slog.LevelWarn: + zaplevel = zapcore.WarnLevel + case slog.LevelError: + zaplevel = zapcore.ErrorLevel + default: + panic("unknown slog level: " + level.String()) + } - return slog.New(clogger) + return log.ZapLoggerToSlog(log.NewZapJSONLogger(w, zaplevel)) } diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 95f1d95e0a6..a14f76e9d81 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -1,586 +1,60 @@ package main import ( + "bytes" "context" "errors" "flag" "fmt" - "log/slog" - "net/http" "os" - "path/filepath" - "time" - "github.com/gnolang/gno/contribs/gnodev/pkg/address" - gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" - "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" - "github.com/gnolang/gno/contribs/gnodev/pkg/rawterm" - "github.com/gnolang/gno/contribs/gnodev/pkg/watcher" - "github.com/gnolang/gno/gno.land/pkg/integration" - "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" - osm "github.com/gnolang/gno/tm2/pkg/os" ) -const ( - NodeLogName = "Node" - WebLogName = "GnoWeb" - KeyPressLogName = "KeyPress" - EventServerLogName = "Event" - AccountsLogName = "Accounts" -) - -var ErrConflictingFileArgs = errors.New("cannot specify `balances-file` or `txs-file` along with `genesis-file`") - -var ( - DefaultDeployerName = integration.DefaultAccount_Name - DefaultDeployerAddress = crypto.MustAddressFromString(integration.DefaultAccount_Address) - DefaultDeployerSeed = integration.DefaultAccount_Seed -) - -type devCfg struct { - // Listeners - nodeRPCListenerAddr string - nodeP2PListenerAddr string - nodeProxyAppListenerAddr string - - // Users default - deployKey string - home string - root string - premineAccounts varPremineAccounts - - // Files - balancesFile string - genesisFile string - 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 - 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", - deployKey: DefaultDeployerAddress.String(), - home: gnoenv.HomeDir(), - root: gnoenv.RootDir(), - - // As we have no reason to configure this yet, set this to random port - // to avoid potential conflict with other app - nodeP2PListenerAddr: "tcp://127.0.0.1:0", - nodeProxyAppListenerAddr: "tcp://127.0.0.1:0", -} - func main() { - cfg := &devCfg{} - stdio := commands.NewDefaultIO() + + localcmd := NewLocalCmd(stdio) // default + cmd := commands.NewCommand( commands.Metadata{ Name: "gnodev", - ShortUsage: "gnodev [flags] [path ...]", - ShortHelp: "runs an in-memory node and gno.land web server for development purposes.", - LongHelp: `The gnodev command starts an in-memory node and a gno.land web interface primarily for realm package development. It automatically loads the 'examples' directory and any additional specified paths.`, - }, - cfg, - func(_ context.Context, args []string) error { - return execDev(cfg, args, stdio) - }) - - cmd.Execute(context.Background(), os.Args[1:]) -} - -func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { - fs.StringVar( - &c.home, - "home", - defaultDevOptions.home, - "user's local directory for keys", - ) - - fs.StringVar( - &c.root, - "root", - defaultDevOptions.root, - "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, - "gnoweb: web server listener address", - ) - - fs.StringVar( - &c.webRemoteHelperAddr, - "web-help-remote", - defaultDevOptions.webRemoteHelperAddr, - "gnoweb: web server help page's remote addr (default to )", - ) - - fs.StringVar( - &c.nodeRPCListenerAddr, - "node-rpc-listener", - defaultDevOptions.nodeRPCListenerAddr, - "listening address for GnoLand RPC node", - ) - - fs.Var( - &c.premineAccounts, - "add-account", - "add (or set) a premine account in the form `[=]`, can be used multiple time", - ) - - fs.StringVar( - &c.balancesFile, - "balance-file", - defaultDevOptions.balancesFile, - "load the provided balance file (refer to the documentation for format)", - ) - - fs.StringVar( - &c.txsFile, - "txs-file", - defaultDevOptions.txsFile, - "load the provided transactions file (refer to the documentation for format)", - ) - - fs.StringVar( - &c.genesisFile, - "genesis", - defaultDevOptions.genesisFile, - "load the given genesis file", - ) - - fs.StringVar( - &c.deployKey, - "deploy-key", - defaultDevOptions.deployKey, - "default key name or Bech32 address for deploying packages", - ) + ShortUsage: "gnodev [flags] ", + ShortHelp: "Runs an in-memory node and gno.land web server for development purposes.", + LongHelp: `The gnodev command starts an in-memory node and a gno.land web interface, primarily for realm package development. - fs.BoolVar( - &c.minimal, - "minimal", - defaultDevOptions.minimal, - "do not load packages from the examples directory", - ) - - fs.BoolVar( - &c.serverMode, - "server-mode", - defaultDevOptions.serverMode, - "disable interaction, and adjust logging for server use.", - ) - - fs.BoolVar( - &c.verbose, - "v", - defaultDevOptions.verbose, - "enable verbose output for development", - ) - - fs.StringVar( - &c.chainId, - "chain-id", - defaultDevOptions.chainId, - "set node ChainID", - ) - - fs.StringVar( - &c.chainDomain, - "chain-domain", - defaultDevOptions.chainDomain, - "set node ChainDomain", - ) - - fs.BoolVar( - &c.noWatch, - "no-watch", - defaultDevOptions.noWatch, - "do not watch for file changes", - ) - - fs.BoolVar( - &c.noReplay, - "no-replay", - defaultDevOptions.noReplay, - "do not replay previous transactions upon reload", - ) - - fs.Int64Var( - &c.maxGas, - "max-gas", - defaultDevOptions.maxGas, - "set the maximum gas per block", - ) - - fs.BoolVar( - &c.unsafeAPI, - "unsafe-api", - defaultDevOptions.unsafeAPI, - "enable /reset and /reload endpoints which are not safe to expose publicly", +If no command is provided, gnodev will automatically start in mode. +For more information and flags usage description, use 'gnodev local -h'.`, + }, + nil, + func(ctx context.Context, _ []string) error { + localcmd.Execute(ctx, os.Args[1:]) + return nil + }, ) -} - -func (c *devCfg) validateConfigFlags() error { - if (c.balancesFile != "" || c.txsFile != "") && c.genesisFile != "" { - return ErrConflictingFileArgs - } - - return nil -} - -func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { - ctx, cancel := context.WithCancelCause(context.Background()) - defer cancel(nil) - - if err := cfg.validateConfigFlags(); err != nil { - return fmt.Errorf("validate error: %w", err) - } - // Setup Raw Terminal - rt, restore, err := setupRawTerm(cfg, io) - if err != nil { - return fmt.Errorf("unable to init raw term: %w", err) - } - defer restore() - - // Setup trap signal - osm.TrapSignal(func() { - cancel(nil) - restore() - }) - - logger := setuplogger(cfg, rt) - loggerEvents := logger.WithGroup(EventServerLogName) - emitterServer := emitter.NewServer(loggerEvents) - - // load keybase - book, err := setupAddressBook(logger.WithGroup(AccountsLogName), cfg) - if err != nil { - return fmt.Errorf("unable to load keybase: %w", err) - } - - // Check and Parse packages - pkgpaths, err := resolvePackagesPathFromArgs(cfg, book, args) - if err != nil { - return fmt.Errorf("unable to parse package paths: %w", err) - } - - // generate balances - balances, err := generateBalances(book, cfg) - if err != nil { - return fmt.Errorf("unable to generate balances: %w", err) - } - logger.Debug("balances loaded", "list", balances.List()) - - // Setup Dev Node - // XXX: find a good way to export or display node logs - nodeLogger := logger.WithGroup(NodeLogName) - nodeCfg := setupDevNodeConfig(cfg, logger, emitterServer, balances, pkgpaths) - devNode, err := setupDevNode(ctx, cfg, nodeCfg) - if err != nil { - return err - } - defer devNode.Close() - - nodeLogger.Info("node started", "lisn", devNode.GetRemoteAddress(), "chainID", cfg.chainId) - - // Create server - mux := http.NewServeMux() - server := http.Server{ - Handler: mux, - Addr: cfg.webListenerAddr, - ReadHeaderTimeout: time.Second * 60, - } - defer server.Close() - - // Setup gnoweb - 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 { - mux.HandleFunc("/reset", func(res http.ResponseWriter, req *http.Request) { - if err := devNode.Reset(req.Context()); err != nil { - logger.Error("failed to reset", slog.Any("err", err)) - res.WriteHeader(http.StatusInternalServerError) - } - }) - - mux.HandleFunc("/reload", func(res http.ResponseWriter, req *http.Request) { - if err := devNode.Reload(req.Context()); err != nil { - logger.Error("failed to reload", slog.Any("err", err)) - res.WriteHeader(http.StatusInternalServerError) - } - }) - } - - // Setup HotReload if needed - if !cfg.noWatch { - evtstarget := fmt.Sprintf("%s/_events", server.Addr) - mux.Handle("/_events", emitterServer) - mux.Handle("/", emitter.NewMiddleware(evtstarget, webhandler)) - } else { - mux.Handle("/", webhandler) - } - - // 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)) - } - - watcher, err := watcher.NewPackageWatcher(loggerEvents, emitterServer) - if err != nil { - return fmt.Errorf("unable to setup packages watcher: %w", err) - } - defer watcher.Stop() - - // Add node pkgs to watcher - watcher.AddPackages(devNode.ListPkgs()...) - - if !cfg.serverMode { - logger.WithGroup("--- READY").Info("for commands and help, press `h`") - } - - // Run the main event loop - return runEventLoop(ctx, logger, book, rt, devNode, watcher) -} - -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 -N Next TX - Go to the next tx -E Export - Export the current state as genesis doc -A Accounts - Display known accounts and balances -H Help - Display this message -R Reload - Reload all packages to take change into account. -Ctrl+S Save State - Save the current state -Ctrl+R Reset - Reset application to it's initial/save state. -Ctrl+C Exit - Exit the application -` - -func runEventLoop( - ctx context.Context, - logger *slog.Logger, - bk *address.Book, - rt *rawterm.RawTerm, - dnode *gnodev.Node, - watch *watcher.PackageWatcher, -) error { - // XXX: move this in above, but we need to have a proper struct first - // XXX: make this configurable - var exported uint - path, err := os.MkdirTemp("", "gnodev-export") - if err != nil { - return fmt.Errorf("unable to create `export` directory: %w", err) - } + cmd.AddSubCommands(localcmd) + cmd.AddSubCommands(NewStagingCmd(stdio)) - defer func() { - if exported == 0 { - _ = os.RemoveAll(path) - } - }() - - keyPressCh := listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) - for { - var err error - - select { - case <-ctx.Done(): - return context.Cause(ctx) - case pkgs, ok := <-watch.PackagesUpdate: - if !ok { - return nil - } - - // fmt.Fprintln(nodeOut, "Loading package updates...") - if err = dnode.UpdatePackages(pkgs.PackagesPath()...); err != nil { - return fmt.Errorf("unable to update packages: %w", err) - } - - logger.WithGroup(NodeLogName).Info("reloading...") - if err = dnode.Reload(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reload node", "err", err) - } - - case key, ok := <-keyPressCh: - if !ok { - return nil - } - - logger.WithGroup(KeyPressLogName).Debug( - fmt.Sprintf("<%s>", key.String()), - ) - - switch key.Upper() { - case rawterm.KeyH: // Helper - logger.Info("Gno Dev Helper", "helper", helper) - - case rawterm.KeyA: // Accounts - logAccounts(logger.WithGroup(AccountsLogName), bk, dnode) - - case rawterm.KeyR: // Reload - logger.WithGroup(NodeLogName).Info("reloading...") - if err = dnode.ReloadAll(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reload node", "err", err) - } - - case rawterm.KeyCtrlR: // Reset - logger.WithGroup(NodeLogName).Info("reseting node state...") - if err = dnode.Reset(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to reset node state", "err", err) - } - - case rawterm.KeyCtrlS: // Save - logger.WithGroup(NodeLogName).Info("saving state...") - if err := dnode.SaveCurrentState(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to save node state", "err", err) - } - - case rawterm.KeyE: - logger.WithGroup(NodeLogName).Info("exporting state...") - doc, err := dnode.ExportStateAsGenesis(ctx) - if err != nil { - logger.WithGroup(NodeLogName). - Error("unable to export node state", "err", err) - continue - } - - docfile := filepath.Join(path, fmt.Sprintf("export_%d.jsonl", exported)) - if err := doc.SaveAs(docfile); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to save genesis", "err", err) - } - exported++ - - logger.WithGroup(NodeLogName).Info("node state exported", "file", docfile) - - case rawterm.KeyN: // Next tx - logger.Info("moving forward...") - if err := dnode.MoveToNextTX(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to move forward", "err", err) - } - - case rawterm.KeyP: // Next tx - logger.Info("moving backward...") - if err := dnode.MoveToPreviousTX(ctx); err != nil { - logger.WithGroup(NodeLogName). - Error("unable to move backward", "err", err) - } - - case rawterm.KeyCtrlC: // Exit - return nil - - default: - } - - // Reset listen for the next keypress - keyPressCh = listenForKeyPress(logger.WithGroup(KeyPressLogName), rt) - } - } -} - -func listenForKeyPress(logger *slog.Logger, rt *rawterm.RawTerm) <-chan rawterm.KeyPress { - cc := make(chan rawterm.KeyPress, 1) - go func() { - defer close(cc) - key, err := rt.ReadKeyPress() - if err != nil { - logger.Error("unable to read keypress", "err", err) + // XXX: This part is a bit hacky; it mostly configures the command to + // use the local command as default, but still falls back on gnodev root + // help if asked. + var buff bytes.Buffer + cmd.SetOutput(&buff) + if err := cmd.Parse(os.Args[1:]); err != nil { + if !errors.Is(err, flag.ErrHelp) { + localcmd.Execute(context.Background(), os.Args[1:]) return } - cc <- key - }() - - return cc -} - -func resolvePackagesPathFromArgs(cfg *devCfg, bk *address.Book, args []string) ([]gnodev.PackagePath, error) { - paths := make([]gnodev.PackagePath, 0, len(args)) - - if cfg.deployKey == "" { - return nil, fmt.Errorf("default deploy key cannot be empty") - } - - defaultKey, _, ok := bk.GetFromNameOrAddress(cfg.deployKey) - if !ok { - return nil, fmt.Errorf("unable to get deploy key %q", cfg.deployKey) - } - - for _, arg := range args { - path, err := gnodev.ResolvePackagePathQuery(bk, arg) - if err != nil { - return nil, fmt.Errorf("invalid package path/query %q: %w", arg, err) - } - - // Assign a default creator if user haven't specified it. - if path.Creator.IsZero() { - path.Creator = defaultKey + if buff.Len() > 0 { + fmt.Fprint(stdio.Err(), buff.String()) } - paths = append(paths, path) + return } - // Add examples folder if minimal is set to false - if !cfg.minimal { - paths = append(paths, gnodev.PackagePath{ - Path: filepath.Join(cfg.root, "examples"), - Creator: defaultKey, - Deposit: nil, - }) + if err := cmd.Run(context.Background()); err != nil { + stdio.ErrPrintfln(err.Error()) } - - return paths, nil } diff --git a/contribs/gnodev/cmd/gnodev/path_manager.go b/contribs/gnodev/cmd/gnodev/path_manager.go new file mode 100644 index 00000000000..705e90fe2c4 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/path_manager.go @@ -0,0 +1,45 @@ +package main + +import ( + "sync" +) + +// pathManager manages a set of unique paths. +type pathManager struct { + paths map[string]struct{} + mu sync.RWMutex +} + +func newPathManager() *pathManager { + return &pathManager{ + paths: make(map[string]struct{}), + } +} + +// Save add one path to the PathManager. If a path already exists, it is not added again. +func (p *pathManager) Save(path string) (exist bool) { + p.mu.Lock() + defer p.mu.Unlock() + if _, exist = p.paths[path]; !exist { + p.paths[path] = struct{}{} + } + return exist +} + +func (p *pathManager) List() []string { + p.mu.RLock() + defer p.mu.RUnlock() + + paths := make([]string, 0, len(p.paths)) + for path := range p.paths { + paths = append(paths, path) + } + + return paths +} + +func (p *pathManager) Reset() { + p.mu.Lock() + defer p.mu.Unlock() + p.paths = make(map[string]struct{}) +} diff --git a/contribs/gnodev/cmd/gnodev/setup_address_book.go b/contribs/gnodev/cmd/gnodev/setup_address_book.go index a1a1c8f58ac..5d10b748a22 100644 --- a/contribs/gnodev/cmd/gnodev/setup_address_book.go +++ b/contribs/gnodev/cmd/gnodev/setup_address_book.go @@ -9,7 +9,7 @@ import ( osm "github.com/gnolang/gno/tm2/pkg/os" ) -func setupAddressBook(logger *slog.Logger, cfg *devCfg) (*address.Book, error) { +func setupAddressBook(logger *slog.Logger, cfg *AppConfig) (*address.Book, error) { book := address.NewBook() // Check for home folder @@ -40,24 +40,24 @@ func setupAddressBook(logger *slog.Logger, cfg *devCfg) (*address.Book, error) { } // Ensure that we have a default address - names, ok := book.GetByAddress(DefaultDeployerAddress) + names, ok := book.GetByAddress(defaultDeployerAddress) if ok { // Account already exist in the keybase if len(names) > 0 && names[0] != "" { - logger.Info("default address imported", "name", names[0], "addr", DefaultDeployerAddress.String()) + logger.Info("default address imported", "name", names[0], "addr", defaultDeployerAddress.String()) } else { - logger.Info("default address imported", "addr", DefaultDeployerAddress.String()) + logger.Info("default address imported", "addr", defaultDeployerAddress.String()) } return book, nil } // If the key isn't found, create a default one - creatorName := fmt.Sprintf("_default#%.6s", DefaultDeployerAddress.String()) - book.Add(DefaultDeployerAddress, creatorName) + creatorName := fmt.Sprintf("_default#%.6s", defaultDeployerAddress.String()) + book.Add(defaultDeployerAddress, creatorName) logger.Warn("default address created", "name", creatorName, - "addr", DefaultDeployerAddress.String(), + "addr", defaultDeployerAddress.String(), "mnemonic", DefaultDeployerSeed, ) diff --git a/contribs/gnodev/cmd/gnodev/setup_loader.go b/contribs/gnodev/cmd/gnodev/setup_loader.go new file mode 100644 index 00000000000..8f10a6a5a76 --- /dev/null +++ b/contribs/gnodev/cmd/gnodev/setup_loader.go @@ -0,0 +1,107 @@ +package main + +import ( + "fmt" + "log/slog" + "path/filepath" + "regexp" + "strings" + + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" + "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" +) + +type varResolver []packages.Resolver + +func (va varResolver) String() string { + resolvers := packages.ChainedResolver(va) + return resolvers.Name() +} + +func (va *varResolver) Set(value string) error { + name, location, found := strings.Cut(value, "=") + if !found { + return fmt.Errorf("invalid resolver format %q, should be `=`", value) + } + + var res packages.Resolver + switch name { + case "remote": + rpc, err := client.NewHTTPClient(location) + if err != nil { + return fmt.Errorf("invalid resolver remote: %q", location) + } + + res = packages.NewRemoteResolver(location, rpc) + case "root": // process everything from a root directory + res = packages.NewRootResolver(location) + case "local": // process a single directory + path, ok := guessPathGnoMod(location) + if !ok { + return fmt.Errorf("unable to read module path from gno.mod in %q", location) + } + + res = packages.NewLocalResolver(path, location) + default: + return fmt.Errorf("invalid resolver name: %q", name) + } + + *va = append(*va, res) + return nil +} + +func setupPackagesResolver(logger *slog.Logger, cfg *AppConfig, dirs ...string) (packages.Resolver, []string) { + // Add root resolvers + localResolvers := make([]packages.Resolver, len(dirs)) + + var paths []string + for i, dir := range dirs { + path := guessPath(cfg, dir) + resolver := packages.NewLocalResolver(path, dir) + + if resolver.IsValid() { + logger.Info("guessing directory path", "path", path, "dir", dir) + paths = append(paths, path) // append local path + } else { + logger.Warn("no gno package found", "dir", dir) + } + + localResolvers[i] = resolver + } + + resolver := packages.ChainResolvers( + packages.ChainResolvers(localResolvers...), // Resolve local directories + packages.ChainResolvers(cfg.resolvers...), // Use user's custom resolvers + ) + + // Enrich resolver with middleware + return packages.MiddlewareResolver(resolver, + packages.CacheMiddleware(func(pkg *packages.Package) bool { + return pkg.Kind == packages.PackageKindRemote // Only cache remote package + }), + packages.FilterStdlibs, // Filter stdlib package from resolving + packages.PackageCheckerMiddleware(logger), // Pre-check syntax to avoid bothering the node reloading on invalid files + packages.LogMiddleware(logger), // Log request + ), paths +} + +func guessPathGnoMod(dir string) (path string, ok bool) { + modfile, err := gnomod.ParseAt(dir) + if err == nil { + return modfile.Module.Mod.Path, true + } + + return "", false +} + +var reInvalidChar = regexp.MustCompile(`[^\w_-]`) + +func guessPath(cfg *AppConfig, dir string) (path string) { + if path, ok := guessPathGnoMod(dir); ok { + return path + } + + rname := reInvalidChar.ReplaceAllString(filepath.Base(dir), "-") + return filepath.Join(cfg.chainDomain, "/r/dev/", rname) +} diff --git a/contribs/gnodev/cmd/gnodev/setup_node.go b/contribs/gnodev/cmd/gnodev/setup_node.go index eaeb89b7e95..761bdef0aef 100644 --- a/contribs/gnodev/cmd/gnodev/setup_node.go +++ b/contribs/gnodev/cmd/gnodev/setup_node.go @@ -9,28 +9,25 @@ import ( gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/types" ) // setupDevNode initializes and returns a new DevNode. -func setupDevNode( - ctx context.Context, - devCfg *devCfg, - nodeConfig *gnodev.NodeConfig, -) (*gnodev.Node, error) { +func setupDevNode(ctx context.Context, cfg *AppConfig, nodeConfig *gnodev.NodeConfig, paths ...string) (*gnodev.Node, error) { logger := nodeConfig.Logger - if devCfg.txsFile != "" { // Load txs files + if cfg.txsFile != "" { // Load txs files var err error - nodeConfig.InitialTxs, err = gnoland.ReadGenesisTxs(ctx, devCfg.txsFile) + nodeConfig.InitialTxs, err = gnoland.ReadGenesisTxs(ctx, cfg.txsFile) if err != nil { return nil, fmt.Errorf("unable to load transactions: %w", err) } - } else if devCfg.genesisFile != "" { // Load genesis file - state, err := extractAppStateFromGenesisFile(devCfg.genesisFile) + } else if cfg.genesisFile != "" { // Load genesis file + state, err := extractAppStateFromGenesisFile(cfg.genesisFile) if err != nil { - return nil, fmt.Errorf("unable to load genesis file %q: %w", devCfg.genesisFile, err) + return nil, fmt.Errorf("unable to load genesis file %q: %w", cfg.genesisFile, err) } // Override balances and txs @@ -43,34 +40,40 @@ func setupDevNode( nodeConfig.InitialTxs[index] = nodeTx } - logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(stateTxs)) + logger.Info("genesis file loaded", "path", cfg.genesisFile, "txs", len(stateTxs)) } - return gnodev.NewDevNode(ctx, nodeConfig) + if len(paths) > 0 { + logger.Info("packages", "paths", paths) + } else { + logger.Debug("no path(s) provided") + } + + return gnodev.NewDevNode(ctx, nodeConfig, paths...) } // setupDevNodeConfig creates and returns a new dev.NodeConfig. func setupDevNodeConfig( - cfg *devCfg, + cfg *AppConfig, logger *slog.Logger, emitter emitter.Emitter, balances gnoland.Balances, - pkgspath []gnodev.PackagePath, + loader packages.Loader, ) *gnodev.NodeConfig { config := gnodev.DefaultNodeConfig(cfg.root, cfg.chainDomain) + config.Loader = loader config.Logger = logger config.Emitter = emitter config.BalancesList = balances.List() - config.PackagesPathList = pkgspath - config.TMConfig.RPC.ListenAddress = resolveUnixOrTCPAddr(cfg.nodeRPCListenerAddr) + config.TMConfig.RPC.ListenAddress = cfg.nodeRPCListenerAddr config.NoReplay = cfg.noReplay config.MaxGasPerBlock = cfg.maxGas config.ChainID = cfg.chainId // other listeners - config.TMConfig.P2P.ListenAddress = defaultDevOptions.nodeP2PListenerAddr - config.TMConfig.ProxyApp = defaultDevOptions.nodeProxyAppListenerAddr + config.TMConfig.P2P.ListenAddress = defaultLocalAppConfig.nodeP2PListenerAddr + config.TMConfig.ProxyApp = defaultLocalAppConfig.nodeProxyAppListenerAddr return config } @@ -89,21 +92,20 @@ func extractAppStateFromGenesisFile(path string) (*gnoland.GnoGenesisState, erro return &state, nil } -func resolveUnixOrTCPAddr(in string) (out string) { +func resolveUnixOrTCPAddr(in string) (addr net.Addr) { var err error - var addr net.Addr if strings.HasPrefix(in, "unix://") { in = strings.TrimPrefix(in, "unix://") - if addr, err := net.ResolveUnixAddr("unix", in); err == nil { - return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) + if addr, err = net.ResolveUnixAddr("unix", in); err == nil { + return addr } err = fmt.Errorf("unable to resolve unix address `unix://%s`: %w", in, err) } else { // don't bother to checking prefix in = strings.TrimPrefix(in, "tcp://") if addr, err = net.ResolveTCPAddr("tcp", in); err == nil { - return fmt.Sprintf("%s://%s", addr.Network(), addr.String()) + return addr } err = fmt.Errorf("unable to resolve tcp address `tcp://%s`: %w", in, err) diff --git a/contribs/gnodev/cmd/gnodev/setup_term.go b/contribs/gnodev/cmd/gnodev/setup_term.go index 1f8f3046969..fb8b5593abf 100644 --- a/contribs/gnodev/cmd/gnodev/setup_term.go +++ b/contribs/gnodev/cmd/gnodev/setup_term.go @@ -7,10 +7,10 @@ import ( var noopRestore = func() error { return nil } -func setupRawTerm(cfg *devCfg, io commands.IO) (*rawterm.RawTerm, func() error, error) { +func setupRawTerm(cfg *AppConfig, io commands.IO) (*rawterm.RawTerm, func() error, error) { rt := rawterm.NewRawTerm() restore := noopRestore - if !cfg.serverMode { + if cfg.interactive { var err error restore, err = rt.Init() if err != nil { diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index e509768d2a1..09df8ce009c 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -5,24 +5,23 @@ import ( "log/slog" "net/http" - gnodev "github.com/gnolang/gno/contribs/gnodev/pkg/dev" "github.com/gnolang/gno/gno.land/pkg/gnoweb" ) // setupGnowebServer initializes and starts the Gnoweb server. -func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) (http.Handler, error) { +func setupGnoWebServer(logger *slog.Logger, cfg *AppConfig, remoteAddr string) (http.Handler, error) { if cfg.noWeb { return http.HandlerFunc(http.NotFound), nil } - remote := dnode.GetRemoteAddress() - appcfg := gnoweb.NewDefaultAppConfig() appcfg.UnsafeHTML = cfg.webHTML - appcfg.NodeRemote = remote + appcfg.NodeRemote = remoteAddr appcfg.ChainID = cfg.chainId if cfg.webRemoteHelperAddr != "" { appcfg.RemoteHelp = cfg.webRemoteHelperAddr + } else { + appcfg.RemoteHelp = remoteAddr } router, err := gnoweb.NewRouter(logger, appcfg) @@ -30,5 +29,11 @@ func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) (ht return nil, fmt.Errorf("unable to create router app: %w", err) } + logger.Debug("gnoweb router created", + "remote", appcfg.NodeRemote, + "helper_remote", appcfg.RemoteHelp, + "html", appcfg.UnsafeHTML, + "chain_id", cfg.chainId, + ) return router, nil } diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index a4c106a24ee..9aea08c4a30 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -18,6 +18,7 @@ require ( github.com/gnolang/gno v0.0.0-00010101000000-000000000000 github.com/gorilla/websocket v1.5.3 github.com/lrstanley/bubblezone v0.0.0-20240624011428-67235275f80c + github.com/mattn/go-isatty v0.0.20 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.15.2 github.com/sahilm/fuzzy v0.1.1 @@ -62,7 +63,6 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect @@ -79,6 +79,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index e87c2de6441..f4bf32aafd5 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -226,6 +226,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 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= diff --git a/contribs/gnodev/internal/mock/server_emitter.go b/contribs/gnodev/internal/mock/emitter/server_emitter.go similarity index 100% rename from contribs/gnodev/internal/mock/server_emitter.go rename to contribs/gnodev/internal/mock/emitter/server_emitter.go diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 12a88490515..0b25234f350 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -6,6 +6,7 @@ import ( "log/slog" "os" "path/filepath" + "slices" "strings" "sync" "time" @@ -13,11 +14,14 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" - "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" @@ -32,18 +36,52 @@ import ( ) type NodeConfig struct { - Logger *slog.Logger - DefaultDeployer crypto.Address - BalancesList []gnoland.Balance - PackagesPathList []PackagePath - Emitter emitter.Emitter - InitialTxs []gnoland.TxWithMetadata - TMConfig *tmcfg.Config + // Logger is used for logging node activities. It can be set to a custom logger or a noop logger for + // silent operation. + Logger *slog.Logger + + // Loader is responsible for loading packages. It abstracts the mechanism for retrieving and managing + // package data. + Loader packages.Loader + + // DefaultCreator specifies the default address used for creating packages and transactions. + DefaultCreator crypto.Address + + // DefaultDeposit is the default amount of coins deposited when creating a package. + DefaultDeposit std.Coins + + // BalancesList defines the initial balance of accounts in the genesis state. + BalancesList []gnoland.Balance + + // PackagesModifier allows modifications to be applied to packages during initialization. + PackagesModifier []QueryPath + + // Emitter is used to emit events for various node operations. It can be set to a noop emitter if no + // event emission is required. + Emitter emitter.Emitter + + // InitialTxs contains the transactions that are included in the genesis state. + InitialTxs []gnoland.TxWithMetadata + + // TMConfig holds the Tendermint configuration settings. + TMConfig *tmcfg.Config + + // SkipFailingGenesisTxs indicates whether to skip failing transactions during the genesis + // initialization. SkipFailingGenesisTxs bool - NoReplay bool - MaxGasPerBlock int64 - ChainID string - ChainDomain string + + // NoReplay, if set to true, prevents replaying of transactions from the block store during node + // initialization. + NoReplay bool + + // MaxGasPerBlock sets the maximum amount of gas that can be used in a single block. + MaxGasPerBlock int64 + + // ChainID is the unique identifier for the blockchain. + ChainID string + + // ChainDomain specifies the domain name associated with the blockchain network. + ChainDomain string } func DefaultNodeConfig(rootdir, domain string) *NodeConfig { @@ -60,10 +98,15 @@ func DefaultNodeConfig(rootdir, domain string) *NodeConfig { }, } + exampleFolder := filepath.Join(gnoenv.RootDir(), "example") // XXX: we should avoid having to hardcoding this here + defaultLoader := packages.NewLoader(packages.NewRootResolver(exampleFolder)) + return &NodeConfig{ Logger: log.NewNoopLogger(), Emitter: &emitter.NoopServer{}, - DefaultDeployer: defaultDeployer, + Loader: defaultLoader, + DefaultCreator: defaultDeployer, + DefaultDeposit: nil, BalancesList: balances, ChainID: tmc.ChainID(), ChainDomain: domain, @@ -78,11 +121,14 @@ type Node struct { *node.Node muNode sync.RWMutex - config *NodeConfig - emitter emitter.Emitter - client client.Client - logger *slog.Logger - pkgs PackagesMap // path -> pkg + config *NodeConfig + emitter emitter.Emitter + client client.Client + logger *slog.Logger + loader packages.Loader + pkgs []packages.Package + pkgsModifier map[string]QueryPath // path -> QueryPath + paths []string // keep track of number of loaded package to be able to skip them on restore loadedPackages int @@ -97,36 +143,30 @@ type Node struct { var DefaultFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) -func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { - mpkgs, err := NewPackagesMap(cfg.PackagesPathList) - if err != nil { - return nil, fmt.Errorf("unable map pkgs list: %w", err) - } - +func NewDevNode(ctx context.Context, cfg *NodeConfig, pkgpaths ...string) (*Node, error) { startTime := time.Now() - pkgsTxs, err := mpkgs.Load(DefaultFee, startTime) - if err != nil { - return nil, fmt.Errorf("unable to load genesis packages: %w", err) + + pkgsModifier := make(map[string]QueryPath, len(cfg.PackagesModifier)) + for _, qpath := range cfg.PackagesModifier { + pkgsModifier[qpath.Path] = qpath } - cfg.Logger.Info("pkgs loaded", "path", cfg.PackagesPathList) devnode := &Node{ + loader: cfg.Loader, config: cfg, client: client.NewLocal(), emitter: cfg.Emitter, - pkgs: mpkgs, logger: cfg.Logger, - loadedPackages: len(pkgsTxs), startTime: startTime, state: cfg.InitialTxs, initialState: cfg.InitialTxs, currentStateIndex: len(cfg.InitialTxs), + paths: pkgpaths, + pkgsModifier: pkgsModifier, } - genesis := gnoland.DefaultGenState() - genesis.Balances = cfg.BalancesList - genesis.Txs = append(pkgsTxs, cfg.InitialTxs...) - if err := devnode.rebuildNode(ctx, genesis); err != nil { + // XXX: MOVE THIS, passing context here can be confusing + if err := devnode.Reset(ctx); err != nil { return nil, fmt.Errorf("unable to initialize the node: %w", err) } @@ -140,11 +180,11 @@ func (n *Node) Close() error { return n.Node.Stop() } -func (n *Node) ListPkgs() []gnomod.Pkg { +func (n *Node) ListPkgs() []packages.Package { n.muNode.RLock() defer n.muNode.RUnlock() - return n.pkgs.toList() + return n.pkgs } func (n *Node) Client() client.Client { @@ -158,8 +198,38 @@ func (n *Node) GetRemoteAddress() string { return n.Node.Config().RPC.ListenAddress } +// AddPackagePaths to load +func (n *Node) AddPackagePaths(paths ...string) { + n.muNode.Lock() + defer n.muNode.Unlock() + + n.paths = append(n.paths, paths...) +} + +func (n *Node) SetPackagePaths(paths ...string) { + n.muNode.Lock() + defer n.muNode.Unlock() + + n.paths = paths +} + +// HasPackageLoaded returns true if the specified package has already been loaded. +// NOTE: This only checks if the package was loaded at the genesis level. +func (n *Node) HasPackageLoaded(path string) bool { + n.muNode.RLock() + defer n.muNode.RUnlock() + + for _, pkg := range n.pkgs { + if pkg.MemPackage.Path == path { + return true + } + } + + return false +} + // GetBlockTransactions returns the transactions contained -// within the specified block, if any +// within the specified block, if any. func (n *Node) GetBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) { n.muNode.RLock() defer n.muNode.RUnlock() @@ -168,36 +238,63 @@ func (n *Node) GetBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, } // GetBlockTransactions returns the transactions contained -// within the specified block, if any +// within the specified block, if any. func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) { int64BlockNum := int64(blockNum) b, err := n.client.Block(&int64BlockNum) if err != nil { - return []gnoland.TxWithMetadata{}, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) // nothing to see here + return nil, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) + } + txs := b.Block.Data.Txs + + bres, err := n.client.BlockResults(&int64BlockNum) + if err != nil { + return nil, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) + } + deliverTxs := bres.Results.DeliverTxs + + // Sanity check + if len(txs) != len(deliverTxs) { + panic(fmt.Errorf("invalid block txs len (%d) vs block result txs len (%d)", + len(txs), len(deliverTxs), + )) } - txs := make([]gnoland.TxWithMetadata, len(b.Block.Data.Txs)) - for i, encodedTx := range b.Block.Data.Txs { - // fallback on std tx + txResults := make([]*abci.ResponseDeliverTx, len(deliverTxs)) + for i, tx := range deliverTxs { + txResults[i] = &tx + } + + // XXX: Consider replacing a failed transaction with an empty transaction + // to preserve the transaction height ? + // Note that this would also require committing instead of using the + // genesis block. + + metaTxs := make([]gnoland.TxWithMetadata, 0, len(txs)) + for i, encodedTx := range txs { + if deliverTx := deliverTxs[i]; !deliverTx.IsOK() { + continue // skip failed tx + } + var tx std.Tx if unmarshalErr := amino.Unmarshal(encodedTx, &tx); unmarshalErr != nil { return nil, fmt.Errorf("unable to unmarshal tx: %w", unmarshalErr) } - txs[i] = gnoland.TxWithMetadata{ + metaTxs = append(metaTxs, gnoland.TxWithMetadata{ Tx: tx, Metadata: &gnoland.GnoTxMetadata{ Timestamp: b.BlockMeta.Header.Time.Unix(), }, - } + }) } - return txs, nil + return slices.Clip(metaTxs), nil } // GetBlockTransactions returns the transactions contained -// within the specified block, if any -// GetLatestBlockNumber returns the latest block height from the chain +// within the specified block, if any. +// GetLatestBlockNumber returns the latest block height from the chain. func (n *Node) GetLatestBlockNumber() (uint64, error) { n.muNode.RLock() defer n.muNode.RUnlock() @@ -209,82 +306,25 @@ func (n *Node) getLatestBlockNumber() uint64 { return uint64(n.Node.BlockStore().Height()) } -// UpdatePackages updates the currently known packages. It will be taken into -// consideration in the next reload of the node. -func (n *Node) UpdatePackages(paths ...string) error { - n.muNode.Lock() - defer n.muNode.Unlock() - - return n.updatePackages(paths...) -} - -func (n *Node) updatePackages(paths ...string) error { - var pkgsUpdated int - for _, path := range paths { - abspath, err := filepath.Abs(path) - if err != nil { - return fmt.Errorf("unable to resolve abs path of %q: %w", path, err) - } - - // Check if we already know the path (or its parent) and set - // associated deployer and deposit - deployer := n.config.DefaultDeployer - var deposit std.Coins - for _, ppath := range n.config.PackagesPathList { - if !strings.HasPrefix(abspath, ppath.Path) { - continue - } - - deployer = ppath.Creator - deposit = ppath.Deposit - } - - // List all packages from target path - pkgslist, err := gnomod.ListPkgs(abspath) - if err != nil { - return fmt.Errorf("failed to list gno packages for %q: %w", path, err) - } - - // Update or add package in the current known list. - for _, pkg := range pkgslist { - n.pkgs[pkg.Dir] = Package{ - Pkg: pkg, - Creator: deployer, - Deposit: deposit, - } - - n.logger.Debug("pkgs update", "name", pkg.Name, "path", pkg.Dir) - } - - pkgsUpdated += len(pkgslist) - } - - n.logger.Info(fmt.Sprintf("updated %d packages", pkgsUpdated)) - return nil -} - // Reset stops the node, if running, and reloads it with a new genesis state, // effectively ignoring the current state. func (n *Node) Reset(ctx context.Context) error { n.muNode.Lock() defer n.muNode.Unlock() - // Stop the node if it's currently running. - if err := n.stopIfRunning(); err != nil { - 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, startTime) + pkgs, err := n.loader.Load(n.paths...) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } // Append initialTxs + pkgsTxs := n.generateTxs(DefaultFee, pkgs) txs := append(pkgsTxs, n.initialState...) + genesis := gnoland.DefaultGenState() genesis.Balances = n.config.BalancesList genesis.Txs = txs @@ -295,6 +335,7 @@ func (n *Node) Reset(ctx context.Context) error { return fmt.Errorf("unable to initialize a new node: %w", err) } + n.pkgs = pkgs n.loadedPackages = len(pkgsTxs) n.currentStateIndex = len(n.initialState) n.startTime = startTime @@ -308,16 +349,6 @@ func (n *Node) ReloadAll(ctx context.Context) error { n.muNode.Lock() defer n.muNode.Unlock() - pkgs := n.pkgs.toList() - paths := make([]string, len(pkgs)) - for i, pkg := range pkgs { - paths[i] = pkg.Dir - } - - if err := n.updatePackages(paths...); err != nil { - return fmt.Errorf("unable to reload packages: %w", err) - } - return n.rebuildNodeFromState(ctx) } @@ -386,10 +417,51 @@ func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata state = append(state, txs...) } - // override current state return state, nil } +func (n *Node) generateTxs(fee std.Fee, pkgs []packages.Package) []gnoland.TxWithMetadata { + metatxs := make([]gnoland.TxWithMetadata, 0, len(pkgs)) + for _, pkg := range pkgs { + msg := vm.MsgAddPackage{ + Creator: n.config.DefaultCreator, + Deposit: n.config.DefaultDeposit, + Package: &pkg.MemPackage, + } + + if m, ok := n.pkgsModifier[pkg.Path]; ok { + if !m.Creator.IsZero() { + msg.Creator = m.Creator + } + + if m.Deposit != nil { + msg.Deposit = m.Deposit + } + + n.logger.Debug("applying pkgs modifier", + "path", pkg.Path, + "creator", msg.Creator, + "deposit", msg.Deposit, + ) + } + + // Create transaction + tx := std.Tx{Fee: fee, Msgs: []std.Msg{msg}} + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + + // Wrap it with metadata + metatx := gnoland.TxWithMetadata{ + Tx: tx, + Metadata: &gnoland.GnoTxMetadata{ + Timestamp: n.startTime.Unix(), + }, + } + metatxs = append(metatxs, metatx) + } + + return metatxs +} + func (n *Node) stopIfRunning() error { if n.Node != nil && n.Node.IsRunning() { if err := n.Node.Stop(); err != nil { @@ -401,17 +473,20 @@ func (n *Node) stopIfRunning() error { } func (n *Node) rebuildNodeFromState(ctx context.Context) error { + start := time.Now() + if n.config.NoReplay { // If NoReplay is true, simply reset the node to its initial state n.logger.Warn("replay disabled") - txs, err := n.pkgs.Load(DefaultFee, n.startTime) + pkgs, err := n.loader.Load(n.paths...) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } + genesis := gnoland.DefaultGenState() genesis.Balances = n.config.BalancesList - genesis.Txs = txs + genesis.Txs = n.generateTxs(DefaultFee, pkgs) return n.rebuildNode(ctx, genesis) } @@ -421,7 +496,7 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) + pkgs, err := n.loader.Load(n.paths...) if err != nil { return fmt.Errorf("unable to load pkgs: %w", err) } @@ -429,15 +504,24 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { // Create genesis with loaded pkgs + previous state genesis := gnoland.DefaultGenState() genesis.Balances = n.config.BalancesList + + // Generate txs + pkgsTxs := n.generateTxs(DefaultFee, pkgs) genesis.Txs = append(pkgsTxs, state...) // Reset the node with the new genesis state. err = n.rebuildNode(ctx, genesis) - n.logger.Info("reload done", "pkgs", len(pkgsTxs), "state applied", len(state)) + n.logger.Info("reload done", + "pkgs", len(pkgsTxs), + "state applied", len(state), + "took", time.Since(start), + ) // Update node infos + n.pkgs = pkgs n.loadedPackages = len(pkgsTxs) + // Emit reload event n.emitter.Emit(&events.Reload{}) return nil } @@ -534,13 +618,23 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { if !res.IsErr() { + for _, msg := range tx.Msgs { + if addpkg, ok := msg.(vm.MsgAddPackage); ok && addpkg.Package != nil { + n.logger.Debug("add package", + "path", addpkg.Package.Path, + "files", len(addpkg.Package.Files), + "creator", addpkg.Creator.String(), + ) + } + } + return } // XXX: for now, this is only way to catch the error before, after, found := strings.Cut(res.Log, "\n") if !found { - n.logger.Error("unable to send tx", "err", res.Error, "log", res.Log) + n.logger.Error("unable to send tx", "log", res.Log) return } diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 3f996bc7716..557565ea0b1 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -84,14 +84,10 @@ func (n *Node) MoveBy(ctx context.Context, x int) error { } // Load genesis packages - pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) - if err != nil { - return fmt.Errorf("unable to load pkgs: %w", err) - } - - newState := n.state[:newIndex] + pkgsTxs := n.generateTxs(DefaultFee, n.pkgs) // Create genesis with loaded pkgs + previous state + newState := n.state[:newIndex] genesis := gnoland.DefaultGenState() genesis.Balances = n.config.BalancesList genesis.Txs = append(pkgsTxs, newState...) diff --git a/contribs/gnodev/pkg/dev/node_state_test.go b/contribs/gnodev/pkg/dev/node_state_test.go index efaeb979693..32800fd0db6 100644 --- a/contribs/gnodev/pkg/dev/node_state_test.go +++ b/contribs/gnodev/pkg/dev/node_state_test.go @@ -6,10 +6,11 @@ import ( "testing" "time" - emitter "github.com/gnolang/gno/contribs/gnodev/internal/mock" + mock "github.com/gnolang/gno/contribs/gnodev/internal/mock/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -136,28 +137,29 @@ func TestExportState(t *testing.T) { }) } -func testingCounterRealm(t *testing.T, inc int) (*Node, *emitter.ServerEmitter) { +func testingCounterRealm(t *testing.T, inc int) (*Node, *mock.ServerEmitter) { t.Helper() - const ( - // foo package - counterGnoMod = "module gno.land/r/dev/counter\n" - counterFile = `package counter + const counterFile = ` +package counter + import "strconv" var value int = 0 func Inc(v int) { value += v } // method to increment value func Render(_ string) string { return strconv.Itoa(value) } ` - ) - // Generate package counter - counterPkg := generateTestingPackage(t, - "gno.mod", counterGnoMod, - "foo.gno", counterFile) + counterPkg := gnovm.MemPackage{ + Name: "counter", + Path: "gno.land/r/dev/counter", + Files: []*gnovm.MemFile{ + {Name: "file.gno", Body: counterFile}, + }, + } // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, counterPkg) + node, emitter := newTestingDevNode(t, &counterPkg) assert.Len(t, node.ListPkgs(), 1) // Test rendering diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 38fab0a3360..7da976bc13f 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -3,33 +3,28 @@ package dev import ( "context" "encoding/json" - "os" - "path/filepath" "testing" "time" - mock "github.com/gnolang/gno/contribs/gnodev/internal/mock" - + mock "github.com/gnolang/gno/contribs/gnodev/internal/mock/emitter" "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/gnolang/gno/gno.land/pkg/gnoclient" "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" 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" + tm2std "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// XXX: We should probably use txtar to test this package. - -var nodeTestingAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - // TestNewNode_NoPackages tests the NewDevNode method with no package. func TestNewNode_NoPackages(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) @@ -49,32 +44,35 @@ func TestNewNode_NoPackages(t *testing.T) { } // TestNewNode_WithPackage tests the NewDevNode with a single package. -func TestNewNode_WithPackage(t *testing.T) { +func TestNewNode_WithLoader(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - const ( - // foobar package - testGnoMod = "module gno.land/r/dev/foobar\n" - testFile = `package foobar + pkg := gnovm.MemPackage{ + Name: "foobar", + Path: "gno.land/r/dev/foobar", + Files: []*gnovm.MemFile{ + { + Name: "foobar.gno", + Body: `package foobar func Render(_ string) string { return "foo" } -` - ) +`, + }, + }, + } - // Generate package - pkgpath := generateTestingPackage(t, "gno.mod", testGnoMod, "foobar.gno", testFile) logger := log.NewTestingLogger(t) - // Call NewDevNode with no package should work cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") - cfg.PackagesPathList = []PackagePath{pkgpath} + cfg.Loader = packages.NewLoader(packages.NewMockResolver(&pkg)) cfg.Logger = logger - node, err := NewDevNode(ctx, cfg) + + node, err := NewDevNode(ctx, cfg, pkg.Path) require.NoError(t, err) assert.Len(t, node.ListPkgs(), 1) // Test rendering - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foobar") + render, err := testingRenderRealm(t, node, pkg.Path) require.NoError(t, err) assert.Equal(t, render, "foo") @@ -83,24 +81,37 @@ func Render(_ string) string { return "foo" } func TestNodeAddPackage(t *testing.T) { // Setup a Node instance - const ( - // foo package - fooGnoMod = "module gno.land/r/dev/foo\n" - fooFile = `package foo + fooPkg := gnovm.MemPackage{ + Name: "foo", + Path: "gno.land/r/dev/foo", + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foo func Render(_ string) string { return "foo" } -` - // bar package - barGnoMod = "module gno.land/r/dev/bar\n" - barFile = `package bar +`, + }, + }, + } + + barPkg := gnovm.MemPackage{ + Name: "bar", + Path: "gno.land/r/dev/bar", + Files: []*gnovm.MemFile{ + { + Name: "bar.gno", + Body: `package bar func Render(_ string) string { return "bar" } -` - ) +`, + }, + }, + } // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", fooGnoMod, "foo.gno", fooFile) + cfg := newTestingNodeConfig(&fooPkg, &barPkg) // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, foopkg) + node, emitter := newTestingDevNodeWithConfig(t, cfg, fooPkg.Path) assert.Len(t, node.ListPkgs(), 1) // Test render @@ -108,54 +119,60 @@ func Render(_ string) string { return "bar" } require.NoError(t, err) require.Equal(t, render, "foo") - // Generate package bar - barpkg := generateTestingPackage(t, "gno.mod", barGnoMod, "bar.gno", barFile) - err = node.UpdatePackages(barpkg.Path) - require.NoError(t, err) - assert.Len(t, node.ListPkgs(), 2) - // Render should fail as the node hasn't reloaded render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") require.Error(t, err) + // Add bar package + node.AddPackagePaths(barPkg.Path) + err = node.Reload(context.Background()) require.NoError(t, err) assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) // After a reload, render should succeed - render, err = testingRenderRealm(t, node, "gno.land/r/dev/bar") + render, err = testingRenderRealm(t, node, barPkg.Path) require.NoError(t, err) require.Equal(t, render, "bar") } func TestNodeUpdatePackage(t *testing.T) { - // Setup a Node instance - const ( - // foo package - foobarGnoMod = "module gno.land/r/dev/foobar\n" - fooFile = `package foobar + foorbarPkg := gnovm.MemPackage{ + Name: "foobar", + Path: "gno.land/r/dev/foobar", + } + + fooFiles := []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foobar func Render(_ string) string { return "foo" } -` - barFile = `package foobar +`, + }, + } + + barFiles := []*gnovm.MemFile{ + { + Name: "bar.gno", + Body: `package foobar func Render(_ string) string { return "bar" } -` - ) +`, + }, + } - // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) + // Update foobar content with bar content + foorbarPkg.Files = fooFiles - // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, foopkg) + node, emitter := newTestingDevNode(t, &foorbarPkg) assert.Len(t, node.ListPkgs(), 1) // Test that render is correct - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foobar") + render, err := testingRenderRealm(t, node, foorbarPkg.Path) require.NoError(t, err) require.Equal(t, render, "foo") - // Override `foo.gno` file with bar content - err = os.WriteFile(filepath.Join(foopkg.Path, "foo.gno"), []byte(barFile), 0o700) - require.NoError(t, err) + // Update foobar content with bar content + foorbarPkg.Files = barFiles err = node.Reload(context.Background()) require.NoError(t, err) @@ -164,7 +181,7 @@ func Render(_ string) string { return "bar" } assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) // After a reload, render should succeed - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foobar") + render, err = testingRenderRealm(t, node, foorbarPkg.Path) require.NoError(t, err) require.Equal(t, render, "bar") @@ -172,31 +189,32 @@ func Render(_ string) string { return "bar" } } func TestNodeReset(t *testing.T) { - const ( - // foo package - foobarGnoMod = "module gno.land/r/dev/foo\n" - fooFile = `package foo + fooPkg := gnovm.MemPackage{ + Name: "foo", + Path: "gno.land/r/dev/foo", + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foo var str string = "foo" func UpdateStr(newStr string) { str = newStr } // method to update 'str' variable func Render(_ string) string { return str } -` - ) - - // Generate package foo - foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile) +`, + }, + }, + } - // Call NewDevNode with no package should work - node, emitter := newTestingDevNode(t, foopkg) + node, emitter := newTestingDevNode(t, &fooPkg) assert.Len(t, node.ListPkgs(), 1) // Test rendering - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + render, err := testingRenderRealm(t, node, fooPkg.Path) require.NoError(t, err) require.Equal(t, render, "foo") // Call `UpdateStr` to update `str` value with "bar" msg := vm.MsgCall{ - PkgPath: "gno.land/r/dev/foo", + PkgPath: fooPkg.Path, Func: "UpdateStr", Args: []string{"bar"}, Send: nil, @@ -208,7 +226,7 @@ func Render(_ string) string { return str } assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) // Check for correct render update - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + render, err = testingRenderRealm(t, node, fooPkg.Path) require.NoError(t, err) require.Equal(t, render, "bar") @@ -218,18 +236,93 @@ func Render(_ string) string { return str } assert.Equal(t, emitter.NextEvent().Type(), events.EvtReset) // Test rendering should return initial `str` value - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + render, err = testingRenderRealm(t, node, fooPkg.Path) require.NoError(t, err) require.Equal(t, render, "foo") assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type()) } +func TestTxGasFailure(t *testing.T) { + fooPkg := gnovm.MemPackage{ + Name: "foo", + Path: "gno.land/r/dev/foo", + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foo +import "strconv" + +var i int +func Inc() { i++ } // method to increment i +func Render(_ string) string { return strconv.Itoa(i) } +`, + }, + }, + } + + node, emitter := newTestingDevNode(t, &fooPkg) + assert.Len(t, node.ListPkgs(), 1) + + // Test rendering + render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + require.Equal(t, "0", render) + + // Call `Inc` to update counter + msg := vm.MsgCall{ + PkgPath: "gno.land/r/dev/foo", + Func: "Inc", + Args: nil, + Send: nil, + } + + 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) + + // Check for correct render update + render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + require.Equal(t, "1", render) + + // Not Enough gas wanted + callCfg := gnoclient.BaseTxCfg{ + GasFee: ugnot.ValueString(10000), // Gas fee + + // Ensure sufficient gas is provided for the transaction to be committed. + // However, avoid providing too much gas to allow the + // transaction to succeed (OutOfGasError). + GasWanted: 100_000, + } + + res, err = testingCallRealmWithConfig(t, node, callCfg, msg) + require.Error(t, err) + require.ErrorAs(t, err, &tm2std.OutOfGasError{}) + + // Transaction should be committed regardless the error + require.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult, + "(probably) not enough gas for the transaction to be committed") + + // Reload the node + err = node.Reload(context.Background()) + require.NoError(t, err) + assert.Equal(t, events.EvtReload, emitter.NextEvent().Type()) + + // Check for correct render update + render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + + // Assert that the previous transaction hasn't succeeded during genesis reload + require.Equal(t, "1", render) +} + func TestTxTimestampRecover(t *testing.T) { - const ( - // foo package - foobarGnoMod = "module gno.land/r/dev/foo\n" - fooFile = `package foo + const fooFile = ` +package foo + import ( "strconv" "strings" @@ -259,12 +352,35 @@ func Render(_ string) string { return strs.String() } ` - ) + fooPkg := gnovm.MemPackage{ + Name: "foo", + Path: "gno.land/r/dev/foo", + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: fooFile, + }, + }, + } // 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() + // XXX(gfanton): Setting this to `false` somehow makes the time block + // drift from the time spanned by the VM. + cfg := newTestingNodeConfig(&fooPkg) + 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, fooPkg.Path) + + render, err := testingRenderRealm(t, node, fooPkg.Path) + require.NoError(t, err) + require.NotEmpty(t, render) + parseJSONTimesList := func(t *testing.T, render string) []time.Time { t.Helper() @@ -282,21 +398,6 @@ func Render(_ string) string { 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 @@ -329,7 +430,7 @@ func Render(_ string) string { // Span multiple time for i := 0; i < nevents; i++ { - t.Logf("waiting for a bock greater than height(%d) and unix(%d)", refHeight, refTimestamp) + t.Logf("waiting for a block greater than height(%d) and unix(%d)", refHeight, refTimestamp) for { var block types.EventNewBlock select { @@ -357,7 +458,7 @@ func Render(_ string) string { // Span a new time msg := vm.MsgCall{ - PkgPath: "gno.land/r/dev/foo", + PkgPath: fooPkg.Path, Func: "SpanTime", } @@ -373,7 +474,7 @@ func Render(_ string) string { } // Render JSON times list - render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + render, err = testingRenderRealm(t, node, fooPkg.Path) require.NoError(t, err) // Parse times list @@ -396,12 +497,12 @@ func Render(_ string) string { assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) // Fetch time list again from render - render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + render, err = testingRenderRealm(t, node, fooPkg.Path) require.NoError(t, err) timesList2 := parseJSONTimesList(t, render) - // Times list should be identical from the orignal list + // Times list should be identical from the original list require.Len(t, timesList2, len(timesList1)) for i := 0; i < len(timesList1); i++ { t1nsec, t2nsec := timesList1[i].UnixNano(), timesList2[i].UnixNano() @@ -430,17 +531,23 @@ func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) { t.Helper() + defaultCfg := gnoclient.BaseTxCfg{ + GasFee: ugnot.ValueString(1000000), // Gas fee + GasWanted: 3_000_000, // Gas wanted + } + + return testingCallRealmWithConfig(t, node, defaultCfg, msgs...) +} + +func testingCallRealmWithConfig(t *testing.T, node *Node, bcfg gnoclient.BaseTxCfg, msgs ...vm.MsgCall) (*core_types.ResultBroadcastTxCommit, error) { + t.Helper() + signer := newInMemorySigner(t, node.Config().ChainID()) cli := gnoclient.Client{ Signer: signer, RPCClient: node.Client(), } - txcfg := gnoclient.BaseTxCfg{ - GasFee: ugnot.ValueString(1000000), // Gas fee - GasWanted: 3_000_000, // Gas wanted - } - // Set Caller in the msgs caller, err := signer.Info() require.NoError(t, err) @@ -449,45 +556,35 @@ func testingCallRealm(t *testing.T, node *Node, msgs ...vm.MsgCall) (*core_types vmMsgs = append(vmMsgs, vm.NewMsgCall(caller.GetAddress(), msg.Send, msg.PkgPath, msg.Func, msg.Args)) } - return cli.Call(txcfg, vmMsgs...) + return cli.Call(bcfg, vmMsgs...) } -func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath { - t.Helper() - workdir := t.TempDir() - - if len(nameFile)%2 != 0 { - require.FailNow(t, "Generate testing packages require paired arguments.") - } - - for i := 0; i < len(nameFile); i += 2 { - name := nameFile[i] - content := nameFile[i+1] - - err := os.WriteFile(filepath.Join(workdir, name), []byte(content), 0o700) - require.NoError(t, err) - } - - return PackagePath{ - Path: workdir, - Creator: nodeTestingAddress, - } -} +func newTestingNodeConfig(pkgs ...*gnovm.MemPackage) *NodeConfig { + var loader packages.BaseLoader + gnoroot := gnoenv.RootDir() -func createDefaultTestingNodeConfig(pkgslist ...PackagePath) *NodeConfig { + loader.Resolver = packages.MiddlewareResolver( + packages.NewMockResolver(pkgs...), + packages.FilterStdlibs) cfg := DefaultNodeConfig(gnoenv.RootDir(), "gno.land") - cfg.PackagesPathList = pkgslist + cfg.TMConfig = integration.DefaultTestingTMConfig(gnoroot) + cfg.Loader = &loader return cfg } -func newTestingDevNode(t *testing.T, pkgslist ...PackagePath) (*Node, *mock.ServerEmitter) { +func newTestingDevNode(t *testing.T, pkgs ...*gnovm.MemPackage) (*Node, *mock.ServerEmitter) { t.Helper() - cfg := createDefaultTestingNodeConfig(pkgslist...) - return newTestingDevNodeWithConfig(t, cfg) + cfg := newTestingNodeConfig(pkgs...) + paths := make([]string, len(pkgs)) + for i, pkg := range pkgs { + paths[i] = pkg.Path + } + + return newTestingDevNodeWithConfig(t, cfg, paths...) } -func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.ServerEmitter) { +func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig, pkgpaths ...string) (*Node, *mock.ServerEmitter) { t.Helper() ctx, cancel := context.WithCancel(context.Background()) @@ -497,9 +594,9 @@ func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.Se cfg.Emitter = emitter cfg.Logger = logger - node, err := NewDevNode(ctx, cfg) + node, err := NewDevNode(ctx, cfg, pkgpaths...) require.NoError(t, err) - assert.Len(t, node.ListPkgs(), len(cfg.PackagesPathList)) + require.Equal(t, emitter.NextEvent().Type(), events.EvtReset) t.Cleanup(func() { node.Close() diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go deleted file mode 100644 index 62c1907b8c9..00000000000 --- a/contribs/gnodev/pkg/dev/packages.go +++ /dev/null @@ -1,170 +0,0 @@ -package dev - -import ( - "errors" - "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" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/std" -) - -type PackagePath struct { - Path string - Creator crypto.Address - Deposit std.Coins -} - -func ResolvePackagePathQuery(bk *address.Book, path string) (PackagePath, error) { - var ppath PackagePath - - upath, err := url.Parse(path) - if err != nil { - return ppath, fmt.Errorf("malformed path/query: %w", err) - } - ppath.Path = filepath.Clean(upath.Path) - - // Check for creator option - creator := upath.Query().Get("creator") - if creator != "" { - address, err := crypto.AddressFromBech32(creator) - if err != nil { - var ok bool - address, ok = bk.GetByName(creator) - if !ok { - return ppath, fmt.Errorf("invalid name or address for creator %q", creator) - } - } - - ppath.Creator = address - } - - // Check for deposit option - deposit := upath.Query().Get("deposit") - if deposit != "" { - coins, err := std.ParseCoins(deposit) - if err != nil { - return ppath, fmt.Errorf( - "unable to parse deposit amount %q (should be in the form xxxugnot): %w", deposit, err, - ) - } - - ppath.Deposit = coins - } - - return ppath, nil -} - -type Package struct { - gnomod.Pkg - Creator crypto.Address - Deposit std.Coins -} - -type PackagesMap map[string]Package - -var ( - ErrEmptyCreatorPackage = errors.New("no creator specified for package") - ErrEmptyDepositPackage = errors.New("no deposit specified for package") -) - -func NewPackagesMap(ppaths []PackagePath) (PackagesMap, error) { - pkgs := make(map[string]Package) - for _, ppath := range ppaths { - if ppath.Creator.IsZero() { - return nil, fmt.Errorf("unable to load package %q: %w", ppath.Path, ErrEmptyCreatorPackage) - } - - abspath, err := filepath.Abs(ppath.Path) - if err != nil { - return nil, fmt.Errorf("unable to guess absolute path for %q: %w", ppath.Path, err) - } - - // list all packages from target path - pkgslist, err := gnomod.ListPkgs(abspath) - if err != nil { - return nil, fmt.Errorf("listing gno packages: %w", err) - } - - for _, pkg := range pkgslist { - if pkg.Dir == "" { - continue - } - - if _, ok := pkgs[pkg.Dir]; ok { - continue // skip - } - pkgs[pkg.Dir] = Package{ - Pkg: pkg, - Creator: ppath.Creator, - Deposit: ppath.Deposit, - } - } - } - - return pkgs, nil -} - -func (pm PackagesMap) toList() gnomod.PkgList { - list := make([]gnomod.Pkg, 0, len(pm)) - for _, pkg := range pm { - list = append(list, pkg.Pkg) - } - return list -} - -func (pm PackagesMap) Load(fee std.Fee, start time.Time) ([]gnoland.TxWithMetadata, error) { - pkgs := pm.toList() - - sorted, err := pkgs.Sort() - if err != nil { - return nil, fmt.Errorf("unable to sort pkgs: %w", err) - } - - nonDraft := sorted.GetNonDraftPkgs() - - metatxs := make([]gnoland.TxWithMetadata, 0, len(nonDraft)) - for _, modPkg := range nonDraft { - pkg := pm[modPkg.Dir] - if pkg.Creator.IsZero() { - return nil, fmt.Errorf("no creator set for %q", pkg.Dir) - } - - // Open files in directory as MemPackage. - memPkg := gno.MustReadMemPackage(modPkg.Dir, modPkg.Name) - if err := memPkg.Validate(); err != nil { - return nil, fmt.Errorf("invalid package: %w", err) - } - - // Create transaction - tx := std.Tx{ - Fee: fee, - Msgs: []std.Msg{ - vmm.MsgAddPackage{ - Creator: pkg.Creator, - Deposit: pkg.Deposit, - Package: memPkg, - }, - }, - } - - tx.Signatures = make([]std.Signature, len(tx.GetSigners())) - metatx := gnoland.TxWithMetadata{ - Tx: tx, - Metadata: &gnoland.GnoTxMetadata{ - Timestamp: start.Unix(), - }, - } - - metatxs = append(metatxs, metatx) - } - - return metatxs, nil -} diff --git a/contribs/gnodev/pkg/dev/packages_test.go b/contribs/gnodev/pkg/dev/packages_test.go deleted file mode 100644 index 151a89a7815..00000000000 --- a/contribs/gnodev/pkg/dev/packages_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package dev - -import ( - "testing" - - "github.com/gnolang/gno/contribs/gnodev/pkg/address" - "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestResolvePackagePathQuery(t *testing.T) { - t.Parallel() - - var ( - testingName = "testAccount" - testingAddress = crypto.MustAddressFromString("g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na") - ) - - book := address.NewBook() - book.Add(testingAddress, testingName) - - cases := []struct { - Path string - ExpectedPackagePath PackagePath - ShouldFail bool - }{ - { - Path: ".", - ExpectedPackagePath: PackagePath{ - Path: ".", - }, - }, - { - Path: "/simple/path", - ExpectedPackagePath: PackagePath{ - Path: "/simple/path", - }, - }, - { - Path: "/ambiguo/u//s/path///", - ExpectedPackagePath: PackagePath{ - Path: "/ambiguo/u/s/path", - }, - }, - { - Path: "/path/with/creator?creator=testAccount", - ExpectedPackagePath: PackagePath{ - Path: "/path/with/creator", - Creator: testingAddress, - }, - }, - { - Path: "/path/with/deposit?deposit=" + ugnot.ValueString(100), - ExpectedPackagePath: PackagePath{ - Path: "/path/with/deposit", - Deposit: std.MustParseCoins(ugnot.ValueString(100)), - }, - }, - { - Path: ".?creator=g1hr3dl82qdy84a5h3dmckh0suc7zgwm5rnns6na&deposit=" + ugnot.ValueString(100), - ExpectedPackagePath: PackagePath{ - Path: ".", - Creator: testingAddress, - Deposit: std.MustParseCoins(ugnot.ValueString(100)), - }, - }, - - // errors cases - { - Path: "/invalid/account?creator=UnknownAccount", - ShouldFail: true, - }, - { - Path: "/invalid/address?creator=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", - ShouldFail: true, - }, - { - Path: "/invalid/deposit?deposit=abcd", - ShouldFail: true, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.Path, func(t *testing.T) { - t.Parallel() - - result, err := ResolvePackagePathQuery(book, tc.Path) - if tc.ShouldFail { - assert.Error(t, err) - return - } - require.NoError(t, err) - - assert.Equal(t, tc.ExpectedPackagePath.Path, result.Path) - assert.Equal(t, tc.ExpectedPackagePath.Creator, result.Creator) - assert.Equal(t, tc.ExpectedPackagePath.Deposit.String(), result.Deposit.String()) - }) - } -} diff --git a/contribs/gnodev/pkg/dev/query_path.go b/contribs/gnodev/pkg/dev/query_path.go new file mode 100644 index 00000000000..e899d8212e4 --- /dev/null +++ b/contribs/gnodev/pkg/dev/query_path.go @@ -0,0 +1,58 @@ +package dev + +import ( + "fmt" + "net/url" + "path/filepath" + + "github.com/gnolang/gno/contribs/gnodev/pkg/address" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type QueryPath struct { + Path string + Creator crypto.Address + Deposit std.Coins +} + +func ResolveQueryPath(bk *address.Book, query string) (QueryPath, error) { + var qpath QueryPath + + upath, err := url.Parse(query) + if err != nil { + return qpath, fmt.Errorf("malformed path/query: %w", err) + } + + qpath.Path = filepath.Clean(upath.Path) + + // Check for creator option + creator := upath.Query().Get("creator") + if creator != "" { + address, err := crypto.AddressFromBech32(creator) + if err != nil { + var ok bool + address, ok = bk.GetByName(creator) + if !ok { + return qpath, fmt.Errorf("invalid name or address for creator %q", creator) + } + } + + qpath.Creator = address + } + + // Check for deposit option + deposit := upath.Query().Get("deposit") + if deposit != "" { + coins, err := std.ParseCoins(deposit) + if err != nil { + return qpath, fmt.Errorf( + "unable to parse deposit amount %q (should be in the form xxxugnot): %w", deposit, err, + ) + } + + qpath.Deposit = coins + } + + return qpath, nil +} diff --git a/contribs/gnodev/pkg/dev/query_path_test.go b/contribs/gnodev/pkg/dev/query_path_test.go new file mode 100644 index 00000000000..519edcc7739 --- /dev/null +++ b/contribs/gnodev/pkg/dev/query_path_test.go @@ -0,0 +1,132 @@ +package dev_test + +import ( + "testing" + + "github.com/gnolang/gno/contribs/gnodev/pkg/address" + "github.com/gnolang/gno/contribs/gnodev/pkg/dev" + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolvePackageModifierQuery(t *testing.T) { + validAddr := crypto.MustAddressFromString(integration.DefaultAccount_Address) + validBech32Addr := validAddr.String() + validCoins := std.MustParseCoins("100ugnot") + + tests := []struct { + name string + path string + book *address.Book + wantQuery dev.QueryPath + wantErrMsg string + }{ + { + name: "valid creator bech32", + path: "abc.xy/some/path?creator=" + validBech32Addr, + book: address.NewBook(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/some/path", + Creator: validAddr, + }, + }, + + { + name: "valid creator name", + path: "abc.xy/path?creator=alice", + book: func() *address.Book { + bk := address.NewBook() + bk.Add(validAddr, "alice") + return bk + }(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/path", + Creator: validAddr, + }, + }, + + { + name: "invalid creator", + path: "abc.xy/path?creator=bob", + book: address.NewBook(), + wantErrMsg: `invalid name or address for creator "bob"`, + }, + + { + name: "invalid bech32 creator", + path: "abc.xy/path?creator=invalid", + book: address.NewBook(), + wantErrMsg: `invalid name or address for creator "invalid"`, + }, + + { + name: "valid deposit", + path: "abc.xy/path?deposit=100ugnot", + book: address.NewBook(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/path", + Deposit: validCoins, + }, + }, + + { + name: "invalid deposit", + path: "abc.xy/path?deposit=invalid", + book: address.NewBook(), + wantErrMsg: `unable to parse deposit amount "invalid" (should be in the form xxxugnot)`, + }, + + { + name: "both creator and deposit", + path: "abc.xy/path?creator=" + validBech32Addr + "&deposit=100ugnot", + book: address.NewBook(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/path", + Creator: validAddr, + Deposit: validCoins, + }, + }, + + { + name: "malformed path", + path: "://invalid", + book: address.NewBook(), + wantErrMsg: "malformed path/query", + }, + + { + name: "no creator or deposit", + path: "abc.xy/path", + book: address.NewBook(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/path", + }, + }, + + { + name: "clean path with ..", + path: "abc.xy/foo/../bar", + book: address.NewBook(), + wantQuery: dev.QueryPath{ + Path: "abc.xy/bar", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotQuery, err := dev.ResolveQueryPath(tt.book, tt.path) + if tt.wantErrMsg != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErrMsg) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.wantQuery, gotQuery) + }) + } +} diff --git a/contribs/gnodev/pkg/emitter/server.go b/contribs/gnodev/pkg/emitter/server.go index 3e32984268d..29f8e3050ba 100644 --- a/contribs/gnodev/pkg/emitter/server.go +++ b/contribs/gnodev/pkg/emitter/server.go @@ -1,6 +1,7 @@ package emitter import ( + "encoding/json" "log/slog" "net/http" "sync" @@ -32,6 +33,10 @@ func NewServer(logger *slog.Logger) *Server { } } +func (s *Server) LockEmit() { s.muClients.Lock() } + +func (s *Server) UnlockEmit() { s.muClients.Unlock() } + // ws handler func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { conn, err := s.upgrader.Upgrade(w, r, nil) @@ -69,13 +74,9 @@ func (s *Server) emit(evt events.Event) { s.muClients.Lock() defer s.muClients.Unlock() - jsonEvt := EventJSON{evt.Type(), evt} - - s.logger.Info("sending event to clients", - "clients", len(s.clients), - "type", evt.Type(), - "event", evt) + s.logEvent(evt) + jsonEvt := EventJSON{evt.Type(), evt} for conn := range s.clients { err := conn.WriteJSON(jsonEvt) if err != nil { @@ -96,3 +97,15 @@ func (s *Server) conns() []*websocket.Conn { return conns } + +func (s *Server) logEvent(evt events.Event) { + var logEvt string + if rawEvt, err := json.Marshal(evt); err == nil { + logEvt = string(rawEvt) + } + + s.logger.Info("sending event to clients", + "clients", len(s.clients), + "type", evt.Type(), + "event", logEvt) +} diff --git a/contribs/gnodev/pkg/emitter/static/hotreload.js b/contribs/gnodev/pkg/emitter/static/hotreload.js index 28e47c1ea15..7b58fc35004 100644 --- a/contribs/gnodev/pkg/emitter/static/hotreload.js +++ b/contribs/gnodev/pkg/emitter/static/hotreload.js @@ -1,19 +1,34 @@ -(function() { +document.addEventListener('DOMContentLoaded', function() { // Define the events that will trigger a page reload const eventsReload = [ {{range .ReloadEvents}}'{{.}}',{{end}} ]; - + // Establish the WebSocket connection to the event server const ws = new WebSocket('ws://{{- .Remote -}}'); - + // `gracePeriod` mitigates reload loops due to excessive events. This period // occurs post-loading and lasts for the `graceTimeout` duration. const graceTimeout = 1000; // ms let gracePeriod = true; let debounceTimeout = setTimeout(function() { gracePeriod = false; - }, graceTimeout); + }, graceTimeout); + + // Flag to track if a link click is in progress + let clickInProgress = false; + + // Capture clicks on tags to prevent reloading appening when clicking on link + document.addEventListener('click', function(event) { + const target = event.target; + if (target.tagName === 'A' && target.href) { + clickInProgress = true; + // Wait a bit before allowing reload again + setTimeout(function() { + clickInProgress = false; + }, 5000); + } + }); // Handle incoming WebSocket messages ws.onmessage = function(event) { @@ -23,19 +38,21 @@ // Ignore events not in the reload-triggering list if (!eventsReload.includes(message.type)) { - return; + return; } - // Reload the page immediately if we're not in the grace period - if (!gracePeriod) { + // Reload the page immediately if we're not in the grace period and no clicks are in progress + if (!gracePeriod && !clickInProgress) { window.location.reload(); return; } - // If still in the grace period, debounce the reload + // If still in the grace period or a click is in progress, debounce the reload clearTimeout(debounceTimeout); debounceTimeout = setTimeout(function() { - window.location.reload(); + if (!clickInProgress) { + window.location.reload(); + } }, graceTimeout); } catch (e) { @@ -50,4 +67,4 @@ ws.onclose = function() { console.log('WebSocket connection closed'); }; -})(); +}); diff --git a/contribs/gnodev/pkg/logger/log_column.go b/contribs/gnodev/pkg/logger/log_column.go index 2a720525903..0e6c181ad6d 100644 --- a/contribs/gnodev/pkg/logger/log_column.go +++ b/contribs/gnodev/pkg/logger/log_column.go @@ -21,7 +21,9 @@ func NewColumnLogger(w io.Writer, level slog.Level, profile termenv.Profile) *Co }) // Default column output - defaultOutput := newColumeWriter(lipgloss.NewStyle(), "", w) + renderer := lipgloss.NewRenderer(nil, termenv.WithProfile(profile)) + + defaultOutput := newColumeWriter(w, lipgloss.NewStyle(), "") charmLogger.SetOutput(defaultOutput) charmLogger.SetStyles(defaultStyles()) charmLogger.SetColorProfile(profile) @@ -40,10 +42,12 @@ func NewColumnLogger(w io.Writer, level slog.Level, profile termenv.Profile) *Co } return &ColumnLogger{ - Logger: charmLogger, - writer: w, - prefix: charmLogger.GetPrefix(), - colors: map[string]lipgloss.Color{}, + Logger: charmLogger, + writer: w, + prefix: charmLogger.GetPrefix(), + colors: map[string]lipgloss.Color{}, + colorProfile: profile, + renderer: renderer, } } @@ -52,6 +56,7 @@ type ColumnLogger struct { prefix string writer io.Writer + renderer *lipgloss.Renderer colorProfile termenv.Profile colors map[string]lipgloss.Color @@ -72,10 +77,11 @@ func (cl *ColumnLogger) WithGroup(group string) slog.Handler { // generate bright color based on the group name fg = colorFromString(group, 0.5, 0.6) } - baseStyle := lipgloss.NewStyle().Foreground(fg) + + baseStyle := lipgloss.NewStyle().Foreground(fg).Renderer(cl.renderer) nlog := cl.Logger.With() // clone logger - nlog.SetOutput(newColumeWriter(baseStyle, group, cl.writer)) + nlog.SetOutput(newColumeWriter(cl.writer, baseStyle, group)) nlog.SetColorProfile(cl.colorProfile) return &ColumnLogger{ Logger: nlog, @@ -99,7 +105,7 @@ type columnWriter struct { writer io.Writer } -func newColumeWriter(baseStyle lipgloss.Style, prefix string, writer io.Writer) *columnWriter { +func newColumeWriter(w io.Writer, baseStyle lipgloss.Style, prefix string) *columnWriter { const width = 12 style := baseStyle. @@ -112,7 +118,7 @@ func newColumeWriter(baseStyle lipgloss.Style, prefix string, writer io.Writer) prefix = prefix[:width-3] + "..." } - return &columnWriter{style: style, prefix: prefix, writer: writer} + return &columnWriter{style: style, prefix: prefix, writer: w} } func (cl *columnWriter) Write(buf []byte) (n int, err error) { diff --git a/contribs/gnodev/pkg/packages/glob.go b/contribs/gnodev/pkg/packages/glob.go new file mode 100644 index 00000000000..1b76425deb4 --- /dev/null +++ b/contribs/gnodev/pkg/packages/glob.go @@ -0,0 +1,214 @@ +// Inspired by: https://cs.opensource.google/go/x/tools/+/master:gopls/internal/test/integration/fake/glob/glob.go + +package packages + +import ( + "errors" + "fmt" + "strings" +) + +var ErrAdjacentSlash = errors.New("** may only be adjacent to '/'") + +// Glob patterns can have the following syntax: +// - `*` to match one or more characters in a path segment +// - `**` to match any number of path segments, including none +// +// Expanding on this: +// - '/' matches one or more literal slashes. +// - any other character matches itself literally. +type Glob struct { + elems []element // pattern elements +} + +// Parse builds a Glob for the given pattern, returning an error if the pattern +// is invalid. +func Parse(pattern string) (*Glob, error) { + g, _, err := parse(pattern) + return g, err +} + +func parse(pattern string) (*Glob, string, error) { + g := new(Glob) + for len(pattern) > 0 { + switch pattern[0] { + case '/': + // Skip consecutive slashes + for len(pattern) > 0 && pattern[0] == '/' { + pattern = pattern[1:] + } + g.elems = append(g.elems, slash{}) + + case '*': + if len(pattern) > 1 && pattern[1] == '*' { + if (len(g.elems) > 0 && g.elems[len(g.elems)-1] != slash{}) || (len(pattern) > 2 && pattern[2] != '/') { + return nil, "", ErrAdjacentSlash + } + pattern = pattern[2:] + g.elems = append(g.elems, starStar{}) + break + } + pattern = pattern[1:] + g.elems = append(g.elems, star{}) + + default: + pattern = g.parseLiteral(pattern) + } + } + return g, "", nil +} + +func (g *Glob) parseLiteral(pattern string) string { + end := strings.IndexAny(pattern, "*/") + if end == -1 { + end = len(pattern) + } + g.elems = append(g.elems, literal(pattern[:end])) + return pattern[end:] +} + +func (g *Glob) String() string { + var b strings.Builder + for _, e := range g.elems { + fmt.Fprint(&b, e) + } + return b.String() +} + +func (g *Glob) StarFreeBase() string { + var b strings.Builder + for _, e := range g.elems { + if e == (star{}) || e == (starStar{}) { + break + } + fmt.Fprint(&b, e) + } + return b.String() +} + +// element holds a glob pattern element, as defined below. +type element fmt.Stringer + +// element types. +type ( + slash struct{} // One or more '/' separators + literal string // string literal, not containing / or * + star struct{} // * + starStar struct{} // ** +) + +func (s slash) String() string { return "/" } +func (l literal) String() string { return string(l) } +func (s star) String() string { return "*" } +func (s starStar) String() string { return "**" } + +// Match reports whether the input string matches the glob pattern. +func (g *Glob) Match(input string) bool { + return match(g.elems, input) +} + +func match(elems []element, input string) (ok bool) { + var elem interface{} + for len(elems) > 0 { + elem, elems = elems[0], elems[1:] + switch elem := elem.(type) { + case slash: + // Skip consecutive slashes in the input + if len(input) == 0 || input[0] != '/' { + return false + } + for len(input) > 0 && input[0] == '/' { + input = input[1:] + } + + case starStar: + // Special cases: + // - **/a matches "a" + // - **/ matches everything + // + // Note that if ** is followed by anything, it must be '/' (this is + // enforced by Parse). + if len(elems) > 0 { + elems = elems[1:] + } + + // A trailing ** matches anything. + if len(elems) == 0 { + return true + } + + // Backtracking: advance pattern segments until the remaining pattern + // elements match. + for len(input) != 0 { + if match(elems, input) { + return true + } + _, input = split(input) + } + return false + + case literal: + if !strings.HasPrefix(input, string(elem)) { + return false + } + input = input[len(elem):] + + case star: + var segInput string + segInput, input = split(input) + + elemEnd := len(elems) + for i, e := range elems { + if e == (slash{}) { + elemEnd = i + break + } + } + segElems := elems[:elemEnd] + elems = elems[elemEnd:] + + // A trailing * matches the entire segment. + if len(segElems) == 0 { + if len(elems) > 0 && elems[0] == (slash{}) { + elems = elems[1:] // shift elems + } + break + } + + // Backtracking: advance characters until remaining subpattern elements + // match. + matched := false + for i := range segInput { + if match(segElems, segInput[i:]) { + matched = true + break + } + } + if !matched { + return false + } + + default: + panic(fmt.Sprintf("segment type %T not implemented", elem)) + } + } + + return len(input) == 0 +} + +// split returns the portion before and after the first slash +// (or sequence of consecutive slashes). If there is no slash +// it returns (input, nil). +func split(input string) (first, rest string) { + i := strings.IndexByte(input, '/') + if i < 0 { + return input, "" + } + first = input[:i] + for j := i; j < len(input); j++ { + if input[j] != '/' { + return first, input[j:] + } + } + return first, "" +} diff --git a/contribs/gnodev/pkg/packages/glob_test.go b/contribs/gnodev/pkg/packages/glob_test.go new file mode 100644 index 00000000000..7fad4eb2fe1 --- /dev/null +++ b/contribs/gnodev/pkg/packages/glob_test.go @@ -0,0 +1,93 @@ +// Inspired by: https://cs.opensource.google/go/x/tools/+/master:gopls/internal/test/integration/fake/glob/glob_test.go + +package packages + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMatch(t *testing.T) { + t.Parallel() + + tests := []struct { + pattern, input string + want bool + }{ + // Basic cases. + {"", "", true}, + {"", "a", false}, + {"", "/", false}, + {"abc", "abc", true}, + + // ** behavior + {"**", "abc", true}, + {"**/abc", "abc", true}, + {"**", "abc/def", true}, + + // * behavior + {"/*", "/a", true}, + {"*", "foo", true}, + {"*o", "foo", true}, + {"*o", "foox", false}, + {"f*o", "foo", true}, + {"f*o", "fo", true}, + + // Dirs cases + {"**/", "path/to/foo/", true}, + {"**/", "path/to/foo", true}, + + {"path/to/foo", "path/to/foo", true}, + {"path/to/foo", "path/to/bar", false}, + {"path/*/foo", "path/to/foo", true}, + {"path/*/1/*/3/*/5*/foo", "path/to/1/2/3/4/522/foo", true}, + {"path/*/1/*/3/*/5*/foo", "path/to/1/2/3/4/722/foo", false}, + {"path/*/1/*/3/*/5*/foo", "path/to/1/2/3/4/522/bar", false}, + {"path/*/foo", "path/to/to/foo", false}, + {"path/**/foo", "path/to/to/foo", true}, + {"path/**/foo", "path/to/to/bar", false}, + {"path/**/foo", "path/foo", true}, + {"**/abc/**", "foo/r/x/abc/bar", true}, + + // Realistic examples. + {"**/*.ts", "path/to/foo.ts", true}, + {"**/*.js", "path/to/foo.js", true}, + {"**/*.go", "path/to/foo.go", true}, + } + + for _, test := range tests { + g, err := Parse(test.pattern) + require.NoErrorf(t, err, "Parse(%q) failed unexpectedly: %v", test.pattern, err) + assert.Equalf(t, test.want, g.Match(test.input), + "Parse(%q).Match(%q) = %t, want %t", test.pattern, test.input, !test.want, test.want) + } +} + +func TestBaseFreeStar(t *testing.T) { + t.Parallel() + + tests := []struct { + pattern, baseFree string + }{ + // Basic cases. + {"", ""}, + {"foo", "foo"}, + {"foo/bar", "foo/bar"}, + {"foo///bar", "foo/bar"}, + {"foo/bar/", "foo/bar/"}, + {"foo/bar/*/*/z", "foo/bar/"}, + {"foo/bar/**", "foo/bar/"}, + {"**", ""}, + {"/**", "/"}, + } + + for _, test := range tests { + g, err := Parse(test.pattern) + require.NoErrorf(t, err, "Parse(%q) failed unexpectedly: %v", test.pattern, err) + got := g.StarFreeBase() + assert.Equalf(t, test.baseFree, got, + "Parse(%q).Match(%q) = %q, want %q", test.pattern, test.baseFree, got, test.baseFree) + } +} diff --git a/contribs/gnodev/pkg/packages/loader.go b/contribs/gnodev/pkg/packages/loader.go new file mode 100644 index 00000000000..3bc978721e6 --- /dev/null +++ b/contribs/gnodev/pkg/packages/loader.go @@ -0,0 +1,12 @@ +package packages + +type Loader interface { + // Load resolves package package paths and all their dependencies in the correct order. + Load(paths ...string) ([]Package, error) + + // Resolve processes a single package path and returns the corresponding Package. + Resolve(path string) (*Package, error) + + // Name of the loader + Name() string +} diff --git a/contribs/gnodev/pkg/packages/loader_base.go b/contribs/gnodev/pkg/packages/loader_base.go new file mode 100644 index 00000000000..039932bd400 --- /dev/null +++ b/contribs/gnodev/pkg/packages/loader_base.go @@ -0,0 +1,104 @@ +package packages + +import ( + "errors" + "fmt" + "go/parser" + "go/token" +) + +type BaseLoader struct { + Resolver +} + +func NewLoader(res ...Resolver) *BaseLoader { + return &BaseLoader{ChainResolvers(res...)} +} + +func (l BaseLoader) Name() string { + return l.Resolver.Name() +} + +func (l BaseLoader) Load(paths ...string) ([]Package, error) { + fset := token.NewFileSet() + visited, stack := map[string]bool{}, map[string]bool{} + pkgs := make([]Package, 0) + for _, root := range paths { + deps, err := load(root, fset, l.Resolver, visited, stack) + if err != nil { + return nil, err + } + pkgs = append(pkgs, deps...) + } + + return pkgs, nil +} + +func (l BaseLoader) Resolve(path string) (*Package, error) { + fset := token.NewFileSet() + return l.Resolver.Resolve(fset, path) +} + +func load(path string, fset *token.FileSet, resolver Resolver, visited, stack map[string]bool) ([]Package, error) { + if stack[path] { + return nil, fmt.Errorf("cycle detected: %s", path) + } + if visited[path] { + return nil, nil + } + + visited[path] = true + + mempkg, err := resolver.Resolve(fset, path) + if err != nil { + if errors.Is(err, ErrResolverPackageSkip) { + return nil, nil + } + + return nil, fmt.Errorf("unable to resolve package %q: %w", path, err) + } + + var name string + imports := map[string]struct{}{} + for _, file := range mempkg.Files { + fname := file.Name + if !isGnoFile(fname) || isTestFile(fname) { + continue + } + + f, err := parser.ParseFile(fset, fname, file.Body, parser.ImportsOnly) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", file.Name, err) + } + + if name != "" && name != f.Name.Name { + return nil, fmt.Errorf("conflict package name between %q and %q", name, f.Name.Name) + } + + for _, imp := range f.Imports { + if len(imp.Path.Value) <= 2 { + continue + } + + val := imp.Path.Value[1 : len(imp.Path.Value)-1] + imports[val] = struct{}{} + } + + name = f.Name.Name + } + + pkgs := []Package{} + for imp := range imports { + subDeps, err := load(imp, fset, resolver, visited, stack) + if err != nil { + return nil, fmt.Errorf("importing %q: %w", imp, err) + } + + pkgs = append(pkgs, subDeps...) + } + pkgs = append(pkgs, *mempkg) + + stack[path] = false + + return pkgs, nil +} diff --git a/contribs/gnodev/pkg/packages/loader_glob.go b/contribs/gnodev/pkg/packages/loader_glob.go new file mode 100644 index 00000000000..dabfe613574 --- /dev/null +++ b/contribs/gnodev/pkg/packages/loader_glob.go @@ -0,0 +1,94 @@ +package packages + +import ( + "fmt" + "go/token" + "io/fs" + "os" + "path/filepath" + "strings" +) + +type GlobLoader struct { + Root string + Resolver Resolver +} + +func NewGlobLoader(rootpath string, res ...Resolver) *GlobLoader { + return &GlobLoader{rootpath, ChainResolvers(res...)} +} + +func (l GlobLoader) Name() string { + return l.Resolver.Name() +} + +func (l GlobLoader) MatchPaths(globs ...string) ([]string, error) { + if l.Root == "" { + return globs, nil + } + + if _, err := os.Stat(l.Root); err != nil { + return nil, fmt.Errorf("unable to stat root: %w", err) + } + + mpaths := []string{} + for _, input := range globs { + cleanInput := filepath.Clean(input) + gpath, err := Parse(cleanInput) + if err != nil { + return nil, fmt.Errorf("invalid glob path %q: %w", input, err) + } + + base := gpath.StarFreeBase() + if base == cleanInput { + mpaths = append(mpaths, base) + continue + } + + // root := filepath.Join(l.Root, base) + root := l.Root + err = filepath.WalkDir(root, func(dirpath string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + relPath, relErr := filepath.Rel(root, dirpath) + if relErr != nil { + return relErr + } + + if !d.IsDir() { + return nil + } + + if strings.HasPrefix(d.Name(), ".") { + return fs.SkipDir + } + + if gpath.Match(relPath) { + mpaths = append(mpaths, relPath) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("walking directory %q: %w", root, err) + } + } + + return mpaths, nil +} + +func (l GlobLoader) Load(gpaths ...string) ([]Package, error) { + paths, err := l.MatchPaths(gpaths...) + if err != nil { + return nil, fmt.Errorf("match glob pattern error: %w", err) + } + + loader := &BaseLoader{Resolver: l.Resolver} + return loader.Load(paths...) +} + +func (l GlobLoader) Resolve(path string) (*Package, error) { + return l.Resolver.Resolve(token.NewFileSet(), path) +} diff --git a/contribs/gnodev/pkg/packages/loader_test.go b/contribs/gnodev/pkg/packages/loader_test.go new file mode 100644 index 00000000000..1fa338587b0 --- /dev/null +++ b/contribs/gnodev/pkg/packages/loader_test.go @@ -0,0 +1,83 @@ +package packages + +import ( + "testing" + + "github.com/gnolang/gno/gnovm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoader_LoadWithDeps(t *testing.T) { + t.Parallel() + + fsresolver := NewRootResolver("./testdata") + loader := NewLoader(fsresolver) + + // package c depend on package b + pkgs, err := loader.Load(TestdataPkgC) + require.NoError(t, err) + require.Len(t, pkgs, 3) + for i, path := range []string{TestdataPkgA, TestdataPkgB, TestdataPkgC} { + assert.Equal(t, path, pkgs[i].Path) + } +} + +func TestLoader_ResolverPriority(t *testing.T) { + t.Parallel() + + const commonPath = "abc.yz/pkg/a" + + pkgA := gnovm.MemPackage{Name: "pkga", Path: commonPath} + resolverA := NewMockResolver(&pkgA) + + pkgB := gnovm.MemPackage{Name: "pkgb", Path: commonPath} + resolverB := NewMockResolver(&pkgB) + + t.Run("pkgA then pkgB", func(t *testing.T) { + t.Parallel() + + loader := NewLoader(resolverA, resolverB) + pkg, err := loader.Resolve(commonPath) + require.NoError(t, err) + require.Equal(t, pkgA.Name, pkg.Name) + require.Equal(t, commonPath, pkg.Path) + }) + + t.Run("pkgB then pkgA", func(t *testing.T) { + t.Parallel() + + loader := NewLoader(resolverB, resolverA) + pkg, err := loader.Resolve(commonPath) + require.NoError(t, err) + require.Equal(t, pkgB.Name, pkg.Name) + require.Equal(t, commonPath, pkg.Path) + }) +} + +func TestLoader_Glob(t *testing.T) { + const root = "./testdata" + cases := []struct { + GlobPath string + PkgResults []string + }{ + {"abc.xy/pkg/*", []string{TestdataPkgA, TestdataPkgB, TestdataPkgC}}, + {"abc.xy/nested/*", []string{TestdataNestedA}}, + {"abc.xy/**/cc", []string{TestdataNestedC, TestdataPkgA, TestdataPkgB, TestdataPkgC}}, + {"abc.xy/*/aa", []string{TestdataNestedA, TestdataPkgA}}, + } + + fsresolver := NewRootResolver("./testdata") + globloader := NewGlobLoader("./testdata", fsresolver) + + for _, tc := range cases { + t.Run(tc.GlobPath, func(t *testing.T) { + pkgs, err := globloader.Load(tc.GlobPath) + require.NoError(t, err) + require.Len(t, pkgs, len(tc.PkgResults)) + for i, expected := range tc.PkgResults { + assert.Equal(t, expected, pkgs[i].Path) + } + }) + } +} diff --git a/contribs/gnodev/pkg/packages/package.go b/contribs/gnodev/pkg/packages/package.go new file mode 100644 index 00000000000..d6aa532ce64 --- /dev/null +++ b/contribs/gnodev/pkg/packages/package.go @@ -0,0 +1,102 @@ +package packages + +import ( + "fmt" + "go/parser" + "go/token" + "os" + "path/filepath" + + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" +) + +type PackageKind int + +const ( + PackageKindOther = iota + PackageKindRemote = iota + PackageKindFS +) + +type Package struct { + gnovm.MemPackage + Kind PackageKind + Location string +} + +func ReadPackageFromDir(fset *token.FileSet, path, dir string) (*Package, error) { + modpath := filepath.Join(dir, "gno.mod") + if _, err := os.Stat(modpath); err == nil { + draft, err := isDraftFile(modpath) + if err != nil { + return nil, err + } + + // Skip draft package + // XXX: We could potentially do that in a middleware, but doing this + // here avoid to potentially parse broken files + if draft { + return nil, ErrResolverPackageSkip + } + } + + mempkg, err := gnolang.ReadMemPackage(dir, path) + switch { + case err == nil: // ok + case os.IsNotExist(err): + return nil, ErrResolverPackageNotFound + default: + return nil, fmt.Errorf("unable to read package %q: %w", dir, err) + } + + if err := validateMemPackage(fset, mempkg); err != nil { + return nil, err + } + + return &Package{ + MemPackage: *mempkg, + Location: dir, + Kind: PackageKindFS, + }, nil +} + +func validateMemPackage(fset *token.FileSet, mempkg *gnovm.MemPackage) error { + if mempkg.IsEmpty() { + return fmt.Errorf("empty package: %w", ErrResolverPackageSkip) + } + + // Validate package name + for _, file := range mempkg.Files { + if !isGnoFile(file.Name) || isTestFile(file.Name) { + continue + } + + f, err := parser.ParseFile(fset, file.Name, file.Body, parser.PackageClauseOnly) + if err != nil { + return fmt.Errorf("unable to parse file %q: %w", file.Name, err) + } + + if f.Name.Name != mempkg.Name { + return fmt.Errorf("%q package name conflict, expected %q found %q", + mempkg.Path, mempkg.Name, f.Name.Name) + } + } + + return nil +} + +func isDraftFile(modpath string) (bool, error) { + modfile, err := os.ReadFile(modpath) + if err != nil { + return false, fmt.Errorf("unable to read file %q: %w", modpath, err) + } + + mod, err := gnomod.Parse(modpath, modfile) + if err != nil { + return false, fmt.Errorf("unable to parse `gno.mod`: %w", err) + } + + return mod.Draft, nil +} diff --git a/contribs/gnodev/pkg/packages/resolver.go b/contribs/gnodev/pkg/packages/resolver.go new file mode 100644 index 00000000000..9ed9269b6d8 --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver.go @@ -0,0 +1,234 @@ +package packages + +import ( + "errors" + "fmt" + "go/parser" + "go/scanner" + "go/token" + "log/slog" + "strings" + "time" +) + +var ( + ErrResolverPackageNotFound = errors.New("package not found") + ErrResolverPackageSkip = errors.New("package has been skip") +) + +type Resolver interface { + Name() string + Resolve(fset *token.FileSet, path string) (*Package, error) +} + +type NoopResolver struct{} + +func (NoopResolver) Name() string { return "" } +func (NoopResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + return nil, ErrResolverPackageNotFound +} + +// Chain Resolver + +type ChainedResolver []Resolver + +func ChainResolvers(rs ...Resolver) Resolver { + switch len(rs) { + case 0: + return &NoopResolver{} + case 1: + return rs[0] + default: + return ChainedResolver(rs) + } +} + +func (cr ChainedResolver) Name() string { + names := make([]string, 0, len(cr)) + for _, r := range cr { + rname := r.Name() + if rname == "" { + continue + } + + names = append(names, rname) + } + + return strings.Join(names, "/") +} + +func (cr ChainedResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + for _, resolver := range cr { + pkg, err := resolver.Resolve(fset, path) + if err == nil { + return pkg, nil + } else if errors.Is(err, ErrResolverPackageNotFound) { + continue + } + + return nil, fmt.Errorf("resolver %q error: %w", resolver.Name(), err) + } + + return nil, ErrResolverPackageNotFound +} + +type MiddlewareHandler func(fset *token.FileSet, path string, next Resolver) (*Package, error) + +type middlewareResolver struct { + Handler MiddlewareHandler + Next Resolver +} + +func MiddlewareResolver(r Resolver, handlers ...MiddlewareHandler) Resolver { + // Start with the final resolver + start := r + + // Wrap each handler around the previous one + for _, handler := range handlers { + start = &middlewareResolver{ + Next: start, + Handler: handler, + } + } + + return start +} + +func (r middlewareResolver) Name() string { + return r.Next.Name() +} + +func (r *middlewareResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + if r.Handler != nil { + return r.Handler(fset, path, r.Next) + } + + return r.Next.Resolve(fset, path) +} + +// LogMiddleware creates a logging middleware handler. +func LogMiddleware(logger *slog.Logger) MiddlewareHandler { + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + start := time.Now() + pkg, err := next.Resolve(fset, path) + switch { + case err == nil: + logger.Debug("path resolved", + "resolver", next.Name(), + "path", path, + "name", pkg.Name, + "took", time.Since(start).String(), + "location", pkg.Location, + ) + case errors.Is(err, ErrResolverPackageSkip): + logger.Debug(err.Error(), + "resolver", next.Name(), + "path", path, + "took", time.Since(start).String(), + ) + + case errors.Is(err, ErrResolverPackageNotFound): + logger.Warn(err.Error(), + "resolver", next.Name(), + "path", path, + "took", time.Since(start).String()) + + default: + logger.Error(err.Error(), + "resolver", next.Name(), + "path", path, + "took", time.Since(start).String()) + } + + return pkg, err + } +} + +type ShouldCacheFunc func(pkg *Package) bool + +func CacheAll(_ *Package) bool { return true } + +// CacheMiddleware creates a caching middleware handler. +func CacheMiddleware(shouldCache ShouldCacheFunc) MiddlewareHandler { + cacheMap := make(map[string]*Package) + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + if pkg, ok := cacheMap[path]; ok { + return pkg, nil + } + + pkg, err := next.Resolve(fset, path) + if pkg != nil && shouldCache(pkg) { + cacheMap[path] = pkg + } + + return pkg, err + } +} + +// FilterPathHandler defines the function signature for filter handlers. +type FilterPathHandler func(path string) bool + +func FilterPathMiddleware(name string, filter FilterPathHandler) MiddlewareHandler { + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + if filter(path) { + return nil, fmt.Errorf("filter %q: %w", name, ErrResolverPackageSkip) + } + + return next.Resolve(fset, path) + } +} + +var FilterStdlibs = FilterPathMiddleware("stdlibs", isStdPath) + +func isStdPath(path string) bool { + if i := strings.IndexRune(path, '/'); i > 0 { + if j := strings.IndexRune(path[:i], '.'); j >= 0 { + return false + } + } + + return true +} + +// PackageCheckerMiddleware creates a middleware handler for post-processing syntax. +func PackageCheckerMiddleware(logger *slog.Logger) MiddlewareHandler { + return func(fset *token.FileSet, path string, next Resolver) (*Package, error) { + // First, resolve the package using the next resolver in the chain. + pkg, err := next.Resolve(fset, path) + if err != nil { + return nil, err + } + + if err := pkg.Validate(); err != nil { + return nil, fmt.Errorf("invalid package %q: %w", path, err) + } + + // Post-process each file in the package. + for _, file := range pkg.Files { + fname := file.Name + if !isGnoFile(fname) { + continue + } + + logger.Debug("checking syntax", "path", path, "filename", fname) + _, err := parser.ParseFile(fset, file.Name, file.Body, parser.AllErrors) + if err == nil { + continue + } + + if el, ok := err.(scanner.ErrorList); ok { + for _, e := range el { + logger.Error("syntax error", + "path", path, + "filename", fname, + "err", e.Error(), + ) + } + } + + return nil, fmt.Errorf("file %q have error(s)", file.Name) + } + + return pkg, nil + } +} diff --git a/contribs/gnodev/pkg/packages/resolver_local.go b/contribs/gnodev/pkg/packages/resolver_local.go new file mode 100644 index 00000000000..13448aca52d --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_local.go @@ -0,0 +1,39 @@ +package packages + +import ( + "fmt" + "go/token" + "path/filepath" + "strings" +) + +type LocalResolver struct { + Path string + Dir string +} + +func NewLocalResolver(path, dir string) *LocalResolver { + return &LocalResolver{ + Path: path, + Dir: dir, + } +} + +func (r *LocalResolver) Name() string { + return fmt.Sprintf("local<%s>", filepath.Base(r.Dir)) +} + +func (r LocalResolver) IsValid() bool { + pkg, err := r.Resolve(token.NewFileSet(), r.Path) + return err == nil && pkg != nil +} + +func (r LocalResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + after, found := strings.CutPrefix(path, r.Path) + if !found { + return nil, ErrResolverPackageNotFound + } + + dir := filepath.Join(r.Dir, after) + return ReadPackageFromDir(fset, path, dir) +} diff --git a/contribs/gnodev/pkg/packages/resolver_mock.go b/contribs/gnodev/pkg/packages/resolver_mock.go new file mode 100644 index 00000000000..f6a09af8883 --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_mock.go @@ -0,0 +1,40 @@ +package packages + +import ( + "go/token" + + "github.com/gnolang/gno/gnovm" +) + +type MockResolver struct { + pkgs map[string]*gnovm.MemPackage + resolveCalls map[string]int // Track resolve calls per path +} + +func NewMockResolver(pkgs ...*gnovm.MemPackage) *MockResolver { + mappkgs := make(map[string]*gnovm.MemPackage, len(pkgs)) + for _, pkg := range pkgs { + mappkgs[pkg.Path] = pkg + } + return &MockResolver{ + pkgs: mappkgs, + resolveCalls: make(map[string]int), + } +} + +func (m *MockResolver) ResolveCalls(fset *token.FileSet, path string) int { + count, _ := m.resolveCalls[path] + return count +} + +func (m *MockResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + m.resolveCalls[path]++ // Increment call count + if mempkg, ok := m.pkgs[path]; ok { + return &Package{MemPackage: *mempkg}, nil + } + return nil, ErrResolverPackageNotFound +} + +func (m *MockResolver) Name() string { + return "mock" +} diff --git a/contribs/gnodev/pkg/packages/resolver_remote.go b/contribs/gnodev/pkg/packages/resolver_remote.go new file mode 100644 index 00000000000..94396f70c83 --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_remote.go @@ -0,0 +1,94 @@ +package packages + +import ( + "bytes" + "errors" + "fmt" + "go/parser" + "go/token" + "path/filepath" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" +) + +type remoteResolver struct { + *client.RPCClient + name string + fset *token.FileSet +} + +func NewRemoteResolver(name string, cl *client.RPCClient) Resolver { + return &remoteResolver{ + RPCClient: cl, + name: name, + fset: token.NewFileSet(), + } +} + +func (res *remoteResolver) Name() string { + return fmt.Sprintf("remote<%s>", res.name) +} + +func (res *remoteResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + const qpath = "vm/qfile" + + // First query files + data := []byte(path) + qres, err := res.RPCClient.ABCIQuery(qpath, data) + if err != nil { + return nil, fmt.Errorf("client unable to query: %w", err) + } + + if err := qres.Response.Error; err != nil { + if errors.Is(err, vm.InvalidPkgPathError{}) || + strings.HasSuffix(err.Error(), "is not available") { // XXX: find a better to check this + return nil, ErrResolverPackageNotFound + } + + return nil, fmt.Errorf("querying %q error: %w", path, err) + } + + var name string + memFiles := []*gnovm.MemFile{} + files := bytes.Split(qres.Response.Data, []byte{'\n'}) + for _, filename := range files { + fname := string(filename) + fpath := filepath.Join(path, fname) + qres, err := res.RPCClient.ABCIQuery(qpath, []byte(fpath)) + if err != nil { + return nil, fmt.Errorf("unable to query path") + } + + if err := qres.Response.Error; err != nil { + return nil, fmt.Errorf("unable to query file %q on path %q: %w", fname, path, err) + } + body := qres.Response.Data + + // Check package name + if name == "" && isGnoFile(fname) && !isTestFile(fname) { + // Check package name + f, err := parser.ParseFile(fset, fname, body, parser.PackageClauseOnly) + if err != nil { + return nil, fmt.Errorf("unable to parse file %q: %w", fname, err) + } + name = f.Name.Name + } + + memFiles = append(memFiles, &gnovm.MemFile{ + Name: fname, Body: string(body), + }) + } + + return &Package{ + MemPackage: gnovm.MemPackage{ + Name: name, + Path: path, + Files: memFiles, + }, + Kind: PackageKindRemote, + Location: path, + }, nil +} diff --git a/contribs/gnodev/pkg/packages/resolver_remote_test.go b/contribs/gnodev/pkg/packages/resolver_remote_test.go new file mode 100644 index 00000000000..69347c0ad4d --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_remote_test.go @@ -0,0 +1 @@ +package packages diff --git a/contribs/gnodev/pkg/packages/resolver_root.go b/contribs/gnodev/pkg/packages/resolver_root.go new file mode 100644 index 00000000000..ae6a9d416ea --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_root.go @@ -0,0 +1,30 @@ +package packages + +import ( + "fmt" + "go/token" + "os" + "path/filepath" +) + +type rootResolver struct { + root string // Root folder +} + +func NewRootResolver(path string) Resolver { + return &rootResolver{root: path} +} + +func (r *rootResolver) Name() string { + return fmt.Sprintf("root<%s>", filepath.Base(r.root)) +} + +func (r *rootResolver) Resolve(fset *token.FileSet, path string) (*Package, error) { + dir := filepath.Join(r.root, path) + _, err := os.Stat(dir) + if err != nil { + return nil, ErrResolverPackageNotFound + } + + return ReadPackageFromDir(fset, path, dir) +} diff --git a/contribs/gnodev/pkg/packages/resolver_test.go b/contribs/gnodev/pkg/packages/resolver_test.go new file mode 100644 index 00000000000..9341bb80d7b --- /dev/null +++ b/contribs/gnodev/pkg/packages/resolver_test.go @@ -0,0 +1,290 @@ +package packages + +import ( + "bytes" + "errors" + "go/token" + "log/slog" + "path/filepath" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLogMiddleware(t *testing.T) { + t.Parallel() + + mockResolver := NewMockResolver(&gnovm.MemPackage{ + Path: "abc.xy/test/pkg", + Name: "pkg", + Files: []*gnovm.MemFile{ + {Name: "file.gno", Body: "package pkg"}, + }, + }) + + t.Run("logs package not found", func(t *testing.T) { + t.Parallel() + + var buff bytes.Buffer + + logger := slog.New(slog.NewTextHandler(&buff, &slog.HandlerOptions{})) + middleware := LogMiddleware(logger) + + resolver := MiddlewareResolver(mockResolver, middleware) + pkg, err := resolver.Resolve(token.NewFileSet(), "abc.xy/invalid/pkg") + require.Error(t, err) + require.Nil(t, pkg) + assert.Contains(t, buff.String(), "package not found") + }) + + t.Run("logs package resolution", func(t *testing.T) { + t.Parallel() + + var buff bytes.Buffer + logger := slog.New(slog.NewTextHandler(&buff, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + middleware := LogMiddleware(logger) + + resolver := MiddlewareResolver(mockResolver, middleware) + pkg, err := resolver.Resolve(token.NewFileSet(), "abc.xy/test/pkg") + require.NoError(t, err) + require.NotNil(t, pkg) + assert.Contains(t, buff.String(), "path resolved") + }) +} + +func TestCacheMiddleware(t *testing.T) { + t.Parallel() + + pkg := &gnovm.MemPackage{Path: "abc.xy/cached/pkg", Name: "pkg"} + t.Run("caches resolved packages", func(t *testing.T) { + t.Parallel() + + mockResolver := NewMockResolver(pkg) + cacheMiddleware := CacheMiddleware(CacheAll) + cachedResolver := MiddlewareResolver(mockResolver, cacheMiddleware) + + // First call + pkg1, err := cachedResolver.Resolve(token.NewFileSet(), pkg.Path) + require.NoError(t, err) + require.Equal(t, 1, mockResolver.resolveCalls[pkg.Path]) + + // Second call + pkg2, err := cachedResolver.Resolve(token.NewFileSet(), pkg.Path) + require.NoError(t, err) + require.Same(t, pkg1, pkg2) + require.Equal(t, 1, mockResolver.resolveCalls[pkg.Path]) + }) + + t.Run("no cache when shouldCache is false", func(t *testing.T) { + t.Parallel() + + mockResolver := NewMockResolver(pkg) + cacheMiddleware := CacheMiddleware(func(*Package) bool { return false }) + cachedResolver := MiddlewareResolver(mockResolver, cacheMiddleware) + + pkg1, err := cachedResolver.Resolve(token.NewFileSet(), pkg.Path) + require.NoError(t, err) + pkg2, err := cachedResolver.Resolve(token.NewFileSet(), pkg.Path) + require.NoError(t, err) + require.NotSame(t, pkg1, pkg2) + require.Equal(t, 2, mockResolver.resolveCalls[pkg.Path]) + }) +} + +func TestFilterStdlibsMiddleware(t *testing.T) { + t.Parallel() + + middleware := FilterStdlibs + mockResolver := NewMockResolver(&gnovm.MemPackage{ + Path: "abc.xy/pkg", + Name: "pkg", + Files: []*gnovm.MemFile{ + {Name: "file.gno", Body: "package pkg"}, + }, + }) + filteredResolver := MiddlewareResolver(mockResolver, middleware) + + t.Run("filters stdlib paths", func(t *testing.T) { + t.Parallel() + + _, err := filteredResolver.Resolve(token.NewFileSet(), "fmt") + require.Error(t, err) + require.True(t, errors.Is(err, ErrResolverPackageSkip)) + require.Equal(t, 0, mockResolver.resolveCalls["fmt"]) + }) + + t.Run("allows non-stdlib paths", func(t *testing.T) { + t.Parallel() + + pkg, err := filteredResolver.Resolve(token.NewFileSet(), "abc.xy/pkg") + require.NoError(t, err) + require.NotNil(t, pkg) + require.Equal(t, 1, mockResolver.resolveCalls["abc.xy/pkg"]) + }) +} + +func TestPackageCheckerMiddleware(t *testing.T) { + t.Parallel() + + logger := log.NewTestingLogger(t) + t.Run("valid package syntax", func(t *testing.T) { + t.Parallel() + + validPkg := &gnovm.MemPackage{ + Path: "abc.xy/r/valid/pkg", + Name: "valid", + Files: []*gnovm.MemFile{ + {Name: "valid.gno", Body: "package valid; func Foo() {}"}, + }, + } + mockResolver := NewMockResolver(validPkg) + middleware := PackageCheckerMiddleware(logger) + resolver := MiddlewareResolver(mockResolver, middleware) + + pkg, err := resolver.Resolve(token.NewFileSet(), validPkg.Path) + require.NoError(t, err) + require.NotNil(t, pkg) + }) + + t.Run("invalid package syntax", func(t *testing.T) { + t.Parallel() + + invalidPkg := &gnovm.MemPackage{ + Path: "abc.xy/r/invalid/pkg", + Name: "invalid", + Files: []*gnovm.MemFile{ + {Name: "invalid.gno", Body: "package invalid\nfunc Foo() {"}, + }, + } + mockResolver := NewMockResolver(invalidPkg) + middleware := PackageCheckerMiddleware(logger) + resolver := MiddlewareResolver(mockResolver, middleware) + + _, err := resolver.Resolve(token.NewFileSet(), invalidPkg.Path) + require.Error(t, err) + require.Contains(t, err.Error(), `file "invalid.gno" have error(s)`) + }) + + t.Run("ignores non-gno files", func(t *testing.T) { + t.Parallel() + + nonGnoPkg := &gnovm.MemPackage{ + Path: "abc.xy/r/non/gno/pkg", + Name: "pkg", + Files: []*gnovm.MemFile{ + {Name: "README.md", Body: "# Documentation"}, + }, + } + mockResolver := NewMockResolver(nonGnoPkg) + middleware := PackageCheckerMiddleware(logger) + resolver := MiddlewareResolver(mockResolver, middleware) + + _, err := resolver.Resolve(token.NewFileSet(), nonGnoPkg.Path) + require.NoError(t, err) + }) +} + +func TestResolverLocal_Resolve(t *testing.T) { + t.Parallel() + + const anotherPath = "abc.xy/another/path" + localResolver := NewLocalResolver(anotherPath, filepath.Join("./testdata", TestdataPkgA)) + + t.Run("valid package", func(t *testing.T) { + t.Parallel() + + pkg, err := localResolver.Resolve(token.NewFileSet(), anotherPath) + require.NoError(t, err) + require.NotNil(t, pkg) + require.Equal(t, pkg.Name, "aa") + }) + + t.Run("invalid package", func(t *testing.T) { + t.Parallel() + + pkg, err := localResolver.Resolve(token.NewFileSet(), "abc.xy/wrong/package") + require.Nil(t, pkg) + require.Error(t, err) + require.ErrorIs(t, err, ErrResolverPackageNotFound) + }) +} + +func TestResolver_ResolveRemote(t *testing.T) { + const targetPath = "gno.land/r/target/path" + + mempkg := gnovm.MemPackage{ + Name: "foo", + Path: targetPath, + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foo; func Render(_ string) string { return "bar" }`, + }, + {Name: "gno.mod", Body: `module ` + targetPath}, + }, + } + + rootdir := gnoenv.RootDir() + cfg := integration.TestingMinimalNodeConfig(rootdir) + logger := log.NewTestingLogger(t) + + // Setup genesis state + privKey := secp256k1.GenPrivKey() + cfg.Genesis.AppState = integration.GenerateTestingGenesisState(privKey, mempkg) + + _, address := integration.TestingInMemoryNode(t, logger, cfg) + cl, err := client.NewHTTPClient(address) + require.NoError(t, err) + + remoteResolver := NewRemoteResolver(address, cl) + t.Run("valid package", func(t *testing.T) { + pkg, err := remoteResolver.Resolve(token.NewFileSet(), mempkg.Path) + require.NoError(t, err) + require.NotNil(t, pkg) + assert.Equal(t, mempkg, pkg.MemPackage) + }) + + t.Run("invalid package", func(t *testing.T) { + pkg, err := remoteResolver.Resolve(token.NewFileSet(), "gno.land/r/not/a/valid/package") + require.Nil(t, pkg) + require.Error(t, err) + require.ErrorIs(t, err, ErrResolverPackageNotFound) + }) +} + +func TestResolverRoot_Resolve(t *testing.T) { + t.Parallel() + + fsResolver := NewRootResolver("./testdata") + t.Run("valid packages", func(t *testing.T) { + t.Parallel() + + for _, tpkg := range []string{TestdataPkgA, TestdataPkgB, TestdataPkgC} { + t.Run(tpkg, func(t *testing.T) { + pkg, err := fsResolver.Resolve(token.NewFileSet(), tpkg) + require.NoError(t, err) + require.NotNil(t, pkg) + require.Equal(t, tpkg, pkg.Path) + require.Equal(t, filepath.Base(tpkg), pkg.Name) + }) + } + }) + + t.Run("invalid packages", func(t *testing.T) { + t.Parallel() + + pkg, err := fsResolver.Resolve(token.NewFileSet(), "abc.xy/wrong/package") + require.Nil(t, pkg) + require.Error(t, err) + require.ErrorIs(t, err, ErrResolverPackageNotFound) + }) +} diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/file.gno new file mode 100644 index 00000000000..14492ef76f3 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/file.gno @@ -0,0 +1 @@ +package aa diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/gno.mod new file mode 100644 index 00000000000..071e676d43e --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/aa/gno.mod @@ -0,0 +1 @@ +module abc.xy/nested/aa diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/file.gno new file mode 100644 index 00000000000..592f1946da0 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/file.gno @@ -0,0 +1 @@ +package bb diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/gno.mod new file mode 100644 index 00000000000..2e0f55a7954 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/bb/gno.mod @@ -0,0 +1 @@ +module abc.xy/nested/nested/bb \ No newline at end of file diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/file.gno new file mode 100644 index 00000000000..10702f6990c --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/file.gno @@ -0,0 +1 @@ +package cc diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/gno.mod new file mode 100644 index 00000000000..0932deb1366 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/nested/nested/cc/gno.mod @@ -0,0 +1 @@ +module abc.xy/nested/nested/cc \ No newline at end of file diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/file.gno new file mode 100644 index 00000000000..b809785a376 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/file.gno @@ -0,0 +1,3 @@ +package aa + +type SA struct{} diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/gno.mod new file mode 100644 index 00000000000..02d58054ca6 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/aa/gno.mod @@ -0,0 +1 @@ +module abc.xy/pkg/aa \ No newline at end of file diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/file.gno new file mode 100644 index 00000000000..5cca9ec3c21 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/file.gno @@ -0,0 +1,5 @@ +package bb + +import "abc.xy/pkg/aa" + +type SB = aa.SA diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/gno.mod new file mode 100644 index 00000000000..b5d760d6f75 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/bb/gno.mod @@ -0,0 +1 @@ +module abc.xy/pkg/bb \ No newline at end of file diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/file.gno b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/file.gno new file mode 100644 index 00000000000..21819a7b686 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/file.gno @@ -0,0 +1,5 @@ +package cc + +import "abc.xy/pkg/bb" + +type SC = bb.SB diff --git a/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/gno.mod b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/gno.mod new file mode 100644 index 00000000000..bc993583fd3 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata/abc.xy/pkg/cc/gno.mod @@ -0,0 +1 @@ +module abc.xy/pkg/cc \ No newline at end of file diff --git a/contribs/gnodev/pkg/packages/testdata_test.go b/contribs/gnodev/pkg/packages/testdata_test.go new file mode 100644 index 00000000000..5c9a8b45cd5 --- /dev/null +++ b/contribs/gnodev/pkg/packages/testdata_test.go @@ -0,0 +1,44 @@ +// This test file serves as a reference for the testdata directory tree. + +package packages + +// The structure of the testdata directory is as follows: +// +// testdata +// ├── abc.xy +// ├── nested +// │ ├── aa +// │ │ └── gno.mod +// │ └── nested +// │ ├── bb +// │ │ └── gno.mod +// │ └── cc +// │ └── gno.mod +// └── pkg +// ├── aa +// │ ├── file1.gno +// │ └── gno.mod +// ├── bb // depends on aa +// │ ├── file1.gno +// │ └── gno.mod +// └── cc // depends on bb +// ├── file1.gno +// └── gno.mod + +const ( + TestdataPkgA = "abc.xy/pkg/aa" + TestdataPkgB = "abc.xy/pkg/bb" + TestdataPkgC = "abc.xy/pkg/cc" +) + +// List of testdata package paths +var testdataPkgs = []string{TestdataPkgA, TestdataPkgB, TestdataPkgC} + +const ( + TestdataNestedA = "abc.xy/nested/aa" // Path to nested package A + TestdataNestedB = "abc.xy/nested/nested/bb" // Path to nested package B + TestdataNestedC = "abc.xy/nested/nested/cc" // Path to nested package C +) + +// List of nested package paths +var testdataNested = []string{TestdataNestedA, TestdataNestedB, TestdataNestedC} diff --git a/contribs/gnodev/pkg/packages/utils.go b/contribs/gnodev/pkg/packages/utils.go new file mode 100644 index 00000000000..93160a3a1a5 --- /dev/null +++ b/contribs/gnodev/pkg/packages/utils.go @@ -0,0 +1,14 @@ +package packages + +import ( + "path/filepath" + "strings" +) + +func isGnoFile(name string) bool { + return filepath.Ext(name) == ".gno" && !strings.HasPrefix(name, ".") +} + +func isTestFile(name string) bool { + return strings.HasSuffix(name, "_filetest.gno") || strings.HasSuffix(name, "_test.gno") +} diff --git a/contribs/gnodev/pkg/proxy/path_interceptor.go b/contribs/gnodev/pkg/proxy/path_interceptor.go new file mode 100644 index 00000000000..84d2f92b22f --- /dev/null +++ b/contribs/gnodev/pkg/proxy/path_interceptor.go @@ -0,0 +1,330 @@ +package proxy + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "path/filepath" + "strings" + "sync" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/tm2/pkg/amino" + rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type PathHandler func(path ...string) + +type PathInterceptor struct { + proxyAddr, targetAddr net.Addr + + logger *slog.Logger + listener net.Listener + handlers []PathHandler + muHandlers sync.RWMutex +} + +// NewPathInterceptor creates a new path proxy interceptor. +func NewPathInterceptor(logger *slog.Logger, target net.Addr) (*PathInterceptor, error) { + // Create a listener on the target address + proxyListener, err := net.Listen(target.Network(), target.String()) + if err != nil { + return nil, fmt.Errorf("failed to listen on %s://%s", target.Network(), target.String()) + } + + // Find on a new random port for the target + targetListener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, fmt.Errorf("failed to listen on tcp://127.0.0.1:0") + } + proxyAddr := targetListener.Addr() + // Immediately close this listener after proxy initialization + defer targetListener.Close() + + proxy := &PathInterceptor{ + listener: proxyListener, + logger: logger, + targetAddr: target, + proxyAddr: proxyAddr, + } + + go proxy.handleConnections() + + return proxy, nil +} + +// HandlePath adds a new path handler to the interceptor. +func (proxy *PathInterceptor) HandlePath(fn PathHandler) { + proxy.muHandlers.Lock() + defer proxy.muHandlers.Unlock() + proxy.handlers = append(proxy.handlers, fn) +} + +// ProxyAddress returns the network address of the proxy. +func (proxy *PathInterceptor) ProxyAddress() string { + return fmt.Sprintf("%s://%s", proxy.proxyAddr.Network(), proxy.proxyAddr.String()) +} + +// TargetAddress returns the network address of the target. +func (proxy *PathInterceptor) TargetAddress() string { + return fmt.Sprintf("%s://%s", proxy.targetAddr.Network(), proxy.targetAddr.String()) +} + +// handleConnections manages incoming connections to the proxy. +func (proxy *PathInterceptor) handleConnections() { + defer proxy.listener.Close() + + for { + conn, err := proxy.listener.Accept() + if err != nil { + if !errors.Is(err, net.ErrClosed) { + proxy.logger.Debug("failed to accept connection", "error", err) + } + + return + } + + proxy.logger.Debug("new connection", "remote", conn.RemoteAddr()) + go proxy.handleConnection(conn) + } +} + +// handleConnection processes a single connection between client and target. +func (proxy *PathInterceptor) handleConnection(inConn net.Conn) { + logger := proxy.logger.With(slog.String("in", inConn.RemoteAddr().String())) + + // Establish a connection to the target + outConn, err := net.Dial(proxy.proxyAddr.Network(), proxy.proxyAddr.String()) + if err != nil { + logger.Error("target connection failed", "target", proxy.proxyAddr.String(), "error", err) + inConn.Close() + return + } + logger = logger.With(slog.String("out", outConn.RemoteAddr().String())) + + // Coordinate connection closure + var closeOnce sync.Once + closeConnections := func() { + inConn.Close() + outConn.Close() + } + + // Setup bidirectional copying + var wg sync.WaitGroup + wg.Add(2) + + // Response path (target -> client) + go func() { + defer wg.Done() + defer closeOnce.Do(closeConnections) + + _, err := io.Copy(inConn, outConn) + if err == nil || errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { + return // Connection has been closed + } + + logger.Debug("response copy error", "error", err) + }() + + // Request path (client -> target) + go func() { + defer wg.Done() + defer closeOnce.Do(closeConnections) + + var buffer bytes.Buffer + tee := io.TeeReader(inConn, &buffer) + reader := bufio.NewReader(tee) + + // Process HTTP requests + if err := proxy.processHTTPRequests(reader, &buffer, outConn); err != nil { + if errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { + return // Connection has been closed + } + + if _, isNetError := err.(net.Error); isNetError { + logger.Debug("request processing error", "error", err) + return + } + + // Continue processing the connection if not a network error + } + + // Forward remaining data after HTTP processing + if buffer.Len() > 0 { + if _, err := outConn.Write(buffer.Bytes()); err != nil { + logger.Debug("buffer flush failed", "error", err) + } + } + + // Directly pipe remaining traffic + if _, err := io.Copy(outConn, inConn); err != nil && !errors.Is(err, net.ErrClosed) { + logger.Debug("raw copy failed", "error", err) + } + }() + + wg.Wait() + logger.Debug("connection closed") +} + +// processHTTPRequests handles the HTTP request/response cycle. +func (proxy *PathInterceptor) processHTTPRequests(reader *bufio.Reader, buffer *bytes.Buffer, outConn net.Conn) error { + for { + request, err := http.ReadRequest(reader) + if err != nil { + return fmt.Errorf("read request failed: %w", err) + } + + // Check for websocket upgrade + if isWebSocket(request) { + return errors.New("websocket upgrade requested") + } + + // Read and process the request body + body, err := io.ReadAll(request.Body) + request.Body.Close() + if err != nil { + return fmt.Errorf("body read failed: %w", err) + } + + if err := proxy.handleRequest(body); err != nil { + proxy.logger.Debug("request handler warning", "error", err) + } + + // Forward the original request bytes + if _, err := outConn.Write(buffer.Bytes()); err != nil { + return fmt.Errorf("request forward failed: %w", err) + } + + buffer.Reset() // Prepare for the next request + } +} + +func isWebSocket(req *http.Request) bool { + return strings.EqualFold(req.Header.Get("Upgrade"), "websocket") +} + +type uniqPaths map[string]struct{} + +func (upaths uniqPaths) list() []string { + paths := make([]string, 0, len(upaths)) + for p := range upaths { + paths = append(paths, p) + } + return paths +} + +func (upaths uniqPaths) add(path string) { upaths[path] = struct{}{} } + +// handleRequest parses and processes the RPC request body. +func (proxy *PathInterceptor) handleRequest(body []byte) error { + ps := make(uniqPaths) + if err := parseRPCRequest(body, ps); err != nil { + return fmt.Errorf("unable to parse RPC request: %w", err) + } + + paths := ps.list() + if len(paths) == 0 { + return nil + } + + proxy.logger.Debug("parsed request paths", "paths", paths) + + proxy.muHandlers.RLock() + defer proxy.muHandlers.RUnlock() + + for _, handle := range proxy.handlers { + handle(paths...) + } + + return nil +} + +// Close closes the proxy listener. +func (proxy *PathInterceptor) Close() error { + return proxy.listener.Close() +} + +// parseRPCRequest unmarshals and processes RPC requests, returning paths. +func parseRPCRequest(body []byte, upaths uniqPaths) error { + var req rpctypes.RPCRequest + if err := json.Unmarshal(body, &req); err != nil { + return fmt.Errorf("unable to unmarshal RPC request: %w", err) + } + + switch req.Method { + case "abci_query": + var squery struct { + Path string `json:"path"` + Data []byte `json:"data,omitempty"` + } + if err := json.Unmarshal(req.Params, &squery); err != nil { + return fmt.Errorf("unable to unmarshal params: %w", err) + } + + return handleQuery(squery.Path, squery.Data, upaths) + + case "broadcast_tx_commit": + var stx struct { + Tx []byte `json:"tx"` + } + if err := json.Unmarshal(req.Params, &stx); err != nil { + return fmt.Errorf("unable to unmarshal params: %w", err) + } + + return handleTx(stx.Tx, upaths) + } + + return fmt.Errorf("unhandled method: %q", req.Method) +} + +// handleTx processes the transaction and returns relevant paths. +func handleTx(bz []byte, upaths uniqPaths) error { + var tx std.Tx + if err := amino.Unmarshal(bz, &tx); err != nil { + return fmt.Errorf("unable to unmarshal tx: %w", err) + } + + for _, msg := range tx.Msgs { + switch msg := msg.(type) { + case vm.MsgAddPackage: // MsgAddPackage should not be handled + case vm.MsgCall: + upaths.add(msg.PkgPath) + case vm.MsgRun: + upaths.add(msg.Package.Path) + } + } + + return nil +} + +// handleQuery processes the query and returns relevant paths. +func handleQuery(path string, data []byte, upaths uniqPaths) error { + switch path { + case ".app/simulate": + return handleTx(data, upaths) + + case "vm/qrender", "vm/qfile", "vm/qfuncs", "vm/qeval": + path, _, _ := strings.Cut(string(data), ":") // Cut arguments out + path = filepath.Clean(path) + + // If path is a file, grab the directory instead + if ext := filepath.Ext(path); ext != "" { + path = filepath.Dir(path) + } + + upaths.add(path) + return nil + + default: + return fmt.Errorf("unhandled: %q", path) + } + + // XXX: handle more cases +} diff --git a/contribs/gnodev/pkg/proxy/path_interceptor_test.go b/contribs/gnodev/pkg/proxy/path_interceptor_test.go new file mode 100644 index 00000000000..c7082adfa30 --- /dev/null +++ b/contribs/gnodev/pkg/proxy/path_interceptor_test.go @@ -0,0 +1,179 @@ +package proxy_test + +import ( + "net" + "net/http" + "path/filepath" + "testing" + + "github.com/gnolang/gno/contribs/gnodev/pkg/proxy" + "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" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProxy(t *testing.T) { + const targetPath = "gno.land/r/target/foo" + + pkg := gnovm.MemPackage{ + Name: "foo", + Path: targetPath, + Files: []*gnovm.MemFile{ + { + Name: "foo.gno", + Body: `package foo; func Render(_ string) string { return "foo" }`, + }, + {Name: "gno.mod", Body: `module ` + targetPath}, + }, + } + + rootdir := gnoenv.RootDir() + cfg := integration.TestingMinimalNodeConfig(rootdir) + logger := log.NewTestingLogger(t) + + tmp := t.TempDir() + sock := filepath.Join(tmp, "node.sock") + addr, err := net.ResolveUnixAddr("unix", sock) + require.NoError(t, err) + + // Create proxy + interceptor, err := proxy.NewPathInterceptor(logger, addr) + require.NoError(t, err) + defer interceptor.Close() + cfg.TMConfig.RPC.ListenAddress = interceptor.ProxyAddress() + cfg.SkipGenesisVerification = true + + // Setup genesis + privKey := secp256k1.GenPrivKey() + cfg.Genesis.AppState = integration.GenerateTestingGenesisState(privKey, pkg) + creator := privKey.PubKey().Address() + + integration.TestingInMemoryNode(t, logger, cfg) + pathChan := make(chan []string, 1) + interceptor.HandlePath(func(paths ...string) { + pathChan <- paths + }) + + // ---- Test Cases ---- + + t.Run("valid_vm_query", func(t *testing.T) { + cli, err := client.NewHTTPClient(interceptor.TargetAddress()) + require.NoError(t, err) + + res, err := cli.ABCIQuery("vm/qrender", []byte(targetPath+":\n")) + require.NoError(t, err) + assert.Nil(t, res.Response.Error) + + select { + case paths := <-pathChan: + require.Len(t, paths, 1) + assert.Equal(t, []string{targetPath}, paths) + default: + t.Fatal("paths not captured") + } + }) + + t.Run("valid_vm_query_file", func(t *testing.T) { + cli, err := client.NewHTTPClient(interceptor.TargetAddress()) + require.NoError(t, err) + + res, err := cli.ABCIQuery("vm/qfile", []byte(filepath.Join(targetPath, "foo.gno"))) + require.NoError(t, err) + assert.Nil(t, res.Response.Error) + + select { + case paths := <-pathChan: + require.Len(t, paths, 1) + assert.Equal(t, []string{targetPath}, paths) + default: + t.Fatal("paths not captured") + } + }) + + t.Run("simulate_tx_paths", func(t *testing.T) { + // Build transaction with multiple messages + var tx std.Tx + send := std.MustParseCoins(ugnot.ValueString(10_000_000)) + tx.Fee = std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}} + tx.Msgs = []std.Msg{ + vm.NewMsgCall(creator, send, targetPath, "Render", []string{""}), + vm.NewMsgCall(creator, send, targetPath, "Render", []string{""}), + vm.NewMsgCall(creator, send, targetPath, "Render", []string{""}), + } + + bytes, err := tx.GetSignBytes(cfg.Genesis.ChainID, 0, 0) + require.NoError(t, err) + signature, err := privKey.Sign(bytes) + require.NoError(t, err) + tx.Signatures = []std.Signature{{PubKey: privKey.PubKey(), Signature: signature}} + + bz, err := amino.Marshal(tx) + require.NoError(t, err) + + cli, err := client.NewHTTPClient(interceptor.TargetAddress()) + require.NoError(t, err) + + res, err := cli.BroadcastTxCommit(bz) + require.NoError(t, err) + assert.NoError(t, res.CheckTx.Error) + assert.NoError(t, res.DeliverTx.Error) + + select { + case paths := <-pathChan: + require.Len(t, paths, 1) + assert.Equal(t, []string{targetPath}, paths) + default: + t.Fatal("paths not captured") + } + }) + + t.Run("websocket_forward", func(t *testing.T) { + // For now simply try to connect and upgrade the connection + // XXX: fully support ws + + conn, err := net.Dial(addr.Network(), addr.String()) + require.NoError(t, err) + defer conn.Close() + + // Send WebSocket handshake + req, _ := http.NewRequest("GET", "http://"+interceptor.TargetAddress(), nil) + req.Header.Set("Upgrade", "websocket") + req.Header.Set("Connection", "Upgrade") + err = req.Write(conn) + require.NoError(t, err) + }) + + t.Run("invalid_query_data", func(t *testing.T) { + // Making a valid call but not supported by the proxy + // should succeed + query := "auth/accounts/" + creator.String() + + cli, err := client.NewHTTPClient(interceptor.TargetAddress()) + require.NoError(t, err) + defer cli.Close() + + res, err := cli.ABCIQuery(query, []byte{}) + require.NoError(t, err) + require.NoError(t, res.Response.Error) + + var qret struct{ BaseAccount std.BaseAccount } + err = amino.UnmarshalJSON(res.Response.Data, &qret) + require.NoError(t, err) + assert.Equal(t, qret.BaseAccount.Address, creator) + + select { + case paths := <-pathChan: + require.FailNowf(t, "should not catch a path", "catched: %+v", paths) + default: + } + }) +} diff --git a/contribs/gnodev/pkg/rawterm/keypress.go b/contribs/gnodev/pkg/rawterm/keypress.go index 45c64c999dd..e9c1728bd4b 100644 --- a/contribs/gnodev/pkg/rawterm/keypress.go +++ b/contribs/gnodev/pkg/rawterm/keypress.go @@ -26,6 +26,12 @@ const ( KeyN KeyPress = 'N' KeyP KeyPress = 'P' KeyR KeyPress = 'R' + + // Special keys + KeyUp KeyPress = 0x80 // Arbitrary value outside ASCII range + KeyDown KeyPress = 0x81 + KeyLeft KeyPress = 0x82 + KeyRight KeyPress = 0x83 ) func (k KeyPress) Upper() KeyPress { @@ -52,6 +58,14 @@ func (k KeyPress) String() string { return "Ctrl+S" case KeyCtrlT: return "Ctrl+T" + case KeyUp: + return "Up Arrow" + case KeyDown: + return "Down Arrow" + case KeyLeft: + return "Left Arrow" + case KeyRight: + return "Right Arrow" default: // For printable ASCII characters if k > 0x20 && k < 0x7e { diff --git a/contribs/gnodev/pkg/rawterm/rawterm.go b/contribs/gnodev/pkg/rawterm/rawterm.go index 58b8dde1530..7ff4cadaf94 100644 --- a/contribs/gnodev/pkg/rawterm/rawterm.go +++ b/contribs/gnodev/pkg/rawterm/rawterm.go @@ -54,12 +54,31 @@ func (rt *RawTerm) read(buf []byte) (n int, err error) { } func (rt *RawTerm) ReadKeyPress() (KeyPress, error) { - buf := make([]byte, 1) - if _, err := rt.read(buf); err != nil { + buf := make([]byte, 3) + n, err := rt.read(buf) + if err != nil { return KeyNone, err } - return KeyPress(buf[0]), nil + if n == 1 && buf[0] != '\x1b' { + // Single character, not an escape sequence + return KeyPress(buf[0]), nil + } + + if n >= 3 && buf[0] == '\x1b' && buf[1] == '[' { + switch buf[2] { + case 'A': + return KeyUp, nil + case 'B': + return KeyDown, nil + case 'C': + return KeyRight, nil + case 'D': + return KeyLeft, nil + } + } + + return KeyNone, fmt.Errorf("unknown key sequence: %v", buf[:n]) } // writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n. diff --git a/contribs/gnodev/pkg/watcher/watch.go b/contribs/gnodev/pkg/watcher/watch.go index 63158a06c4b..5f277fd6646 100644 --- a/contribs/gnodev/pkg/watcher/watch.go +++ b/contribs/gnodev/pkg/watcher/watch.go @@ -5,15 +5,14 @@ import ( "fmt" "log/slog" "path/filepath" - "sort" "strings" "time" emitter "github.com/gnolang/gno/contribs/gnodev/pkg/emitter" events "github.com/gnolang/gno/contribs/gnodev/pkg/events" + "github.com/gnolang/gno/contribs/gnodev/pkg/packages" "github.com/fsnotify/fsnotify" - "github.com/gnolang/gno/gnovm/pkg/gnomod" ) type PackageWatcher struct { @@ -25,7 +24,6 @@ type PackageWatcher struct { logger *slog.Logger watcher *fsnotify.Watcher - pkgsDir []string emitter emitter.Emitter } @@ -39,7 +37,6 @@ func NewPackageWatcher(logger *slog.Logger, emitter emitter.Emitter) (*PackageWa p := &PackageWatcher{ ctx: ctx, stop: cancel, - pkgsDir: []string{}, logger: logger, watcher: watcher, emitter: emitter, @@ -114,58 +111,61 @@ func (p *PackageWatcher) Stop() { p.stop() } -// AddPackages adds new packages to the watcher. -// Packages are sorted by their length in descending order to facilitate easier -// and more efficient matching with corresponding paths. The longest paths are -// compared first. -func (p *PackageWatcher) AddPackages(pkgs ...gnomod.Pkg) error { +func (p *PackageWatcher) UpdatePackagesWatch(pkgs ...packages.Package) { + watchList := p.watcher.WatchList() + + oldPkgs := make(map[string]struct{}, len(watchList)) + for _, path := range watchList { + oldPkgs[path] = struct{}{} + } + + newPkgs := make(map[string]struct{}, len(pkgs)) for _, pkg := range pkgs { - dir := pkg.Dir + if pkg.Kind != packages.PackageKindFS { + continue + } - abs, err := filepath.Abs(dir) + path, err := filepath.Abs(pkg.Location) if err != nil { - return fmt.Errorf("unable to get absolute path of %q: %w", dir, err) + p.logger.Error("Unable to get absolute path", "path", pkg.Location, "error", err) + continue } - // Use binary search to find the correct insertion point - index := sort.Search(len(p.pkgsDir), func(i int) bool { - return len(p.pkgsDir[i]) <= len(dir) // Longest paths first - }) + newPkgs[path] = struct{}{} + } - // Check for duplicates - if index < len(p.pkgsDir) && p.pkgsDir[index] == dir { - continue // Skip + for path := range oldPkgs { + if _, exists := newPkgs[path]; !exists { + p.watcher.Remove(path) + p.logger.Debug("Watcher list: removed", "path", path) } + } - // Insert the package - p.pkgsDir = append(p.pkgsDir[:index], append([]string{abs}, p.pkgsDir[index:]...)...) - - // Add the package to the watcher and handle any errors - if err := p.watcher.Add(abs); err != nil { - return fmt.Errorf("unable to watch %q: %w", pkg.Dir, err) + for path := range newPkgs { + if _, exists := oldPkgs[path]; !exists { + p.watcher.Add(path) + p.logger.Debug("Watcher list: added", "path", path) } } - - return nil } func (p *PackageWatcher) generatePackagesUpdateList(paths []string) PackageUpdateList { pkgsUpdate := []events.PackageUpdate{} mpkgs := map[string]*events.PackageUpdate{} // Pkg -> Update + watchList := p.watcher.WatchList() for _, path := range paths { - for _, pkg := range p.pkgsDir { - dirPath := filepath.Dir(path) + for _, pkg := range watchList { + if len(pkg) == len(path) { + continue // Skip if pkg == path + } // Check if a package directory contain our path directory + dirPath := filepath.Dir(path) if !strings.HasPrefix(pkg, dirPath) { continue } - if len(pkg) == len(path) { - continue // Skip if pkg == path - } - // Accumulate file updates for each package pkgu, ok := mpkgs[pkg] if !ok { diff --git a/contribs/gnofaucet/go.mod b/contribs/gnofaucet/go.mod index 32d2e322098..88c05e0d778 100644 --- a/contribs/gnofaucet/go.mod +++ b/contribs/gnofaucet/go.mod @@ -32,6 +32,7 @@ require ( github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum index 5b3cfdc3289..e6743b75960 100644 --- a/contribs/gnofaucet/go.sum +++ b/contribs/gnofaucet/go.sum @@ -126,6 +126,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod index 5b28c8774c8..8af370f8169 100644 --- a/contribs/gnogenesis/go.mod +++ b/contribs/gnogenesis/go.mod @@ -36,6 +36,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // 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 diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum index dcd853e9148..e3462f9c431 100644 --- a/contribs/gnogenesis/go.sum +++ b/contribs/gnogenesis/go.sum @@ -133,6 +133,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 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= diff --git a/contribs/gnogenesis/internal/verify/verify.go b/contribs/gnogenesis/internal/verify/verify.go index 9022711ce49..c69f41cad4d 100644 --- a/contribs/gnogenesis/internal/verify/verify.go +++ b/contribs/gnogenesis/internal/verify/verify.go @@ -12,7 +12,10 @@ import ( "github.com/gnolang/gno/tm2/pkg/commands" ) -var errInvalidGenesisState = errors.New("invalid genesis state type") +var ( + errInvalidGenesisState = errors.New("invalid genesis state type") + errInvalidTxSignature = errors.New("invalid tx signature") +) type verifyCfg struct { common.Cfg @@ -60,10 +63,32 @@ func execVerify(cfg *verifyCfg, io commands.IO) error { } // Validate the initial transactions - for _, tx := range state.Txs { + for index, tx := range state.Txs { if validateErr := tx.Tx.ValidateBasic(); validateErr != nil { return fmt.Errorf("invalid transacton, %w", validateErr) } + + // Genesis txs can only be signed by 1 account. + // Basic tx validation ensures there is at least 1 signer + signer := tx.Tx.GetSignatures()[0] + + // Grab the signature bytes of the tx. + // Genesis transactions are signed with + // account number and sequence set to 0 + signBytes, err := tx.Tx.GetSignBytes(genesis.ChainID, 0, 0) + if err != nil { + return fmt.Errorf("unable to get tx signature payload, %w", err) + } + + // Verify the signature using the public key + if !signer.PubKey.VerifyBytes(signBytes, signer.Signature) { + return fmt.Errorf( + "%w #%d, by signer %s", + errInvalidTxSignature, + index, + signer.PubKey.Address(), + ) + } } // Validate the initial balances diff --git a/contribs/gnogenesis/internal/verify/verify_test.go b/contribs/gnogenesis/internal/verify/verify_test.go index 130bd5e09bc..cc80c0423de 100644 --- a/contribs/gnogenesis/internal/verify/verify_test.go +++ b/contribs/gnogenesis/internal/verify/verify_test.go @@ -8,8 +8,12 @@ import ( "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/ed25519" "github.com/gnolang/gno/tm2/pkg/crypto/mock" + "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" ) @@ -63,6 +67,99 @@ func TestGenesis_Verify(t *testing.T) { require.Error(t, cmdErr) }) + t.Run("invalid tx signature", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + + testTable := []struct { + name string + signBytesFn func(tx *std.Tx) []byte + }{ + { + name: "invalid chain ID", + signBytesFn: func(tx *std.Tx) []byte { + // Sign the transaction, but with a chain ID + // that differs from the genesis chain ID + signBytes, err := tx.GetSignBytes(g.ChainID+"wrong", 0, 0) + require.NoError(t, err) + + return signBytes + }, + }, + { + name: "invalid account params", + signBytesFn: func(tx *std.Tx) []byte { + // Sign the transaction, but with an + // account number that is not 0 + signBytes, err := tx.GetSignBytes(g.ChainID, 10, 0) + require.NoError(t, err) + + return signBytes + }, + }, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + // Generate the transaction + signer := ed25519.GenPrivKey() + + sendMsg := bank.MsgSend{ + FromAddress: signer.PubKey().Address(), + ToAddress: signer.PubKey().Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 10)), + } + + tx := std.Tx{ + Msgs: []std.Msg{sendMsg}, + Fee: std.Fee{ + GasWanted: 1000000, + GasFee: std.NewCoin("ugnot", 20), + }, + } + + // Sign the transaction + signBytes := testCase.signBytesFn(&tx) + + signature, err := signer.Sign(signBytes) + require.NoError(t, err) + + tx.Signatures = append(tx.Signatures, std.Signature{ + PubKey: signer.PubKey(), + Signature: signature, + }) + + g.AppState = gnoland.GnoGenesisState{ + Balances: []gnoland.Balance{}, + Txs: []gnoland.TxWithMetadata{ + { + Tx: tx, + }, + }, + } + + 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) + assert.ErrorIs(t, cmdErr, errInvalidTxSignature) + }) + } + }) + t.Run("invalid balances", func(t *testing.T) { t.Parallel() diff --git a/contribs/gnohealth/go.mod b/contribs/gnohealth/go.mod index 203dac360b7..76d7cd9c437 100644 --- a/contribs/gnohealth/go.mod +++ b/contribs/gnohealth/go.mod @@ -23,6 +23,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.10.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect diff --git a/contribs/gnohealth/go.sum b/contribs/gnohealth/go.sum index e51cadf1564..3c8b5de45f2 100644 --- a/contribs/gnohealth/go.sum +++ b/contribs/gnohealth/go.sum @@ -118,6 +118,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= diff --git a/contribs/gnokeykc/go.mod b/contribs/gnokeykc/go.mod index 73e51f6b25e..3abcf3d834f 100644 --- a/contribs/gnokeykc/go.mod +++ b/contribs/gnokeykc/go.mod @@ -38,6 +38,7 @@ require ( github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum index 7a058c85750..6b4f81dfcf5 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -139,6 +139,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod index 83d88c354e7..96f6dc9bdc6 100644 --- a/contribs/gnomigrate/go.mod +++ b/contribs/gnomigrate/go.mod @@ -33,6 +33,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect go.etcd.io/bbolt v1.3.11 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum index dcd853e9148..e3462f9c431 100644 --- a/contribs/gnomigrate/go.sum +++ b/contribs/gnomigrate/go.sum @@ -133,6 +133,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 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= diff --git a/docs/gno-tooling/cli/gnokey/state-changing-calls.md b/docs/gno-tooling/cli/gnokey/state-changing-calls.md index 79a777cca51..b301e99be56 100644 --- a/docs/gno-tooling/cli/gnokey/state-changing-calls.md +++ b/docs/gno-tooling/cli/gnokey/state-changing-calls.md @@ -99,12 +99,11 @@ Next, let's configure the `addpkg` subcommand to publish this package to the the `example/p/` folder, the command will look like this: ```bash -gnokey maketx addpkg \ +gnokey maketx addpkg \ -pkgpath "gno.land/p//hello_world" \ -pkgdir "." \ --send "" \ -gas-fee 10000000ugnot \ --gas-wanted 8000000 \ +-gas-wanted 200000 \ -broadcast \ -chainid portal-loop \ -remote "https://rpc.gno.land:443" @@ -114,15 +113,14 @@ Once we have added a desired [namespace](../../../concepts/namespaces.md) to upl a keypair name to use to execute the transaction: ```bash -gnokey maketx addpkg \ +gnokey maketx addpkg \ -pkgpath "gno.land/p/examplenamespace/hello_world" \ -pkgdir "." \ --send "" \ -gas-fee 10000000ugnot \ -gas-wanted 200000 \ -broadcast \ -chainid portal-loop \ --remote "https://rpc.gno.land:443" +-remote "https://rpc.gno.land:443" \ mykey ``` diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index 6a1da6483fd..f3b2c6be0b8 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -4,18 +4,6 @@ id: chain # Chain-related -## IsOriginCall -```go -func IsOriginCall() bool -``` -Checks if the caller of the function is an EOA. Returns **true** if caller is an EOA, **false** otherwise. - -#### Usage -```go -if !std.IsOriginCall() {...} -``` ---- - ## AssertOriginCall ```go func AssertOriginCall() diff --git a/examples/Makefile b/examples/Makefile index 63a20f78eb9..477fdb6651a 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -63,3 +63,8 @@ fmt: .PHONY: tidy tidy: go run github.com/gnolang/gno/gnovm/cmd/gno mod tidy -v --recursive + +.PHONY: generate +generate: + go generate ./... + $(MAKE) fmt diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index f5f909a473d..6a77ba3eb6f 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -5,13 +5,13 @@ import ( "net/url" "strconv" - "gno.land/p/demo/avl" + "gno.land/p/demo/avl/rotree" "gno.land/p/demo/ufmt" ) // Pager is a struct that holds the AVL tree and pagination parameters. type Pager struct { - Tree avl.ITree + Tree rotree.IReadOnlyTree PageQueryParam string SizeQueryParam string DefaultPageSize int @@ -37,7 +37,7 @@ type Item struct { } // NewPager creates a new Pager with default values. -func NewPager(tree avl.ITree, defaultPageSize int, reversed bool) *Pager { +func NewPager(tree rotree.IReadOnlyTree, defaultPageSize int, reversed bool) *Pager { return &Pager{ Tree: tree, PageQueryParam: "page", diff --git a/examples/gno.land/p/demo/btree/btree_test.gno b/examples/gno.land/p/demo/btree/btree_test.gno index 871e8c25e1d..959fa7a4254 100644 --- a/examples/gno.land/p/demo/btree/btree_test.gno +++ b/examples/gno.land/p/demo/btree/btree_test.gno @@ -523,91 +523,14 @@ func TestBTree(t *testing.T) { } } -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, val.(Content).Key, val.(Content).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. +// Write a test that populates a large B-Tree with 1000 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) + t.Logf("Creating B-Tree of degree 10 with 1000 records\n") + size := 1000 + tree := genericSeeding(New(WithDegree(10)), size) // Clone the tree t.Logf("Cloning the tree\n") @@ -615,7 +538,7 @@ func TestBTreeCloneIsolation(t *testing.T) { // 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 { + for i := 0; i < size; 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)} @@ -628,7 +551,7 @@ func TestBTreeCloneIsolation(t *testing.T) { // Make some changes to all three trees t.Logf("Making changes to all three trees\n") - for i := 0; i < 10000; i += 3 { + for i := 0; i < size; 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)} @@ -639,7 +562,7 @@ func TestBTreeCloneIsolation(t *testing.T) { // 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++ { + for i := 0; i < size; i++ { content := Content{Key: i, Value: fmt.Sprintf("Value_%d", i)} val := tree.Get(content) @@ -676,3 +599,83 @@ func TestBTreeCloneIsolation(t *testing.T) { } } } + +// -------------------- +// Stress tests. Disabled for testing performance + +//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, val.(Content).Key, val.(Content).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) +// } +//} diff --git a/examples/gno.land/p/demo/diff/diff_test.gno b/examples/gno.land/p/demo/diff/diff_test.gno index bbf4fcdf3e0..3993c91664a 100644 --- a/examples/gno.land/p/demo/diff/diff_test.gno +++ b/examples/gno.land/p/demo/diff/diff_test.gno @@ -162,12 +162,13 @@ func TestMyersDiff(t *testing.T) { new: strings.Repeat("b", 1000), expected: "[-" + strings.Repeat("a", 1000) + "][+" + strings.Repeat("b", 1000) + "]", }, - { - name: "Very long strings", - old: strings.Repeat("a", 10000) + "b" + strings.Repeat("a", 10000), - new: strings.Repeat("a", 10000) + "c" + strings.Repeat("a", 10000), - expected: strings.Repeat("a", 10000) + "[-b][+c]" + strings.Repeat("a", 10000), - }, + //{ // disabled for testing performance + // XXX: consider adding a flag to run such tests, not like `-short`, or switching to a `-bench`, maybe. + // name: "Very long strings", + // old: strings.Repeat("a", 10000) + "b" + strings.Repeat("a", 10000), + // new: strings.Repeat("a", 10000) + "c" + strings.Repeat("a", 10000), + // expected: strings.Repeat("a", 10000) + "[-b][+c]" + strings.Repeat("a", 10000), + //}, } for _, tc := range tests { diff --git a/examples/gno.land/p/demo/json/node_test.gno b/examples/gno.land/p/demo/json/node_test.gno index dbc82369f68..c364187ac86 100644 --- a/examples/gno.land/p/demo/json/node_test.gno +++ b/examples/gno.land/p/demo/json/node_test.gno @@ -285,7 +285,7 @@ func TestNode_GetBool(t *testing.T) { } } -func TestNode_GetBool_Fail(t *testing.T) { +func TestNode_GetBool_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"literally null node", NullNode("")}, @@ -357,7 +357,7 @@ func TestNode_GetNull(t *testing.T) { } } -func TestNode_GetNull_Fail(t *testing.T) { +func TestNode_GetNull_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"number node is null", NumberNode("", 42)}, @@ -435,7 +435,7 @@ func TestNode_GetNumeric_With_Unmarshal(t *testing.T) { } } -func TestNode_GetNumeric_Fail(t *testing.T) { +func TestNode_GetNumeric_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -467,7 +467,7 @@ func TestNode_GetString(t *testing.T) { } } -func TestNode_GetString_Fail(t *testing.T) { +func TestNode_GetString_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -577,7 +577,7 @@ func TestNode_GetArray(t *testing.T) { } } -func TestNode_GetArray_Fail(t *testing.T) { +func TestNode_GetArray_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -736,7 +736,7 @@ func TestNode_Index(t *testing.T) { } } -func TestNode_Index_Fail(t *testing.T) { +func TestNode_Index_NotSucceed(t *testing.T) { tests := []struct { name string node *Node @@ -854,7 +854,7 @@ func TestNode_GetKey(t *testing.T) { } } -func TestNode_GetKey_Fail(t *testing.T) { +func TestNode_GetKey_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"null node", NullNode("")}, @@ -998,7 +998,7 @@ func TestNode_GetObject(t *testing.T) { } } -func TestNode_GetObject_Fail(t *testing.T) { +func TestNode_GetObject_NotSucceed(t *testing.T) { tests := []simpleNode{ {"nil node", (*Node)(nil)}, {"get object from null node", NullNode("")}, diff --git a/examples/gno.land/p/demo/mux/handler.gno b/examples/gno.land/p/demo/mux/handler.gno index 835d050a52c..4d937dbacab 100644 --- a/examples/gno.land/p/demo/mux/handler.gno +++ b/examples/gno.land/p/demo/mux/handler.gno @@ -7,6 +7,8 @@ type Handler struct { type HandlerFunc func(*ResponseWriter, *Request) -// TODO: type ErrHandlerFunc func(*ResponseWriter, *Request) error -// TODO: NotFoundHandler +type ErrHandlerFunc func(*ResponseWriter, *Request) error + +type NotFoundHandler func(*ResponseWriter, *Request) + // TODO: AutomaticIndex diff --git a/examples/gno.land/p/demo/mux/request.gno b/examples/gno.land/p/demo/mux/request.gno index 7b5b74da91b..eaa2f287069 100644 --- a/examples/gno.land/p/demo/mux/request.gno +++ b/examples/gno.land/p/demo/mux/request.gno @@ -18,24 +18,29 @@ type Request struct { // GetVar retrieves a variable from the path based on routing rules. func (r *Request) GetVar(key string) string { - var ( - handlerParts = strings.Split(r.HandlerPath, "/") - reqParts = strings.Split(r.Path, "/") - ) - - for i := 0; i < len(handlerParts); i++ { - handlerPart := handlerParts[i] + handlerParts := strings.Split(r.HandlerPath, "/") + reqParts := strings.Split(r.Path, "/") + reqIndex := 0 + for handlerIndex := 0; handlerIndex < len(handlerParts); handlerIndex++ { + handlerPart := handlerParts[handlerIndex] switch { case handlerPart == "*": - // XXX: implement a/b/*/d/e - panic("not implemented") + // If a wildcard "*" is found, consume all remaining segments + wildcardParts := reqParts[reqIndex:] + reqIndex = len(reqParts) // Consume all remaining segments + return strings.Join(wildcardParts, "/") // Return all remaining segments as a string case strings.HasPrefix(handlerPart, "{") && strings.HasSuffix(handlerPart, "}"): + // If a variable of the form {param} is found we compare it with the key parameter := handlerPart[1 : len(handlerPart)-1] if parameter == key { - return reqParts[i] + return reqParts[reqIndex] } + reqIndex++ default: - // continue + if reqIndex >= len(reqParts) || handlerPart != reqParts[reqIndex] { + return "" + } + reqIndex++ } } diff --git a/examples/gno.land/p/demo/mux/request_test.gno b/examples/gno.land/p/demo/mux/request_test.gno index 5f8088b4964..24c611c1f9d 100644 --- a/examples/gno.land/p/demo/mux/request_test.gno +++ b/examples/gno.land/p/demo/mux/request_test.gno @@ -1,8 +1,10 @@ package mux import ( - "fmt" "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" ) func TestRequest_GetVar(t *testing.T) { @@ -12,28 +14,35 @@ func TestRequest_GetVar(t *testing.T) { getVarKey string expectedOutput string }{ + {"users/{id}", "users/123", "id", "123"}, {"users/123", "users/123", "id", ""}, {"users/{id}", "users/123", "nonexistent", ""}, - {"a/{b}/c/{d}", "a/42/c/1337", "b", "42"}, - {"a/{b}/c/{d}", "a/42/c/1337", "d", "1337"}, - {"{a}", "foo", "a", "foo"}, - // TODO: wildcards: a/*/c - // TODO: multiple patterns per slashes: a/{b}-{c}/d - } + {"users/{userId}/posts/{postId}", "users/123/posts/456", "userId", "123"}, + {"users/{userId}/posts/{postId}", "users/123/posts/456", "postId", "456"}, + + // Wildcards + {"*", "users/123", "*", "users/123"}, + {"*", "users/123/posts/456", "*", "users/123/posts/456"}, + {"*", "users/123/posts/456/comments/789", "*", "users/123/posts/456/comments/789"}, + {"users/*", "users/john/posts", "*", "john/posts"}, + {"users/*/comments", "users/jane/comments", "*", "jane/comments"}, + {"api/*/posts/*", "api/v1/posts/123", "*", "v1/posts/123"}, + // wildcards and parameters + {"api/{version}/*", "api/v1/user/settings", "version", "v1"}, + } for _, tt := range cases { - name := fmt.Sprintf("%s-%s", tt.handlerPath, tt.reqPath) + name := ufmt.Sprintf("%s-%s", tt.handlerPath, tt.reqPath) t.Run(name, func(t *testing.T) { req := &Request{ HandlerPath: tt.handlerPath, Path: tt.reqPath, } - output := req.GetVar(tt.getVarKey) - if output != tt.expectedOutput { - t.Errorf("Expected '%q, but got %q", tt.expectedOutput, output) - } + uassert.Equal(t, tt.expectedOutput, output, + "handler: %q, path: %q, key: %q", + tt.handlerPath, tt.reqPath, tt.getVarKey) }) } } diff --git a/examples/gno.land/p/demo/mux/router.gno b/examples/gno.land/p/demo/mux/router.gno index fe6bf70abdf..4fca43a0378 100644 --- a/examples/gno.land/p/demo/mux/router.gno +++ b/examples/gno.land/p/demo/mux/router.gno @@ -5,7 +5,7 @@ import "strings" // Router handles the routing and rendering logic. type Router struct { routes []Handler - NotFoundHandler HandlerFunc + NotFoundHandler NotFoundHandler } // NewRouter creates a new Router instance. @@ -23,8 +23,14 @@ func (r *Router) Render(reqPath string) string { for _, route := range r.routes { patParts := strings.Split(route.Pattern, "/") - - if len(patParts) != len(reqParts) { + wildcard := false + for _, part := range patParts { + if part == "*" { + wildcard = true + break + } + } + if !wildcard && len(patParts) != len(reqParts) { continue } @@ -34,7 +40,7 @@ func (r *Router) Render(reqPath string) string { reqPart := reqParts[i] if patPart == "*" { - continue + break } if strings.HasPrefix(patPart, "{") && strings.HasSuffix(patPart, "}") { continue @@ -63,12 +69,31 @@ func (r *Router) Render(reqPath string) string { return res.Output() } -// Handle registers a route and its handler function. +// HandleFunc registers a route and its handler function. func (r *Router) HandleFunc(pattern string, fn HandlerFunc) { route := Handler{Pattern: pattern, Fn: fn} r.routes = append(r.routes, route) } +// HandleErrFunc registers a route and its error handler function. +func (r *Router) HandleErrFunc(pattern string, fn ErrHandlerFunc) { + + // Convert ErrHandlerFunc to regular HandlerFunc + handler := func(res *ResponseWriter, req *Request) { + if err := fn(res, req); err != nil { + res.Write("Error: " + err.Error()) + } + } + + r.HandleFunc(pattern, handler) +} + +// SetNotFoundHandler sets custom message for 404 defaultNotFoundHandler. +func (r *Router) SetNotFoundHandler(handler NotFoundHandler) { + r.NotFoundHandler = handler +} + +// stripQueryString removes query string from the request path. func stripQueryString(reqPath string) string { i := strings.Index(reqPath, "?") if i == -1 { diff --git a/examples/gno.land/p/demo/mux/router_test.gno b/examples/gno.land/p/demo/mux/router_test.gno index cc6aad62146..c1c5d218165 100644 --- a/examples/gno.land/p/demo/mux/router_test.gno +++ b/examples/gno.land/p/demo/mux/router_test.gno @@ -72,7 +72,33 @@ func TestRouter_Render(t *testing.T) { }) }, }, - + { + label: "wildcard in route", + path: "hello/Alice/Bob", + expectedOutput: "Matched: Alice/Bob", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hello/*", func(rw *ResponseWriter, req *Request) { + path := req.GetVar("*") + uassert.Equal(t, "Alice/Bob", path) + uassert.Equal(t, "hello/Alice/Bob", req.Path) + rw.Write("Matched: " + path) + }) + }, + }, + { + label: "wildcard in route with query string", + path: "hello/Alice/Bob?foo=bar", + expectedOutput: "Matched: Alice/Bob", + setupHandler: func(t *testing.T, r *Router) { + r.HandleFunc("hello/*", func(rw *ResponseWriter, req *Request) { + path := req.GetVar("*") + uassert.Equal(t, "Alice/Bob", path) + uassert.Equal(t, "hello/Alice/Bob?foo=bar", req.RawPath) + uassert.Equal(t, "hello/Alice/Bob", req.Path) + rw.Write("Matched: " + path) + }) + }, + }, // TODO: {"hello", "Hello, world!"}, // TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc } diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno index f565e27c0f2..a8cb5ea95a7 100644 --- a/examples/gno.land/p/demo/ownable/ownable.gno +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -65,18 +65,28 @@ func (o *Ownable) DropOwnership() error { } // Owner returns the owner address from Ownable -func (o Ownable) Owner() std.Address { +func (o *Ownable) Owner() std.Address { + if o == nil { + return std.Address("") + } return o.owner } // CallerIsOwner checks if the caller of the function is the Realm's owner -func (o Ownable) CallerIsOwner() bool { +func (o *Ownable) CallerIsOwner() bool { + if o == nil { + return false + } return std.PrevRealm().Addr() == o.owner } // AssertCallerIsOwner panics if the caller is not the owner -func (o Ownable) AssertCallerIsOwner() { - if std.PrevRealm().Addr() != o.owner { +func (o *Ownable) AssertCallerIsOwner() { + if o == nil { + panic(ErrUnauthorized) + } + caller := std.PrevRealm().Addr() + if caller != o.owner { panic(ErrUnauthorized) } } diff --git a/examples/gno.land/p/demo/ownable/ownable_test.gno b/examples/gno.land/p/demo/ownable/ownable_test.gno index f58af9642c6..d8b7f9a8e3a 100644 --- a/examples/gno.land/p/demo/ownable/ownable_test.gno +++ b/examples/gno.land/p/demo/ownable/ownable_test.gno @@ -93,3 +93,51 @@ func TestErrInvalidAddress(t *testing.T) { err = o.TransferOwnership("10000000001000000000100000000010000000001000000000") uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) } + +func TestAssertCallerIsOwner(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + // Should not panic when caller is owner + o.AssertCallerIsOwner() + + // Should panic when caller is not owner + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + defer func() { + r := recover() + if r == nil { + t.Error("expected panic but got none") + } + if r != ErrUnauthorized { + t.Errorf("expected ErrUnauthorized but got %v", r) + } + }() + o.AssertCallerIsOwner() +} + +func TestNilReceiver(t *testing.T) { + var o *Ownable + + owner := o.Owner() + if owner != std.Address("") { + t.Errorf("expected empty address but got %v", owner) + } + + isOwner := o.CallerIsOwner() + uassert.False(t, isOwner) + + defer func() { + r := recover() + if r == nil { + t.Error("expected panic but got none") + } + if r != ErrUnauthorized { + t.Errorf("expected ErrUnauthorized but got %v", r) + } + }() + o.AssertCallerIsOwner() +} diff --git a/examples/gno.land/p/demo/pausable/pausable.gno b/examples/gno.land/p/demo/pausable/pausable.gno index e6a85771fa6..fa3962cab41 100644 --- a/examples/gno.land/p/demo/pausable/pausable.gno +++ b/examples/gno.land/p/demo/pausable/pausable.gno @@ -7,23 +7,15 @@ import ( ) type Pausable struct { - *ownable.Ownable + o *ownable.Ownable paused bool } -// New returns a new Pausable struct with non-paused state as default -func New() *Pausable { - return &Pausable{ - Ownable: ownable.New(), - paused: false, - } -} - // NewFromOwnable is the same as New, but with a pre-existing top-level ownable func NewFromOwnable(ownable *ownable.Ownable) *Pausable { return &Pausable{ - Ownable: ownable, - paused: false, + o: ownable, + paused: false, } } @@ -34,24 +26,29 @@ 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 !p.CallerIsOwner() { + if !p.o.CallerIsOwner() { return ownable.ErrUnauthorized } p.paused = true - std.Emit("Paused", "account", p.Owner().String()) + std.Emit("Paused", "account", p.o.Owner().String()) return nil } // Unpause sets the state of Pausable to false, meaning all pausable functions are resumed func (p *Pausable) Unpause() error { - if !p.CallerIsOwner() { + if !p.o.CallerIsOwner() { return ownable.ErrUnauthorized } p.paused = false - std.Emit("Unpaused", "account", p.Owner().String()) + std.Emit("Unpaused", "account", p.o.Owner().String()) return nil } + +// Ownable returns the underlying ownable +func (p *Pausable) Ownable() *ownable.Ownable { + return p.o +} diff --git a/examples/gno.land/p/demo/pausable/pausable_test.gno b/examples/gno.land/p/demo/pausable/pausable_test.gno index c9557245bdf..47028cd85c8 100644 --- a/examples/gno.land/p/demo/pausable/pausable_test.gno +++ b/examples/gno.land/p/demo/pausable/pausable_test.gno @@ -5,57 +5,49 @@ import ( "testing" "gno.land/p/demo/ownable" + "gno.land/p/demo/uassert" "gno.land/p/demo/urequire" ) var ( - firstCaller = std.Address("g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de") - secondCaller = std.Address("g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa") + firstCaller = std.Address("g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de") + o = ownable.NewWithAddress(firstCaller) ) -func TestNew(t *testing.T) { - std.TestSetOrigCaller(firstCaller) - - result := New() - - urequire.False(t, result.paused, "Expected result to be unpaused") - urequire.Equal(t, firstCaller.String(), result.Owner().String()) -} - func TestNewFromOwnable(t *testing.T) { std.TestSetOrigCaller(firstCaller) - o := ownable.New() - std.TestSetOrigCaller(secondCaller) result := NewFromOwnable(o) - - urequire.Equal(t, firstCaller.String(), result.Owner().String()) + urequire.Equal(t, firstCaller.String(), result.Ownable().Owner().String()) } func TestSetUnpaused(t *testing.T) { std.TestSetOrigCaller(firstCaller) + result := NewFromOwnable(o) - result := New() result.Unpause() - - urequire.False(t, result.IsPaused(), "Expected result to be unpaused") + uassert.False(t, result.IsPaused(), "Expected result to be unpaused") } func TestSetPaused(t *testing.T) { std.TestSetOrigCaller(firstCaller) + result := NewFromOwnable(o) - result := New() result.Pause() - - urequire.True(t, result.IsPaused(), "Expected result to be paused") + uassert.True(t, result.IsPaused(), "Expected result to be paused") } func TestIsPaused(t *testing.T) { - std.TestSetOrigCaller(firstCaller) - - result := New() + result := NewFromOwnable(o) urequire.False(t, result.IsPaused(), "Expected result to be unpaused") + std.TestSetOrigCaller(firstCaller) result.Pause() - urequire.True(t, result.IsPaused(), "Expected result to be paused") + uassert.True(t, result.IsPaused(), "Expected result to be paused") +} + +func TestOwnable(t *testing.T) { + result := NewFromOwnable(o) + + uassert.Equal(t, result.Ownable().Owner(), o.Owner()) } diff --git a/examples/gno.land/p/demo/simpledao/dao_test.gno b/examples/gno.land/p/demo/simpledao/dao_test.gno index 46251e24dad..275455d1479 100644 --- a/examples/gno.land/p/demo/simpledao/dao_test.gno +++ b/examples/gno.land/p/demo/simpledao/dao_test.gno @@ -752,7 +752,7 @@ func TestSimpleDAO_ExecuteProposal(t *testing.T) { dao.ExecutionSuccessful, }, { - "execution failed", + "execution not succeeded", dao.ExecutionFailed, }, } diff --git a/examples/gno.land/p/moul/cow/gno.mod b/examples/gno.land/p/moul/cow/gno.mod new file mode 100644 index 00000000000..e5dec0bc5b4 --- /dev/null +++ b/examples/gno.land/p/moul/cow/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/cow diff --git a/examples/gno.land/p/moul/cow/node.gno b/examples/gno.land/p/moul/cow/node.gno new file mode 100644 index 00000000000..0c30871d7c4 --- /dev/null +++ b/examples/gno.land/p/moul/cow/node.gno @@ -0,0 +1,518 @@ +package cow + +//---------------------------------------- +// Node + +// Node represents a node in an AVL tree. +type Node struct { + key string // key is the unique identifier for the node. + value interface{} // value is the data stored in the node. + height int8 // height is the height of the node in the tree. + size int // size is the number of nodes in the subtree rooted at this node. + leftNode *Node // leftNode is the left child of the node. + rightNode *Node // rightNode is the right child of the node. +} + +// NewNode creates a new node with the given key and value. +func NewNode(key string, value interface{}) *Node { + return &Node{ + key: key, + value: value, + height: 0, + size: 1, + } +} + +// Size returns the size of the subtree rooted at the node. +func (node *Node) Size() int { + if node == nil { + return 0 + } + return node.size +} + +// IsLeaf checks if the node is a leaf node (has no children). +func (node *Node) IsLeaf() bool { + return node.height == 0 +} + +// Key returns the key of the node. +func (node *Node) Key() string { + return node.key +} + +// Value returns the value of the node. +func (node *Node) Value() interface{} { + return node.value +} + +// _copy creates a copy of the node (excluding value). +func (node *Node) _copy() *Node { + if node.height == 0 { + panic("Why are you copying a value node?") + } + return &Node{ + key: node.key, + height: node.height, + size: node.size, + leftNode: node.leftNode, + rightNode: node.rightNode, + } +} + +// Has checks if a node with the given key exists in the subtree rooted at the node. +func (node *Node) Has(key string) (has bool) { + if node == nil { + return false + } + if node.key == key { + return true + } + if node.height == 0 { + return false + } + if key < node.key { + return node.getLeftNode().Has(key) + } + return node.getRightNode().Has(key) +} + +// Get searches for a node with the given key in the subtree rooted at the node +// and returns its index, value, and whether it exists. +func (node *Node) Get(key string) (index int, value interface{}, exists bool) { + if node == nil { + return 0, nil, false + } + + if node.height == 0 { + if node.key == key { + return 0, node.value, true + } + if node.key < key { + return 1, nil, false + } + return 0, nil, false + } + + if key < node.key { + return node.getLeftNode().Get(key) + } + + rightNode := node.getRightNode() + index, value, exists = rightNode.Get(key) + index += node.size - rightNode.size + return index, value, exists +} + +// GetByIndex retrieves the key-value pair of the node at the given index +// in the subtree rooted at the node. +func (node *Node) GetByIndex(index int) (key string, value interface{}) { + if node.height == 0 { + if index == 0 { + return node.key, node.value + } + panic("GetByIndex asked for invalid index") + } + // TODO: could improve this by storing the sizes + leftNode := node.getLeftNode() + if index < leftNode.size { + return leftNode.GetByIndex(index) + } + return node.getRightNode().GetByIndex(index - leftNode.size) +} + +// Set inserts a new node with the given key-value pair into the subtree rooted at the node, +// and returns the new root of the subtree and whether an existing node was updated. +// +// XXX consider a better way to do this... perhaps split Node from Node. +func (node *Node) Set(key string, value interface{}) (newSelf *Node, updated bool) { + if node == nil { + return NewNode(key, value), false + } + + // Always create a new node for leaf nodes + if node.height == 0 { + return node.setLeaf(key, value) + } + + // Copy the node before modifying + newNode := node._copy() + if key < node.key { + newNode.leftNode, updated = node.getLeftNode().Set(key, value) + } else { + newNode.rightNode, updated = node.getRightNode().Set(key, value) + } + + if !updated { + newNode.calcHeightAndSize() + return newNode.balance(), updated + } + + return newNode, updated +} + +// setLeaf inserts a new leaf node with the given key-value pair into the subtree rooted at the node, +// and returns the new root of the subtree and whether an existing node was updated. +func (node *Node) setLeaf(key string, value interface{}) (newSelf *Node, updated bool) { + if key == node.key { + return NewNode(key, value), true + } + + if key < node.key { + return &Node{ + key: node.key, + height: 1, + size: 2, + leftNode: NewNode(key, value), + rightNode: node, + }, false + } + + return &Node{ + key: key, + height: 1, + size: 2, + leftNode: node, + rightNode: NewNode(key, value), + }, false +} + +// Remove deletes the node with the given key from the subtree rooted at the node. +// returns the new root of the subtree, the new leftmost leaf key (if changed), +// the removed value and the removal was successful. +func (node *Node) Remove(key string) ( + newNode *Node, newKey string, value interface{}, removed bool, +) { + if node == nil { + return nil, "", nil, false + } + if node.height == 0 { + if key == node.key { + return nil, "", node.value, true + } + return node, "", nil, false + } + if key < node.key { + var newLeftNode *Node + newLeftNode, newKey, value, removed = node.getLeftNode().Remove(key) + if !removed { + return node, "", value, false + } + if newLeftNode == nil { // left node held value, was removed + return node.rightNode, node.key, value, true + } + node = node._copy() + node.leftNode = newLeftNode + node.calcHeightAndSize() + node = node.balance() + return node, newKey, value, true + } + + var newRightNode *Node + newRightNode, newKey, value, removed = node.getRightNode().Remove(key) + if !removed { + return node, "", value, false + } + if newRightNode == nil { // right node held value, was removed + return node.leftNode, "", value, true + } + node = node._copy() + node.rightNode = newRightNode + if newKey != "" { + node.key = newKey + } + node.calcHeightAndSize() + node = node.balance() + return node, "", value, true +} + +// getLeftNode returns the left child of the node. +func (node *Node) getLeftNode() *Node { + return node.leftNode +} + +// getRightNode returns the right child of the node. +func (node *Node) getRightNode() *Node { + return node.rightNode +} + +// rotateRight performs a right rotation on the node and returns the new root. +// NOTE: overwrites node +// TODO: optimize balance & rotate +func (node *Node) rotateRight() *Node { + node = node._copy() + l := node.getLeftNode() + _l := l._copy() + + _lrCached := _l.rightNode + _l.rightNode = node + node.leftNode = _lrCached + + node.calcHeightAndSize() + _l.calcHeightAndSize() + + return _l +} + +// rotateLeft performs a left rotation on the node and returns the new root. +// NOTE: overwrites node +// TODO: optimize balance & rotate +func (node *Node) rotateLeft() *Node { + node = node._copy() + r := node.getRightNode() + _r := r._copy() + + _rlCached := _r.leftNode + _r.leftNode = node + node.rightNode = _rlCached + + node.calcHeightAndSize() + _r.calcHeightAndSize() + + return _r +} + +// calcHeightAndSize updates the height and size of the node based on its children. +// NOTE: mutates height and size +func (node *Node) calcHeightAndSize() { + node.height = maxInt8(node.getLeftNode().height, node.getRightNode().height) + 1 + node.size = node.getLeftNode().size + node.getRightNode().size +} + +// calcBalance calculates the balance factor of the node. +func (node *Node) calcBalance() int { + return int(node.getLeftNode().height) - int(node.getRightNode().height) +} + +// balance balances the subtree rooted at the node and returns the new root. +// NOTE: assumes that node can be modified +// TODO: optimize balance & rotate +func (node *Node) balance() (newSelf *Node) { + balance := node.calcBalance() + if balance >= -1 { + return node + } + if balance > 1 { + if node.getLeftNode().calcBalance() >= 0 { + // Left Left Case + return node.rotateRight() + } + // Left Right Case + left := node.getLeftNode() + node.leftNode = left.rotateLeft() + return node.rotateRight() + } + + if node.getRightNode().calcBalance() <= 0 { + // Right Right Case + return node.rotateLeft() + } + + // Right Left Case + right := node.getRightNode() + node.rightNode = right.rotateRight() + return node.rotateLeft() +} + +// Shortcut for TraverseInRange. +func (node *Node) Iterate(start, end string, cb func(*Node) bool) bool { + return node.TraverseInRange(start, end, true, true, cb) +} + +// Shortcut for TraverseInRange. +func (node *Node) ReverseIterate(start, end string, cb func(*Node) bool) bool { + return node.TraverseInRange(start, end, false, true, cb) +} + +// TraverseInRange traverses all nodes, including inner nodes. +// Start is inclusive and end is exclusive when ascending, +// Start and end are inclusive when descending. +// Empty start and empty end denote no start and no end. +// If leavesOnly is true, only visit leaf nodes. +// NOTE: To simulate an exclusive reverse traversal, +// just append 0x00 to start. +func (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly bool, cb func(*Node) bool) bool { + if node == nil { + return false + } + afterStart := (start == "" || start < node.key) + startOrAfter := (start == "" || start <= node.key) + beforeEnd := false + if ascending { + beforeEnd = (end == "" || node.key < end) + } else { + beforeEnd = (end == "" || node.key <= end) + } + + // Run callback per inner/leaf node. + stop := false + if (!node.IsLeaf() && !leavesOnly) || + (node.IsLeaf() && startOrAfter && beforeEnd) { + stop = cb(node) + if stop { + return stop + } + } + if node.IsLeaf() { + return stop + } + + if ascending { + // check lower nodes, then higher + if afterStart { + stop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + if stop { + return stop + } + if beforeEnd { + stop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + } else { + // check the higher nodes first + if beforeEnd { + stop = node.getRightNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + if stop { + return stop + } + if afterStart { + stop = node.getLeftNode().TraverseInRange(start, end, ascending, leavesOnly, cb) + } + } + + return stop +} + +// TraverseByOffset traverses all nodes, including inner nodes. +// A limit of math.MaxInt means no limit. +func (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { + if node == nil { + return false + } + + // fast paths. these happen only if TraverseByOffset is called directly on a leaf. + if limit <= 0 || offset >= node.size { + return false + } + if node.IsLeaf() { + if offset > 0 { + return false + } + return cb(node) + } + + // go to the actual recursive function. + return node.traverseByOffset(offset, limit, descending, leavesOnly, cb) +} + +// TraverseByOffset traverses the subtree rooted at the node by offset and limit, +// in either ascending or descending order, and applies the callback function to each traversed node. +// If leavesOnly is true, only leaf nodes are visited. +func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { + // caller guarantees: offset < node.size; limit > 0. + if !leavesOnly { + if cb(node) { + return true + } + } + first, second := node.getLeftNode(), node.getRightNode() + if descending { + first, second = second, first + } + if first.IsLeaf() { + // either run or skip, based on offset + if offset > 0 { + offset-- + } else { + cb(first) + limit-- + if limit <= 0 { + return false + } + } + } else { + // possible cases: + // 1 the offset given skips the first node entirely + // 2 the offset skips none or part of the first node, but the limit requires some of the second node. + // 3 the offset skips none or part of the first node, and the limit stops our search on the first node. + if offset >= first.size { + offset -= first.size // 1 + } else { + if first.traverseByOffset(offset, limit, descending, leavesOnly, cb) { + return true + } + // number of leaves which could actually be called from inside + delta := first.size - offset + offset = 0 + if delta >= limit { + return true // 3 + } + limit -= delta // 2 + } + } + + // because of the caller guarantees and the way we handle the first node, + // at this point we know that limit > 0 and there must be some values in + // this second node that we include. + + // => if the second node is a leaf, it has to be included. + if second.IsLeaf() { + return cb(second) + } + // => if it is not a leaf, it will still be enough to recursively call this + // function with the updated offset and limit + return second.traverseByOffset(offset, limit, descending, leavesOnly, cb) +} + +// Only used in testing... +func (node *Node) lmd() *Node { + if node.height == 0 { + return node + } + return node.getLeftNode().lmd() +} + +// Only used in testing... +func (node *Node) rmd() *Node { + if node.height == 0 { + return node + } + return node.getRightNode().rmd() +} + +func maxInt8(a, b int8) int8 { + if a > b { + return a + } + return b +} + +// Equal compares two nodes for structural equality. +// WARNING: This is an expensive operation that recursively traverses the entire tree structure. +// It should only be used in tests or when absolutely necessary. +func (node *Node) Equal(other *Node) bool { + // Handle nil cases + if node == nil || other == nil { + return node == other + } + + // Compare node properties + if node.key != other.key || + node.value != other.value || + node.height != other.height || + node.size != other.size { + return false + } + + // Compare children + leftEqual := (node.leftNode == nil && other.leftNode == nil) || + (node.leftNode != nil && other.leftNode != nil && node.leftNode.Equal(other.leftNode)) + if !leftEqual { + return false + } + + rightEqual := (node.rightNode == nil && other.rightNode == nil) || + (node.rightNode != nil && other.rightNode != nil && node.rightNode.Equal(other.rightNode)) + return rightEqual +} diff --git a/examples/gno.land/p/moul/cow/node_test.gno b/examples/gno.land/p/moul/cow/node_test.gno new file mode 100644 index 00000000000..c7225fe1ab0 --- /dev/null +++ b/examples/gno.land/p/moul/cow/node_test.gno @@ -0,0 +1,795 @@ +package cow + +import ( + "fmt" + "sort" + "strings" + "testing" +) + +func TestTraverseByOffset(t *testing.T) { + const testStrings = `Alfa +Alfred +Alpha +Alphabet +Beta +Beth +Book +Browser` + tt := []struct { + name string + desc bool + }{ + {"ascending", false}, + {"descending", true}, + } + + for _, tt := range tt { + t.Run(tt.name, func(t *testing.T) { + sl := strings.Split(testStrings, "\n") + + // sort a first time in the order opposite to how we'll be traversing + // the tree, to ensure that we are not just iterating through with + // insertion order. + sort.Strings(sl) + if !tt.desc { + reverseSlice(sl) + } + + r := NewNode(sl[0], nil) + for _, v := range sl[1:] { + r, _ = r.Set(v, nil) + } + + // then sort sl in the order we'll be traversing it, so that we can + // compare the result with sl. + reverseSlice(sl) + + var result []string + for i := 0; i < len(sl); i++ { + r.TraverseByOffset(i, 1, tt.desc, true, func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + } + + if !slicesEqual(sl, result) { + t.Errorf("want %v got %v", sl, result) + } + + for l := 2; l <= len(sl); l++ { + // "slices" + for i := 0; i <= len(sl); i++ { + max := i + l + if max > len(sl) { + max = len(sl) + } + exp := sl[i:max] + actual := []string{} + + r.TraverseByOffset(i, l, tt.desc, true, func(tr *Node) bool { + actual = append(actual, tr.Key()) + return false + }) + if !slicesEqual(exp, actual) { + t.Errorf("want %v got %v", exp, actual) + } + } + } + }) + } +} + +func TestHas(t *testing.T) { + tests := []struct { + name string + input []string + hasKey string + expected bool + }{ + { + "has key in non-empty tree", + []string{"C", "A", "B", "E", "D"}, + "B", + true, + }, + { + "does not have key in non-empty tree", + []string{"C", "A", "B", "E", "D"}, + "F", + false, + }, + { + "has key in single-node tree", + []string{"A"}, + "A", + true, + }, + { + "does not have key in single-node tree", + []string{"A"}, + "B", + false, + }, + { + "does not have key in empty tree", + []string{}, + "A", + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + result := tree.Has(tt.hasKey) + + if result != tt.expected { + t.Errorf("Expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestGet(t *testing.T) { + tests := []struct { + name string + input []string + getKey string + expectIdx int + expectVal interface{} + expectExists bool + }{ + { + "get existing key", + []string{"C", "A", "B", "E", "D"}, + "B", + 1, + nil, + true, + }, + { + "get non-existent key (smaller)", + []string{"C", "A", "B", "E", "D"}, + "@", + 0, + nil, + false, + }, + { + "get non-existent key (larger)", + []string{"C", "A", "B", "E", "D"}, + "F", + 5, + nil, + false, + }, + { + "get from empty tree", + []string{}, + "A", + 0, + nil, + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + idx, val, exists := tree.Get(tt.getKey) + + if idx != tt.expectIdx { + t.Errorf("Expected index %d, got %d", tt.expectIdx, idx) + } + + if val != tt.expectVal { + t.Errorf("Expected value %v, got %v", tt.expectVal, val) + } + + if exists != tt.expectExists { + t.Errorf("Expected exists %t, got %t", tt.expectExists, exists) + } + }) + } +} + +func TestGetByIndex(t *testing.T) { + tests := []struct { + name string + input []string + idx int + expectKey string + expectVal interface{} + expectPanic bool + }{ + { + "get by valid index", + []string{"C", "A", "B", "E", "D"}, + 2, + "C", + nil, + false, + }, + { + "get by valid index (smallest)", + []string{"C", "A", "B", "E", "D"}, + 0, + "A", + nil, + false, + }, + { + "get by valid index (largest)", + []string{"C", "A", "B", "E", "D"}, + 4, + "E", + nil, + false, + }, + { + "get by invalid index (negative)", + []string{"C", "A", "B", "E", "D"}, + -1, + "", + nil, + true, + }, + { + "get by invalid index (out of range)", + []string{"C", "A", "B", "E", "D"}, + 5, + "", + nil, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + if tt.expectPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected a panic but didn't get one") + } + }() + } + + key, val := tree.GetByIndex(tt.idx) + + if !tt.expectPanic { + if key != tt.expectKey { + t.Errorf("Expected key %s, got %s", tt.expectKey, key) + } + + if val != tt.expectVal { + t.Errorf("Expected value %v, got %v", tt.expectVal, val) + } + } + }) + } +} + +func TestRemove(t *testing.T) { + tests := []struct { + name string + input []string + removeKey string + expected []string + }{ + { + "remove leaf node", + []string{"C", "A", "B", "D"}, + "B", + []string{"A", "C", "D"}, + }, + { + "remove node with one child", + []string{"C", "A", "B", "D"}, + "A", + []string{"B", "C", "D"}, + }, + { + "remove node with two children", + []string{"C", "A", "B", "E", "D"}, + "C", + []string{"A", "B", "D", "E"}, + }, + { + "remove root node", + []string{"C", "A", "B", "E", "D"}, + "C", + []string{"A", "B", "D", "E"}, + }, + { + "remove non-existent key", + []string{"C", "A", "B", "E", "D"}, + "F", + []string{"A", "B", "C", "D", "E"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + tree, _, _, _ = tree.Remove(tt.removeKey) + + result := make([]string, 0) + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + } +} + +func TestTraverse(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + "empty tree", + []string{}, + []string{}, + }, + { + "single node tree", + []string{"A"}, + []string{"A"}, + }, + { + "small tree", + []string{"C", "A", "B", "E", "D"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "large tree", + []string{"H", "D", "L", "B", "F", "J", "N", "A", "C", "E", "G", "I", "K", "M", "O"}, + []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + t.Run("iterate", func(t *testing.T) { + var result []string + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + + t.Run("ReverseIterate", func(t *testing.T) { + var result []string + tree.ReverseIterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + expected := make([]string, len(tt.expected)) + copy(expected, tt.expected) + for i, j := 0, len(expected)-1; i < j; i, j = i+1, j-1 { + expected[i], expected[j] = expected[j], expected[i] + } + if !slicesEqual(expected, result) { + t.Errorf("want %v got %v", expected, result) + } + }) + + t.Run("TraverseInRange", func(t *testing.T) { + var result []string + start, end := "C", "M" + tree.TraverseInRange(start, end, true, true, func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + expected := make([]string, 0) + for _, key := range tt.expected { + if key >= start && key < end { + expected = append(expected, key) + } + } + if !slicesEqual(expected, result) { + t.Errorf("want %v got %v", expected, result) + } + }) + }) + } +} + +func TestRotateWhenHeightDiffers(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + "right rotation when left subtree is higher", + []string{"E", "C", "A", "B", "D"}, + []string{"A", "B", "C", "E", "D"}, + }, + { + "left rotation when right subtree is higher", + []string{"A", "C", "E", "D", "F"}, + []string{"A", "C", "D", "E", "F"}, + }, + { + "left-right rotation", + []string{"E", "A", "C", "B", "D"}, + []string{"A", "B", "C", "E", "D"}, + }, + { + "right-left rotation", + []string{"A", "E", "C", "B", "D"}, + []string{"A", "B", "C", "E", "D"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + // perform rotation or balance + tree = tree.balance() + + // check tree structure + var result []string + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + } +} + +func TestRotateAndBalance(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + { + "right rotation", + []string{"A", "B", "C", "D", "E"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "left rotation", + []string{"E", "D", "C", "B", "A"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "left-right rotation", + []string{"C", "A", "E", "B", "D"}, + []string{"A", "B", "C", "D", "E"}, + }, + { + "right-left rotation", + []string{"C", "E", "A", "D", "B"}, + []string{"A", "B", "C", "D", "E"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var tree *Node + for _, key := range tt.input { + tree, _ = tree.Set(key, nil) + } + + tree = tree.balance() + + var result []string + tree.Iterate("", "", func(n *Node) bool { + result = append(result, n.Key()) + return false + }) + + if !slicesEqual(tt.expected, result) { + t.Errorf("want %v got %v", tt.expected, result) + } + }) + } +} + +func slicesEqual(w1, w2 []string) bool { + if len(w1) != len(w2) { + return false + } + for i := 0; i < len(w1); i++ { + if w1[0] != w2[0] { + return false + } + } + return true +} + +func maxint8(a, b int8) int8 { + if a > b { + return a + } + return b +} + +func reverseSlice(ss []string) { + for i := 0; i < len(ss)/2; i++ { + j := len(ss) - 1 - i + ss[i], ss[j] = ss[j], ss[i] + } +} + +func TestNodeStructuralSharing(t *testing.T) { + t.Run("unmodified paths remain shared", func(t *testing.T) { + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + + originalRight := root.rightNode + newRoot, _ := root.Set("A", 10) + + if newRoot.rightNode != originalRight { + t.Error("Unmodified right subtree should remain shared") + } + }) + + t.Run("multiple modifications reuse shared structure", func(t *testing.T) { + // Create initial tree + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + + // Store original nodes + originalRight := root.rightNode + + // First modification + mod1, _ := root.Set("A", 10) + + // Second modification + mod2, _ := mod1.Set("C", 30) + + // Check sharing in first modification + if mod1.rightNode != originalRight { + t.Error("First modification should share unmodified right subtree") + } + + // Check that second modification creates new right node + if mod2.rightNode == originalRight { + t.Error("Second modification should create new right node") + } + }) +} + +func TestNodeCopyOnWrite(t *testing.T) { + t.Run("copy preserves structure", func(t *testing.T) { + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + + // Only copy non-leaf nodes + if !root.IsLeaf() { + copied := root._copy() + if copied == root { + t.Error("Copy should create new instance") + } + + // Create temporary trees to use Equal method + original := &Tree{node: root} + copiedTree := &Tree{node: copied} + if !original.Equal(copiedTree) { + t.Error("Copied node should preserve structure") + } + } + }) + + t.Run("removal copy pattern", func(t *testing.T) { + // Create a more complex tree to test removal + root := NewNode("B", 2) + root, _ = root.Set("A", 1) + root, _ = root.Set("C", 3) + root, _ = root.Set("D", 4) // Add this to ensure proper tree structure + + // Store references to original nodes + originalRight := root.rightNode + originalRightRight := originalRight.rightNode + + // Remove "A" which should only affect the left subtree + newRoot, _, _, _ := root.Remove("A") + + // Verify right subtree remains unchanged and shared + if newRoot.rightNode != originalRight { + t.Error("Right subtree should remain shared during removal of left node") + } + + // Also verify deeper nodes remain shared + if newRoot.rightNode.rightNode != originalRightRight { + t.Error("Deep right subtree should remain shared during removal") + } + + // Verify original tree is unchanged + if _, _, exists := root.Get("A"); !exists { + t.Error("Original tree should remain unchanged") + } + }) + + t.Run("copy leaf node panic", func(t *testing.T) { + leaf := NewNode("A", 1) + + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic when copying leaf node") + } + }() + + // This should panic with our specific message + leaf._copy() + }) +} + +func TestNodeEqual(t *testing.T) { + tests := []struct { + name string + node1 func() *Node + node2 func() *Node + expected bool + }{ + { + name: "nil nodes", + node1: func() *Node { return nil }, + node2: func() *Node { return nil }, + expected: true, + }, + { + name: "one nil node", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return nil }, + expected: false, + }, + { + name: "single leaf nodes equal", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return NewNode("A", 1) }, + expected: true, + }, + { + name: "single leaf nodes different key", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return NewNode("B", 1) }, + expected: false, + }, + { + name: "single leaf nodes different value", + node1: func() *Node { return NewNode("A", 1) }, + node2: func() *Node { return NewNode("A", 2) }, + expected: false, + }, + { + name: "complex trees equal", + node1: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + n, _ = n.Set("C", 3) + return n + }, + node2: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + n, _ = n.Set("C", 3) + return n + }, + expected: true, + }, + { + name: "complex trees different structure", + node1: func() *Node { + // Create a tree with structure: + // B + // / \ + // A D + n := NewNode("B", 2) + n, _ = n.Set("A", 1) + n, _ = n.Set("D", 4) + return n + }, + node2: func() *Node { + // Create a tree with structure: + // C + // / \ + // A D + n := NewNode("C", 3) + n, _ = n.Set("A", 1) + n, _ = n.Set("D", 4) + return n + }, + expected: false, // These trees should be different + }, + { + name: "complex trees same structure despite different insertion order", + node1: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + n, _ = n.Set("C", 3) + return n + }, + node2: func() *Node { + n, _ := NewNode("A", 1).Set("B", 2) + n, _ = n.Set("C", 3) + return n + }, + expected: true, + }, + { + name: "truly different structures", + node1: func() *Node { + n, _ := NewNode("B", 2).Set("A", 1) + return n // Tree with just two nodes + }, + node2: func() *Node { + n, _ := NewNode("B", 2).Set("C", 3) + return n // Different two-node tree + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node1 := tt.node1() + node2 := tt.node2() + result := node1.Equal(node2) + if result != tt.expected { + t.Errorf("Expected Equal to return %v for %s", tt.expected, tt.name) + println("\nComparison failed:") + println("Tree 1:") + printTree(node1, 0) + println("Tree 2:") + printTree(node2, 0) + } + }) + } +} + +// Helper function to print tree structure +func printTree(node *Node, level int) { + if node == nil { + return + } + indent := strings.Repeat(" ", level) + println(fmt.Sprintf("%sKey: %s, Value: %v, Height: %d, Size: %d", + indent, node.key, node.value, node.height, node.size)) + printTree(node.leftNode, level+1) + printTree(node.rightNode, level+1) +} diff --git a/examples/gno.land/p/moul/cow/tree.gno b/examples/gno.land/p/moul/cow/tree.gno new file mode 100644 index 00000000000..befd0a414f6 --- /dev/null +++ b/examples/gno.land/p/moul/cow/tree.gno @@ -0,0 +1,164 @@ +// Package cow provides a Copy-on-Write (CoW) AVL tree implementation. +// This is a fork of gno.land/p/demo/avl that adds CoW functionality +// while maintaining the original AVL tree interface and properties. +// +// Copy-on-Write creates a copy of a data structure only when it is modified, +// while still presenting the appearance of a full copy. When a tree is cloned, +// it initially shares all its nodes with the original tree. Only when a +// modification is made to either the original or the clone are new nodes created, +// and only along the path from the root to the modified node. +// +// Key features: +// - O(1) cloning operation +// - Minimal memory usage through structural sharing +// - Full AVL tree functionality (self-balancing, ordered operations) +// - Thread-safe for concurrent reads of shared structures +// +// While the CoW mechanism handles structural copying automatically, users need +// to consider how to handle the values stored in the tree: +// +// 1. Simple Values (int, string, etc.): +// - These are copied by value automatically +// - No additional handling needed +// +// 2. Complex Values (structs, pointers): +// - Only the reference is copied by default +// - Users must implement their own deep copy mechanism if needed +// +// Example: +// +// // Create original tree +// original := cow.NewTree() +// original.Set("key1", "value1") +// +// // Create a clone - O(1) operation +// clone := original.Clone() +// +// // Modify clone - only affected nodes are copied +// clone.Set("key1", "modified") +// +// // Original remains unchanged +// val, _ := original.Get("key1") // Returns "value1" +package cow + +type IterCbFn func(key string, value interface{}) bool + +//---------------------------------------- +// Tree + +// The zero struct can be used as an empty tree. +type Tree struct { + node *Node +} + +// NewTree creates a new empty AVL tree. +func NewTree() *Tree { + return &Tree{ + node: nil, + } +} + +// Size returns the number of key-value pair in the tree. +func (tree *Tree) Size() int { + return tree.node.Size() +} + +// Has checks whether a key exists in the tree. +// It returns true if the key exists, otherwise false. +func (tree *Tree) Has(key string) (has bool) { + return tree.node.Has(key) +} + +// Get retrieves the value associated with the given key. +// It returns the value and a boolean indicating whether the key exists. +func (tree *Tree) Get(key string) (value interface{}, exists bool) { + _, value, exists = tree.node.Get(key) + return +} + +// GetByIndex retrieves the key-value pair at the specified index in the tree. +// It returns the key and value at the given index. +func (tree *Tree) GetByIndex(index int) (key string, value interface{}) { + return tree.node.GetByIndex(index) +} + +// Set inserts a key-value pair into the tree. +// If the key already exists, the value will be updated. +// It returns a boolean indicating whether the key was newly inserted or updated. +func (tree *Tree) Set(key string, value interface{}) (updated bool) { + newnode, updated := tree.node.Set(key, value) + tree.node = newnode + return updated +} + +// Remove removes a key-value pair from the tree. +// It returns the removed value and a boolean indicating whether the key was found and removed. +func (tree *Tree) Remove(key string) (value interface{}, removed bool) { + newnode, _, value, removed := tree.node.Remove(key) + tree.node = newnode + return value, removed +} + +// Iterate performs an in-order traversal of the tree within the specified key range. +// It calls the provided callback function for each key-value pair encountered. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) Iterate(start, end string, cb IterCbFn) bool { + return tree.node.TraverseInRange(start, end, true, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range. +// It calls the provided callback function for each key-value pair encountered. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) ReverseIterate(start, end string, cb IterCbFn) bool { + return tree.node.TraverseInRange(start, end, false, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// IterateByOffset performs an in-order traversal of the tree starting from the specified offset. +// It calls the provided callback function for each key-value pair encountered, up to the specified count. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) IterateByOffset(offset int, count int, cb IterCbFn) bool { + return tree.node.TraverseByOffset(offset, count, true, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset. +// It calls the provided callback function for each key-value pair encountered, up to the specified count. +// If the callback returns true, the iteration is stopped. +func (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool { + return tree.node.TraverseByOffset(offset, count, false, true, + func(node *Node) bool { + return cb(node.Key(), node.Value()) + }, + ) +} + +// Equal returns true if the two trees contain the same key-value pairs. +// WARNING: This is an expensive operation that recursively traverses the entire tree structure. +// It should only be used in tests or when absolutely necessary. +func (tree *Tree) Equal(other *Tree) bool { + if tree == nil || other == nil { + return tree == other + } + return tree.node.Equal(other.node) +} + +// Clone creates a shallow copy of the tree +func (tree *Tree) Clone() *Tree { + if tree == nil { + return nil + } + return &Tree{ + node: tree.node, + } +} diff --git a/examples/gno.land/p/moul/cow/tree_test.gno b/examples/gno.land/p/moul/cow/tree_test.gno new file mode 100644 index 00000000000..6ee816455b8 --- /dev/null +++ b/examples/gno.land/p/moul/cow/tree_test.gno @@ -0,0 +1,392 @@ +package cow + +import ( + "testing" +) + +func TestNewTree(t *testing.T) { + tree := NewTree() + if tree.node != nil { + t.Error("Expected tree.node to be nil") + } +} + +func TestTreeSize(t *testing.T) { + tree := NewTree() + if tree.Size() != 0 { + t.Error("Expected empty tree size to be 0") + } + + tree.Set("key1", "value1") + tree.Set("key2", "value2") + if tree.Size() != 2 { + t.Error("Expected tree size to be 2") + } +} + +func TestTreeHas(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + + if !tree.Has("key1") { + t.Error("Expected tree to have key1") + } + + if tree.Has("key2") { + t.Error("Expected tree to not have key2") + } +} + +func TestTreeGet(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + + value, exists := tree.Get("key1") + if !exists || value != "value1" { + t.Error("Expected Get to return value1 and true") + } + + _, exists = tree.Get("key2") + if exists { + t.Error("Expected Get to return false for non-existent key") + } +} + +func TestTreeGetByIndex(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + + key, value := tree.GetByIndex(0) + if key != "key1" || value != "value1" { + t.Error("Expected GetByIndex(0) to return key1 and value1") + } + + key, value = tree.GetByIndex(1) + if key != "key2" || value != "value2" { + t.Error("Expected GetByIndex(1) to return key2 and value2") + } + + defer func() { + if r := recover(); r == nil { + t.Error("Expected GetByIndex to panic for out-of-range index") + } + }() + tree.GetByIndex(2) +} + +func TestTreeRemove(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + + value, removed := tree.Remove("key1") + if !removed || value != "value1" || tree.Size() != 0 { + t.Error("Expected Remove to remove key-value pair") + } + + _, removed = tree.Remove("key2") + if removed { + t.Error("Expected Remove to return false for non-existent key") + } +} + +func TestTreeIterate(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.Iterate("", "", func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key1", "key2", "key3"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +func TestTreeReverseIterate(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.ReverseIterate("", "", func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key3", "key2", "key1"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +func TestTreeIterateByOffset(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.IterateByOffset(1, 2, func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key2", "key3"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +func TestTreeReverseIterateByOffset(t *testing.T) { + tree := NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + var keys []string + tree.ReverseIterateByOffset(1, 2, func(key string, value interface{}) bool { + keys = append(keys, key) + return false + }) + + expectedKeys := []string{"key2", "key1"} + if !slicesEqual(keys, expectedKeys) { + t.Errorf("Expected keys %v, got %v", expectedKeys, keys) + } +} + +// Verify that Tree implements avl.ITree +// var _ avl.ITree = (*Tree)(nil) // TODO: fix gnovm bug: ./examples/gno.land/p/moul/cow: test pkg: panic: gno.land/p/moul/cow/tree_test.gno:166:5: name avl not defined in fileset with files [node.gno tree.gno node_test.gno tree_test.gno]: + +func TestCopyOnWrite(t *testing.T) { + // Create original tree + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + original.Set("C", 3) + + // Create a clone + clone := original.Clone() + + // Modify clone + clone.Set("B", 20) + clone.Set("D", 4) + + // Verify original is unchanged + if val, _ := original.Get("B"); val != 2 { + t.Errorf("Original tree was modified: expected B=2, got B=%v", val) + } + if original.Has("D") { + t.Error("Original tree was modified: found key D") + } + + // Verify clone has new values + if val, _ := clone.Get("B"); val != 20 { + t.Errorf("Clone not updated: expected B=20, got B=%v", val) + } + if val, _ := clone.Get("D"); val != 4 { + t.Errorf("Clone not updated: expected D=4, got D=%v", val) + } +} + +func TestCopyOnWriteEdgeCases(t *testing.T) { + t.Run("nil tree clone", func(t *testing.T) { + var original *Tree + clone := original.Clone() + if clone != nil { + t.Error("Expected nil clone from nil tree") + } + }) + + t.Run("empty tree clone", func(t *testing.T) { + original := NewTree() + clone := original.Clone() + + // Modify clone + clone.Set("A", 1) + + if original.Size() != 0 { + t.Error("Original empty tree was modified") + } + if clone.Size() != 1 { + t.Error("Clone was not modified") + } + }) + + t.Run("multiple clones", func(t *testing.T) { + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + + // Create multiple clones + clone1 := original.Clone() + clone2 := original.Clone() + clone3 := clone1.Clone() + + // Modify each clone differently + clone1.Set("A", 10) + clone2.Set("B", 20) + clone3.Set("C", 30) + + // Check original remains unchanged + if val, _ := original.Get("A"); val != 1 { + t.Errorf("Original modified: expected A=1, got A=%v", val) + } + if val, _ := original.Get("B"); val != 2 { + t.Errorf("Original modified: expected B=2, got B=%v", val) + } + + // Verify each clone has correct values + if val, _ := clone1.Get("A"); val != 10 { + t.Errorf("Clone1 incorrect: expected A=10, got A=%v", val) + } + if val, _ := clone2.Get("B"); val != 20 { + t.Errorf("Clone2 incorrect: expected B=20, got B=%v", val) + } + if val, _ := clone3.Get("C"); val != 30 { + t.Errorf("Clone3 incorrect: expected C=30, got C=%v", val) + } + }) + + t.Run("clone after removal", func(t *testing.T) { + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + original.Set("C", 3) + + // Remove a node and then clone + original.Remove("B") + clone := original.Clone() + + // Modify clone + clone.Set("B", 20) + + // Verify original state + if original.Has("B") { + t.Error("Original tree should not have key B") + } + + // Verify clone state + if val, _ := clone.Get("B"); val != 20 { + t.Errorf("Clone incorrect: expected B=20, got B=%v", val) + } + }) + + t.Run("concurrent modifications", func(t *testing.T) { + original := NewTree() + original.Set("A", 1) + original.Set("B", 2) + + clone1 := original.Clone() + clone2 := original.Clone() + + // Modify same key in different clones + clone1.Set("B", 20) + clone2.Set("B", 30) + + // Each clone should have its own value + if val, _ := clone1.Get("B"); val != 20 { + t.Errorf("Clone1 incorrect: expected B=20, got B=%v", val) + } + if val, _ := clone2.Get("B"); val != 30 { + t.Errorf("Clone2 incorrect: expected B=30, got B=%v", val) + } + }) + + t.Run("deep tree modifications", func(t *testing.T) { + original := NewTree() + // Create a deeper tree + keys := []string{"M", "F", "T", "B", "H", "P", "Z"} + for _, k := range keys { + original.Set(k, k) + } + + clone := original.Clone() + + // Modify a deep node + clone.Set("H", "modified") + + // Check original remains unchanged + if val, _ := original.Get("H"); val != "H" { + t.Errorf("Original modified: expected H='H', got H=%v", val) + } + + // Verify clone modification + if val, _ := clone.Get("H"); val != "modified" { + t.Errorf("Clone incorrect: expected H='modified', got H=%v", val) + } + }) + + t.Run("rebalancing test", func(t *testing.T) { + original := NewTree() + // Insert nodes that will cause rotations + keys := []string{"A", "B", "C", "D", "E"} + for _, k := range keys { + original.Set(k, k) + } + + clone := original.Clone() + + // Add more nodes to clone to trigger rebalancing + clone.Set("F", "F") + clone.Set("G", "G") + + // Verify original structure remains unchanged + originalKeys := collectKeys(original) + expectedOriginal := []string{"A", "B", "C", "D", "E"} + if !slicesEqual(originalKeys, expectedOriginal) { + t.Errorf("Original tree structure changed: got %v, want %v", originalKeys, expectedOriginal) + } + + // Verify clone has all keys + cloneKeys := collectKeys(clone) + expectedClone := []string{"A", "B", "C", "D", "E", "F", "G"} + if !slicesEqual(cloneKeys, expectedClone) { + t.Errorf("Clone tree structure incorrect: got %v, want %v", cloneKeys, expectedClone) + } + }) + + t.Run("value mutation test", func(t *testing.T) { + type MutableValue struct { + Data string + } + + original := NewTree() + mutable := &MutableValue{Data: "original"} + original.Set("key", mutable) + + clone := original.Clone() + + // Modify the mutable value + mutable.Data = "modified" + + // Both original and clone should see the modification + // because we're not deep copying values + origVal, _ := original.Get("key") + cloneVal, _ := clone.Get("key") + + if origVal.(*MutableValue).Data != "modified" { + t.Error("Original value not modified as expected") + } + if cloneVal.(*MutableValue).Data != "modified" { + t.Error("Clone value not modified as expected") + } + }) +} + +// Helper function to collect all keys in order +func collectKeys(tree *Tree) []string { + var keys []string + tree.Iterate("", "", func(key string, _ interface{}) bool { + keys = append(keys, key) + return false + }) + return keys +} diff --git a/examples/gno.land/p/moul/helplink/helplink_test.gno b/examples/gno.land/p/moul/helplink/helplink_test.gno index 29cfd02eb67..440001b94ce 100644 --- a/examples/gno.land/p/moul/helplink/helplink_test.gno +++ b/examples/gno.land/p/moul/helplink/helplink_test.gno @@ -18,7 +18,7 @@ func TestFunc(t *testing.T) { {"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)", ""}, + {"Odd Args", "oddArgsFunc", []string{"key"}, "[Odd Args]($help&func=oddArgsFunc&error=odd+number+of+arguments)", ""}, } for _, tt := range tests { @@ -39,15 +39,15 @@ func TestFuncURL(t *testing.T) { {"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", ""}, + {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc&error=odd+number+of+arguments", ""}, {"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"}, + {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "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"}, + {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "gno.world/r/lorem/ipsum"}, } for _, tt := range tests { diff --git a/examples/gno.land/p/moul/txlink/txlink.gno b/examples/gno.land/p/moul/txlink/txlink.gno index 65edda6911e..8f753b4546d 100644 --- a/examples/gno.land/p/moul/txlink/txlink.gno +++ b/examples/gno.land/p/moul/txlink/txlink.gno @@ -15,6 +15,7 @@ package txlink import ( + "net/url" "std" "strings" ) @@ -51,24 +52,26 @@ func (r Realm) prefix() string { // 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 + if len(args) == 0 { + return r.prefix() + "$help&func=" + fn + } + + // Create url.Values to properly encode parameters. + // But manage &func=fn as a special case to keep it as the first argument. + values := url.Values{} // 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 + values.Add("error", "odd number of arguments") + } else { + // Add key-value pairs to values + for i := 0; i < len(args); i += 2 { + key := args[i] + value := args[i+1] + values.Add(key, value) + } } - return url + // Build the base URL and append encoded query parameters + return r.prefix() + "$help&func=" + fn + "&" + values.Encode() } diff --git a/examples/gno.land/p/moul/txlink/txlink_test.gno b/examples/gno.land/p/moul/txlink/txlink_test.gno index 61b532270d4..1da396b27a3 100644 --- a/examples/gno.land/p/moul/txlink/txlink_test.gno +++ b/examples/gno.land/p/moul/txlink/txlink_test.gno @@ -16,19 +16,22 @@ func TestCall(t *testing.T) { {"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", ""}, + {"oddArgsFunc", []string{"key"}, "$help&func=oddArgsFunc&error=odd+number+of+arguments", ""}, {"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"}, + {"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "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"}, + {"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum$help&func=oddArgsFunc&error=odd+number+of+arguments", "gno.world/r/lorem/ipsum"}, + {"test", []string{"key", "hello world"}, "$help&func=test&key=hello+world", ""}, + {"test", []string{"key", "a&b=c"}, "$help&func=test&key=a%26b%3Dc", ""}, + {"test", []string{"key", ""}, "$help&func=test&key=", ""}, } for _, tt := range tests { - title := tt.fn + title := string(tt.realm) + "_" + 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/oxtekgrinder/ownable2step/errors.gno b/examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno new file mode 100644 index 00000000000..6d91c9eb24b --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/errors.gno @@ -0,0 +1,10 @@ +package ownable2step + +import "errors" + +var ( + ErrNoPendingOwner = errors.New("ownable2step: no pending owner") + ErrUnauthorized = errors.New("ownable2step: caller is not owner") + ErrPendingUnauthorized = errors.New("ownable2step: caller is not pending owner") + ErrInvalidAddress = errors.New("ownable2step: new owner address is invalid") +) diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod b/examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod new file mode 100644 index 00000000000..0132a03418c --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/gno.mod @@ -0,0 +1 @@ +module gno.land/p/oxtekgrinder/ownable2step diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno new file mode 100644 index 00000000000..43afa1cd141 --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable.gno @@ -0,0 +1,98 @@ +package ownable2step + +import ( + "std" +) + +const OwnershipTransferEvent = "OwnershipTransfer" + +// Ownable2Step is a two-step ownership transfer package +// It allows the current owner to set a new owner and the new owner will need to accept the ownership before it is transferred +type Ownable2Step struct { + owner std.Address + pendingOwner std.Address +} + +func New() *Ownable2Step { + return &Ownable2Step{ + owner: std.PrevRealm().Addr(), + pendingOwner: "", + } +} + +func NewWithAddress(addr std.Address) *Ownable2Step { + return &Ownable2Step{ + owner: addr, + pendingOwner: "", + } +} + +// TransferOwnership initiate the transfer of the ownership to a new address by setting the PendingOwner +func (o *Ownable2Step) TransferOwnership(newOwner std.Address) error { + if !o.CallerIsOwner() { + return ErrUnauthorized + } + if !newOwner.IsValid() { + return ErrInvalidAddress + } + + o.pendingOwner = newOwner + return nil +} + +// AcceptOwnership accepts the pending ownership transfer +func (o *Ownable2Step) AcceptOwnership() error { + if o.pendingOwner.String() == "" { + return ErrNoPendingOwner + } + if std.PrevRealm().Addr() != o.pendingOwner { + return ErrPendingUnauthorized + } + + o.owner = o.pendingOwner + o.pendingOwner = "" + + return nil +} + +// DropOwnership removes the owner, effectively disabling any owner-related actions +// Top-level usage: disables all only-owner actions/functions, +// Embedded usage: behaves like a burn functionality, removing the owner from the struct +func (o *Ownable2Step) DropOwnership() error { + if !o.CallerIsOwner() { + return ErrUnauthorized + } + + prevOwner := o.owner + o.owner = "" + + std.Emit( + OwnershipTransferEvent, + "from", prevOwner.String(), + "to", "", + ) + + return nil +} + +// Owner returns the owner address from Ownable +func (o *Ownable2Step) Owner() std.Address { + return o.owner +} + +// PendingOwner returns the pending owner address from Ownable2Step +func (o *Ownable2Step) PendingOwner() std.Address { + return o.pendingOwner +} + +// CallerIsOwner checks if the caller of the function is the Realm's owner +func (o *Ownable2Step) CallerIsOwner() bool { + return std.PrevRealm().Addr() == o.owner +} + +// AssertCallerIsOwner panics if the caller is not the owner +func (o *Ownable2Step) AssertCallerIsOwner() { + if std.PrevRealm().Addr() != o.owner { + panic(ErrUnauthorized) + } +} diff --git a/examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno new file mode 100644 index 00000000000..4cca03b6ef5 --- /dev/null +++ b/examples/gno.land/p/oxtekgrinder/ownable2step/ownable_test.gno @@ -0,0 +1,156 @@ +package ownable2step + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") +) + +func TestNew(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + got := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, got, alice) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestNewWithAddress(t *testing.T) { + o := NewWithAddress(alice) + + got := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, got, alice) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestInitiateTransferOwnership(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + owner := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, owner, alice) + uassert.Equal(t, pendingOwner, bob) +} + +func TestTransferOwnership(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + owner := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, owner, alice) + uassert.Equal(t, pendingOwner, bob) + + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + err = o.AcceptOwnership() + urequire.NoError(t, err) + + owner = o.Owner() + pendingOwner = o.PendingOwner() + + uassert.Equal(t, owner, bob) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestCallerIsOwner(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + unauthorizedCaller := bob + + std.TestSetRealm(std.NewUserRealm(unauthorizedCaller)) + std.TestSetOrigCaller(unauthorizedCaller) + + uassert.False(t, o.CallerIsOwner()) +} + +func TestDropOwnership(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.DropOwnership() + urequire.NoError(t, err, "DropOwnership failed") + + owner := o.Owner() + uassert.Empty(t, owner, "owner should be empty") +} + +// Errors + +func TestErrUnauthorized(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + std.TestSetOrigCaller(alice) + + o := New() + + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) + + uassert.ErrorContains(t, o.TransferOwnership(alice), ErrUnauthorized.Error()) + uassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error()) +} + +func TestErrInvalidAddress(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.TransferOwnership("") + uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) + + err = o.TransferOwnership("10000000001000000000100000000010000000001000000000") + uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) +} + +func TestErrNoPendingOwner(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.AcceptOwnership() + uassert.ErrorContains(t, err, ErrNoPendingOwner.Error()) +} + +func TestErrPendingUnauthorized(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + o := New() + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + std.TestSetRealm(std.NewUserRealm(alice)) + + err = o.AcceptOwnership() + uassert.ErrorContains(t, err, ErrPendingUnauthorized.Error()) +} diff --git a/examples/gno.land/p/sunspirit/md/gno.mod b/examples/gno.land/p/sunspirit/md/gno.mod new file mode 100644 index 00000000000..caee634f66f --- /dev/null +++ b/examples/gno.land/p/sunspirit/md/gno.mod @@ -0,0 +1 @@ +module gno.land/p/sunspirit/md diff --git a/examples/gno.land/p/sunspirit/md/md.gno b/examples/gno.land/p/sunspirit/md/md.gno new file mode 100644 index 00000000000..965373bee85 --- /dev/null +++ b/examples/gno.land/p/sunspirit/md/md.gno @@ -0,0 +1,179 @@ +package md + +import ( + "strings" + + "gno.land/p/demo/ufmt" +) + +// Builder helps to build a Markdown string from individual elements +type Builder struct { + elements []string +} + +// NewBuilder creates a new Builder instance +func NewBuilder() *Builder { + return &Builder{} +} + +// Add adds a Markdown element to the builder +func (m *Builder) Add(md ...string) *Builder { + m.elements = append(m.elements, md...) + return m +} + +// Render returns the final Markdown string joined with the specified separator +func (m *Builder) Render(separator string) string { + return strings.Join(m.elements, separator) +} + +// Bold returns bold text for markdown +func Bold(text string) string { + return ufmt.Sprintf("**%s**", text) +} + +// Italic returns italicized text for markdown +func Italic(text string) string { + return ufmt.Sprintf("*%s*", text) +} + +// Strikethrough returns strikethrough text for markdown +func Strikethrough(text string) string { + return ufmt.Sprintf("~~%s~~", text) +} + +// H1 returns a level 1 header for markdown +func H1(text string) string { + return ufmt.Sprintf("# %s\n", text) +} + +// H2 returns a level 2 header for markdown +func H2(text string) string { + return ufmt.Sprintf("## %s\n", text) +} + +// H3 returns a level 3 header for markdown +func H3(text string) string { + return ufmt.Sprintf("### %s\n", text) +} + +// H4 returns a level 4 header for markdown +func H4(text string) string { + return ufmt.Sprintf("#### %s\n", text) +} + +// H5 returns a level 5 header for markdown +func H5(text string) string { + return ufmt.Sprintf("##### %s\n", text) +} + +// H6 returns a level 6 header for markdown +func H6(text string) string { + return ufmt.Sprintf("###### %s\n", text) +} + +// BulletList returns an bullet list for markdown +func BulletList(items []string) string { + var sb strings.Builder + for _, item := range items { + sb.WriteString(ufmt.Sprintf("- %s\n", item)) + } + return sb.String() +} + +// OrderedList returns an ordered list for markdown +func OrderedList(items []string) string { + var sb strings.Builder + for i, item := range items { + sb.WriteString(ufmt.Sprintf("%d. %s\n", i+1, item)) + } + return sb.String() +} + +// TodoList returns a list of todo items with checkboxes for markdown +func TodoList(items []string, done []bool) string { + var sb strings.Builder + + for i, item := range items { + checkbox := " " + if done[i] { + checkbox = "x" + } + sb.WriteString(ufmt.Sprintf("- [%s] %s\n", checkbox, item)) + } + return sb.String() +} + +// Blockquote returns a blockquote for markdown +func Blockquote(text string) string { + lines := strings.Split(text, "\n") + var sb strings.Builder + for _, line := range lines { + sb.WriteString(ufmt.Sprintf("> %s\n", line)) + } + + return sb.String() +} + +// InlineCode returns inline code for markdown +func InlineCode(code string) string { + return ufmt.Sprintf("`%s`", code) +} + +// CodeBlock creates a markdown code block +func CodeBlock(content string) string { + return ufmt.Sprintf("```\n%s\n```", content) +} + +// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting +func LanguageCodeBlock(language, content string) string { + return ufmt.Sprintf("```%s\n%s\n```", language, content) +} + +// LineBreak returns the specified number of line breaks for markdown +func LineBreak(count uint) string { + if count > 0 { + return strings.Repeat("\n", int(count)+1) + } + return "" +} + +// HorizontalRule returns a horizontal rule for markdown +func HorizontalRule() string { + return "---\n" +} + +// Link returns a hyperlink for markdown +func Link(text, url string) string { + return ufmt.Sprintf("[%s](%s)", text, url) +} + +// Image returns an image for markdown +func Image(altText, url string) string { + return ufmt.Sprintf("![%s](%s)", altText, url) +} + +// Footnote returns a footnote for markdown +func Footnote(reference, text string) string { + return ufmt.Sprintf("[%s]: %s", reference, text) +} + +// Paragraph wraps the given text in a Markdown paragraph +func Paragraph(content string) string { + return ufmt.Sprintf("%s\n", content) +} + +// MdTable is an interface for table types that can be converted to Markdown format +type MdTable interface { + String() string +} + +// Table takes any MdTable implementation and returns its markdown representation +func Table(table MdTable) string { + return table.String() +} + +// EscapeMarkdown escapes special markdown characters in a string +func EscapeMarkdown(text string) string { + return ufmt.Sprintf("``%s``", text) +} diff --git a/examples/gno.land/p/sunspirit/md/md_test.gno b/examples/gno.land/p/sunspirit/md/md_test.gno new file mode 100644 index 00000000000..529cc2535bb --- /dev/null +++ b/examples/gno.land/p/sunspirit/md/md_test.gno @@ -0,0 +1,175 @@ +package md + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/sunspirit/table" +) + +func TestNewBuilder(t *testing.T) { + mdBuilder := NewBuilder() + + uassert.Equal(t, len(mdBuilder.elements), 0, "Expected 0 elements") +} + +func TestAdd(t *testing.T) { + mdBuilder := NewBuilder() + + header := H1("Hi") + body := Paragraph("This is a test") + + mdBuilder.Add(header, body) + + uassert.Equal(t, len(mdBuilder.elements), 2, "Expected 2 element") + uassert.Equal(t, mdBuilder.elements[0], header, "Expected element %s, got %s", header, mdBuilder.elements[0]) + uassert.Equal(t, mdBuilder.elements[1], body, "Expected element %s, got %s", body, mdBuilder.elements[1]) +} + +func TestRender(t *testing.T) { + mdBuilder := NewBuilder() + + header := H1("Hello") + body := Paragraph("This is a test") + + seperator := "\n" + expected := header + seperator + body + + output := mdBuilder.Add(header, body).Render(seperator) + + uassert.Equal(t, output, expected, "Expected rendered string %s, got %s", expected, output) +} + +func Test_Bold(t *testing.T) { + uassert.Equal(t, Bold("Hello"), "**Hello**") +} + +func Test_Italic(t *testing.T) { + uassert.Equal(t, Italic("Hello"), "*Hello*") +} + +func Test_Strikethrough(t *testing.T) { + uassert.Equal(t, Strikethrough("Hello"), "~~Hello~~") +} + +func Test_H1(t *testing.T) { + uassert.Equal(t, H1("Header 1"), "# Header 1\n") +} + +func Test_H2(t *testing.T) { + uassert.Equal(t, H2("Header 2"), "## Header 2\n") +} + +func Test_H3(t *testing.T) { + uassert.Equal(t, H3("Header 3"), "### Header 3\n") +} + +func Test_H4(t *testing.T) { + uassert.Equal(t, H4("Header 4"), "#### Header 4\n") +} + +func Test_H5(t *testing.T) { + uassert.Equal(t, H5("Header 5"), "##### Header 5\n") +} + +func Test_H6(t *testing.T) { + uassert.Equal(t, H6("Header 6"), "###### Header 6\n") +} + +func Test_BulletList(t *testing.T) { + items := []string{"Item 1", "Item 2", "Item 3"} + result := BulletList(items) + expected := "- Item 1\n- Item 2\n- Item 3\n" + uassert.Equal(t, result, expected) +} + +func Test_OrderedList(t *testing.T) { + items := []string{"Item 1", "Item 2", "Item 3"} + result := OrderedList(items) + expected := "1. Item 1\n2. Item 2\n3. Item 3\n" + uassert.Equal(t, result, expected) +} + +func Test_TodoList(t *testing.T) { + items := []string{"Task 1", "Task 2"} + done := []bool{true, false} + result := TodoList(items, done) + expected := "- [x] Task 1\n- [ ] Task 2\n" + uassert.Equal(t, result, expected) +} + +func Test_Blockquote(t *testing.T) { + text := "This is a blockquote.\nIt has multiple lines." + result := Blockquote(text) + expected := "> This is a blockquote.\n> It has multiple lines.\n" + uassert.Equal(t, result, expected) +} + +func Test_InlineCode(t *testing.T) { + result := InlineCode("code") + uassert.Equal(t, result, "`code`") +} + +func Test_LanguageCodeBlock(t *testing.T) { + result := LanguageCodeBlock("python", "print('Hello')") + expected := "```python\nprint('Hello')\n```" + uassert.Equal(t, result, expected) +} + +func Test_CodeBlock(t *testing.T) { + result := CodeBlock("print('Hello')") + expected := "```\nprint('Hello')\n```" + uassert.Equal(t, result, expected) +} + +func Test_LineBreak(t *testing.T) { + result := LineBreak(2) + expected := "\n\n\n" + uassert.Equal(t, result, expected) + + result = LineBreak(0) + expected = "" + uassert.Equal(t, result, expected) +} + +func Test_HorizontalRule(t *testing.T) { + result := HorizontalRule() + uassert.Equal(t, result, "---\n") +} + +func Test_Link(t *testing.T) { + result := Link("Google", "http://google.com") + uassert.Equal(t, result, "[Google](http://google.com)") +} + +func Test_Image(t *testing.T) { + result := Image("Alt text", "http://image.url") + uassert.Equal(t, result, "![Alt text](http://image.url)") +} + +func Test_Footnote(t *testing.T) { + result := Footnote("1", "This is a footnote.") + uassert.Equal(t, result, "[1]: This is a footnote.") +} + +func Test_Paragraph(t *testing.T) { + result := Paragraph("This is a paragraph.") + uassert.Equal(t, result, "This is a paragraph.\n") +} + +func Test_Table(t *testing.T) { + tb, err := table.New([]string{"Header1", "Header2"}, [][]string{ + {"Row1Col1", "Row1Col2"}, + {"Row2Col1", "Row2Col2"}, + }) + uassert.NoError(t, err) + + result := Table(tb) + expected := "| Header1 | Header2 |\n| ---|---|\n| Row1Col1 | Row1Col2 |\n| Row2Col1 | Row2Col2 |\n" + uassert.Equal(t, result, expected) +} + +func Test_EscapeMarkdown(t *testing.T) { + result := EscapeMarkdown("- This is `code`") + uassert.Equal(t, result, "``- This is `code```") +} diff --git a/examples/gno.land/p/sunspirit/table/gno.mod b/examples/gno.land/p/sunspirit/table/gno.mod new file mode 100644 index 00000000000..1814c50b25d --- /dev/null +++ b/examples/gno.land/p/sunspirit/table/gno.mod @@ -0,0 +1 @@ +module gno.land/p/sunspirit/table diff --git a/examples/gno.land/p/sunspirit/table/table.gno b/examples/gno.land/p/sunspirit/table/table.gno new file mode 100644 index 00000000000..8c27516c962 --- /dev/null +++ b/examples/gno.land/p/sunspirit/table/table.gno @@ -0,0 +1,106 @@ +package table + +import ( + "strings" + + "gno.land/p/demo/ufmt" +) + +// Table defines the structure for a markdown table +type Table struct { + header []string + rows [][]string +} + +// Validate checks if the number of columns in each row matches the number of columns in the header +func (t *Table) Validate() error { + numCols := len(t.header) + for _, row := range t.rows { + if len(row) != numCols { + return ufmt.Errorf("row %v does not match header length %d", row, numCols) + } + } + return nil +} + +// New creates a new Table instance, ensuring the header and rows match in size +func New(header []string, rows [][]string) (*Table, error) { + t := &Table{ + header: header, + rows: rows, + } + + if err := t.Validate(); err != nil { + return nil, err + } + + return t, nil +} + +// Table returns a markdown string for the given Table +func (t *Table) String() string { + if err := t.Validate(); err != nil { + panic(err) + } + + var sb strings.Builder + + sb.WriteString("| " + strings.Join(t.header, " | ") + " |\n") + sb.WriteString("| " + strings.Repeat("---|", len(t.header)) + "\n") + + for _, row := range t.rows { + sb.WriteString("| " + strings.Join(row, " | ") + " |\n") + } + + return sb.String() +} + +// AddRow adds a new row to the table +func (t *Table) AddRow(row []string) error { + if len(row) != len(t.header) { + return ufmt.Errorf("row %v does not match header length %d", row, len(t.header)) + } + t.rows = append(t.rows, row) + return nil +} + +// AddColumn adds a new column to the table with the specified values +func (t *Table) AddColumn(header string, values []string) error { + if len(values) != len(t.rows) { + return ufmt.Errorf("values length %d does not match the number of rows %d", len(values), len(t.rows)) + } + + // Add the new header + t.header = append(t.header, header) + + // Add the new column values to each row + for i, value := range values { + t.rows[i] = append(t.rows[i], value) + } + return nil +} + +// RemoveRow removes a row from the table by its index +func (t *Table) RemoveRow(index int) error { + if index < 0 || index >= len(t.rows) { + return ufmt.Errorf("index %d is out of range", index) + } + t.rows = append(t.rows[:index], t.rows[index+1:]...) + return nil +} + +// RemoveColumn removes a column from the table by its index +func (t *Table) RemoveColumn(index int) error { + if index < 0 || index >= len(t.header) { + return ufmt.Errorf("index %d is out of range", index) + } + + // Remove the column from the header + t.header = append(t.header[:index], t.header[index+1:]...) + + // Remove the corresponding column from each row + for i := range t.rows { + t.rows[i] = append(t.rows[i][:index], t.rows[i][index+1:]...) + } + return nil +} diff --git a/examples/gno.land/p/sunspirit/table/table_test.gno b/examples/gno.land/p/sunspirit/table/table_test.gno new file mode 100644 index 00000000000..d4cd56ad0a8 --- /dev/null +++ b/examples/gno.land/p/sunspirit/table/table_test.gno @@ -0,0 +1,146 @@ +package table + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestNew(t *testing.T) { + header := []string{"Name", "Age", "Country"} + rows := [][]string{ + {"Alice", "30", "USA"}, + {"Bob", "25", "UK"}, + } + + table, err := New(header, rows) + urequire.NoError(t, err) + + uassert.Equal(t, len(header), len(table.header)) + uassert.Equal(t, len(rows), len(table.rows)) +} + +func Test_AddRow(t *testing.T) { + header := []string{"Name", "Age"} + rows := [][]string{ + {"Alice", "30"}, + {"Bob", "25"}, + } + + table, err := New(header, rows) + urequire.NoError(t, err) + + // Add a valid row + err = table.AddRow([]string{"Charlie", "28"}) + urequire.NoError(t, err) + + expectedRows := [][]string{ + {"Alice", "30"}, + {"Bob", "25"}, + {"Charlie", "28"}, + } + uassert.Equal(t, len(expectedRows), len(table.rows)) + + // Attempt to add a row with a different number of columns + err = table.AddRow([]string{"David"}) + uassert.Error(t, err) +} + +func Test_AddColumn(t *testing.T) { + header := []string{"Name", "Age"} + rows := [][]string{ + {"Alice", "30"}, + {"Bob", "25"}, + } + + table, err := New(header, rows) + urequire.NoError(t, err) + + // Add a valid column + err = table.AddColumn("Country", []string{"USA", "UK"}) + urequire.NoError(t, err) + + expectedHeader := []string{"Name", "Age", "Country"} + expectedRows := [][]string{ + {"Alice", "30", "USA"}, + {"Bob", "25", "UK"}, + } + uassert.Equal(t, len(expectedHeader), len(table.header)) + uassert.Equal(t, len(expectedRows), len(table.rows)) + + // Attempt to add a column with a different number of values + err = table.AddColumn("City", []string{"New York"}) + uassert.Error(t, err) +} + +func Test_RemoveRow(t *testing.T) { + header := []string{"Name", "Age", "Country"} + rows := [][]string{ + {"Alice", "30", "USA"}, + {"Bob", "25", "UK"}, + } + + table, err := New(header, rows) + urequire.NoError(t, err) + + // Remove the first row + err = table.RemoveRow(0) + urequire.NoError(t, err) + + expectedRows := [][]string{ + {"Bob", "25", "UK"}, + } + uassert.Equal(t, len(expectedRows), len(table.rows)) + + // Attempt to remove a row out of range + err = table.RemoveRow(5) + uassert.Error(t, err) +} + +func Test_RemoveColumn(t *testing.T) { + header := []string{"Name", "Age", "Country"} + rows := [][]string{ + {"Alice", "30", "USA"}, + {"Bob", "25", "UK"}, + } + + table, err := New(header, rows) + urequire.NoError(t, err) + + // Remove the second column (Age) + err = table.RemoveColumn(1) + urequire.NoError(t, err) + + expectedHeader := []string{"Name", "Country"} + expectedRows := [][]string{ + {"Alice", "USA"}, + {"Bob", "UK"}, + } + uassert.Equal(t, len(expectedHeader), len(table.header)) + uassert.Equal(t, len(expectedRows), len(table.rows)) + + // Attempt to remove a column out of range + err = table.RemoveColumn(5) + uassert.Error(t, err) +} + +func Test_Validate(t *testing.T) { + header := []string{"Name", "Age", "Country"} + rows := [][]string{ + {"Alice", "30", "USA"}, + {"Bob", "25"}, + } + + table, err := New(header, rows[:1]) + urequire.NoError(t, err) + + // Validate should pass + err = table.Validate() + urequire.NoError(t, err) + + // Add an invalid row and validate again + table.rows = append(table.rows, rows[1]) + err = table.Validate() + uassert.Error(t, err) +} 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 7b6758c3e4f..7bf2aea4f38 100644 --- a/examples/gno.land/r/demo/banktest/z_3_filetest.gno +++ b/examples/gno.land/r/demo/banktest/z_3_filetest.gno @@ -18,7 +18,6 @@ func main() { banker := std.GetBanker(std.BankerTypeRealmSend) send := std.Coins{{"ugnot", 123}} banker.SendCoins(banktestAddr, mainaddr, send) - } // Error: diff --git a/examples/gno.land/r/demo/boards/public.gno b/examples/gno.land/r/demo/boards/public.gno index 1d26126fcb2..db545446641 100644 --- a/examples/gno.land/r/demo/boards/public.gno +++ b/examples/gno.land/r/demo/boards/public.gno @@ -17,7 +17,7 @@ func GetBoardIDFromName(name string) (BoardID, bool) { } func CreateBoard(name string) BoardID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } bid := incGetBoardID() @@ -43,7 +43,7 @@ func checkAnonFee() bool { } func CreateThread(bid BoardID, title string, body string) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } caller := std.GetOrigCaller() @@ -61,7 +61,7 @@ func CreateThread(bid BoardID, title string, body string) PostID { } func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } caller := std.GetOrigCaller() @@ -91,7 +91,7 @@ func CreateReply(bid BoardID, threadid, postid PostID, body string) PostID { // If dstBoard is private, does not ping back. // If board specified by bid is private, panics. func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoardID BoardID) PostID { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } caller := std.GetOrigCaller() @@ -121,7 +121,7 @@ func CreateRepost(bid BoardID, postid PostID, title string, body string, dstBoar } func DeletePost(bid BoardID, threadid, postid PostID, reason string) { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } caller := std.GetOrigCaller() @@ -153,7 +153,7 @@ func DeletePost(bid BoardID, threadid, postid PostID, reason string) { } func EditPost(bid BoardID, threadid, postid PostID, title, body string) { - if !(std.IsOriginCall() || std.PrevRealm().IsUser()) { + if !std.PrevRealm().IsUser() { panic("invalid non-user call") } caller := std.GetOrigCaller() diff --git a/examples/gno.land/r/demo/boards/z_0_a_filetest.gno b/examples/gno.land/r/demo/boards/z_0_a_filetest.gno index 5e8ff520a54..297231970a5 100644 --- a/examples/gno.land/r/demo/boards/z_0_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_a_filetest.gno @@ -2,12 +2,17 @@ package boards_test import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" ) var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) bid = boards.CreateBoard("test_board") boards.CreateThread(bid, "First Post (title)", "Body of the first post. (body)") pid := boards.CreateThread(bid, "Second Post (title)", "Body of the second post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_0_b_filetest.gno b/examples/gno.land/r/demo/boards/z_0_b_filetest.gno index 9bcbe9ffafa..4830161ac71 100644 --- a/examples/gno.land/r/demo/boards/z_0_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_b_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 19900000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") } diff --git a/examples/gno.land/r/demo/boards/z_0_c_filetest.gno b/examples/gno.land/r/demo/boards/z_0_c_filetest.gno index 99fd339aed8..d0b0930240d 100644 --- a/examples/gno.land/r/demo/boards/z_0_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_c_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") boards.CreateThread(1, "First Post (title)", "Body of the first post. (body)") } diff --git a/examples/gno.land/r/demo/boards/z_0_d_filetest.gno b/examples/gno.land/r/demo/boards/z_0_d_filetest.gno index c77e60e3f3a..7e21f83febd 100644 --- a/examples/gno.land/r/demo/boards/z_0_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_d_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") boards.CreateReply(bid, 0, 0, "Reply of the second post") diff --git a/examples/gno.land/r/demo/boards/z_0_e_filetest.gno b/examples/gno.land/r/demo/boards/z_0_e_filetest.gno index 6db036e87ba..bdf6d63727b 100644 --- a/examples/gno.land/r/demo/boards/z_0_e_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_e_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") boards.CreateReply(bid, 0, 0, "Reply of the second post") } 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 a649895cb01..dc881ce46ad 100644 --- a/examples/gno.land/r/demo/boards/z_0_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_0_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 20000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var bid boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -30,12 +35,12 @@ func main() { // ## [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&postid=1&threadid=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&postid=2&threadid=2)] (1 replies) (0 reposts) // // diff --git a/examples/gno.land/r/demo/boards/z_10_a_filetest.gno b/examples/gno.land/r/demo/boards/z_10_a_filetest.gno index ad57283bfcf..fc04555bf39 100644 --- a/examples/gno.land/r/demo/boards/z_10_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_a_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -23,6 +27,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) // boardId 2 not exist boards.DeletePost(2, pid, pid, "") diff --git a/examples/gno.land/r/demo/boards/z_10_b_filetest.gno b/examples/gno.land/r/demo/boards/z_10_b_filetest.gno index cf8a332174f..2353268ef54 100644 --- a/examples/gno.land/r/demo/boards/z_10_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_b_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -25,6 +29,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) // pid of 2 not exist + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.DeletePost(bid, 2, 2, "") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) 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 7dd460500d6..8f85cf63ecb 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 @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -17,6 +19,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -26,6 +30,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.DeletePost(bid, pid, rid, "") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) @@ -35,15 +41,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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[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&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=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)] \[[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&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // // ---------------------------------------------------- // # 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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[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&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=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 8a6d11c79cf..01e0f9439c7 100644 --- a/examples/gno.land/r/demo/boards/z_10_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_10_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -23,6 +27,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) boards.DeletePost(bid, pid, pid, "") println("----------------------------------------------------") @@ -33,7 +39,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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[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&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // // ---------------------------------------------------- // thread does not exist with id: 1 diff --git a/examples/gno.land/r/demo/boards/z_11_a_filetest.gno b/examples/gno.land/r/demo/boards/z_11_a_filetest.gno index d7dc7b90782..b891e395fe6 100644 --- a/examples/gno.land/r/demo/boards/z_11_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_a_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -25,6 +29,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) // board 2 not exist + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.EditPost(2, pid, pid, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_11_b_filetest.gno b/examples/gno.land/r/demo/boards/z_11_b_filetest.gno index 3aa28095502..9322ac191c5 100644 --- a/examples/gno.land/r/demo/boards/z_11_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_b_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -25,6 +29,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) // thread 2 not exist + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.EditPost(bid, 2, pid, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) diff --git a/examples/gno.land/r/demo/boards/z_11_c_filetest.gno b/examples/gno.land/r/demo/boards/z_11_c_filetest.gno index df764303562..de4f828c1ca 100644 --- a/examples/gno.land/r/demo/boards/z_11_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_c_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -25,6 +29,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) // post 2 not exist + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.EditPost(bid, pid, 2, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) 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 f64b4c84bba..344583de7d4 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 @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -17,6 +19,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -26,6 +30,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.EditPost(bid, pid, rid, "", "Edited: First reply of the First post\n") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) @@ -35,19 +41,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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[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&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=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)] \[[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&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // // ---------------------------------------------------- // # 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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[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&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=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)] \[[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&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // 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 3f56293b3bd..4cea63126c5 100644 --- a/examples/gno.land/r/demo/boards/z_11_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_11_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -24,6 +28,8 @@ func init() { func main() { println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.EditPost(bid, pid, pid, "Edited: First Post in (title)", "Edited: Body of the first post. (body)") println("----------------------------------------------------") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) @@ -33,11 +39,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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[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&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[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&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_12_a_filetest.gno b/examples/gno.land/r/demo/boards/z_12_a_filetest.gno index 909be880efa..380e9bc09e0 100644 --- a/examples/gno.land/r/demo/boards/z_12_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_a_filetest.gno @@ -12,6 +12,8 @@ import ( ) func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") // create a post via registered user bid1 := boards.CreateBoard("test_board1") diff --git a/examples/gno.land/r/demo/boards/z_12_b_filetest.gno b/examples/gno.land/r/demo/boards/z_12_b_filetest.gno index 6b2166895c0..553108df9b3 100644 --- a/examples/gno.land/r/demo/boards/z_12_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_b_filetest.gno @@ -4,11 +4,16 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid1 := boards.CreateBoard("test_board1") pid := boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_12_c_filetest.gno b/examples/gno.land/r/demo/boards/z_12_c_filetest.gno index 7397c487d7d..3b2e7ec04c0 100644 --- a/examples/gno.land/r/demo/boards/z_12_c_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_c_filetest.gno @@ -4,11 +4,16 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid1 := boards.CreateBoard("test_board1") boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") diff --git a/examples/gno.land/r/demo/boards/z_12_d_filetest.gno b/examples/gno.land/r/demo/boards/z_12_d_filetest.gno index 37b6473f7ac..20818b05c0f 100644 --- a/examples/gno.land/r/demo/boards/z_12_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_d_filetest.gno @@ -4,11 +4,16 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid1 := boards.CreateBoard("test_board1") pid := boards.CreateThread(bid1, "First Post (title)", "Body of the first post. (body)") 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 ac4adf6ee7b..cc4439c5934 100644 --- a/examples/gno.land/r/demo/boards/z_12_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_12_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -15,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid1 = boards.CreateBoard("test_board1") @@ -23,6 +28,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) rid := boards.CreateRepost(bid1, pid, "", "Check this out", bid2) println(rid) println(boards.Render("test_board2")) @@ -37,6 +44,6 @@ func main() { // ## [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&postid=1&threadid=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 4d46c81b83d..6db254c661d 100644 --- a/examples/gno.land/r/demo/boards/z_1_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_1_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var board *boards.Board func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") _ = boards.CreateBoard("test_board_1") 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 31b39644b24..bf8b643913c 100644 --- a/examples/gno.land/r/demo/boards/z_2_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_2_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -32,8 +36,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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[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&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=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)] \[[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&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // 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 0b2a2df2f91..4717bfd3958 100644 --- a/examples/gno.land/r/demo/boards/z_3_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_3_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -24,6 +28,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) rid := boards.CreateReply(bid, pid, pid, "Reply of the second post") println(rid) println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) @@ -34,8 +40,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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[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&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=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)] \[[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&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // 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 b781e94e4db..e519e6babfb 100644 --- a/examples/gno.land/r/demo/boards/z_4_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_4_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -26,6 +30,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) rid2 := boards.CreateReply(bid, pid, pid, "Second reply of the second post") println(rid2) println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) @@ -37,13 +43,13 @@ 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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[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&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=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)] \[[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&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // // > 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)] \[[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&postid=4&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=4&threadid=2)] // // Realm: diff --git a/examples/gno.land/r/demo/boards/z_5_b_filetest.gno b/examples/gno.land/r/demo/boards/z_5_b_filetest.gno index e79da5c3677..0ad15ca2600 100644 --- a/examples/gno.land/r/demo/boards/z_5_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_b_filetest.gno @@ -14,6 +14,8 @@ import ( const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") // create board via registered user bid := boards.CreateBoard("test_board") 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 723e6a10204..abe8f1cf0bd 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 @@ -14,6 +14,8 @@ import ( const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") // create board via registered user bid := boards.CreateBoard("test_board") @@ -33,8 +35,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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[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&postid=1&threadid=1)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=1&threadid=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)] \[[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&postid=2&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=1)] // diff --git a/examples/gno.land/r/demo/boards/z_5_d_filetest.gno b/examples/gno.land/r/demo/boards/z_5_d_filetest.gno index 54cfe49eec6..33175efd4f2 100644 --- a/examples/gno.land/r/demo/boards/z_5_d_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_d_filetest.gno @@ -14,6 +14,8 @@ import ( const admin = std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj") func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") // create board via registered user bid := boards.CreateBoard("test_board") 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 712af483891..04ef39e8938 100644 --- a/examples/gno.land/r/demo/boards/z_5_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_5_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -16,6 +18,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -25,6 +29,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) rid2 := boards.CreateReply(bid, pid, pid, "Second reply of the second post\n") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) } @@ -33,12 +39,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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[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&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=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)] \[[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&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // // > 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)] \[[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&postid=4&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=4&threadid=2)] // 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 ec40cf5f8e9..8847c46130a 100644 --- a/examples/gno.land/r/demo/boards/z_6_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_6_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -17,6 +19,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -26,6 +30,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.CreateReply(bid, pid, pid, "Second reply of the second post\n") boards.CreateReply(bid, pid, rid, "First reply of the first reply\n") println(boards.Render("test_board/" + strconv.Itoa(int(pid)))) @@ -35,16 +41,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)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[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&postid=2&threadid=2)] \[[repost](/r/demo/boards$help&func=CreateRepost&bid=1&postid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=2&threadid=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)] \[[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&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // > // > > 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)] \[[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&postid=5&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=5&threadid=2)] // // > 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)] \[[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&postid=4&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=4&threadid=2)] // 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 353b84f6d87..30f39351815 100644 --- a/examples/gno.land/r/demo/boards/z_7_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_7_filetest.gno @@ -4,11 +4,16 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) // register users.Register("", "gnouser", "my profile") @@ -28,6 +33,6 @@ func main() { // ## [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&postid=1&threadid=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 4896dfcfccf..5824dbc8ad6 100644 --- a/examples/gno.land/r/demo/boards/z_8_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_8_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -17,6 +19,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") bid = boards.CreateBoard("test_board") @@ -26,6 +30,8 @@ func init() { } func main() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) boards.CreateReply(bid, pid, pid, "Second reply of the second post\n") rid2 := boards.CreateReply(bid, pid, rid, "First reply of the first reply\n") println(boards.Render("test_board/" + strconv.Itoa(int(pid)) + "/" + strconv.Itoa(int(rid2)))) @@ -35,11 +41,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)] \[[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&postid=3&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=3&threadid=2)] // // _[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)] \[[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&postid=5&threadid=2)] \[[x](/r/demo/boards$help&func=DeletePost&bid=1&postid=5&threadid=2)] // diff --git a/examples/gno.land/r/demo/boards/z_9_a_filetest.gno b/examples/gno.land/r/demo/boards/z_9_a_filetest.gno index 8d07ba0e710..79f68f20200 100644 --- a/examples/gno.land/r/demo/boards/z_9_a_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_a_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -11,6 +14,8 @@ import ( var dstBoard boards.BoardID func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") dstBoard = boards.CreateBoard("dst_board") diff --git a/examples/gno.land/r/demo/boards/z_9_b_filetest.gno b/examples/gno.land/r/demo/boards/z_9_b_filetest.gno index 68daf770b4f..703557cf476 100644 --- a/examples/gno.land/r/demo/boards/z_9_b_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_b_filetest.gno @@ -4,6 +4,9 @@ package boards_test // SEND: 200000000ugnot import ( + "std" + + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -14,6 +17,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") srcBoard = boards.CreateBoard("first_board") 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 ca37e306bda..9e23258553e 100644 --- a/examples/gno.land/r/demo/boards/z_9_filetest.gno +++ b/examples/gno.land/r/demo/boards/z_9_filetest.gno @@ -4,8 +4,10 @@ package boards_test // SEND: 200000000ugnot import ( + "std" "strconv" + "gno.land/p/demo/testutils" "gno.land/r/demo/boards" "gno.land/r/demo/users" ) @@ -17,6 +19,8 @@ var ( ) func init() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) users.Register("", "gnouser", "my profile") firstBoard = boards.CreateBoard("first_board") @@ -34,5 +38,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)] \[[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&postid=1&threadid=1)] \[[x](/r/demo/boards$help&func=DeletePost&bid=2&postid=1&threadid=1)] // diff --git a/examples/gno.land/r/demo/tests/subtests/subtests.gno b/examples/gno.land/r/demo/tests/subtests/subtests.gno index 6bf43cba5eb..5043c704017 100644 --- a/examples/gno.land/r/demo/tests/subtests/subtests.gno +++ b/examples/gno.land/r/demo/tests/subtests/subtests.gno @@ -21,5 +21,5 @@ func CallAssertOriginCall() { } func CallIsOriginCall() bool { - return std.IsOriginCall() + return std.PrevRealm().IsUser() } diff --git a/examples/gno.land/r/demo/tests/tests.gno b/examples/gno.land/r/demo/tests/tests.gno index e7fde94ea08..cdeea62de66 100644 --- a/examples/gno.land/r/demo/tests/tests.gno +++ b/examples/gno.land/r/demo/tests/tests.gno @@ -32,7 +32,7 @@ func CallAssertOriginCall() { } func CallIsOriginCall() bool { - return std.IsOriginCall() + return std.PrevRealm().IsUser() } func CallSubtestsAssertOriginCall() { diff --git a/examples/gno.land/r/demo/tests/tests_test.gno b/examples/gno.land/r/demo/tests/tests_test.gno index ccbc6b91265..fa3872744c8 100644 --- a/examples/gno.land/r/demo/tests/tests_test.gno +++ b/examples/gno.land/r/demo/tests/tests_test.gno @@ -1,17 +1,23 @@ -package tests +package tests_test import ( "std" "testing" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/tests" ) func TestAssertOriginCall(t *testing.T) { // CallAssertOriginCall(): no panic - CallAssertOriginCall() - if !CallIsOriginCall() { + caller := testutils.TestAddress("caller") + std.TestSetRealm(std.NewUserRealm(caller)) + tests.CallAssertOriginCall() + if !tests.CallIsOriginCall() { t.Errorf("expected IsOriginCall=true but got false") } + std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/tests")) // CallAssertOriginCall() from a block: panic expectedReason := "invalid non-origin call" func() { @@ -23,10 +29,10 @@ func TestAssertOriginCall(t *testing.T) { }() // if called inside a function literal, this is no longer an origin call // because there's one additional frame (the function literal block). - if CallIsOriginCall() { + if tests.CallIsOriginCall() { t.Errorf("expected IsOriginCall=false but got true") } - CallAssertOriginCall() + tests.CallAssertOriginCall() }() // CallSubtestsAssertOriginCall(): panic @@ -36,23 +42,24 @@ func TestAssertOriginCall(t *testing.T) { t.Errorf("expected panic with '%v', got '%v'", expectedReason, r) } }() - if CallSubtestsIsOriginCall() { + if tests.CallSubtestsIsOriginCall() { t.Errorf("expected IsOriginCall=false but got true") } - CallSubtestsAssertOriginCall() + tests.CallSubtestsAssertOriginCall() } func TestPrevRealm(t *testing.T) { var ( - user1Addr = std.DerivePkgAddr("user1.gno") + firstRealm = std.DerivePkgAddr("gno.land/r/demo/tests_test") rTestsAddr = std.DerivePkgAddr("gno.land/r/demo/tests") ) - // When a single realm in the frames, PrevRealm returns the user - if addr := GetPrevRealm().Addr(); addr != user1Addr { - t.Errorf("want GetPrevRealm().Addr==%s, got %s", user1Addr, addr) + // When only one realm in the frames, PrevRealm returns the same realm + if addr := tests.GetPrevRealm().Addr(); addr != firstRealm { + println(tests.GetPrevRealm()) + t.Errorf("want GetPrevRealm().Addr==%s, got %s", firstRealm, addr) } // When 2 or more realms in the frames, PrevRealm returns the second to last - if addr := GetRSubtestsPrevRealm().Addr(); addr != rTestsAddr { + if addr := tests.GetRSubtestsPrevRealm().Addr(); addr != rTestsAddr { t.Errorf("want GetRSubtestsPrevRealm().Addr==%s, got %s", rTestsAddr, addr) } } 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 27c7e9813da..212dc169007 100644 --- a/examples/gno.land/r/demo/users/z_11_filetest.gno +++ b/examples/gno.land/r/demo/users/z_11_filetest.gno @@ -11,8 +11,8 @@ import ( const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { - caller := std.GetOrigCaller() // main std.TestSetOrigCaller(admin) + caller := std.GetOrigCaller() // main users.AdminAddRestrictedName("superrestricted") // test restricted name 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 be508963911..6041f4b7113 100644 --- a/examples/gno.land/r/demo/users/z_11b_filetest.gno +++ b/examples/gno.land/r/demo/users/z_11b_filetest.gno @@ -11,8 +11,8 @@ import ( const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { - caller := std.GetOrigCaller() // main std.TestSetOrigCaller(admin) + caller := std.GetOrigCaller() // main // add restricted name users.AdminAddRestrictedName("superrestricted") // grant invite to caller 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 6465cc9c378..9080b509b8e 100644 --- a/examples/gno.land/r/demo/users/z_5_filetest.gno +++ b/examples/gno.land/r/demo/users/z_5_filetest.gno @@ -16,14 +16,17 @@ func main() { users.Register("", "gnouser", "my profile") // as admin, grant invites to gnouser std.TestSetOrigCaller(admin) + std.TestSetRealm(std.NewUserRealm(admin)) users.GrantInvites(caller.String() + ":1") // switch back to caller std.TestSetOrigCaller(caller) + std.TestSetRealm(std.NewUserRealm(caller)) // invite another addr test1 := testutils.TestAddress("test1") users.Invite(test1.String()) // switch to test1 std.TestSetOrigCaller(test1) + std.TestSetRealm(std.NewUserRealm(test1)) std.TestSetOrigSend(std.Coins{{"dontcare", 1}}, nil) users.Register(caller, "satoshi", "my other profile") println(users.Render("")) 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 919088088a2..e6a63d83358 100644 --- a/examples/gno.land/r/demo/users/z_6_filetest.gno +++ b/examples/gno.land/r/demo/users/z_6_filetest.gno @@ -9,7 +9,7 @@ import ( const admin = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") func main() { - caller := std.GetOrigCaller() + caller := std.GetOrigCaller() // main // as admin, grant invites to unregistered user. std.TestSetOrigCaller(admin) users.GrantInvites(caller.String() + ":1") diff --git a/examples/gno.land/r/docs/avl_pager_with_params/gno.mod b/examples/gno.land/r/docs/avl_pager_with_params/gno.mod new file mode 100644 index 00000000000..aeb5b047762 --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager_with_params/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/avl_pager_params diff --git a/examples/gno.land/r/docs/avl_pager_with_params/render.gno b/examples/gno.land/r/docs/avl_pager_with_params/render.gno new file mode 100644 index 00000000000..108f5735b65 --- /dev/null +++ b/examples/gno.land/r/docs/avl_pager_with_params/render.gno @@ -0,0 +1,86 @@ +package avl_pager_params + +import ( + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/realmpath" +) + +// We'll keep some demo data in an AVL tree to showcase pagination. +var ( + items *avl.Tree + idCounter seqid.ID +) + +func init() { + items = avl.NewTree() + // Populate the tree with 15 sample items for demonstration. + for i := 1; i <= 15; i++ { + id := idCounter.Next().String() + items.Set(id, "Some item value: "+id) + } +} + +func Render(path string) string { + // 1) Parse the incoming path to split route vs. query. + req := realmpath.Parse(path) + // - req.Path contains everything *before* ? or $ (? - query params, $ - gnoweb params) + // - The remaining part (page=2, size=5, etc.) is not in req.Path. + + // 2) If no specific route is provided (req.Path == ""), we’ll show a “home” page + // that displays a list of configs in paginated form. + if req.Path == "" { + return renderHome(path) + } + + // 3) If a route *is* provided (e.g. :SomeKey), + // we will interpret it as a request for a specific page. + return renderConfigItem(req.Path) +} + +// renderHome shows a paginated list of config items if route == "". +func renderHome(fullPath string) string { + // Create a Pager for our config tree, with a default page size of 5. + p := pager.NewPager(items, 5, false) + + // MustGetPageByPath uses the *entire* path (including query parts: ?page=2, etc.) + page := p.MustGetPageByPath(fullPath) + + // Start building the output (plain text or markdown). + out := "# AVL Pager + Render paths\n\n" + out += `This realm showcases how to maintain a paginated list while properly parsing render paths. +You can see how a single page can include a paginated element (like the example below), and how clicking +an item can take you to a dedicated page for that specific item. + +No matter how you browse through the paginated list, the introductory text (this section) remains the same. + +` + + out += ufmt.Sprintf("Showing page %d of %d\n\n", page.PageNumber, page.TotalPages) + + // List items for this page. + for _, item := range page.Items { + // Link each item to a details page: e.g. ":Config01" + out += ufmt.Sprintf("- [Item %s](/r/docs/avl_pager_params:%s)\n", item.Key, item.Key) + } + + // Insert pagination controls (previous/next links, etc.). + out += "\n" + page.Picker() + "\n\n" + out += "### [Go back to r/docs](/r/docs)" + + return out +} + +// renderConfigItem shows details for a single item, e.g. ":item001". +func renderConfigItem(itemName string) string { + value, ok := items.Get(itemName) + if !ok { + return ufmt.Sprintf("**No item found** for key: %s", itemName) + } + + out := ufmt.Sprintf("# Item %s\n\n%s\n\n", itemName, value.(string)) + out += "[Go back](/r/docs/avl_pager_params)" + return out +} diff --git a/examples/gno.land/r/docs/buttons/buttons_test.gno b/examples/gno.land/r/docs/buttons/buttons_test.gno index 2903fa1a858..c6164f3c687 100644 --- a/examples/gno.land/r/docs/buttons/buttons_test.gno +++ b/examples/gno.land/r/docs/buttons/buttons_test.gno @@ -7,7 +7,7 @@ import ( func TestRenderMotdLink(t *testing.T) { res := Render("motd") - const wantLink = "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message!" + const wantLink = "/r/docs/buttons$help&func=UpdateMOTD&newmotd=Message%21" 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/docs.gno b/examples/gno.land/r/docs/docs.gno index 28bac4171b5..b4c78205c0a 100644 --- a/examples/gno.land/r/docs/docs.gno +++ b/examples/gno.land/r/docs/docs.gno @@ -13,7 +13,9 @@ Explore various examples to learn more about Gno functionality and usage. - [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. +- [AVL Pager + Render paths](/r/docs/avl_pager_params) - Handle render arguments with pagination. - [Img Embed](/r/docs/img_embed) - Demonstrates how to embed an image. +- [Optional Render](/r/docs/optional_render) - Render() is optional in realms. - ... diff --git a/examples/gno.land/r/docs/optional_render/gno.mod b/examples/gno.land/r/docs/optional_render/gno.mod new file mode 100644 index 00000000000..4c8162ca46d --- /dev/null +++ b/examples/gno.land/r/docs/optional_render/gno.mod @@ -0,0 +1 @@ +module gno.land/r/docs/optional_render diff --git a/examples/gno.land/r/docs/optional_render/optional_render.gno b/examples/gno.land/r/docs/optional_render/optional_render.gno new file mode 100644 index 00000000000..77da30609b3 --- /dev/null +++ b/examples/gno.land/r/docs/optional_render/optional_render.gno @@ -0,0 +1,7 @@ +package optional_render + +func Info() string { + return `Having a Render() function in your realm is optional! +If you do decide to have a Render() function, it must have the following signature: +func Render(path string) string { ... }` +} diff --git a/examples/gno.land/r/gov/dao/bridge/bridge.gno b/examples/gno.land/r/gov/dao/bridge/bridge.gno index ba47978f33f..87a12d94e54 100644 --- a/examples/gno.land/r/gov/dao/bridge/bridge.gno +++ b/examples/gno.land/r/gov/dao/bridge/bridge.gno @@ -3,34 +3,60 @@ package bridge import ( "std" + "gno.land/p/demo/dao" "gno.land/p/demo/ownable" ) -const initialOwner = std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq") // @moul +const ( + initialOwner = std.Address("g1manfred47kzduec920z88wfr64ylksmdcedlf5") // @moul + loader = "gno.land/r/gov/dao/init" +) -var b *Bridge +var ( + b *Bridge + Ownable = ownable.NewWithAddress(initialOwner) +) // 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{}, + dao: nil, // initially set via r/gov/dao/init + } +} + +// LoadGovDAO loads the initial version of GovDAO into the bridge +// All changes to b.dao need to be done via GovDAO proposals after +func LoadGovDAO(d DAO) { + if std.PrevRealm().PkgPath() != loader { + panic("unauthorized") } + + b.dao = d } -// SetDAO sets the currently active GovDAO implementation -func SetDAO(dao DAO) { - b.AssertCallerIsOwner() +// NewGovDAOImplChangeExecutor allows creating a GovDAO proposal +// Which will upgrade the GovDAO version inside the bridge +func NewGovDAOImplChangeExecutor(newImpl DAO) dao.Executor { + callback := func() error { + b.dao = newImpl + return nil + } + + return b.dao.NewGovDAOExecutor(callback) +} - b.dao = dao +// SetGovDAO allows the admin to set the GovDAO version manually +// This functionality can be fully disabled by Ownable.DropOwnership(), +// making this realm fully managed by GovDAO. +func SetGovDAO(d DAO) { + Ownable.AssertCallerIsOwner() + b.dao = d } // GovDAO returns the current GovDAO implementation diff --git a/examples/gno.land/r/gov/dao/bridge/bridge_test.gno b/examples/gno.land/r/gov/dao/bridge/bridge_test.gno index 38b5d4be257..da06db293ab 100644 --- a/examples/gno.land/r/gov/dao/bridge/bridge_test.gno +++ b/examples/gno.land/r/gov/dao/bridge/bridge_test.gno @@ -1,9 +1,8 @@ package bridge import ( - "testing" - "std" + "testing" "gno.land/p/demo/dao" "gno.land/p/demo/ownable" @@ -27,11 +26,47 @@ func TestBridge_DAO(t *testing.T) { uassert.Equal(t, proposalID, GovDAO().Propose(dao.ProposalRequest{})) } +func TestBridge_LoadGovDAO(t *testing.T) { + t.Run("invalid initializer path", func(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/init")) // invalid loader + + // Attempt to set a new DAO implementation + uassert.PanicsWithMessage(t, "unauthorized", func() { + LoadGovDAO(&mockDAO{}) + }) + }) + + t.Run("valid loader", func(t *testing.T) { + var ( + initializer = "gno.land/r/gov/dao/init" + proposalID = uint64(10) + mockDAO = &mockDAO{ + proposeFn: func(_ dao.ProposalRequest) uint64 { + return proposalID + }, + } + ) + + std.TestSetRealm(std.NewCodeRealm(initializer)) + + // Attempt to set a new DAO implementation + uassert.NotPanics(t, func() { + LoadGovDAO(mockDAO) + }) + + uassert.Equal( + t, + mockDAO.Propose(dao.ProposalRequest{}), + 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{}) + SetGovDAO(&mockDAO{}) }) }) @@ -49,10 +84,10 @@ func TestBridge_SetDAO(t *testing.T) { std.TestSetOrigCaller(addr) - b.Ownable = ownable.NewWithAddress(addr) + Ownable = ownable.NewWithAddress(addr) urequire.NotPanics(t, func() { - SetDAO(mockDAO) + SetGovDAO(mockDAO) }) uassert.Equal( diff --git a/examples/gno.land/r/gov/dao/bridge/v2.gno b/examples/gno.land/r/gov/dao/bridge/v2.gno deleted file mode 100644 index 216419cf31d..00000000000 --- a/examples/gno.land/r/gov/dao/bridge/v2.gno +++ /dev/null @@ -1,42 +0,0 @@ -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/init/gno.mod b/examples/gno.land/r/gov/dao/init/gno.mod new file mode 100644 index 00000000000..40541f4f152 --- /dev/null +++ b/examples/gno.land/r/gov/dao/init/gno.mod @@ -0,0 +1 @@ +module gno.land/r/gov/dao/init diff --git a/examples/gno.land/r/gov/dao/init/init.gno b/examples/gno.land/r/gov/dao/init/init.gno new file mode 100644 index 00000000000..39bdbedba83 --- /dev/null +++ b/examples/gno.land/r/gov/dao/init/init.gno @@ -0,0 +1,13 @@ +// Package init's only task is to load the initial GovDAO version into the bridge. +// This is done to avoid gov/dao/v2 as a bridge dependency, +// As this can often lead to cyclic dependency errors. +package init + +import ( + "gno.land/r/gov/dao/bridge" + govdao "gno.land/r/gov/dao/v2" +) + +func init() { + bridge.LoadGovDAO(govdao.GovDAO) +} diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index 5ee8e63236a..d69f9901301 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -11,8 +11,17 @@ import ( var ( d *simpledao.SimpleDAO // the current active DAO implementation members membstore.MemberStore // the member store + + // GovDAO exposes all functions of this contract as methods + GovDAO = &DAO{} ) +// DAO is an empty struct that allows all +// functions of this realm to be methods instead of functions +// This allows a registry, such as r/gov/dao/bridge +// to take this object and match it to a required interface +type DAO struct{} + const daoPkgPath = "gno.land/r/gov/dao/v2" func init() { @@ -33,7 +42,7 @@ func init() { // Propose is designed to be called by another contract or with // `maketx run`, not by a `maketx call`. -func Propose(request dao.ProposalRequest) uint64 { +func (_ DAO) Propose(request dao.ProposalRequest) uint64 { idx, err := d.Propose(request) if err != nil { panic(err) @@ -43,25 +52,25 @@ func Propose(request dao.ProposalRequest) uint64 { } // VoteOnProposal casts a vote for the given proposal -func VoteOnProposal(id uint64, option dao.VoteOption) { +func (_ DAO) VoteOnProposal(id uint64, option dao.VoteOption) { if err := d.VoteOnProposal(id, option); err != nil { panic(err) } } // ExecuteProposal executes the proposal -func ExecuteProposal(id uint64) { +func (_ DAO) ExecuteProposal(id uint64) { if err := d.ExecuteProposal(id); err != nil { panic(err) } } // GetPropStore returns the active proposal store -func GetPropStore() dao.PropStore { +func (_ DAO) GetPropStore() dao.PropStore { return d } // GetMembStore returns the active member store -func GetMembStore() membstore.MemberStore { +func (_ DAO) GetMembStore() membstore.MemberStore { return members } diff --git a/examples/gno.land/r/gov/dao/v2/poc.gno b/examples/gno.land/r/gov/dao/v2/poc.gno index 30d8a403f6e..81bdc7c9b12 100644 --- a/examples/gno.land/r/gov/dao/v2/poc.gno +++ b/examples/gno.land/r/gov/dao/v2/poc.gno @@ -13,7 +13,7 @@ import ( var errNoChangesProposed = errors.New("no set changes proposed") // NewGovDAOExecutor creates the govdao wrapped callback executor -func NewGovDAOExecutor(cb func() error) dao.Executor { +func (_ DAO) NewGovDAOExecutor(cb func() error) dao.Executor { if cb == nil { panic(errNoChangesProposed) } @@ -25,7 +25,7 @@ func NewGovDAOExecutor(cb func() error) dao.Executor { } // NewMemberPropExecutor returns the GOVDAO member change executor -func NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor { +func (_ DAO) NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor { if changesFn == nil { panic(errNoChangesProposed) } @@ -65,10 +65,10 @@ func NewMemberPropExecutor(changesFn func() []membstore.Member) dao.Executor { return errs } - return NewGovDAOExecutor(callback) + return GovDAO.NewGovDAOExecutor(callback) } -func NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor { +func (_ DAO) NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executor { if changeFn == nil { panic(errNoChangesProposed) } @@ -79,7 +79,7 @@ func NewMembStoreImplExecutor(changeFn func() membstore.MemberStore) dao.Executo return nil } - return NewGovDAOExecutor(callback) + return GovDAO.NewGovDAOExecutor(callback) } // setMembStoreImpl sets a new dao.MembStore implementation diff --git a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno index 7d8975e1fe8..c8ea983cc73 100644 --- a/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop1_filetest.gno @@ -12,11 +12,13 @@ import ( "gno.land/p/demo/dao" pVals "gno.land/p/sys/validators" + _ "gno.land/r/gov/dao/init" // so that the govdao initializer is executed govdao "gno.land/r/gov/dao/v2" validators "gno.land/r/sys/validators/v2" ) func init() { + changesFn := func() []pVals.Validator { return []pVals.Validator{ { @@ -51,7 +53,7 @@ func init() { Executor: executor, } - govdao.Propose(prop) + govdao.GovDAO.Propose(prop) } func main() { @@ -60,13 +62,13 @@ func main() { println("--") println(govdao.Render("0")) println("--") - govdao.VoteOnProposal(0, dao.YesVote) + govdao.GovDAO.VoteOnProposal(0, dao.YesVote) println("--") println(govdao.Render("0")) println("--") println(validators.Render("")) println("--") - govdao.ExecuteProposal(0) + govdao.GovDAO.ExecuteProposal(0) println("--") println(govdao.Render("0")) println("--") diff --git a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno index 84a64bc4ee2..f85373a471c 100644 --- a/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop2_filetest.gno @@ -5,6 +5,7 @@ import ( "gno.land/p/demo/dao" gnoblog "gno.land/r/gnoland/blog" + _ "gno.land/r/gov/dao/init" // so that the govdao initializer is executed govdao "gno.land/r/gov/dao/v2" ) @@ -28,7 +29,7 @@ func init() { Executor: ex, } - govdao.Propose(prop) + govdao.GovDAO.Propose(prop) } func main() { @@ -37,13 +38,13 @@ func main() { println("--") println(govdao.Render("0")) println("--") - govdao.VoteOnProposal(0, "YES") + govdao.GovDAO.VoteOnProposal(0, "YES") println("--") println(govdao.Render("0")) println("--") println(gnoblog.Render("")) println("--") - govdao.ExecuteProposal(0) + govdao.GovDAO.ExecuteProposal(0) println("--") println(govdao.Render("0")) println("--") diff --git a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno index 068f520e7e2..4032ba41d55 100644 --- a/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop3_filetest.gno @@ -6,6 +6,7 @@ import ( "gno.land/p/demo/dao" "gno.land/p/demo/membstore" "gno.land/r/gov/dao/bridge" + _ "gno.land/r/gov/dao/init" // so that the govdao initializer is executed govdao "gno.land/r/gov/dao/v2" ) @@ -34,7 +35,7 @@ func init() { prop := dao.ProposalRequest{ Title: title, Description: description, - Executor: govdao.NewMemberPropExecutor(memberFn), + Executor: govdao.GovDAO.NewMemberPropExecutor(memberFn), } bridge.GovDAO().Propose(prop) @@ -42,25 +43,25 @@ func init() { func main() { println("--") - println(govdao.GetMembStore().Size()) + println(govdao.GovDAO.GetMembStore().Size()) println("--") println(govdao.Render("")) println("--") println(govdao.Render("0")) println("--") - govdao.VoteOnProposal(0, "YES") + govdao.GovDAO.VoteOnProposal(0, "YES") println("--") println(govdao.Render("0")) println("--") println(govdao.Render("")) println("--") - govdao.ExecuteProposal(0) + govdao.GovDAO.ExecuteProposal(0) println("--") println(govdao.Render("0")) println("--") println(govdao.Render("")) println("--") - println(govdao.GetMembStore().Size()) + println(govdao.GovDAO.GetMembStore().Size()) } // Output: diff --git a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno index 13ca572c512..49326495dac 100644 --- a/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno +++ b/examples/gno.land/r/gov/dao/v2/prop4_filetest.gno @@ -3,6 +3,7 @@ package main import ( "gno.land/p/demo/dao" "gno.land/r/gov/dao/bridge" + _ "gno.land/r/gov/dao/init" // so that the govdao initializer is executed govdaov2 "gno.land/r/gov/dao/v2" "gno.land/r/sys/params" ) diff --git a/examples/gno.land/r/jjoptimist/home/config.gno b/examples/gno.land/r/jjoptimist/home/config.gno new file mode 100644 index 00000000000..7f6ad955806 --- /dev/null +++ b/examples/gno.land/r/jjoptimist/home/config.gno @@ -0,0 +1,32 @@ +package home + +import ( + "std" + + "gno.land/p/demo/ownable" +) + +type Config struct { + Title string + Description string + Github string +} + +var config = Config{ + Title: "JJOptimist's Home Realm 🏠", + Description: "Exploring Gno and building on-chain", + Github: "jjoptimist", +} + +var Ownable = ownable.NewWithAddress(std.Address("g16vfw3r7zuz43fhky3xfsuc2hdv9tnhvlkyn0nj")) + +func GetConfig() Config { + return config +} + +func UpdateConfig(newTitle, newDescription, newGithub string) { + Ownable.AssertCallerIsOwner() + config.Title = newTitle + config.Description = newDescription + config.Github = newGithub +} diff --git a/examples/gno.land/r/jjoptimist/home/gno.mod b/examples/gno.land/r/jjoptimist/home/gno.mod new file mode 100644 index 00000000000..b4b591f6ab7 --- /dev/null +++ b/examples/gno.land/r/jjoptimist/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/jjoptimist/home diff --git a/examples/gno.land/r/jjoptimist/home/home.gno b/examples/gno.land/r/jjoptimist/home/home.gno new file mode 100644 index 00000000000..91a23670271 --- /dev/null +++ b/examples/gno.land/r/jjoptimist/home/home.gno @@ -0,0 +1,82 @@ +package home + +import ( + "std" + "strconv" + "time" + + "gno.land/r/leon/hof" +) + +const ( + gnomeArt1 = ` /\ + / \ + ,,,,, +(o.o) +(\_/) +-"-"-` + + gnomeArt2 = ` /\ + / \ + ,,,,, +(^.^) +(\_/) + -"-` + + gnomeArt3 = ` /\ + / \ + ,,,,, +(*.*) +(\_/) +"-"-"` + + gnomeArt4 = ` /\ + / \ + ,,,,, +(o.~) +(\_/) + -"-` +) + +var creation time.Time + +func getGnomeArt(height int64) string { + var art string + switch { + case height%7 == 0: + art = gnomeArt4 // winking gnome + case height%5 == 0: + art = gnomeArt3 // starry-eyed gnome + case height%3 == 0: + art = gnomeArt2 // happy gnome + default: + art = gnomeArt1 // regular gnome + } + return "```\n" + art + "\n```\n" +} + +func init() { + creation = time.Now() + hof.Register() +} + +func Render(path string) string { + height := std.GetHeight() + + output := "# " + config.Title + "\n\n" + + output += "## About Me\n" + output += "- 👋 Hi, I'm JJOptimist\n" + output += getGnomeArt(height) + output += "- 🌱 " + config.Description + "\n" + + output += "## Contact\n" + output += "- 📫 GitHub: [" + config.Github + "](https://github.com/" + config.Github + ")\n" + + output += "\n---\n" + output += "_Realm created: " + creation.Format("2006-01-02 15:04:05 UTC") + "_\n" + output += "_Owner: " + Ownable.Owner().String() + "_\n" + output += "_Current Block Height: " + strconv.Itoa(int(height)) + "_" + + return output +} diff --git a/examples/gno.land/r/jjoptimist/home/home_test.gno b/examples/gno.land/r/jjoptimist/home/home_test.gno new file mode 100644 index 00000000000..742204cca71 --- /dev/null +++ b/examples/gno.land/r/jjoptimist/home/home_test.gno @@ -0,0 +1,60 @@ +package home + +import ( + "strings" + "testing" +) + +func TestConfig(t *testing.T) { + cfg := GetConfig() + + if cfg.Title != "JJOptimist's Home Realm 🏠" { + t.Errorf("Expected title to be 'JJOptimist's Home Realm 🏠', got %s", cfg.Title) + } + if cfg.Description != "Exploring Gno and building on-chain" { + t.Errorf("Expected description to be 'Exploring Gno and building on-chain', got %s", cfg.Description) + } + if cfg.Github != "jjoptimist" { + t.Errorf("Expected github to be 'jjoptimist', got %s", cfg.Github) + } +} + +func TestRender(t *testing.T) { + output := Render("") + + // Test that required sections are present + if !strings.Contains(output, "# "+config.Title) { + t.Error("Rendered output missing title") + } + if !strings.Contains(output, "## About Me") { + t.Error("Rendered output missing About Me section") + } + if !strings.Contains(output, "## Contact") { + t.Error("Rendered output missing Contact section") + } + if !strings.Contains(output, config.Description) { + t.Error("Rendered output missing description") + } + if !strings.Contains(output, config.Github) { + t.Error("Rendered output missing github link") + } +} + +func TestGetGnomeArt(t *testing.T) { + tests := []struct { + height int64 + expected string + }{ + {7, gnomeArt4}, // height divisible by 7 + {5, gnomeArt3}, // height divisible by 5 + {3, gnomeArt2}, // height divisible by 3 + {2, gnomeArt1}, // default case + } + + for _, tt := range tests { + art := getGnomeArt(tt.height) + if !strings.Contains(art, tt.expected) { + t.Errorf("For height %d, expected art containing %s, got %s", tt.height, tt.expected, art) + } + } +} diff --git a/examples/gno.land/r/leon/config/config.gno b/examples/gno.land/r/leon/config/config.gno index bc800ec8263..bb90a6c21d7 100644 --- a/examples/gno.land/r/leon/config/config.gno +++ b/examples/gno.land/r/leon/config/config.gno @@ -3,61 +3,116 @@ package config import ( "errors" "std" + "strconv" + "strings" + "time" + + "gno.land/p/demo/avl" + p "gno.land/p/demo/avl/pager" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/p/moul/realmpath" ) var ( - main std.Address // leon's main address - backup std.Address // backup address + configs = avl.NewTree() + pager = p.NewPager(configs, 10, false) + banner = "---\n[[Leon's Home page]](/r/leon/home) | [[GitHub: @leohhhn]](https://github.com/leohhhn)\n\n---" + absPath = strings.TrimPrefix(std.CurrentRealm().PkgPath(), std.GetChainDomain()) + + // SafeObjects + OwnableMain = ownable.NewWithAddress("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5") + OwnableBackup = ownable.NewWithAddress("g1lavlav7zwsjqlzzl3qdl3nl242qtf638vnhdjh") - ErrInvalidAddr = errors.New("leon's config: invalid address") ErrUnauthorized = errors.New("leon's config: unauthorized") ) -func init() { - main = "g125em6arxsnj49vx35f0n0z34putv5ty3376fg5" +type Config struct { + lines string + updated time.Time } -func Address() std.Address { - return main -} +func AddConfig(name, lines string) { + if !IsAuthorized(std.PrevRealm().Addr()) { + panic(ErrUnauthorized) + } -func Backup() std.Address { - return backup + configs.Set(name, Config{ + lines: lines, + updated: time.Now(), + }) // no overwrite check } -func SetAddress(a std.Address) error { - if !a.IsValid() { - return ErrInvalidAddr +func RemoveConfig(name string) { + if !IsAuthorized(std.PrevRealm().Addr()) { + panic(ErrUnauthorized) } - if err := checkAuthorized(); err != nil { - return err + if _, ok := configs.Remove(name); !ok { + panic("no config with that name") } - - main = a - return nil } -func SetBackup(a std.Address) error { - if !a.IsValid() { - return ErrInvalidAddr +func UpdateBanner(newBanner string) { + if !IsAuthorized(std.PrevRealm().Addr()) { + panic(ErrUnauthorized) } - if err := checkAuthorized(); err != nil { - return err - } + banner = newBanner +} - backup = a - return nil +func IsAuthorized(addr std.Address) bool { + return addr == OwnableMain.Owner() || addr == OwnableBackup.Owner() } -func checkAuthorized() error { - caller := std.PrevRealm().Addr() - isAuthorized := caller == main || caller == backup +func Banner() string { + return banner +} + +func Render(path string) (out string) { + req := realmpath.Parse(path) + if req.Path == "" { + out += md.H1("Leon's config package") + + out += ufmt.Sprintf("Leon's main address: %s\n\n", OwnableMain.Owner().String()) + out += ufmt.Sprintf("Leon's backup address: %s\n\n", OwnableBackup.Owner().String()) - if !isAuthorized { - return ErrUnauthorized + out += md.H2("Leon's configs") + + if configs.Size() == 0 { + out += "No configs yet :c\n\n" + } + + page := pager.MustGetPageByPath(path) + for _, item := range page.Items { + out += ufmt.Sprintf("- [%s](%s:%s)\n\n", item.Key, absPath, item.Key) + } + + out += page.Picker() + out += "\n\n" + out += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n" + + out += Banner() + + return out } - return nil + return renderConfPage(req.Path) +} + +func renderConfPage(confName string) (out string) { + raw, ok := configs.Get(confName) + if !ok { + out += md.H1("404") + out += "That config does not exist :/" + return out + } + + conf := raw.(Config) + out += md.H1(confName) + out += ufmt.Sprintf("```\n%s\n```\n\n", conf.lines) + out += ufmt.Sprintf("_Last updated on %s_", conf.updated.Format("02 Jan, 2006")) + + return out } diff --git a/examples/gno.land/r/leon/hof/datasource_test.gno b/examples/gno.land/r/leon/hof/datasource_test.gno index 376f981875f..2cbabb08ddb 100644 --- a/examples/gno.land/r/leon/hof/datasource_test.gno +++ b/examples/gno.land/r/leon/hof/datasource_test.gno @@ -151,7 +151,7 @@ func TestItemRecord(t *testing.T) { 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" + "**[2👍](/r/leon/hof$help&func=Upvote&pkgpath=gno.land%2Fr%2Fdemo%2Ftest) - " + + "[1👎](/r/leon/hof$help&func=Downvote&pkgpath=gno.land%2Fr%2Fdemo%2Ftest)**\n\n" uassert.Equal(t, wantContent, content) } diff --git a/examples/gno.land/r/leon/hof/hof.gno b/examples/gno.land/r/leon/hof/hof.gno index 147a0dd1a95..96266ffe380 100644 --- a/examples/gno.land/r/leon/hof/hof.gno +++ b/examples/gno.land/r/leon/hof/hof.gno @@ -10,6 +10,8 @@ import ( "gno.land/p/demo/ownable" "gno.land/p/demo/pausable" "gno.land/p/demo/seqid" + + "gno.land/r/leon/config" ) var ( @@ -24,7 +26,7 @@ type ( Exhibition struct { itemCounter seqid.ID description string - items *avl.Tree // pkgPath > Item + items *avl.Tree // pkgPath > &Item itemsSorted *avl.Tree // same data but sorted, storing pointers } @@ -43,7 +45,7 @@ func init() { itemsSorted: avl.NewTree(), } - Ownable = ownable.NewWithAddress(std.Address("g125em6arxsnj49vx35f0n0z34putv5ty3376fg5")) + Ownable = ownable.NewWithAddress(config.OwnableMain.Owner()) // OrigSendOwnable? Pausable = pausable.NewFromOwnable(Ownable) } @@ -85,14 +87,14 @@ func Register() { func Upvote(pkgpath string) { rawItem, ok := exhibition.items.Get(pkgpath) if !ok { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } item := rawItem.(*Item) caller := std.PrevRealm().Addr().String() if item.upvote.Has(caller) { - panic(ErrDoubleUpvote.Error()) + panic(ErrDoubleUpvote) } item.upvote.Set(caller, struct{}{}) @@ -101,14 +103,14 @@ func Upvote(pkgpath string) { func Downvote(pkgpath string) { rawItem, ok := exhibition.items.Get(pkgpath) if !ok { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } item := rawItem.(*Item) caller := std.PrevRealm().Addr().String() if item.downvote.Has(caller) { - panic(ErrDoubleDownvote.Error()) + panic(ErrDoubleDownvote) } item.downvote.Set(caller, struct{}{}) @@ -116,19 +118,19 @@ func Downvote(pkgpath string) { func Delete(pkgpath string) { if !Ownable.CallerIsOwner() { - panic(ownable.ErrUnauthorized.Error()) + panic(ownable.ErrUnauthorized) } i, ok := exhibition.items.Get(pkgpath) if !ok { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } if _, removed := exhibition.itemsSorted.Remove(i.(*Item).id.String()); !removed { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } if _, removed := exhibition.items.Remove(pkgpath); !removed { - panic(ErrNoSuchItem.Error()) + panic(ErrNoSuchItem) } } diff --git a/examples/gno.land/r/leon/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno index 868262bedc7..cb986e6b42c 100644 --- a/examples/gno.land/r/leon/hof/render.gno +++ b/examples/gno.land/r/leon/hof/render.gno @@ -38,11 +38,9 @@ func (e Exhibition) Render(path string, dashboard bool) string { 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] + page := pager.NewPager(e.itemsSorted, pageSize, true).MustGetPageByPath(path) + for _, item := range page.Items { out += "
\n\n" id, _ := seqid.FromString(item.Key) out += ufmt.Sprintf("### Submission #%d\n\n", int(id)) @@ -63,7 +61,7 @@ func (i Item) Render(dashboard bool) string { 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", + 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), ) diff --git a/examples/gno.land/r/leon/home/home.gno b/examples/gno.land/r/leon/home/home.gno index cf33260cc6b..aef261fcd60 100644 --- a/examples/gno.land/r/leon/home/home.gno +++ b/examples/gno.land/r/leon/home/home.gno @@ -19,7 +19,24 @@ var ( abtMe [2]string ) +func Render(path string) string { + out := "# Leon's Homepage\n\n" + + out += renderAboutMe() + out += renderBlogPosts() + out += "\n\n" + out += renderArt() + out += "\n\n" + out += config.Banner() + out += "\n\n" + + return out +} + func init() { + hof.Register() + mirror.Register(std.CurrentRealm().PkgPath(), Render) + pfp = "https://i.imgflip.com/91vskx.jpg" pfpCaption = "[My favourite painting & pfp](https://en.wikipedia.org/wiki/Wanderer_above_the_Sea_of_Fog)" abtMe = [2]string{ @@ -30,16 +47,12 @@ life-long learner, and sharer of knowledge.`, My contributions to gno.land can mainly be found [here](https://github.com/gnolang/gno/issues?q=sort:updated-desc+author:leohhhn). -TODO import r/gh -`, +TODO import r/gh`, } - - hof.Register() - mirror.Register(std.CurrentRealm().PkgPath(), Render) } func UpdatePFP(url, caption string) { - if !isAuthorized(std.PrevRealm().Addr()) { + if !config.IsAuthorized(std.PrevRealm().Addr()) { panic(config.ErrUnauthorized) } @@ -48,7 +61,7 @@ func UpdatePFP(url, caption string) { } func UpdateAboutMe(col1, col2 string) { - if !isAuthorized(std.PrevRealm().Addr()) { + if !config.IsAuthorized(std.PrevRealm().Addr()) { panic(config.ErrUnauthorized) } @@ -56,17 +69,6 @@ func UpdateAboutMe(col1, col2 string) { abtMe[1] = col2 } -func Render(path string) string { - out := "# Leon's Homepage\n\n" - - out += renderAboutMe() - out += renderBlogPosts() - out += "\n\n" - out += renderArt() - - return out -} - func renderBlogPosts() string { out := "" //out += "## Leon's Blog Posts" @@ -130,7 +132,3 @@ 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/moul/microposts/README.md b/examples/gno.land/r/moul/microposts/README.md new file mode 100644 index 00000000000..5c7763020cd --- /dev/null +++ b/examples/gno.land/r/moul/microposts/README.md @@ -0,0 +1,5 @@ +# fork of `leon/fosdem25/microposts` + +removing optional lines to make the code more concise for slides. + +Original work here: https://gno.land/r/leon/fosdem25/microposts diff --git a/examples/gno.land/r/moul/microposts/gno.mod b/examples/gno.land/r/moul/microposts/gno.mod new file mode 100644 index 00000000000..00386f6e856 --- /dev/null +++ b/examples/gno.land/r/moul/microposts/gno.mod @@ -0,0 +1 @@ +module gno.land/r/moul/microposts diff --git a/examples/gno.land/r/moul/microposts/microposts_test.gno b/examples/gno.land/r/moul/microposts/microposts_test.gno new file mode 100644 index 00000000000..61929081e34 --- /dev/null +++ b/examples/gno.land/r/moul/microposts/microposts_test.gno @@ -0,0 +1,3 @@ +package microposts + +// empty file just to make sure that `gno test` tries to parse the implementation. diff --git a/examples/gno.land/r/moul/microposts/post.gno b/examples/gno.land/r/moul/microposts/post.gno new file mode 100644 index 00000000000..0832d8ac3c6 --- /dev/null +++ b/examples/gno.land/r/moul/microposts/post.gno @@ -0,0 +1,18 @@ +package microposts + +import ( + "std" + "time" +) + +type Post struct { + text string + author std.Address + createdAt time.Time +} + +func (p Post) String() string { + out := p.text + "\n" + out += "_" + p.createdAt.Format("02 Jan 2006, 15:04") + ", by " + p.author.String() + "_" + return out +} diff --git a/examples/gno.land/r/moul/microposts/realm.gno b/examples/gno.land/r/moul/microposts/realm.gno new file mode 100644 index 00000000000..a03b6dd958b --- /dev/null +++ b/examples/gno.land/r/moul/microposts/realm.gno @@ -0,0 +1,25 @@ +package microposts + +import ( + "std" + "strconv" + "time" +) + +var posts []*Post + +func CreatePost(text string) { + posts = append(posts, &Post{ + text: text, + author: std.PrevRealm().Addr(), // provided by env + createdAt: time.Now(), + }) +} + +func Render(_ string) string { + out := "# Posts\n" + for i := len(posts) - 1; i >= 0; i-- { + out += "### Post " + strconv.Itoa(i) + "\n" + posts[i].String() + } + return out +} diff --git a/examples/gno.land/r/moul/present/admin.gno b/examples/gno.land/r/moul/present/admin.gno deleted file mode 100644 index ab99b1725c5..00000000000 --- a/examples/gno.land/r/moul/present/admin.gno +++ /dev/null @@ -1,96 +0,0 @@ -package present - -import ( - "std" - "strings" - - "gno.land/p/demo/avl" -) - -var ( - adminAddr std.Address - moderatorList avl.Tree - inPause bool -) - -func init() { - // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. - adminAddr = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" -} - -func AdminSetAdminAddr(addr std.Address) { - assertIsAdmin() - adminAddr = addr -} - -func AdminSetInPause(state bool) { - assertIsAdmin() - inPause = state -} - -func AdminAddModerator(addr std.Address) { - assertIsAdmin() - moderatorList.Set(addr.String(), true) -} - -func AdminRemoveModerator(addr std.Address) { - assertIsAdmin() - moderatorList.Set(addr.String(), false) // XXX: delete instead? -} - -func ModAddPost(slug, title, body, publicationDate, authors, tags string) { - assertIsModerator() - - caller := std.GetOrigCaller() - tagList := strings.Split(tags, ",") - authorList := strings.Split(authors, ",") - - err := b.NewPost(caller, slug, title, body, publicationDate, authorList, tagList) - checkErr(err) -} - -func ModEditPost(slug, title, body, publicationDate, authors, tags string) { - assertIsModerator() - - tagList := strings.Split(tags, ",") - authorList := strings.Split(authors, ",") - - err := b.GetPost(slug).Update(title, body, publicationDate, authorList, tagList) - checkErr(err) -} - -func isAdmin(addr std.Address) bool { - return addr == adminAddr -} - -func isModerator(addr std.Address) bool { - _, found := moderatorList.Get(addr.String()) - return found -} - -func assertIsAdmin() { - caller := std.GetOrigCaller() - if !isAdmin(caller) { - panic("access restricted.") - } -} - -func assertIsModerator() { - caller := std.GetOrigCaller() - if isAdmin(caller) || isModerator(caller) { - return - } - panic("access restricted") -} - -func assertNotInPause() { - if inPause { - panic("access restricted (pause)") - } -} - -func checkErr(err error) { - if err != nil { - panic(err) - } -} diff --git a/examples/gno.land/r/moul/present/present.gno b/examples/gno.land/r/moul/present/present.gno new file mode 100644 index 00000000000..b4f880318bf --- /dev/null +++ b/examples/gno.land/r/moul/present/present.gno @@ -0,0 +1,353 @@ +package present + +import ( + "net/url" + "std" + "strconv" + "strings" + "time" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/ownable" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/collection" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/moul/realmpath" + "gno.land/p/moul/txlink" +) + +type Presentation struct { + Slug string + Title string + Event string + Author string + Uploader std.Address + Date time.Time + Content string + EditDate time.Time + NumSlides int +} + +var ( + presentations *collection.Collection + Ownable *ownable.Ownable +) + +func init() { + presentations = collection.New() + // for /view and /slides + presentations.AddIndex("slug", func(v interface{}) string { + return v.(*Presentation).Slug + }, collection.UniqueIndex) + + // for table sorting + presentations.AddIndex("date", func(v interface{}) string { + return v.(*Presentation).Date.String() + }, collection.DefaultIndex) + presentations.AddIndex("author", func(v interface{}) string { + return v.(*Presentation).Author + }, collection.DefaultIndex) + presentations.AddIndex("title", func(v interface{}) string { + return v.(*Presentation).Title + }, collection.DefaultIndex) + + Ownable = ownable.New() +} + +// Render handles the realm's rendering logic +func Render(path string) string { + req := realmpath.Parse(path) + + // Get slug from path + slug := req.PathPart(0) + + // List view (home) + if slug == "" { + return renderList(req) + } + + // Slides view + if req.PathPart(1) == "slides" { + page := 1 + if pageStr := req.Query.Get("page"); pageStr != "" { + var err error + page, err = strconv.Atoi(pageStr) + if err != nil { + return "400: invalid page number" + } + } + return renderSlides(slug, page) + } + + // Regular view + return renderView(slug) +} + +// Set adds or updates a presentation +func Set(slug, title, event, author, date, content string) string { + Ownable.AssertCallerIsOwner() + + parsedDate, err := time.Parse("2006-01-02", date) + if err != nil { + return "400: invalid date format (expected: YYYY-MM-DD)" + } + + numSlides := 1 // Count intro slide + for _, line := range strings.Split(content, "\n") { + if strings.HasPrefix(line, "## ") { + numSlides++ + } + } + numSlides++ // Count thank you slide + + p := &Presentation{ + Slug: slug, + Title: title, + Event: event, + Author: author, + Uploader: std.PrevRealm().Addr(), + Date: parsedDate, + Content: content, + EditDate: time.Now(), + NumSlides: numSlides, + } + + presentations.Set(p) + return "presentation saved successfully" +} + +// Delete removes a presentation +func Delete(slug string) string { + Ownable.AssertCallerIsOwner() + + entry := presentations.GetFirst("slug", slug) + if entry == nil { + return "404: presentation not found" + } + + // XXX: consider this: + // if entry.Obj.(*Presentation).Uploader != std.PrevRealm().Addr() { + // return "401: unauthorized - only the uploader can delete their presentations" + // } + + // Convert the entry's ID from string to uint64 and delete + numericID, err := seqid.FromString(entry.ID) + if err != nil { + return "500: invalid entry ID format" + } + + presentations.Delete(uint64(numericID)) + return "presentation deleted successfully" +} + +func renderList(req *realmpath.Request) string { + var out strings.Builder + out.WriteString(md.H1("Presentations")) + + // Setup pager + index := presentations.GetIndex(getSortField(req)) + pgr := pager.NewPager(index, 10, isSortReversed(req)) + + // Get current page + page := pgr.MustGetPageByPath(req.String()) + + // Create table + dateColumn := renderSortLink(req, "date", "Date") + titleColumn := renderSortLink(req, "title", "Title") + authorColumn := renderSortLink(req, "author", "Author") + table := mdtable.Table{ + Headers: []string{dateColumn, titleColumn, "Event", authorColumn, "Slides"}, + } + + // Add rows from current page + for _, item := range page.Items { + // Get the actual presentation using the ID from the index + // XXX: improve p/moul/collection to make this more convenient. + // - no need to make per-id lookup. + // - transparently support multi-values. + // - integrate a sortable pager? + var ids []string + if ids_, ok := item.Value.([]string); ok { + ids = ids_ + } else if id, ok := item.Value.(string); ok { + ids = []string{id} + } + + for _, id := range ids { + entry := presentations.GetFirst(collection.IDIndex, id) + if entry == nil { + continue + } + p := entry.Obj.(*Presentation) + + table.Append([]string{ + p.Date.Format("2006-01-02"), + md.Link(p.Title, localPath(p.Slug, nil)), + p.Event, + p.Author, + ufmt.Sprintf("%d", p.NumSlides), + }) + } + } + + out.WriteString(table.String()) + out.WriteString(page.Picker()) // XXX: picker is not preserving the previous flags, should take "req" as argument. + return out.String() +} + +func (p *Presentation) FirstSlide() string { + var out strings.Builder + out.WriteString(md.H1(p.Title)) + out.WriteString(md.Paragraph(md.Bold(p.Event) + ", " + p.Date.Format("2 Jan 2006"))) + out.WriteString(md.Paragraph("by " + md.Bold(p.Author))) // XXX: link to u/? + return out.String() +} + +func (p *Presentation) LastSlide() string { + var out strings.Builder + out.WriteString(md.H1(p.Title)) + out.WriteString(md.H2("Thank You!")) + out.WriteString(md.Paragraph(p.Author)) + fullPath := "https://" + std.GetChainDomain() + localPath(p.Slug, nil) + out.WriteString(md.Paragraph("🔗 " + md.Link(fullPath, fullPath))) + // XXX: QRCode + return out.String() +} + +func renderView(slug string) string { + if slug == "" { + return "400: missing presentation slug" + } + + entry := presentations.GetFirst("slug", slug) + if entry == nil { + return "404: presentation not found" + } + + p := entry.Obj.(*Presentation) + var out strings.Builder + + // Header using FirstSlide helper + out.WriteString(p.FirstSlide()) + + // Slide mode link + out.WriteString(md.Link("View as slides", localPath(p.Slug+"/slides", nil)) + "\n\n") + out.WriteString(md.HorizontalRule()) + out.WriteString(md.Paragraph(p.Content)) + + // Metadata footer + out.WriteString(md.HorizontalRule()) + out.WriteString(ufmt.Sprintf("Last edited: %s\n\n", p.EditDate.Format("2006-01-02 15:04:05"))) + out.WriteString(ufmt.Sprintf("Uploader: `%s`\n\n", p.Uploader)) + out.WriteString(ufmt.Sprintf("Number of slides: %d\n\n", p.NumSlides)) + + // Admin actions + // XXX: consider a dynamic toggle for admin actions + editLink := txlink.Call("Set", + "slug", p.Slug, + "title", p.Title, + "author", p.Author, + "event", p.Event, + "date", p.Date.Format("2006-01-02"), + ) + deleteLink := txlink.Call("Delete", "slug", p.Slug) + out.WriteString(md.Paragraph(md.Link("Edit", editLink) + " | " + md.Link("Delete", deleteLink))) + + return out.String() +} + +// renderSlidesNavigation returns the navigation bar for slides +func renderSlidesNavigation(slug string, currentPage, totalSlides int) string { + var out strings.Builder + if currentPage > 1 { + prevLink := localPath(slug+"/slides", url.Values{"page": {ufmt.Sprintf("%d", currentPage-1)}}) + out.WriteString(md.Link("← Prev", prevLink) + " ") + } + out.WriteString(ufmt.Sprintf("| %d/%d |", currentPage, totalSlides)) + if currentPage < totalSlides { + nextLink := localPath(slug+"/slides", url.Values{"page": {ufmt.Sprintf("%d", currentPage+1)}}) + out.WriteString(" " + md.Link("Next →", nextLink)) + } + return md.Paragraph(out.String()) +} + +func renderSlides(slug string, currentPage int) string { + if slug == "" { + return "400: missing presentation ID" + } + + entry := presentations.GetFirst("slug", slug) + if entry == nil { + return "404: presentation not found" + } + + p := entry.Obj.(*Presentation) + slides := strings.Split("\n"+p.Content, "\n## ") + if currentPage < 1 || currentPage > p.NumSlides { + return "404: invalid slide number" + } + + var out strings.Builder + + // Display current slide + if currentPage == 1 { + out.WriteString(p.FirstSlide()) + } else if currentPage == p.NumSlides { + out.WriteString(p.LastSlide()) + } else { + out.WriteString(md.H1(p.Title)) + out.WriteString("## " + slides[currentPage-1] + "\n\n") + } + + out.WriteString(renderSlidesNavigation(slug, currentPage, p.NumSlides)) + return out.String() +} + +// Helper functions for sorting and pagination +func getSortField(req *realmpath.Request) string { + field := req.Query.Get("sort") + switch field { + case "date", "slug", "author", "title": + return field + } + return "date" +} + +func isSortReversed(req *realmpath.Request) bool { + return req.Query.Get("order") != "asc" +} + +func renderSortLink(req *realmpath.Request, field, label string) string { + currentField := getSortField(req) + currentOrder := req.Query.Get("order") + + newOrder := "desc" + if field == currentField && currentOrder != "asc" { + newOrder = "asc" + } + + query := req.Query + query.Set("sort", field) + query.Set("order", newOrder) + + if field == currentField { + if newOrder == "asc" { + label += " ↑" + } else { + label += " ↓" + } + } + + return md.Link(label, "?"+query.Encode()) +} + +// helper to create local realm links +func localPath(path string, query url.Values) string { + req := &realmpath.Request{ + Path: path, + Query: query, + } + return req.String() +} diff --git a/examples/gno.land/r/moul/present/present_filetest.gno b/examples/gno.land/r/moul/present/present_filetest.gno new file mode 100644 index 00000000000..7e9385454b9 --- /dev/null +++ b/examples/gno.land/r/moul/present/present_filetest.gno @@ -0,0 +1,233 @@ +package main + +import ( + "gno.land/r/moul/present" +) + +func main() { + // Cleanup initial state + ret := present.Delete("demo") + if ret != "presentation deleted successfully" { + panic("internal error") + } + + // Create presentations with IDs from 10-20 + presentations := []struct { + id string + title string + event string + author string + date string + content string + }{ + {"s10", "title10", "event3", "author1", "2024-01-01", "## s10.0\n## s10.1"}, + {"s11", "title11", "event1", "author2", "2024-01-15", "## s11.0\n## s11.1"}, + {"s12", "title12", "event2", "author1", "2024-02-01", "## s12.0\n## s12.1"}, + {"s13", "title13", "event1", "author3", "2024-01-20", "## s13.0\n## s13.1"}, + {"s14", "title14", "event3", "author2", "2024-03-01", "## s14.0\n## s14.1"}, + {"s15", "title15", "event2", "author1", "2024-02-15", "## s15.0\n## s15.1\n## s15.2"}, + {"s16", "title16", "event1", "author4", "2024-03-15", "## s16.0\n## s16.1"}, + {"s17", "title17", "event3", "author2", "2024-01-10", "## s17.0\n## s17.1"}, + {"s18", "title18", "event2", "author3", "2024-02-20", "## s18.0\n## s18.1"}, + {"s19", "title19", "event1", "author1", "2024-03-10", "## s19.0\n## s19.1"}, + {"s20", "title20", "event3", "author4", "2024-01-05", "## s20.0\n## s20.1"}, + } + + for _, p := range presentations { + result := present.Set(p.id, p.title, p.event, p.author, p.date, p.content) + if result != "presentation saved successfully" { + panic("failed to add presentation: " + result) + } + } + + // Test different sorting scenarios + printRender("") // default + printRender("?order=asc&sort=date") // by date ascending + printRender("?order=asc&sort=title") // by title ascending + printRender("?order=asc&sort=author") // by author ascending (multiple entries per author) + + // Test pagination + printRender("?order=asc&sort=title&page=2") // second page + + // Test view + printRender("s15") // view by slug + + // Test slides + printRender("s15/slides") // slides by slug + printRender("s15/slides?page=2") // slides by slug, second page + printRender("s15/slides?page=3") // slides by slug, third page + printRender("s15/slides?page=4") // slides by slug, fourth page + printRender("s15/slides?page=5") // slides by slug, fifth page +} + +// Helper function to print path and render result +func printRender(path string) { + println("+-------------------------------") + println("| PATH:", path) + println("| RESULT:\n" + present.Render(path) + "\n") +} + +// Output: +// +------------------------------- +// | PATH: +// | RESULT: +// # Presentations +// | [Date ↑](?order=asc&sort=date) | [Title](?order=desc&sort=title) | Event | [Author](?order=desc&sort=author) | Slides | +// | --- | --- | --- | --- | --- | +// | 2024-03-15 | [title16](/r/moul/present:s16) | event1 | author4 | 4 | +// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 | +// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 | +// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 | +// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 | +// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 | +// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 | +// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 | +// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 | +// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 | +// **1** | [2](?page=2) +// +// +------------------------------- +// | PATH: ?order=asc&sort=date +// | RESULT: +// # Presentations +// | [Date ↓](?order=desc&sort=date) | [Title](?order=desc&sort=title) | Event | [Author](?order=desc&sort=author) | Slides | +// | --- | --- | --- | --- | --- | +// | 2024-01-01 | [title10](/r/moul/present:s10) | event3 | author1 | 4 | +// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 | +// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 | +// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 | +// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 | +// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 | +// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 | +// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 | +// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 | +// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 | +// **1** | [2](?page=2) +// +// +------------------------------- +// | PATH: ?order=asc&sort=title +// | RESULT: +// # Presentations +// | [Date](?order=desc&sort=date) | [Title](?order=desc&sort=title) | Event | [Author](?order=desc&sort=author) | Slides | +// | --- | --- | --- | --- | --- | +// | 2024-01-01 | [title10](/r/moul/present:s10) | event3 | author1 | 4 | +// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 | +// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 | +// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 | +// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 | +// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 | +// | 2024-03-15 | [title16](/r/moul/present:s16) | event1 | author4 | 4 | +// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 | +// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 | +// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 | +// **1** | [2](?page=2) +// +// +------------------------------- +// | PATH: ?order=asc&sort=author +// | RESULT: +// # Presentations +// | [Date](?order=desc&sort=date) | [Title](?order=desc&sort=title) | Event | [Author](?order=desc&sort=author) | Slides | +// | --- | --- | --- | --- | --- | +// | 2024-01-01 | [title10](/r/moul/present:s10) | event3 | author1 | 4 | +// | 2024-02-01 | [title12](/r/moul/present:s12) | event2 | author1 | 4 | +// | 2024-02-15 | [title15](/r/moul/present:s15) | event2 | author1 | 5 | +// | 2024-03-10 | [title19](/r/moul/present:s19) | event1 | author1 | 4 | +// | 2024-01-15 | [title11](/r/moul/present:s11) | event1 | author2 | 4 | +// | 2024-03-01 | [title14](/r/moul/present:s14) | event3 | author2 | 4 | +// | 2024-01-10 | [title17](/r/moul/present:s17) | event3 | author2 | 4 | +// | 2024-01-20 | [title13](/r/moul/present:s13) | event1 | author3 | 4 | +// | 2024-02-20 | [title18](/r/moul/present:s18) | event2 | author3 | 4 | +// | 2024-03-15 | [title16](/r/moul/present:s16) | event1 | author4 | 4 | +// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 | +// +// +// +------------------------------- +// | PATH: ?order=asc&sort=title&page=2 +// | RESULT: +// # Presentations +// | [Date](?order=desc&page=2&sort=date) | [Title](?order=desc&page=2&sort=title) | Event | [Author](?order=desc&page=2&sort=author) | Slides | +// | --- | --- | --- | --- | --- | +// | 2024-01-05 | [title20](/r/moul/present:s20) | event3 | author4 | 4 | +// [1](?page=1) | **2** +// +// +------------------------------- +// | PATH: s15 +// | RESULT: +// # title15 +// **event2**, 15 Feb 2024 +// +// by **author1** +// +// [View as slides](/r/moul/present:s15/slides) +// +// --- +// ## s15.0 +// ## s15.1 +// ## s15.2 +// +// --- +// Last edited: 2009-02-13 23:31:30 +// +// Uploader: `g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm` +// +// Number of slides: 5 +// +// [Edit](/r/moul/present$help&func=Set&author=author1&date=2024-02-15&event=event2&slug=s15&title=title15) | [Delete](/r/moul/present$help&func=Delete&slug=s15) +// +// +// +// +------------------------------- +// | PATH: s15/slides +// | RESULT: +// # title15 +// **event2**, 15 Feb 2024 +// +// by **author1** +// +// | 1/5 | [Next →](/r/moul/present:s15/slides?page=2) +// +// +// +// +------------------------------- +// | PATH: s15/slides?page=2 +// | RESULT: +// # title15 +// ## s15.0 +// +// [← Prev](/r/moul/present:s15/slides?page=1) | 2/5 | [Next →](/r/moul/present:s15/slides?page=3) +// +// +// +// +------------------------------- +// | PATH: s15/slides?page=3 +// | RESULT: +// # title15 +// ## s15.1 +// +// [← Prev](/r/moul/present:s15/slides?page=2) | 3/5 | [Next →](/r/moul/present:s15/slides?page=4) +// +// +// +// +------------------------------- +// | PATH: s15/slides?page=4 +// | RESULT: +// # title15 +// ## s15.2 +// +// [← Prev](/r/moul/present:s15/slides?page=3) | 4/5 | [Next →](/r/moul/present:s15/slides?page=5) +// +// +// +// +------------------------------- +// | PATH: s15/slides?page=5 +// | RESULT: +// # title15 +// ## Thank You! +// author1 +// +// 🔗 [https://tests\.gno\.land/r/moul/present:s15](https://tests.gno.land/r/moul/present:s15) +// +// [← Prev](/r/moul/present:s15/slides?page=4) | 5/5 | +// +// +// diff --git a/examples/gno.land/r/moul/present/present_init.gno b/examples/gno.land/r/moul/present/present_init.gno new file mode 100644 index 00000000000..b103bdf8cd6 --- /dev/null +++ b/examples/gno.land/r/moul/present/present_init.gno @@ -0,0 +1,25 @@ +package present + +func init() { + _ = Set( + "demo", // id + "Demo Slides", // title + "Demo Event", // event + "@demo", // author + "2025-02-02", // date + `## Slide One +- Point A +- Point B +- Point C + +## Slide Two +- Feature 1 +- Feature 2 +- Feature 3 + +## Slide Three +- Next step +- Another step +- Final step`, + ) +} diff --git a/examples/gno.land/r/moul/present/present_miami23.gno b/examples/gno.land/r/moul/present/present_miami23.gno deleted file mode 100644 index ca2160de3a9..00000000000 --- a/examples/gno.land/r/moul/present/present_miami23.gno +++ /dev/null @@ -1,42 +0,0 @@ -package present - -func init() { - path := "miami23" - title := "Portal Loop Demo (Miami 2023)" - body := ` -Rendered by Gno. - -[Source (WIP)](https://github.com/gnolang/gno/pull/1176) - -## Portal Loop - -- DONE: Dynamic homepage, key pages, aliases, and redirects. -- TODO: Deploy with history, complete worxdao v0. -- Will replace the static gno.land site. -- Enhances local development. - -[GitHub Issue](https://github.com/gnolang/gno/issues/1108) - -## Roadmap - -- Crafting the roadmap this week, open to collaboration. -- Combining onchain (portal loop) and offchain (GitHub). -- Next week: Unveiling the official v0 roadmap. - -## Teams, DAOs, Projects - -- Developing worxDAO contracts for directories of projects and teams. -- GitHub teams and projects align with this structure. -- CODEOWNER file updates coming. -- Initial teams announced next week. - -## Tech Team Retreat Plan - -- Continue Portal Loop. -- Consider dApp development. -- Explore new topics [here](https://github.com/orgs/gnolang/projects/15/). -- Engage in workshops. -- Connect and have fun with colleagues. -` - _ = b.NewPost(adminAddr, path, title, body, "2023-10-15T13:17:24Z", []string{"moul"}, []string{"demo", "portal-loop", "miami"}) -} diff --git a/examples/gno.land/r/moul/present/present_miami23_filetest.gno b/examples/gno.land/r/moul/present/present_miami23_filetest.gno deleted file mode 100644 index 09d332ec6e4..00000000000 --- a/examples/gno.land/r/moul/present/present_miami23_filetest.gno +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - "gno.land/r/moul/present" -) - -func main() { - println(present.Render("")) - println("------------------------------------") - println(present.Render("p/miami23")) -} diff --git a/examples/gno.land/r/moul/present/presentations.gno b/examples/gno.land/r/moul/present/presentations.gno deleted file mode 100644 index c5529804751..00000000000 --- a/examples/gno.land/r/moul/present/presentations.gno +++ /dev/null @@ -1,17 +0,0 @@ -package present - -import ( - "gno.land/p/demo/blog" -) - -// TODO: switch from p/blog to p/present - -var b = &blog.Blog{ - Title: "Manfred's Presentations", - Prefix: "/r/moul/present:", - NoBreadcrumb: true, -} - -func Render(path string) string { - return b.Render(path) -} diff --git a/examples/gno.land/r/stefann/fomo3d/errors.gno b/examples/gno.land/r/stefann/fomo3d/errors.gno new file mode 100644 index 00000000000..df70ab08c55 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/errors.gno @@ -0,0 +1,30 @@ +package fomo3d + +import "errors" + +var ( + // Game state errors + ErrGameInProgress = errors.New("fomo3d: game already in progress") + ErrGameNotInProgress = errors.New("fomo3d: game not in progress") + ErrGameEnded = errors.New("fomo3d: game has ended") + ErrGameTimeExpired = errors.New("fomo3d: game time expired") + ErrNoKeysPurchased = errors.New("fomo3d: no keys purchased") + ErrPlayerNotInGame = errors.New("fomo3d: player is not in the game") + + // Payment errors + ErrInvalidPayment = errors.New("fomo3d: must send ugnot only") + ErrInsufficientPayment = errors.New("fomo3d: insufficient payment for key") + + // Dividend errors + ErrNoDividendsToClaim = errors.New("fomo3d: no dividends to claim") + + // Fee errors + ErrNoFeesToClaim = errors.New("fomo3d: no owner fees to claim") + + // Resolution errors + ErrInvalidAddressOrName = errors.New("fomo3d: invalid address or unregistered username") + + // NFT errors + ErrUnauthorizedMint = errors.New("fomo3d: only the Fomo3D game realm can mint winner NFTs") + ErrZeroAddress = errors.New("fomo3d: zero address") +) diff --git a/examples/gno.land/r/stefann/fomo3d/events.gno b/examples/gno.land/r/stefann/fomo3d/events.gno new file mode 100644 index 00000000000..ea404466955 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/events.gno @@ -0,0 +1,94 @@ +package fomo3d + +import ( + "std" + + "gno.land/p/demo/ufmt" +) + +// Event names +const ( + // Game events + GameStartedEvent = "GameStarted" + GameEndedEvent = "GameEnded" + KeysPurchasedEvent = "KeysPurchased" + + // Player events + DividendsClaimedEvent = "DividendsClaimed" + + // Admin events + OwnerFeeClaimedEvent = "OwnerFeeClaimed" +) + +// Event keys +const ( + // Common keys + EventRoundKey = "round" + EventAmountKey = "amount" + + // Game keys + EventStartBlockKey = "startBlock" + EventEndBlockKey = "endBlock" + EventStartingPotKey = "startingPot" + EventWinnerKey = "winner" + EventJackpotKey = "jackpot" + + // Player keys + EventBuyerKey = "buyer" + EventNumKeysKey = "numKeys" + EventPriceKey = "price" + EventJackpotShareKey = "jackpotShare" + EventDividendShareKey = "dividendShare" + EventClaimerKey = "claimer" + + // Admin keys + EventOwnerKey = "owner" + EventPreviousOwnerKey = "previousOwner" + EventNewOwnerKey = "newOwner" +) + +func emitGameStarted(round, startBlock, endBlock, startingPot int64) { + std.Emit( + GameStartedEvent, + EventRoundKey, ufmt.Sprintf("%d", round), + EventStartBlockKey, ufmt.Sprintf("%d", startBlock), + EventEndBlockKey, ufmt.Sprintf("%d", endBlock), + EventStartingPotKey, ufmt.Sprintf("%d", startingPot), + ) +} + +func emitGameEnded(round int64, winner std.Address, jackpot int64) { + std.Emit( + GameEndedEvent, + EventRoundKey, ufmt.Sprintf("%d", round), + EventWinnerKey, winner.String(), + EventJackpotKey, ufmt.Sprintf("%d", jackpot), + ) +} + +func emitKeysPurchased(buyer std.Address, numKeys, price, jackpotShare, dividendShare int64) { + std.Emit( + KeysPurchasedEvent, + EventBuyerKey, buyer.String(), + EventNumKeysKey, ufmt.Sprintf("%d", numKeys), + EventPriceKey, ufmt.Sprintf("%d", price), + EventJackpotShareKey, ufmt.Sprintf("%d", jackpotShare), + EventDividendShareKey, ufmt.Sprintf("%d", dividendShare), + ) +} + +func emitDividendsClaimed(claimer std.Address, amount int64) { + std.Emit( + DividendsClaimedEvent, + EventClaimerKey, claimer.String(), + EventAmountKey, ufmt.Sprintf("%d", amount), + ) +} + +func emitOwnerFeeClaimed(owner std.Address, amount int64) { + std.Emit( + OwnerFeeClaimedEvent, + EventOwnerKey, owner.String(), + EventAmountKey, ufmt.Sprintf("%d", amount), + ) +} diff --git a/examples/gno.land/r/stefann/fomo3d/fomo3d.gno b/examples/gno.land/r/stefann/fomo3d/fomo3d.gno new file mode 100644 index 00000000000..b2384ba07f4 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/fomo3d.gno @@ -0,0 +1,358 @@ +package fomo3d + +import ( + "std" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/ufmt" + + "gno.land/r/demo/users" + "gno.land/r/leon/hof" +) + +// FOMO3D (Fear Of Missing Out 3D) is a blockchain-based game that combines elements +// of a lottery and investment mechanics. Players purchase keys using GNOT tokens, +// where each key purchase: +// - Extends the game timer +// - Increases the key price by 1% +// - Makes the buyer the potential winner of the jackpot +// - Distributes dividends to all key holders +// +// Game Mechanics: +// - The last person to buy a key before the timer expires wins the jackpot (47% of all purchases) +// - Key holders earn dividends from each purchase (28% of all purchases) +// - 20% of purchases go to the next round's starting pot +// - 5% goes to development fee +// - Game ends when the timer expires +// +// Inspired by the original Ethereum FOMO3D game but implemented in Gno. + +const ( + MIN_KEY_PRICE int64 = 100000 // minimum key price in ugnot + TIME_EXTENSION int64 = 86400 // time extension in blocks when new key is bought (~24 hours @ 1s blocks) + + // Distribution percentages (total 100%) + JACKPOT_PERCENT int64 = 47 // 47% goes to jackpot + DIVIDENDS_PERCENT int64 = 28 // 28% distributed to key holders + NEXT_ROUND_POT int64 = 20 // 20% goes to next round's starting pot + OWNER_FEE_PERCENT int64 = 5 // 5% goes to contract owner +) + +type PlayerInfo struct { + Keys int64 // number of keys owned + Dividends int64 // unclaimed dividends in ugnot +} + +// GameState represents the current state of the FOMO3D game +type GameState struct { // TODO: Separate GameState and RoundState and save round history tree in GameState + StartBlock int64 // Block when the game started + EndBlock int64 // Block when the game will end + LastKeyBlock int64 // Block of last key purchase + LastBuyer std.Address // Address of last key buyer + Jackpot int64 // Current jackpot in ugnot + KeyPrice int64 // Current price of keys in ugnot + TotalKeys int64 // Total number of keys in circulation + Ended bool // Whether the game has ended + CurrentRound int64 // Current round number + NextPot int64 // Next round's starting pot + OwnerFee int64 // Accumulated owner fees + BuyKeysLink string // Link to BuyKeys function + ClaimDividendsLink string // Link to ClaimDividends function + StartGameLink string // Link to StartGame function +} + +var ( + gameState GameState + players *avl.Tree // maps address -> PlayerInfo + Ownable *ownable.Ownable +) + +func init() { + Ownable = ownable.New() + players = avl.NewTree() + gameState.Ended = true + hof.Register() +} + +// StartGame starts a new game round +func StartGame() { + if !gameState.Ended && gameState.StartBlock != 0 { + panic(ErrGameInProgress.Error()) + } + + gameState.CurrentRound++ + gameState.StartBlock = std.GetHeight() + gameState.EndBlock = gameState.StartBlock + TIME_EXTENSION // Initial 24h window + gameState.LastKeyBlock = gameState.StartBlock + gameState.Jackpot = gameState.NextPot + gameState.NextPot = 0 + gameState.Ended = false + gameState.KeyPrice = MIN_KEY_PRICE + gameState.TotalKeys = 0 + + // Clear previous round's player data + players = avl.NewTree() + + emitGameStarted( + gameState.CurrentRound, + gameState.StartBlock, + gameState.EndBlock, + gameState.Jackpot, + ) +} + +// BuyKeys allows players to purchase keys +func BuyKeys() { + if gameState.Ended { + panic(ErrGameEnded.Error()) + } + + currentBlock := std.GetHeight() + if currentBlock > gameState.EndBlock { + panic(ErrGameTimeExpired.Error()) + } + + // Get sent coins + sent := std.GetOrigSend() + if len(sent) != 1 || sent[0].Denom != "ugnot" { + panic(ErrInvalidPayment.Error()) + } + + payment := sent.AmountOf("ugnot") + if payment < gameState.KeyPrice { + panic(ErrInsufficientPayment.Error()) + } + + // Calculate number of keys that can be bought and actual cost + numKeys := payment / gameState.KeyPrice + actualCost := numKeys * gameState.KeyPrice + excess := payment - actualCost + + // Update buyer's info + buyer := std.PrevRealm().Addr() + var buyerInfo PlayerInfo + if info, exists := players.Get(buyer.String()); exists { + buyerInfo = info.(PlayerInfo) + } + + buyerInfo.Keys += numKeys + gameState.TotalKeys += numKeys + + // Distribute actual cost + jackpotShare := actualCost * JACKPOT_PERCENT / 100 + dividendShare := actualCost * DIVIDENDS_PERCENT / 100 + nextPotShare := actualCost * NEXT_ROUND_POT / 100 + ownerShare := actualCost * OWNER_FEE_PERCENT / 100 + + // Update pools + gameState.Jackpot += jackpotShare + gameState.NextPot += nextPotShare + gameState.OwnerFee += ownerShare + + // Return excess payment to buyer if any + if excess > 0 { + banker := std.GetBanker(std.BankerTypeOrigSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + buyer, + std.NewCoins(std.NewCoin("ugnot", excess)), + ) + } + + // Distribute dividends to all key holders + if players.Size() > 0 && gameState.TotalKeys > 0 { + dividendPerKey := dividendShare / gameState.TotalKeys + players.Iterate("", "", func(key string, value interface{}) bool { + playerInfo := value.(PlayerInfo) + playerInfo.Dividends += playerInfo.Keys * dividendPerKey + players.Set(key, playerInfo) + return false + }) + } + + // Update game state + gameState.LastBuyer = buyer + gameState.LastKeyBlock = currentBlock + gameState.EndBlock = currentBlock + TIME_EXTENSION // Always extend 24h from current block + gameState.KeyPrice += (gameState.KeyPrice * numKeys) / 100 + + // Save buyer's updated info + players.Set(buyer.String(), buyerInfo) + + emitKeysPurchased( + buyer, + numKeys, + gameState.KeyPrice, + jackpotShare, + dividendShare, + ) +} + +// ClaimDividends allows players to withdraw their earned dividends +func ClaimDividends() { + caller := std.PrevRealm().Addr() + + info, exists := players.Get(caller.String()) + if !exists { + panic(ErrNoDividendsToClaim.Error()) + } + + playerInfo := info.(PlayerInfo) + if playerInfo.Dividends == 0 { + panic(ErrNoDividendsToClaim.Error()) + } + + // Reset dividends and send coins + amount := playerInfo.Dividends + playerInfo.Dividends = 0 + players.Set(caller.String(), playerInfo) + + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + caller, + std.NewCoins(std.NewCoin("ugnot", amount)), + ) + + emitDividendsClaimed(caller, amount) +} + +// ClaimOwnerFee allows the owner to withdraw accumulated fees +func ClaimOwnerFee() { + Ownable.AssertCallerIsOwner() + + if gameState.OwnerFee == 0 { + panic(ErrNoFeesToClaim.Error()) + } + + amount := gameState.OwnerFee + gameState.OwnerFee = 0 + + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + Ownable.Owner(), + std.NewCoins(std.NewCoin("ugnot", amount)), + ) + + emitOwnerFeeClaimed(Ownable.Owner(), amount) +} + +// EndGame ends the current round and distributes the jackpot +func EndGame() { + if gameState.Ended { + panic(ErrGameEnded.Error()) + } + + currentBlock := std.GetHeight() + if currentBlock <= gameState.EndBlock { + panic(ErrGameNotInProgress.Error()) + } + + if gameState.LastBuyer == "" { + panic(ErrNoKeysPurchased.Error()) + } + + gameState.Ended = true + + // Send jackpot to winner + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + gameState.LastBuyer, + std.NewCoins(std.NewCoin("ugnot", gameState.Jackpot)), + ) + + emitGameEnded( + gameState.CurrentRound, + gameState.LastBuyer, + gameState.Jackpot, + ) + + // Mint NFT for the winner + if err := mintRoundWinnerNFT(gameState.LastBuyer, gameState.CurrentRound); err != nil { + panic(err.Error()) + } +} + +// GetGameState returns current game state +func GetGameState() (int64, int64, int64, std.Address, int64, int64, int64, bool, int64, int64) { + return gameState.StartBlock, + gameState.EndBlock, + gameState.LastKeyBlock, + gameState.LastBuyer, + gameState.Jackpot, + gameState.KeyPrice, + gameState.TotalKeys, + gameState.Ended, + gameState.NextPot, + gameState.CurrentRound +} + +// GetOwnerInfo returns the owner address and unclaimed fees +func GetOwnerInfo() (std.Address, int64) { + return Ownable.Owner(), gameState.OwnerFee +} + +// Helper to convert string (address or username) to address +func stringToAddress(input string) std.Address { + // Check if input is valid address + addr := std.Address(input) + if addr.IsValid() { + return addr + } + + // Not an address, try to find namespace + if user := users.GetUserByName(input); user != nil { + return user.Address + } + + return "" +} + +func isPlayerInGame(addr std.Address) bool { + _, exists := players.Get(addr.String()) + return exists +} + +// GetPlayerInfo returns a player's keys and dividends +func GetPlayerInfo(addrOrName string) (int64, int64) { + addr := stringToAddress(addrOrName) + + if addr == "" { + panic(ErrInvalidAddressOrName.Error()) + } + + if !isPlayerInGame(addr) { + panic(ErrPlayerNotInGame.Error()) + } + + info, _ := players.Get(addr.String()) + playerInfo := info.(PlayerInfo) + return playerInfo.Keys, playerInfo.Dividends +} + +// Render handles the rendering of game state +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return RenderHome() + case c == 2 && parts[0] == "player": + if gameState.Ended { + return ufmt.Sprintf("🔴 Game has not started yet.\n\n Call [`StartGame()`](%s) to start a new round.\n\n", gameState.StartGameLink) + } + addr := stringToAddress(parts[1]) + if addr == "" || !isPlayerInGame(addr) { + return "Address not found in game. You need to buy keys first to view your stats.\n\n" + } + keys, dividends := GetPlayerInfo(parts[1]) + return RenderPlayer(addr, keys, dividends) + default: + return "404: Invalid path\n\n" + } +} diff --git a/examples/gno.land/r/stefann/fomo3d/fomo3d_test.gno b/examples/gno.land/r/stefann/fomo3d/fomo3d_test.gno new file mode 100644 index 00000000000..29f2a9b07a9 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/fomo3d_test.gno @@ -0,0 +1,294 @@ +package fomo3d + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ownable" + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" +) + +// Reset game state +func setupTestGame(t *testing.T) { + gameState = GameState{ + StartBlock: 0, + EndBlock: 0, + LastKeyBlock: 0, + LastBuyer: "", + Jackpot: 0, + KeyPrice: MIN_KEY_PRICE, + TotalKeys: 0, + Ended: true, + CurrentRound: 0, + NextPot: 0, + OwnerFee: 0, + } + players = avl.NewTree() + Ownable = ownable.New() +} + +// Test ownership functionality +func TestOwnership(t *testing.T) { + owner := testutils.TestAddress("owner") + nonOwner := testutils.TestAddress("nonOwner") + + // Set up initial owner + std.TestSetOrigCaller(owner) + std.TestSetOrigPkgAddr(owner) + setupTestGame(t) + + // Transfer ownership to nonOwner first to test ownership functions + std.TestSetOrigCaller(owner) + urequire.NotPanics(t, func() { + Ownable.TransferOwnership(nonOwner) + }) + + // Test fee accumulation + StartGame() + payment := MIN_KEY_PRICE * 10 + std.TestSetOrigCaller(owner) + std.TestSetOrigSend(std.Coins{{"ugnot", payment}}, nil) + std.TestIssueCoins(owner, std.Coins{{"ugnot", payment}}) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", payment}}) + BuyKeys() + + // Verify fee accumulation + _, fees := GetOwnerInfo() + expectedFees := payment * OWNER_FEE_PERCENT / 100 + urequire.Equal(t, expectedFees, fees) + + // Test unauthorized fee claim (using old owner) + std.TestSetOrigCaller(owner) + urequire.PanicsWithMessage(t, "ownable: caller is not owner", ClaimOwnerFee) + + // Test authorized fee claim (using new owner) + std.TestSetOrigCaller(nonOwner) + initialBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(nonOwner) + std.TestIssueCoins(std.CurrentRealm().Addr(), std.Coins{{"ugnot", expectedFees}}) + urequire.NotPanics(t, ClaimOwnerFee) + + // Verify fees were claimed + _, feesAfter := GetOwnerInfo() + urequire.Equal(t, int64(0), feesAfter) + + finalBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(nonOwner) + urequire.Equal(t, initialBalance.AmountOf("ugnot")+expectedFees, finalBalance.AmountOf("ugnot")) +} + +// Test full game flow +func TestFullGameFlow(t *testing.T) { + setupTestGame(t) + + player1 := testutils.TestAddress("player1") + player2 := testutils.TestAddress("player2") + player3 := testutils.TestAddress("player3") + + // Test initial state + urequire.Equal(t, int64(0), gameState.CurrentRound) + urequire.Equal(t, MIN_KEY_PRICE, gameState.KeyPrice) + urequire.Equal(t, true, gameState.Ended) + + // Start game + urequire.NotPanics(t, StartGame) + urequire.Equal(t, false, gameState.Ended) + urequire.Equal(t, std.GetHeight(), gameState.StartBlock) + urequire.Equal(t, int64(1), gameState.CurrentRound) + + t.Run("buying keys", func(t *testing.T) { + // Test insufficient payment + std.TestSetOrigCaller(player1) + std.TestIssueCoins(player1, std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + urequire.PanicsWithMessage(t, ErrInsufficientPayment.Error(), BuyKeys) + + // Test successful key purchase + payment := MIN_KEY_PRICE * 3 + std.TestSetOrigSend(std.Coins{{"ugnot", payment}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", payment}}) + + currentBlock := std.GetHeight() + urequire.NotPanics(t, BuyKeys) + + // Verify time extension + _, endBlock, _, _, _, _, _, _, _, _ := GetGameState() + urequire.Equal(t, currentBlock+TIME_EXTENSION, endBlock) + + // Verify player state + keys, dividends := GetPlayerInfo(player1.String()) + + urequire.Equal(t, int64(3), keys) + urequire.Equal(t, int64(0), dividends) + urequire.Equal(t, player1, gameState.LastBuyer) + + // Verify game state + _, endBlock, _, buyer, pot, price, keys, isEnded, nextPot, round := GetGameState() + urequire.Equal(t, player1, buyer) + urequire.Equal(t, int64(3), keys) + urequire.Equal(t, false, isEnded) + + urequire.Equal(t, payment*JACKPOT_PERCENT/100, pot) + + // Verify owner fee + _, ownerFees := GetOwnerInfo() + urequire.Equal(t, payment*OWNER_FEE_PERCENT/100, ownerFees) + }) + + t.Run("dividend distribution and claiming", func(t *testing.T) { + // Player 2 buys keys + std.TestSetOrigCaller(player2) + payment := gameState.KeyPrice * 2 // Buy 2 keys using current keyPrice + std.TestSetOrigSend(std.Coins{{"ugnot", payment}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", payment}}) + urequire.NotPanics(t, BuyKeys) + + // Check player1 received dividends + keys1, dividends1 := GetPlayerInfo(player1.String()) + + urequire.Equal(t, int64(3), keys1) + expectedDividends := payment * DIVIDENDS_PERCENT / 100 * 3 / gameState.TotalKeys + urequire.Equal(t, expectedDividends, dividends1) + + // Test claiming dividends + { + // Player1 claims dividends + std.TestSetOrigCaller(player1) + initialBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(player1) + urequire.NotPanics(t, ClaimDividends) + + // Verify dividends were claimed + _, dividendsAfter := GetPlayerInfo(player1.String()) + urequire.Equal(t, int64(0), dividendsAfter) + + lastBuyerBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(player1) + urequire.Equal(t, initialBalance.AmountOf("ugnot")+expectedDividends, lastBuyerBalance.AmountOf("ugnot")) + } + }) + + t.Run("game ending", func(t *testing.T) { + // Try ending too early + urequire.PanicsWithMessage(t, ErrGameNotInProgress.Error(), EndGame) + + // Skip to end of current time window + currentEndBlock := gameState.EndBlock + std.TestSkipHeights(currentEndBlock - std.GetHeight() + 1) + + // End game successfully + urequire.NotPanics(t, EndGame) + urequire.Equal(t, true, gameState.Ended) + urequire.Equal(t, int64(1), gameState.CurrentRound) + + // Verify winner received jackpot + lastBuyerBalance := std.GetBanker(std.BankerTypeRealmSend).GetCoins(gameState.LastBuyer) + urequire.Equal(t, gameState.Jackpot, lastBuyerBalance.AmountOf("ugnot")) + + // Verify NFT was minted to winner + balance, err := BalanceOf(gameState.LastBuyer) + urequire.NoError(t, err) + urequire.Equal(t, uint64(1), balance) + + // Check NFT metadata + tokenID := grc721.TokenID("1") + metadata, err := TokenMetadata(tokenID) + + urequire.NoError(t, err) + urequire.Equal(t, "Fomo3D Winner - Round #1", metadata.Name) + }) + + // Test new round + t.Run("new round", func(t *testing.T) { + // Calculate expected next pot from previous round + payment1 := MIN_KEY_PRICE * 3 + // After buying 3 keys, price increased by 3% (1% per key) + secondKeyPrice := MIN_KEY_PRICE + (MIN_KEY_PRICE * 3 / 100) + payment2 := secondKeyPrice * 2 + expectedNextPot := (payment1 * NEXT_ROUND_POT / 100) + (payment2 * NEXT_ROUND_POT / 100) + + // Start new round + urequire.NotPanics(t, StartGame) + urequire.Equal(t, false, gameState.Ended) + urequire.Equal(t, int64(2), gameState.CurrentRound) + + start, end, last, buyer, pot, price, keys, isEnded, nextPot, round := GetGameState() + urequire.Equal(t, int64(2), round) + urequire.Equal(t, expectedNextPot, pot) + urequire.Equal(t, int64(0), nextPot) + }) +} + +// Test individual components +func TestStartGame(t *testing.T) { + setupTestGame(t) + + // Test starting first game + urequire.NotPanics(t, StartGame) + urequire.Equal(t, false, gameState.Ended) + urequire.Equal(t, std.GetHeight(), gameState.StartBlock) + + // Test cannot start while game in progress + urequire.PanicsWithMessage(t, ErrGameInProgress.Error(), StartGame) +} + +func TestBuyKeys(t *testing.T) { + setupTestGame(t) + StartGame() + + player := testutils.TestAddress("player") + std.TestSetOrigCaller(player) + + // Test invalid coin denomination + std.TestIssueCoins(player, std.Coins{{"invalid", MIN_KEY_PRICE}}) + std.TestSetOrigSend(std.Coins{{"invalid", MIN_KEY_PRICE}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"invalid", MIN_KEY_PRICE}}) + urequire.PanicsWithMessage(t, ErrInvalidPayment.Error(), BuyKeys) + + // Test multiple coin types + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE}, {"other", 100}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE}, {"other", 100}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE}, {"other", 100}}) + urequire.PanicsWithMessage(t, ErrInvalidPayment.Error(), BuyKeys) + + // Test insufficient payment + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE - 1}}) + urequire.PanicsWithMessage(t, ErrInsufficientPayment.Error(), BuyKeys) + + // Test successful purchase + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + urequire.NotPanics(t, BuyKeys) +} + +func TestClaimDividends(t *testing.T) { + setupTestGame(t) + StartGame() + + player := testutils.TestAddress("player") + std.TestSetOrigCaller(player) + + // Test claiming with no dividends + urequire.PanicsWithMessage(t, ErrNoDividendsToClaim.Error(), ClaimDividends) + + // Setup player with dividends + std.TestIssueCoins(player, std.Coins{{"ugnot", MIN_KEY_PRICE}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE}}) + BuyKeys() + + // Have another player buy to generate dividends + player2 := testutils.TestAddress("player2") + std.TestSetOrigCaller(player2) + std.TestIssueCoins(player2, std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + std.TestSetOrigSend(std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}, nil) + std.TestIssueCoins(std.GetOrigPkgAddr(), std.Coins{{"ugnot", MIN_KEY_PRICE * 2}}) + BuyKeys() + + // Test successful claim + std.TestSetOrigCaller(player) + urequire.NotPanics(t, ClaimDividends) +} diff --git a/examples/gno.land/r/stefann/fomo3d/gno.mod b/examples/gno.land/r/stefann/fomo3d/gno.mod new file mode 100644 index 00000000000..1b4e630a285 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/gno.mod @@ -0,0 +1 @@ +module gno.land/r/stefann/fomo3d diff --git a/examples/gno.land/r/stefann/fomo3d/nft.gno b/examples/gno.land/r/stefann/fomo3d/nft.gno new file mode 100644 index 00000000000..adea2fee795 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/nft.gno @@ -0,0 +1,88 @@ +package fomo3d + +import ( + "std" + "strconv" + + "gno.land/p/demo/grc/grc721" +) + +var ( + fomo3dNFT = grc721.NewNFTWithMetadata("Fomo3D Winner", "FOMO") +) + +// Public getters + +func Name() string { + return fomo3dNFT.Name() +} + +func Symbol() string { + return fomo3dNFT.Symbol() +} + +func BalanceOf(owner std.Address) (uint64, error) { + return fomo3dNFT.BalanceOf(owner) +} + +func OwnerOf(tokenID grc721.TokenID) (std.Address, error) { + return fomo3dNFT.OwnerOf(tokenID) +} + +func TokenMetadata(tokenID grc721.TokenID) (grc721.Metadata, error) { + return fomo3dNFT.TokenMetadata(tokenID) +} + +// Transfer and approval methods + +func TransferFrom(from, to std.Address, tokenID grc721.TokenID) error { + return fomo3dNFT.TransferFrom(from, to, tokenID) +} + +func SafeTransferFrom(from, to std.Address, tokenID grc721.TokenID) error { + return fomo3dNFT.SafeTransferFrom(from, to, tokenID) +} + +func Approve(approved std.Address, tokenID grc721.TokenID) error { + return fomo3dNFT.Approve(approved, tokenID) +} + +func GetApproved(tokenID grc721.TokenID) (std.Address, error) { + return fomo3dNFT.GetApproved(tokenID) +} + +func SetApprovalForAll(operator std.Address, approved bool) error { + return fomo3dNFT.SetApprovalForAll(operator, approved) +} + +func IsApprovedForAll(owner, operator std.Address) bool { + return fomo3dNFT.IsApprovedForAll(owner, operator) +} + +// Mints a new NFT for the round winner +func mintRoundWinnerNFT(winner std.Address, roundNumber int64) error { + if winner == "" { + return ErrZeroAddress + } + + roundStr := strconv.FormatInt(roundNumber, 10) + tokenID := grc721.TokenID(roundStr) + + // Create metadata + metadata := grc721.Metadata{ + Name: "Fomo3D Winner - Round #" + roundStr, + Description: "Winner of Fomo3D round #" + roundStr, + Image: "https://ipfs.io/ipfs/bafybeidayyli6bpewkhgtwqpgubmo77kmgjn4r5zq2i7usoyadcmvynhhq", + ExternalURL: "https://gno.land/r/stefann/fomo3d:round/" + roundStr, // TODO: Add this render in main realm that shows details of specific round + Attributes: []grc721.Trait{}, + BackgroundColor: "2D2D2D", // Dark theme background + } + + if err := fomo3dNFT.Mint(winner, tokenID); err != nil { + return err + } + + fomo3dNFT.SetTokenMetadata(tokenID, metadata) + + return nil +} diff --git a/examples/gno.land/r/stefann/fomo3d/render.gno b/examples/gno.land/r/stefann/fomo3d/render.gno new file mode 100644 index 00000000000..ba0c7b8f147 --- /dev/null +++ b/examples/gno.land/r/stefann/fomo3d/render.gno @@ -0,0 +1,138 @@ +package fomo3d + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ufmt" + + "gno.land/r/demo/users" +) + +// RenderHome renders the main game state +func RenderHome() string { + var builder strings.Builder + builder.WriteString("# FOMO3D - The Ultimate Game of Greed\n\n") + + // About section + builder.WriteString("## About the Game\n\n") + builder.WriteString("FOMO3D is a game that combines elements of lottery and investment mechanics. ") + builder.WriteString("Players purchase keys using GNOT tokens, where each key purchase:\n\n") + builder.WriteString("* Extends the game timer\n") + builder.WriteString("* Increases the key price by 1%\n") + builder.WriteString("* Makes you the potential winner of the jackpot\n") + builder.WriteString("* Distributes dividends to all key holders\n\n") + builder.WriteString("## How to Win\n\n") + builder.WriteString("* Be the last person to buy a key before the timer expires!\n\n") + builder.WriteString("**Rewards Distribution:**\n") + builder.WriteString("* 47% goes to the jackpot (for the winner)\n") + builder.WriteString("* 28% distributed as dividends to all key holders\n") + builder.WriteString("* 20% goes to next round's starting pot\n") + builder.WriteString("* 5% development fee for continuous improvement\n\n") + + // Play Game section + builder.WriteString("## How to Play\n\n") + builder.WriteString(ufmt.Sprintf("1. **Buy Keys** - Send GNOT to this realm with function [`BuyKeys()`](%s)\n", gameState.BuyKeysLink)) + builder.WriteString(ufmt.Sprintf("2. **Collect Dividends** - Call [`ClaimDividends()`](%s) to collect your earnings\n", gameState.ClaimDividendsLink)) + builder.WriteString("3. **Check Your Stats** - Append `:player/` followed by your address or namespace to the current URL to view your keys and dividends\n") + if gameState.Ended { + builder.WriteString(ufmt.Sprintf("4. **Start New Round** - Call [`StartGame()`](%s) to begin a new round\n", gameState.StartGameLink)) + } + builder.WriteString("\n") + + // Game Status section + builder.WriteString("## Game Status\n\n") + if gameState.StartBlock == 0 { + builder.WriteString("🔴 Game has not started yet.\n\n") + } else { + if gameState.Ended { + builder.WriteString("🔴 **Game Status:** Ended\n") + builder.WriteString(ufmt.Sprintf("🏆 **Winner:** %s\n\n", gameState.LastBuyer)) + } else { + builder.WriteString("🟢 **Game Status:** Active\n\n") + builder.WriteString(ufmt.Sprintf("🔄 **Round:** %d\n\n", gameState.CurrentRound)) + builder.WriteString(ufmt.Sprintf("⏱️ **Time Remaining:** %d blocks\n\n", gameState.EndBlock-std.GetHeight())) + } + builder.WriteString(ufmt.Sprintf("💰 **Jackpot:** %d ugnot\n\n", gameState.Jackpot)) + builder.WriteString(ufmt.Sprintf("🔑 **Key Price:** %d ugnot\n\n", gameState.KeyPrice)) + builder.WriteString(ufmt.Sprintf("📊 **Total Keys:** %d\n\n", gameState.TotalKeys)) + builder.WriteString(ufmt.Sprintf("👤 **Last Buyer:** %s\n\n", getDisplayName(gameState.LastBuyer))) + builder.WriteString(ufmt.Sprintf("🎮 **Next Round Pot:** %d ugnot\n\n", gameState.NextPot)) + } + + // Separator before less important sections + builder.WriteString("---\n\n") + + // Vote For Me section + builder.WriteString("### Vote For Us! 🗳️\n\n") + builder.WriteString("If you enjoy playing FOMO3D, please consider upvoting this game in the [Hall of Realms](https://gno.land/r/leon/hof)!\n\n") + builder.WriteString("Your support helps more players discover the game and grow our community! 🚀\n\n") + + // Report Bug section + builder.WriteString("### Report a Bug 🪲\n\n") + builder.WriteString("Something unusual happened? Help us improve the game by reporting bugs!\n") + builder.WriteString("[Visit our GitHub repository](https://github.com/gnolang/gno/issues)\n\n") + builder.WriteString("Please include:\n") + builder.WriteString("* Detailed description of what happened\n") + builder.WriteString("* Transaction hash (if applicable)\n") + builder.WriteString("* Your address\n") + builder.WriteString("* Current round number\n") + + return builder.String() +} + +// RenderPlayer renders specific player information +func RenderPlayer(addr std.Address, keys int64, dividends int64) string { + var builder strings.Builder + displayName := getDisplayName(addr) + builder.WriteString(ufmt.Sprintf("# Player Stats: %s\n\n", displayName)) + builder.WriteString("## Your Holdings\n\n") + builder.WriteString(ufmt.Sprintf("🔑 **Keys Owned:** %d\n\n", keys)) + builder.WriteString(ufmt.Sprintf("💰 **Unclaimed Dividends:** %d ugnot\n\n", dividends)) + + // Check if player has any NFTs + nftBalance, err := BalanceOf(addr) + if err == nil && nftBalance > 0 { + builder.WriteString("## Your Victory NFTs 🏆\n\n") + + // Iterate through all rounds up to current round to find player's NFTs + for i := int64(1); i <= gameState.CurrentRound; i++ { + tokenID := grc721.TokenID(strconv.FormatInt(i, 10)) + owner, err := OwnerOf(tokenID) + if err == nil && owner == addr { + metadata, err := TokenMetadata(tokenID) + if err == nil { + builder.WriteString(ufmt.Sprintf("### Round #%d Winner\n", i)) + builder.WriteString(ufmt.Sprintf("![NFT](%s)\n\n", metadata.Image)) + builder.WriteString("---\n\n") + } + } + } + } + + builder.WriteString("## Actions\n\n") + builder.WriteString(ufmt.Sprintf("* To buy more keys, send GNOT to this realm with [`BuyKeys()`](%s)\n", gameState.BuyKeysLink)) + if dividends > 0 { + builder.WriteString("* You have unclaimed dividends! Call `ClaimDividends()` to collect them\n") + } + + return builder.String() +} + +// Helper to get display name - just returns namespace if exists, otherwise address +func getDisplayName(addr std.Address) string { + if user := users.GetUserByAddress(addr); user != nil { + return user.Name + } + return addr.String() +} + +// UpdateFunctionLinks updates the links for game functions +func UpdateFunctionLinks(buyKeysLink string, claimDividendsLink string, startGameLink string) { + Ownable.AssertCallerIsOwner() + gameState.BuyKeysLink = buyKeysLink + gameState.ClaimDividendsLink = claimDividendsLink + gameState.StartGameLink = startGameLink +} diff --git a/examples/gno.land/r/sunspirit/home/gno.mod b/examples/gno.land/r/sunspirit/home/gno.mod new file mode 100644 index 00000000000..2aea0280fff --- /dev/null +++ b/examples/gno.land/r/sunspirit/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/sunspirit/home diff --git a/examples/gno.land/r/sunspirit/home/home.gno b/examples/gno.land/r/sunspirit/home/home.gno new file mode 100644 index 00000000000..fbf9709e8d4 --- /dev/null +++ b/examples/gno.land/r/sunspirit/home/home.gno @@ -0,0 +1,34 @@ +package home + +import ( + "strings" + + "gno.land/p/demo/ufmt" + "gno.land/p/sunspirit/md" +) + +func Render(path string) string { + var sb strings.Builder + + sb.WriteString(md.H1("Sunspirit's Home") + md.LineBreak(1)) + + sb.WriteString(md.Paragraph(ufmt.Sprintf( + "Welcome to Sunspirit’s home! This is where I’ll bring %s to Gno.land, crafted with my experience and creativity.", + md.Italic(md.Bold("simple, useful dapps")), + )) + md.LineBreak(1)) + + sb.WriteString(md.Paragraph(ufmt.Sprintf( + "📚 I’ve created a Markdown rendering library at %s. Feel free to use it for your own projects!", + md.Link("gno.land/p/sunspirit/md", "/p/sunspirit/md"), + )) + md.LineBreak(1)) + + sb.WriteString(md.Paragraph("💬 I’d love to hear your feedback to help improve this library!") + md.LineBreak(1)) + + sb.WriteString(md.Paragraph(ufmt.Sprintf( + "🌐 You can check out a demo of this package in action at %s.", + md.Link("gno.land/r/sunspirit/md", "/r/sunspirit/md"), + )) + md.LineBreak(1)) + sb.WriteString(md.HorizontalRule()) + + return sb.String() +} diff --git a/examples/gno.land/r/sunspirit/md/gno.mod b/examples/gno.land/r/sunspirit/md/gno.mod new file mode 100644 index 00000000000..ff3a7c54d96 --- /dev/null +++ b/examples/gno.land/r/sunspirit/md/gno.mod @@ -0,0 +1 @@ +module gno.land/r/sunspirit/md diff --git a/examples/gno.land/r/sunspirit/md/md.gno b/examples/gno.land/r/sunspirit/md/md.gno new file mode 100644 index 00000000000..8c21ea0215c --- /dev/null +++ b/examples/gno.land/r/sunspirit/md/md.gno @@ -0,0 +1,158 @@ +package md + +import ( + "gno.land/p/sunspirit/md" + "gno.land/p/sunspirit/table" +) + +func Render(path string) string { + title := "A simple, flexible, and easy-to-use library for creating markdown documents in gno.land" + + mdBuilder := md.NewBuilder(). + Add(md.H1(md.Italic(md.Bold(title)))). + + // Bold Text section + Add( + md.H3(md.Bold("1. Bold Text")), + md.Paragraph("To make text bold, use the `md.Bold()` function:"), + md.Bold("This is bold text"), + ). + + // Italic Text section + Add( + md.H3(md.Bold("2. Italic Text")), + md.Paragraph("To make text italic, use the `md.Italic()` function:"), + md.Italic("This is italic text"), + ). + + // Strikethrough Text section + Add( + md.H3(md.Bold("3. Strikethrough Text")), + md.Paragraph("To add strikethrough, use the `md.Strikethrough()` function:"), + md.Strikethrough("This text is strikethrough"), + ). + + // Headers section + Add( + md.H3(md.Bold("4. Headers (H1 to H6)")), + md.Paragraph("You can create headers (H1 to H6) using the `md.H1()` to `md.H6()` functions:"), + md.H1("This is a level 1 header"), + md.H2("This is a level 2 header"), + md.H3("This is a level 3 header"), + md.H4("This is a level 4 header"), + md.H5("This is a level 5 header"), + md.H6("This is a level 6 header"), + ). + + // Bullet List section + Add( + md.H3(md.Bold("5. Bullet List")), + md.Paragraph("To create bullet lists, use the `md.BulletList()` function:"), + md.BulletList([]string{"Item 1", "Item 2", "Item 3"}), + ). + + // Ordered List section + Add( + md.H3(md.Bold("6. Ordered List")), + md.Paragraph("To create ordered lists, use the `md.OrderedList()` function:"), + md.OrderedList([]string{"First", "Second", "Third"}), + ). + + // Todo List section + Add( + md.H3(md.Bold("7. Todo List")), + md.Paragraph("You can create a todo list using the `md.TodoList()` function, which supports checkboxes:"), + md.TodoList([]string{"Task 1", "Task 2"}, []bool{true, false}), + ). + + // Blockquote section + Add( + md.H3(md.Bold("8. Blockquote")), + md.Paragraph("To create blockquotes, use the `md.Blockquote()` function:"), + md.Blockquote("This is a blockquote.\nIt can span multiple lines."), + ). + + // Inline Code section + Add( + md.H3(md.Bold("9. Inline Code")), + md.Paragraph("To insert inline code, use the `md.InlineCode()` function:"), + md.InlineCode("fmt.Println() // inline code"), + ). + + // Code Block section + Add( + md.H3(md.Bold("10. Code Block")), + md.Paragraph("For multi-line code blocks, use the `md.CodeBlock()` function:"), + md.CodeBlock("package main\n\nfunc main() {\n\t// Your code here\n}"), + ). + + // Horizontal Rule section + Add( + md.H3(md.Bold("11. Horizontal Rule")), + md.Paragraph("To add a horizontal rule (separator), use the `md.HorizontalRule()` function:"), + md.LineBreak(1), + md.HorizontalRule(), + ). + + // Language-specific Code Block section + Add( + md.H3(md.Bold("12. Language-specific Code Block")), + md.Paragraph("To create language-specific code blocks, use the `md.LanguageCodeBlock()` function:"), + md.LanguageCodeBlock("go", "package main\n\nfunc main() {}"), + ). + + // Hyperlink section + Add( + md.H3(md.Bold("13. Hyperlink")), + md.Paragraph("To create a hyperlink, use the `md.Link()` function:"), + md.Link("Gnoland official docs", "https://docs.gno.land"), + ). + + // Image section + Add( + md.H3(md.Bold("14. Image")), + md.Paragraph("To insert an image, use the `md.Image()` function:"), + md.LineBreak(1), + md.Image("Gnoland Logo", "https://gnolang.github.io/blog/2024-05-21_the-gnome/src/banner.png"), + ). + + // Footnote section + Add( + md.H3(md.Bold("15. Footnote")), + md.Paragraph("To create footnotes, use the `md.Footnote()` function:"), + md.LineBreak(1), + md.Footnote("1", "This is a footnote."), + ). + + // Table section + Add( + md.H3(md.Bold("16. Table")), + md.Paragraph("To create a table, use the `md.Table()` function. Here's an example of a table:"), + ) + + // Create a table using the table package + tb, _ := table.New([]string{"Feature", "Description"}, [][]string{ + {"Bold", "Make text bold using " + md.Bold("double asterisks")}, + {"Italic", "Make text italic using " + md.Italic("single asterisks")}, + {"Strikethrough", "Cross out text using " + md.Strikethrough("double tildes")}, + }) + mdBuilder.Add(md.Table(tb)) + + // Escaping Markdown section + mdBuilder.Add( + md.H3(md.Bold("17. Escaping Markdown")), + md.Paragraph("Sometimes, you need to escape special Markdown characters (like *, _, and `). Use the `md.EscapeMarkdown()` function for this:"), + ) + + // Example of escaping markdown + text := "- Escape special chars like *, _, and ` in markdown" + mdBuilder.Add( + md.H4("Text Without Escape:"), + text, + md.LineBreak(1), + md.H4("Text With Escape:"), + md.EscapeMarkdown(text), + ) + + return mdBuilder.Render(md.LineBreak(1)) +} diff --git a/examples/gno.land/r/sunspirit/md/md_test.gno b/examples/gno.land/r/sunspirit/md/md_test.gno new file mode 100644 index 00000000000..2e1ce9b9931 --- /dev/null +++ b/examples/gno.land/r/sunspirit/md/md_test.gno @@ -0,0 +1,13 @@ +package md + +import ( + "strings" + "testing" +) + +func TestRender(t *testing.T) { + output := Render("") + if !strings.Contains(output, "A simple, flexible, and easy-to-use library for creating markdown documents in gno.land") { + t.Errorf("invalid output") + } +} diff --git a/examples/gno.land/r/sys/params/params_test.gno b/examples/gno.land/r/sys/params/params_test.gno index eaa1ad039d3..a15da1e7499 100644 --- a/examples/gno.land/r/sys/params/params_test.gno +++ b/examples/gno.land/r/sys/params/params_test.gno @@ -1,6 +1,10 @@ package params -import "testing" +import ( + "testing" + + _ "gno.land/r/gov/dao/init" // so that loader.init is executed +) // Testing this package is limited because it only contains an `std.Set` method // without a corresponding `std.Get` method. For comprehensive testing, refer to diff --git a/gno.land/Makefile b/gno.land/Makefile index 075560f44a9..90ba7451c35 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -50,9 +50,13 @@ install.gnokey:; go install ./cmd/gnokey .PHONY: dev.gnoweb generate.gnoweb dev.gnoweb: make -C ./pkg/gnoweb dev -generate.gnoweb: + +.PHONY: generate +generate: + go generate -x ./... make -C ./pkg/gnoweb generate + .PHONY: fclean fclean: clean rm -rf gnoland-data genesis.json diff --git a/gno.land/cmd/gnoland/imports_test.go b/gno.land/cmd/gnoland/imports_test.go new file mode 100644 index 00000000000..c5ae81599b4 --- /dev/null +++ b/gno.land/cmd/gnoland/imports_test.go @@ -0,0 +1,20 @@ +package main + +import ( + "os/exec" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNoTestingStdlibImport(t *testing.T) { + // See: https://github.com/gnolang/gno/issues/3585 + // The gno.land binary should not import testing stdlibs, which contain unsafe + // code in the respective native bindings. + + res, err := exec.Command("go", "list", "-f", `{{ join .Deps "\n" }}`, ".").CombinedOutput() + require.NoError(t, err) + assert.Contains(t, string(res), "github.com/gnolang/gno/gnovm/stdlibs\n", "should contain normal stdlibs") + assert.NotContains(t, string(res), "github.com/gnolang/gno/gnovm/tests/stdlibs\n", "should not contain test stdlibs") +} diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index cc9e74a78d8..3fd3063f8b9 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -60,10 +60,10 @@ func NewDefaultGenesisConfig(chainid, chaindomain string) *bft.GenesisDoc { func defaultBlockParams() *abci.BlockParams { return &abci.BlockParams{ - MaxTxBytes: 1_000_000, // 1MB, - MaxDataBytes: 2_000_000, // 2MB, - MaxGas: 100_000_000, // 100M gas - TimeIotaMS: 100, // 100ms + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 3_000_000_000, // 3B gas + TimeIotaMS: 100, // 100ms } } diff --git a/gno.land/pkg/gnoweb/app_test.go b/gno.land/pkg/gnoweb/app_test.go index 6fb69c6d984..ce10cae12d5 100644 --- a/gno.land/pkg/gnoweb/app_test.go +++ b/gno.land/pkg/gnoweb/app_test.go @@ -47,6 +47,7 @@ func TestRoutes(t *testing.T) { {"/game-of-realms", found, "/contribute"}, {"/gor", found, "/contribute"}, {"/blog", found, "/r/gnoland/blog"}, + {"/r/docs/optional_render", http.StatusOK, "No Render"}, {"/r/not/found/", notFound, ""}, {"/404/not/found", notFound, ""}, {"/아스키문자가아닌경로", notFound, ""}, diff --git a/gno.land/pkg/gnoweb/components/layout_header.go b/gno.land/pkg/gnoweb/components/layout_header.go index b85efde5f85..8ab5e95b391 100644 --- a/gno.land/pkg/gnoweb/components/layout_header.go +++ b/gno.land/pkg/gnoweb/components/layout_header.go @@ -2,6 +2,8 @@ package components import ( "net/url" + + "github.com/gnolang/gno/gno.land/pkg/gnoweb/weburl" ) type HeaderLink struct { @@ -13,36 +15,44 @@ type HeaderLink struct { type HeaderData struct { RealmPath string + RealmURL weburl.GnoURL Breadcrumb BreadcrumbData - WebQuery url.Values Links []HeaderLink + ChainId string + Remote string } -func StaticHeaderLinks(realmPath string, webQuery url.Values) []HeaderLink { +func StaticHeaderLinks(u weburl.GnoURL) []HeaderLink { + contentURL, sourceURL, helpURL := u, u, u + contentURL.WebQuery = url.Values{} + sourceURL.WebQuery = url.Values{"source": {""}} + helpURL.WebQuery = url.Values{"help": {""}} + return []HeaderLink{ { Label: "Content", - URL: realmPath, + URL: contentURL.EncodeWebURL(), Icon: "ico-info", - IsActive: isActive(webQuery, "Content"), + IsActive: isActive(u.WebQuery, "Content"), }, { Label: "Source", - URL: realmPath + "$source", + URL: sourceURL.EncodeWebURL(), Icon: "ico-code", - IsActive: isActive(webQuery, "Source"), + IsActive: isActive(u.WebQuery, "Source"), }, { Label: "Docs", - URL: realmPath + "$help", + URL: helpURL.EncodeWebURL(), Icon: "ico-docs", - IsActive: isActive(webQuery, "Docs"), + IsActive: isActive(u.WebQuery, "Docs"), }, } } func EnrichHeaderData(data HeaderData) HeaderData { - data.Links = StaticHeaderLinks(data.RealmPath, data.WebQuery) + data.RealmPath = data.RealmURL.EncodeURL() + data.Links = StaticHeaderLinks(data.RealmURL) return data } diff --git a/gno.land/pkg/gnoweb/components/layouts/header.html b/gno.land/pkg/gnoweb/components/layouts/header.html index 8a1433ccd1c..851833b1dc0 100644 --- a/gno.land/pkg/gnoweb/components/layouts/header.html +++ b/gno.land/pkg/gnoweb/components/layouts/header.html @@ -6,19 +6,70 @@ Gno username profile pic -
{{ range .Links }} {{ template "ui/header_link" . }} {{ end }}
-{{ end }} +{{ end }} diff --git a/gno.land/pkg/gnoweb/components/ui/icons.html b/gno.land/pkg/gnoweb/components/ui/icons.html index feef8226be7..f1145d74359 100644 --- a/gno.land/pkg/gnoweb/components/ui/icons.html +++ b/gno.land/pkg/gnoweb/components/ui/icons.html @@ -120,5 +120,50 @@ fill="transparent" /> + + + + + + + + + + + + + + + {{ end }} diff --git a/gno.land/pkg/gnoweb/components/view_status.go b/gno.land/pkg/gnoweb/components/view_status.go index 46f998c45cb..56477a4db0a 100644 --- a/gno.land/pkg/gnoweb/components/view_status.go +++ b/gno.land/pkg/gnoweb/components/view_status.go @@ -2,10 +2,38 @@ package components const StatusViewType ViewType = "status-view" +// StatusData holds the dynamic fields for the "status" template type StatusData struct { - Message string + Title string + Body string + ButtonURL string + ButtonText string } -func StatusComponent(message string) *View { - return NewTemplateView(StatusViewType, "status", StatusData{message}) +// StatusErrorComponent returns a view for error scenarios +func StatusErrorComponent(message string) *View { + return NewTemplateView( + StatusViewType, + "status", + StatusData{ + Title: "Error: " + message, + Body: "Something went wrong.", + ButtonURL: "/", + ButtonText: "Go Back Home", + }, + ) +} + +// StatusNoRenderComponent returns a view for non-error notifications +func StatusNoRenderComponent(pkgPath string) *View { + return NewTemplateView( + StatusViewType, + "status", + StatusData{ + Title: "No Render", + Body: "This realm does not implement a Render() function.", + ButtonURL: pkgPath + "$source", + ButtonText: "View Realm Source", + }, + ) } diff --git a/gno.land/pkg/gnoweb/components/views/status.html b/gno.land/pkg/gnoweb/components/views/status.html index ab068cbf7e4..f4533275789 100644 --- a/gno.land/pkg/gnoweb/components/views/status.html +++ b/gno.land/pkg/gnoweb/components/views/status.html @@ -1,8 +1,12 @@ {{ define "status" }}
gno land -

Error: {{ .Message }}

-

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

- Go Back Home +

+ {{ .Title }} +

+

{{ .Body }}

+ + {{ .ButtonText }} +
{{ end }} diff --git a/gno.land/pkg/gnoweb/frontend/css/tx.config.js b/gno.land/pkg/gnoweb/frontend/css/tx.config.js index 451688d7da6..06aa685676a 100644 --- a/gno.land/pkg/gnoweb/frontend/css/tx.config.js +++ b/gno.land/pkg/gnoweb/frontend/css/tx.config.js @@ -26,6 +26,7 @@ export default { borderRadius: { sm: `${pxToRem(4)}rem`, DEFAULT: `${pxToRem(6)}rem`, + full: "9999px", }, colors: { light: "#FFFFFF", diff --git a/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts index 3177e034257..950c85cdbe3 100644 --- a/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts +++ b/gno.land/pkg/gnoweb/frontend/js/realmhelp.ts @@ -1,4 +1,4 @@ -import { debounce } from "./utils"; +import { debounce, escapeShellSpecialChars } from "./utils"; class Help { private DOM: { @@ -67,7 +67,7 @@ class Help { localStorage.setItem("helpAddressInput", address); this.funcList.forEach((func) => func.updateAddr(address)); - }); + }, 50); addressInput?.addEventListener("input", () => debouncedUpdate(addressInput)); cmdModeSelect?.addEventListener("change", (e) => { @@ -124,7 +124,7 @@ class HelpFunc { private bindEvents(): void { const debouncedUpdate = debounce((paramName: string, paramValue: string) => { if (paramName) this.updateArg(paramName, paramValue); - }); + }, 50); this.DOM.el.addEventListener("input", (e) => { const target = e.target as HTMLInputElement; @@ -143,10 +143,11 @@ class HelpFunc { } public updateArg(paramName: string, paramValue: string): void { + const escapedValue = escapeShellSpecialChars(paramValue); this.DOM.args .filter((arg) => arg.dataset.arg === paramName) .forEach((arg) => { - arg.textContent = paramValue || ""; + arg.textContent = escapedValue || ""; }); } diff --git a/gno.land/pkg/gnoweb/frontend/js/utils.ts b/gno.land/pkg/gnoweb/frontend/js/utils.ts index 83de509efa5..d975b4516f3 100644 --- a/gno.land/pkg/gnoweb/frontend/js/utils.ts +++ b/gno.land/pkg/gnoweb/frontend/js/utils.ts @@ -10,3 +10,7 @@ export function debounce void>(func: T, delay: num }, delay); }; } + +export function escapeShellSpecialChars(arg: string): string { + return arg.replace(/([$`"\\!|&;<>*?{}()])/g, "\\$1"); +} diff --git a/gno.land/pkg/gnoweb/handler.go b/gno.land/pkg/gnoweb/handler.go index cdaaa63e1bc..cfad9919506 100644 --- a/gno.land/pkg/gnoweb/handler.go +++ b/gno.land/pkg/gnoweb/handler.go @@ -11,6 +11,7 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/gnoweb/components" + "github.com/gnolang/gno/gno.land/pkg/gnoweb/weburl" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" // For error types ) @@ -74,6 +75,7 @@ func (h *WebHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + w.Header().Add("Content-Type", "text/html; charset=utf-8") h.Get(w, r) } @@ -111,18 +113,19 @@ func (h *WebHandler) Get(w http.ResponseWriter, r *http.Request) { // prepareIndexBodyView prepares the data and main view for the index. func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components.IndexData) (int, *components.View) { - gnourl, err := ParseGnoURL(r.URL) + gnourl, err := weburl.ParseGnoURL(r.URL) if err != nil { h.Logger.Warn("unable to parse url path", "path", r.URL.Path, "error", err) - return http.StatusNotFound, components.StatusComponent("invalid path") + return http.StatusNotFound, components.StatusErrorComponent("invalid path") } breadcrumb := generateBreadcrumbPaths(gnourl) indexData.HeadData.Title = h.Static.Domain + " - " + gnourl.Path indexData.HeaderData = components.HeaderData{ - RealmPath: gnourl.Encode(EncodePath | EncodeArgs | EncodeQuery | EncodeNoEscape), Breadcrumb: breadcrumb, - WebQuery: gnourl.WebQuery, + RealmURL: *gnourl, + ChainId: h.Static.ChainId, + Remote: h.Static.RemoteHelp, } switch { @@ -130,12 +133,12 @@ func (h *WebHandler) prepareIndexBodyView(r *http.Request, indexData *components return h.GetPackageView(gnourl) default: h.Logger.Debug("invalid path: path is neither a pure package or a realm") - return http.StatusBadRequest, components.StatusComponent("invalid path") + return http.StatusBadRequest, components.StatusErrorComponent("invalid path") } } // GetPackageView handles package pages. -func (h *WebHandler) GetPackageView(gnourl *GnoURL) (int, *components.View) { +func (h *WebHandler) GetPackageView(gnourl *weburl.GnoURL) (int, *components.View) { // Handle Help page if gnourl.WebQuery.Has("help") { return h.GetHelpView(gnourl) @@ -155,11 +158,15 @@ func (h *WebHandler) GetPackageView(gnourl *GnoURL) (int, *components.View) { return h.GetRealmView(gnourl) } -func (h *WebHandler) GetRealmView(gnourl *GnoURL) (int, *components.View) { +func (h *WebHandler) GetRealmView(gnourl *weburl.GnoURL) (int, *components.View) { var content bytes.Buffer meta, err := h.Client.RenderRealm(&content, gnourl.Path, gnourl.EncodeArgs()) if err != nil { + if errors.Is(err, ErrRenderNotDeclared) { + return http.StatusOK, components.StatusNoRenderComponent(gnourl.Path) + } + h.Logger.Error("unable to render realm", "error", err, "path", gnourl.EncodeURL()) return GetClientErrorStatusPage(gnourl, err) } @@ -175,7 +182,7 @@ func (h *WebHandler) GetRealmView(gnourl *GnoURL) (int, *components.View) { }) } -func (h *WebHandler) GetHelpView(gnourl *GnoURL) (int, *components.View) { +func (h *WebHandler) GetHelpView(gnourl *weburl.GnoURL) (int, *components.View) { fsigs, err := h.Client.Functions(gnourl.Path) if err != nil { h.Logger.Error("unable to fetch path functions", "error", err) @@ -213,7 +220,7 @@ func (h *WebHandler) GetHelpView(gnourl *GnoURL) (int, *components.View) { }) } -func (h *WebHandler) GetSourceView(gnourl *GnoURL) (int, *components.View) { +func (h *WebHandler) GetSourceView(gnourl *weburl.GnoURL) (int, *components.View) { pkgPath := gnourl.Path files, err := h.Client.Sources(pkgPath) if err != nil { @@ -223,7 +230,7 @@ func (h *WebHandler) GetSourceView(gnourl *GnoURL) (int, *components.View) { if len(files) == 0 { h.Logger.Debug("no files available", "path", gnourl.Path) - return http.StatusOK, components.StatusComponent("no files available") + return http.StatusOK, components.StatusErrorComponent("no files available") } var fileName string @@ -256,7 +263,7 @@ func (h *WebHandler) GetSourceView(gnourl *GnoURL) (int, *components.View) { }) } -func (h *WebHandler) GetDirectoryView(gnourl *GnoURL) (int, *components.View) { +func (h *WebHandler) GetDirectoryView(gnourl *weburl.GnoURL) (int, *components.View) { pkgPath := strings.TrimSuffix(gnourl.Path, "/") files, err := h.Client.Sources(pkgPath) if err != nil { @@ -266,7 +273,7 @@ func (h *WebHandler) GetDirectoryView(gnourl *GnoURL) (int, *components.View) { if len(files) == 0 { h.Logger.Debug("no files available", "path", gnourl.Path) - return http.StatusOK, components.StatusComponent("no files available") + return http.StatusOK, components.StatusErrorComponent("no files available") } return http.StatusOK, components.DirectoryView(components.DirData{ @@ -276,24 +283,24 @@ func (h *WebHandler) GetDirectoryView(gnourl *GnoURL) (int, *components.View) { }) } -func GetClientErrorStatusPage(_ *GnoURL, err error) (int, *components.View) { +func GetClientErrorStatusPage(_ *weburl.GnoURL, err error) (int, *components.View) { if err == nil { return http.StatusOK, nil } switch { case errors.Is(err, ErrClientPathNotFound): - return http.StatusNotFound, components.StatusComponent(err.Error()) + return http.StatusNotFound, components.StatusErrorComponent(err.Error()) case errors.Is(err, ErrClientBadRequest): - return http.StatusInternalServerError, components.StatusComponent("bad request") + return http.StatusInternalServerError, components.StatusErrorComponent("bad request") case errors.Is(err, ErrClientResponse): fallthrough // XXX: for now fallback as internal error default: - return http.StatusInternalServerError, components.StatusComponent("internal error") + return http.StatusInternalServerError, components.StatusErrorComponent("internal error") } } -func generateBreadcrumbPaths(url *GnoURL) components.BreadcrumbData { +func generateBreadcrumbPaths(url *weburl.GnoURL) components.BreadcrumbData { split := strings.Split(url.Path, "/") var data components.BreadcrumbData diff --git a/gno.land/pkg/gnoweb/handler_test.go b/gno.land/pkg/gnoweb/handler_test.go index 624e3390a97..8321ad24be2 100644 --- a/gno.land/pkg/gnoweb/handler_test.go +++ b/gno.land/pkg/gnoweb/handler_test.go @@ -24,12 +24,13 @@ func (t *testingLogger) Write(b []byte) (n int, err error) { // TestWebHandler_Get tests the Get method of WebHandler using table-driven tests. func TestWebHandler_Get(t *testing.T) { + t.Parallel() // Set up a mock package with some files and functions mockPackage := &gnoweb.MockPackage{ Domain: "example.com", Path: "/r/mock/path", Files: map[string]string{ - "render.gno": `package main; func Render(path string) { return "one more time" }`, + "render.gno": `package main; func Render(path string) string { return "one more time" }`, "gno.mod": `module example.com/r/mock/path`, "LicEnse": `my super license`, }, @@ -37,6 +38,10 @@ func TestWebHandler_Get(t *testing.T) { {FuncName: "SuperRenderFunction", Params: []vm.NamedType{ {Name: "my_super_arg", Type: "string"}, }}, + { + FuncName: "Render", Params: []vm.NamedType{{Name: "path", Type: "string"}}, + Results: []vm.NamedType{{Name: "", Type: "string"}}, + }, }, } @@ -82,6 +87,7 @@ func TestWebHandler_Get(t *testing.T) { for _, tc := range cases { t.Run(strings.TrimPrefix(tc.Path, "/"), func(t *testing.T) { + t.Parallel() t.Logf("input: %+v", tc) // Initialize testing logger @@ -110,3 +116,38 @@ func TestWebHandler_Get(t *testing.T) { }) } } + +// TestWebHandler_NoRender checks if gnoweb displays the `No Render` page properly. +// This happens when the render being queried does not have a Render function declared. +func TestWebHandler_NoRender(t *testing.T) { + t.Parallel() + + mockPath := "/r/mock/path" + mockPackage := &gnoweb.MockPackage{ + Domain: "gno.land", + Path: "/r/mock/path", + Files: map[string]string{ + "render.gno": `package main; func init() {}`, + "gno.mod": `module gno.land/r/mock/path`, + }, + } + + webclient := gnoweb.NewMockWebClient(mockPackage) + config := gnoweb.WebHandlerConfig{ + WebClient: webclient, + } + + logger := slog.New(slog.NewTextHandler(&testingLogger{t}, &slog.HandlerOptions{})) + handler, err := gnoweb.NewWebHandler(logger, config) + require.NoError(t, err, "failed to create WebHandler") + + req, err := http.NewRequest(http.MethodGet, mockPath, nil) + require.NoError(t, err, "failed to create HTTP request") + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code, "unexpected status code") + expectedBody := "This realm does not implement a Render() function." + assert.Contains(t, rr.Body.String(), expectedBody, "rendered body should contain: %q", expectedBody) +} diff --git a/gno.land/pkg/gnoweb/public/js/realmhelp.js b/gno.land/pkg/gnoweb/public/js/realmhelp.js index 68bcafbb75f..7008a54514e 100644 --- a/gno.land/pkg/gnoweb/public/js/realmhelp.js +++ b/gno.land/pkg/gnoweb/public/js/realmhelp.js @@ -1 +1 @@ -function d(a,e=250){let t;return function(...s){t!==void 0&&clearTimeout(t),t=setTimeout(()=>{a.apply(this,s)},e)}}var l=class a{DOM;funcList;static SELECTORS={container:".js-help-view",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),this.funcList=this.DOM.funcs.map(t=>new o(t)),this.restoreAddress(),this.bindEvents())}restoreAddress(){let{addressInput:e}=this.DOM;if(e){let t=localStorage.getItem("helpAddressInput");t&&(e.value=t,this.funcList.forEach(s=>s.updateAddr(t)))}}bindEvents(){let{addressInput:e,cmdModeSelect:t}=this.DOM,s=d(r=>{let n=r.value;localStorage.setItem("helpAddressInput",n),this.funcList.forEach(i=>i.updateAddr(n))});e?.addEventListener("input",()=>s(e)),t?.addEventListener("change",r=>{let n=r.target;this.funcList.forEach(i=>i.updateMode(n.value))})}},o=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)),paramInputs:Array.from(e.querySelectorAll(a.SELECTORS.paramInput))},this.funcName=e.dataset.func||null,this.initializeArgs(),this.bindEvents()}static sanitizeArgsInput(e){let t=e.dataset.param||"",s=e.value.trim();return t||console.warn("sanitizeArgsInput: param is missing in arg input dataset."),{paramName:t,paramValue:s}}bindEvents(){let e=d((t,s)=>{t&&this.updateArg(t,s)});this.DOM.el.addEventListener("input",t=>{let s=t.target;if(s.dataset.role==="help-param-input"){let{paramName:r,paramValue:n}=a.sanitizeArgsInput(s);e(r,n)}})}initializeArgs(){this.DOM.paramInputs.forEach(e=>{let{paramName:t,paramValue:s}=a.sanitizeArgsInput(e);t&&this.updateArg(t,s)})}updateArg(e,t){this.DOM.args.filter(s=>s.dataset.arg===e).forEach(s=>{s.textContent=t||""})}updateAddr(e){this.DOM.addrs.forEach(t=>{t.textContent=e.trim()||"ADDRESS"})}updateMode(e){this.DOM.modes.forEach(t=>{let s=t.dataset.codeMode===e;t.classList.toggle("inline",s),t.classList.toggle("hidden",!s),t.dataset.copyContent=s?`help-cmd-${this.funcName}`:""})}},p=()=>new l;export{p as default}; +function d(s,e=250){let t;return function(...a){t!==void 0&&clearTimeout(t),t=setTimeout(()=>{s.apply(this,a)},e)}}function u(s){return s.replace(/([$`"\\!|&;<>*?{}()])/g,"\\$1")}var l=class s{DOM;funcList;static SELECTORS={container:".js-help-view",func:"[data-func]",addressInput:"[data-role='help-input-addr']",cmdModeSelect:"[data-role='help-select-mode']"};constructor(){this.DOM={el:document.querySelector(s.SELECTORS.container),funcs:[],addressInput:null,cmdModeSelect:null},this.funcList=[],this.DOM.el?this.init():console.warn("Help: Main container not found.")}init(){let{el:e}=this.DOM;e&&(this.DOM.funcs=Array.from(e.querySelectorAll(s.SELECTORS.func)),this.DOM.addressInput=e.querySelector(s.SELECTORS.addressInput),this.DOM.cmdModeSelect=e.querySelector(s.SELECTORS.cmdModeSelect),this.funcList=this.DOM.funcs.map(t=>new o(t)),this.restoreAddress(),this.bindEvents())}restoreAddress(){let{addressInput:e}=this.DOM;if(e){let t=localStorage.getItem("helpAddressInput");t&&(e.value=t,this.funcList.forEach(a=>a.updateAddr(t)))}}bindEvents(){let{addressInput:e,cmdModeSelect:t}=this.DOM,a=d(n=>{let r=n.value;localStorage.setItem("helpAddressInput",r),this.funcList.forEach(i=>i.updateAddr(r))},50);e?.addEventListener("input",()=>a(e)),t?.addEventListener("change",n=>{let r=n.target;this.funcList.forEach(i=>i.updateMode(r.value))})}},o=class s{DOM;funcName;static SELECTORS={address:"[data-role='help-code-address']",args:"[data-role='help-code-args']",mode:"[data-code-mode]",paramInput:"[data-role='help-param-input']"};constructor(e){this.DOM={el:e,addrs:Array.from(e.querySelectorAll(s.SELECTORS.address)),args:Array.from(e.querySelectorAll(s.SELECTORS.args)),modes:Array.from(e.querySelectorAll(s.SELECTORS.mode)),paramInputs:Array.from(e.querySelectorAll(s.SELECTORS.paramInput))},this.funcName=e.dataset.func||null,this.initializeArgs(),this.bindEvents()}static sanitizeArgsInput(e){let t=e.dataset.param||"",a=e.value.trim();return t||console.warn("sanitizeArgsInput: param is missing in arg input dataset."),{paramName:t,paramValue:a}}bindEvents(){let e=d((t,a)=>{t&&this.updateArg(t,a)},50);this.DOM.el.addEventListener("input",t=>{let a=t.target;if(a.dataset.role==="help-param-input"){let{paramName:n,paramValue:r}=s.sanitizeArgsInput(a);e(n,r)}})}initializeArgs(){this.DOM.paramInputs.forEach(e=>{let{paramName:t,paramValue:a}=s.sanitizeArgsInput(e);t&&this.updateArg(t,a)})}updateArg(e,t){let a=u(t);this.DOM.args.filter(n=>n.dataset.arg===e).forEach(n=>{n.textContent=a||""})}updateAddr(e){this.DOM.addrs.forEach(t=>{t.textContent=e.trim()||"ADDRESS"})}updateMode(e){this.DOM.modes.forEach(t=>{let a=t.dataset.codeMode===e;t.classList.toggle("inline",a),t.classList.toggle("hidden",!a),t.dataset.copyContent=a?`help-cmd-${this.funcName}`:""})}},m=()=>new l;export{m as default}; diff --git a/gno.land/pkg/gnoweb/public/js/utils.js b/gno.land/pkg/gnoweb/public/js/utils.js index e27fb93bc1c..ce96def444a 100644 --- a/gno.land/pkg/gnoweb/public/js/utils.js +++ b/gno.land/pkg/gnoweb/public/js/utils.js @@ -1 +1 @@ -function r(t,n=250){let e;return function(...i){e!==void 0&&clearTimeout(e),e=setTimeout(()=>{t.apply(this,i)},n)}}export{r as debounce}; +function i(e,n=250){let t;return function(...r){t!==void 0&&clearTimeout(t),t=setTimeout(()=>{e.apply(this,r)},n)}}function a(e){return e.replace(/([$`"\\!|&;<>*?{}()])/g,"\\$1")}export{i as debounce,a as escapeShellSpecialChars}; diff --git a/gno.land/pkg/gnoweb/public/styles.css b/gno.land/pkg/gnoweb/public/styles.css index ec575bb3735..96e768a313e 100644 --- a/gno.land/pkg/gnoweb/public/styles.css +++ b/gno.land/pkg/gnoweb/public/styles.css @@ -1,3 +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-view{overflow-wrap:break-word;padding-top:1.5rem;font-size:1rem}@media (min-width:51.25rem){.realm-view{padding-top:2.5rem}}.realm-view>:first-child{margin-top:0!important}.realm-view a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-view a:hover{text-decoration-line:underline}.realm-view h1,.realm-view h2,.realm-view h3,.realm-view h4{margin-top:3rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-view h2,.realm-view h2 *{font-weight:700}.realm-view h3,.realm-view h3 *,.realm-view h4,.realm-view h4 *{font-weight:600}.realm-view h1+h2,.realm-view h2+h3,.realm-view h3+h4{margin-top:1rem}.realm-view h1{font-size:2.375rem;font-weight:700}.realm-view h2{font-size:1.5rem}.realm-view h3{margin-top:2.5rem;font-size:1.25rem}.realm-view h3,.realm-view h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view h4{margin-top:1.5rem;margin-bottom:1.5rem;font-size:1.125rem;font-weight:500}.realm-view p{margin-top:1.25rem;margin-bottom:1.25rem}.realm-view strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-view strong *{font-weight:700}.realm-view em{font-style:oblique 14deg}.realm-view 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 14deg}.realm-view ol,.realm-view ul{margin-top:1.5rem;margin-bottom:1.5rem;padding-left:1rem}.realm-view ol li,.realm-view ul li{margin-bottom:.5rem}.realm-view img{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-view figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-view figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.96em}.realm-view :not(pre)>code,.realm-view pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-view pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-view 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-view table{margin-top:2rem;margin-bottom:2rem;display:block;width:100%;max-width:100%;border-collapse:collapse;overflow-x:auto}.realm-view td,.realm-view th{white-space:normal;overflow-wrap:break-word;border-width:1px;padding:.5rem 1rem}.realm-view th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-view caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view 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 14deg;quotes:"“" "”" "‘" "’"}.realm-view q:after,.realm-view 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-view q:after{content:close-quote}.realm-view q:before{content:open-quote}.realm-view q:after{content:close-quote}.realm-view ol ol,.realm-view ol ul,.realm-view ul ol,.realm-view ul ul{margin-top:.75rem;margin-bottom:.5rem;padding-left:1rem}.realm-view ul{list-style-type:disc}.realm-view ol{list-style-type:decimal}.realm-view abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-view details{margin-top:1.25rem;margin-bottom:1.25rem}.realm-view summary{cursor:pointer;font-weight:700}.realm-view a code{color:inherit}.realm-view video{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-view math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-view small{font-size:.875rem}.realm-view del{text-decoration-line:line-through}.realm-view sub{vertical-align:sub;font-size:.75rem}.realm-view sup{vertical-align:super;font-size:.75rem}.realm-view button,.realm-view 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-view{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-view>pre a:hover{text-decoration-line:none}main :is(.realm-view,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-view,.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-view,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.dev-mode .toc-expend-btn{cursor:pointer;border-width:1px;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.dev-mode .toc-expend-btn:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}@media (min-width:51.25rem){.dev-mode .toc-expend-btn{border-style:none;background-color:transparent}}.dev-mode #sidebar-summary{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}@media (min-width:51.25rem){.dev-mode #sidebar-summary{background-color:transparent}}.dev-mode .toc-nav{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.bottom-1{bottom:.25rem}.left-0{left:0}.right-2{right:.5rem}.right-3{right:.75rem}.top-0{top:0}.top-1\/2{top:50%}.top-14{top:3.5rem}.top-2{top:.5rem}.z-1{z-index:1}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.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-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.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}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(153 153 153/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.text-light{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.before\:content-\[\'\:\'\]:before{--tw-content:":";content:var(--tw-content)}.before\:content-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:top-0:after{content:var(--tw-content);top:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:h-full:after{content:var(--tw-content);height:100%}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:rounded-t-sm:after{content:var(--tw-content);border-top-left-radius:.25rem;border-top-right-radius:.25rem}.after\:bg-gray-100:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:mt-8:first-child{margin-top:2rem}.first\:border-t:first-child{border-top-width:1px}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.hover\:text-green-600:hover{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.hover\:text-light:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.focus\:border-l-gray-300:focus{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-gray-300{--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.group:hover .group-hover\:border-l-gray-300{--tw-border-opacity:1;border-left-color:rgb(153 153 153/var(--tw-border-opacity))}.group.is-active .group-\[\.is-active\]\:text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.peer:checked~.peer-checked\:before\:content-\[\'close\'\]:before{--tw-content:"close";content:var(--tw-content)}.peer:focus-within~.peer-focus-within\:hidden{display:none}.has-\[ul\:empty\]\:hidden:has(ul:empty){display:none}.has-\[\:focus-within\]\:border-gray-300:has(:focus-within){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}.has-\[\:focus\]\:border-gray-300:has(:focus){--tw-border-opacity:1;border-color:rgb(153 153 153/var(--tw-border-opacity))}@media (min-width:30rem){.sm\:gap-6{gap:1.5rem}}@media (min-width:40rem){.md\:col-span-3{grid-column:span 3/span 3}.md\:mb-0{margin-bottom:0}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}}@media (min-width:51.25rem){.lg\:order-2{order:2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-start-1{grid-row-start:1}.lg\: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\:first\:mt-0:first-child{margin-top:0}.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 +/*! 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-view{overflow-wrap:break-word;padding-top:1.5rem;font-size:1rem}@media (min-width:51.25rem){.realm-view{padding-top:2.5rem}}.realm-view>:first-child{margin-top:0!important}.realm-view a{font-weight:500;--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.realm-view a:hover{text-decoration-line:underline}.realm-view h1,.realm-view h2,.realm-view h3,.realm-view h4{margin-top:3rem;line-height:1.25;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-view h2,.realm-view h2 *{font-weight:700}.realm-view h3,.realm-view h3 *,.realm-view h4,.realm-view h4 *{font-weight:600}.realm-view h1+h2,.realm-view h2+h3,.realm-view h3+h4{margin-top:1rem}.realm-view h1{font-size:2.375rem;font-weight:700}.realm-view h2{font-size:1.5rem}.realm-view h3{margin-top:2.5rem;font-size:1.25rem}.realm-view h3,.realm-view h4{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view h4{margin-top:1.5rem;margin-bottom:1.5rem;font-size:1.125rem;font-weight:500}.realm-view p{margin-top:1.25rem;margin-bottom:1.25rem}.realm-view strong{font-weight:700;--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.realm-view strong *{font-weight:700}.realm-view em{font-style:oblique 14deg}.realm-view 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 14deg}.realm-view ol,.realm-view ul{margin-top:1.5rem;margin-bottom:1.5rem;padding-left:1rem}.realm-view ol li,.realm-view ul li{margin-bottom:.5rem}.realm-view img{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-view figure{margin-top:1.5rem;margin-bottom:1.5rem;text-align:center}.realm-view figcaption{font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view :not(pre)>code{border-radius:.25rem;background-color:rgb(226 226 226/var(--tw-bg-opacity));padding:.125rem .25rem;font-size:.96em}.realm-view :not(pre)>code,.realm-view pre{--tw-bg-opacity:1;font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-view pre{overflow-x:auto;border-radius:.375rem;background-color:rgb(240 240 240/var(--tw-bg-opacity));padding:1rem}.realm-view 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-view table{margin-top:2rem;margin-bottom:2rem;display:block;width:100%;max-width:100%;border-collapse:collapse;overflow-x:auto}.realm-view td,.realm-view th{white-space:normal;overflow-wrap:break-word;border-width:1px;padding:.5rem 1rem}.realm-view th{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity));font-weight:700}.realm-view caption{margin-top:.5rem;text-align:left;font-size:.875rem;--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.realm-view 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 14deg;quotes:"“" "”" "‘" "’"}.realm-view q:after,.realm-view 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-view q:after{content:close-quote}.realm-view q:before{content:open-quote}.realm-view q:after{content:close-quote}.realm-view ol ol,.realm-view ol ul,.realm-view ul ol,.realm-view ul ul{margin-top:.75rem;margin-bottom:.5rem;padding-left:1rem}.realm-view ul{list-style-type:disc}.realm-view ol{list-style-type:decimal}.realm-view abbr[title]{cursor:help;border-bottom-width:1px;border-style:dotted}.realm-view details{margin-top:1.25rem;margin-bottom:1.25rem}.realm-view summary{cursor:pointer;font-weight:700}.realm-view a code{color:inherit}.realm-view video{margin-top:2rem;margin-bottom:2rem;max-width:100%}.realm-view math{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.realm-view small{font-size:.875rem}.realm-view del{text-decoration-line:line-through}.realm-view sub{vertical-align:sub;font-size:.75rem}.realm-view sup{vertical-align:super;font-size:.75rem}.realm-view button,.realm-view 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-view{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-view>pre a:hover{text-decoration-line:none}main :is(.realm-view,.source-code)>pre .chroma-ln:target{background-color:transparent!important}main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-ln:target),main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-ln:target) .chroma-cl,main :is(.realm-view,.source-code)>pre .chroma-line:has(.chroma-lnlinks:hover),main :is(.realm-view,.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-view,.source-code)>pre .chroma-ln{scroll-margin-top:6rem}.dev-mode .toc-expend-btn{cursor:pointer;border-width:1px;--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.dev-mode .toc-expend-btn:hover{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}@media (min-width:51.25rem){.dev-mode .toc-expend-btn{border-style:none;background-color:transparent}}.dev-mode #sidebar-summary{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}@media (min-width:51.25rem){.dev-mode #sidebar-summary{background-color:transparent}}.dev-mode .toc-nav{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-1{bottom:.25rem}.left-0{left:0}.right-0{right:0}.right-2{right:.5rem}.right-3{right:.75rem}.right-px{right:1px}.top-0{top:0}.top-1\/2{top:50%}.top-14{top:3.5rem}.top-2{top:.5rem}.top-px{top:1px}.z-1{z-index:1}.z-max{z-index:9999}.col-span-1{grid-column:span 1/span 1}.col-span-10{grid-column:span 10/span 10}.col-span-3{grid-column:span 3/span 3}.col-span-7{grid-column:span 7/span 7}.row-span-1{grid-row:span 1/span 1}.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-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.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-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-\[calc\(100\%-2px\)\]{height:calc(100% - 2px)}.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-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-72{width:18rem}.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-x-full{--tw-translate-x:-100%}.-translate-x-full,.-translate-y-1\/2{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))}.-translate-y-1\/2{--tw-translate-y:-50%}.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}.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-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-20{-moz-column-gap:5rem;column-gap:5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-2{row-gap:.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.375rem}.rounded-sm{border-radius:.25rem}.rounded-r{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-gray-100{--tw-border-opacity:1;border-color:rgb(226 226 226/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(153 153 153/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(240 240 240/var(--tw-bg-opacity))}.bg-light{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-px{padding-top:1px;padding-bottom:1px}.pb-24{padding-bottom:6rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pb-8{padding-bottom:2rem}.pl-4{padding-left:1rem}.pr-10{padding-right:2.5rem}.pt-0\.5{padding-top:.125rem}.pt-2{padding-top:.5rem}.font-mono{font-family:Roboto,Menlo,Consolas,Ubuntu Mono,Roboto Mono,DejaVu Sans Mono,monospace;}.text-100{font-size:.875rem}.text-200{font-size:1rem}.text-50{font-size:.75rem}.text-600{font-size:1.5rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-tight{line-height:1.25}.text-gray-300{--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(124 124 124/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(84 89 93/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(19 19 19/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(8 8 9/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(34 108 87/var(--tw-text-opacity))}.text-light{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.outline-none{outline:2px solid transparent;outline-offset:2px}.text-stroke{-webkit-text-stroke:currentColor;-webkit-text-stroke-width:.6px}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.\*\:pl-0>*{padding-left:0}.before\:px-\[0\.18rem\]:before{content:var(--tw-content);padding-left:.18rem;padding-right:.18rem}.before\:text-gray-300:before{content:var(--tw-content);--tw-text-opacity:1;color:rgb(153 153 153/var(--tw-text-opacity))}.before\:content-\[\'\/\'\]:before{--tw-content:"/";content:var(--tw-content)}.before\:content-\[\'\:\'\]:before{--tw-content:":";content:var(--tw-content)}.before\:content-\[\'open\'\]:before{--tw-content:"open";content:var(--tw-content)}.after\:pointer-events-none:after{content:var(--tw-content);pointer-events:none}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:bottom-0:after{content:var(--tw-content);bottom:0}.after\:left-0:after{content:var(--tw-content);left:0}.after\:top-0:after{content:var(--tw-content);top:0}.after\:block:after{content:var(--tw-content);display:block}.after\:h-1:after{content:var(--tw-content);height:.25rem}.after\:h-full:after{content:var(--tw-content);height:100%}.after\:w-full:after{content:var(--tw-content);width:100%}.after\:rounded-t-sm:after{content:var(--tw-content);border-top-left-radius:.25rem;border-top-right-radius:.25rem}.after\:bg-gray-100:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(226 226 226/var(--tw-bg-opacity))}.after\:bg-green-600:after{content:var(--tw-content);--tw-bg-opacity:1;background-color:rgb(34 108 87/var(--tw-bg-opacity))}.first\:mt-8:first-child{margin-top:2rem}.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-gray-900:hover{--tw-text-opacity:1;color:rgb(8 8 9/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\:visible{visibility:visible}.peer:checked~.peer-checked\:opacity-100{opacity:1}.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\:flex{display:flex}.md\:h-4{height:1rem}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:pb-0{padding-bottom:0}.md\:pr-8{padding-right:2rem}}@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\: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\:first\:mt-0:first-child{margin-top:0}.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/webclient.go b/gno.land/pkg/gnoweb/webclient.go index de44303f352..1def3bc3812 100644 --- a/gno.land/pkg/gnoweb/webclient.go +++ b/gno.land/pkg/gnoweb/webclient.go @@ -10,6 +10,7 @@ import ( var ( ErrClientPathNotFound = errors.New("package not found") + ErrRenderNotDeclared = errors.New("render function not declared") ErrClientBadRequest = errors.New("bad request") ErrClientResponse = errors.New("node response error") ) @@ -23,7 +24,7 @@ type RealmMeta struct { Toc md.Toc } -// WebClient is an interface for interacting with package and node ressources. +// WebClient is an interface for interacting with package and node resources. type WebClient interface { // RenderRealm renders the content of a realm from a given path and // arguments into the giver `writer`. The method should ensures the rendered diff --git a/gno.land/pkg/gnoweb/webclient_html.go b/gno.land/pkg/gnoweb/webclient_html.go index d856c6f87a0..72b1b3f8b06 100644 --- a/gno.land/pkg/gnoweb/webclient_html.go +++ b/gno.land/pkg/gnoweb/webclient_html.go @@ -49,6 +49,7 @@ func NewDefaultHTMLWebClientConfig(client *client.RPCClient) *HTMLWebClientConfi markdown.NewHighlighting( markdown.WithFormatOptions(chromaOptions...), ), + extension.Strikethrough, extension.Table, ), } @@ -177,6 +178,7 @@ func (s *HTMLWebClient) RenderRealm(w io.Writer, pkgPath string, args string) (* pkgPath = strings.Trim(pkgPath, "/") data := fmt.Sprintf("%s/%s:%s", s.domain, pkgPath, args) + rawres, err := s.query(qpath, []byte(data)) if err != nil { return nil, err @@ -213,6 +215,10 @@ func (s *HTMLWebClient) query(qpath string, data []byte) ([]byte, error) { return nil, ErrClientPathNotFound } + if errors.Is(err, vm.NoRenderDeclError{}) { + return nil, ErrRenderNotDeclared + } + s.logger.Error("response error", "path", qpath, "log", qres.Response.Log) return nil, fmt.Errorf("%w: %s", ErrClientResponse, err.Error()) } diff --git a/gno.land/pkg/gnoweb/webclient_mock.go b/gno.land/pkg/gnoweb/webclient_mock.go index 451f5e237c3..8a037c181e0 100644 --- a/gno.land/pkg/gnoweb/webclient_mock.go +++ b/gno.land/pkg/gnoweb/webclient_mock.go @@ -31,13 +31,18 @@ func NewMockWebClient(pkgs ...*MockPackage) *MockWebClient { return &MockWebClient{Packages: mpkgs} } -// Render simulates rendering a package by writing its content to the writer. +// RenderRealm simulates rendering a package by writing its content to the writer. func (m *MockWebClient) RenderRealm(w io.Writer, path string, args string) (*RealmMeta, error) { pkg, exists := m.Packages[path] if !exists { return nil, ErrClientPathNotFound } + if !pkgHasRender(pkg) { + return nil, ErrRenderNotDeclared + } + + // Write to the realm render fmt.Fprintf(w, "[%s]%s:", pkg.Domain, pkg.Path) // Return a dummy RealmMeta for simplicity @@ -89,3 +94,21 @@ func (m *MockWebClient) Sources(path string) ([]string, error) { return fileNames, nil } + +func pkgHasRender(pkg *MockPackage) bool { + if len(pkg.Functions) == 0 { + return false + } + + for _, fn := range pkg.Functions { + if fn.FuncName == "Render" && + len(fn.Params) == 1 && + len(fn.Results) == 1 && + fn.Params[0].Type == "string" && + fn.Results[0].Type == "string" { + return true + } + } + + return false +} diff --git a/gno.land/pkg/gnoweb/url.go b/gno.land/pkg/gnoweb/weburl/url.go similarity index 87% rename from gno.land/pkg/gnoweb/url.go rename to gno.land/pkg/gnoweb/weburl/url.go index 9127225d490..cbe861e9e42 100644 --- a/gno.land/pkg/gnoweb/url.go +++ b/gno.land/pkg/gnoweb/weburl/url.go @@ -1,4 +1,4 @@ -package gnoweb +package weburl import ( "errors" @@ -97,20 +97,12 @@ func (gnoURL GnoURL) Encode(encodeFlags EncodeFlag) string { if encodeFlags.Has(EncodeWebQuery) && len(gnoURL.WebQuery) > 0 { urlstr.WriteRune('$') - if noEscape { - urlstr.WriteString(NoEscapeQuery(gnoURL.WebQuery)) - } else { - urlstr.WriteString(gnoURL.WebQuery.Encode()) - } + urlstr.WriteString(EncodeValues(gnoURL.WebQuery, !noEscape)) } if encodeFlags.Has(EncodeQuery) && len(gnoURL.Query) > 0 { urlstr.WriteRune('?') - if noEscape { - urlstr.WriteString(NoEscapeQuery(gnoURL.Query)) - } else { - urlstr.WriteString(gnoURL.Query.Encode()) - } + urlstr.WriteString(EncodeValues(gnoURL.Query, !noEscape)) } return urlstr.String() @@ -140,7 +132,7 @@ func (gnoURL GnoURL) EncodeURL() string { // EncodeWebURL encodes the path, package arguments, web query, and query into a string. // This function provides the full representation of the URL. func (gnoURL GnoURL) EncodeWebURL() string { - return gnoURL.Encode(EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery) + return gnoURL.Encode(EncodePath | EncodeArgs | EncodeWebQuery | EncodeQuery | EncodeNoEscape) } // IsPure checks if the URL path represents a pure path. @@ -226,11 +218,11 @@ func ParseGnoURL(u *url.URL) (*GnoURL, error) { }, nil } -// NoEscapeQuery generates a URL-encoded query string from the given url.Values, -// without escaping the keys and values. The query parameters are sorted by key. -func NoEscapeQuery(v url.Values) string { - // Encode encodes the values into “URL encoded” form - // ("bar=baz&foo=quux") sorted by key. +// EncodeValues generates a URL-encoded query string from the given url.Values. +// This function is a modified version of Go's `url.Values.Encode()`: https://pkg.go.dev/net/url#Values.Encode +// It takes an additional `escape` boolean argument that disables escaping on keys and values. +// Additionally, if an empty string value is passed, it omits the `=` sign, resulting in `?key` instead of `?key=` to enhance URL readability. +func EncodeValues(v url.Values, escape bool) string { if len(v) == 0 { return "" } @@ -240,16 +232,29 @@ func NoEscapeQuery(v url.Values) string { keys = append(keys, k) } slices.Sort(keys) + for _, k := range keys { vs := v[k] - keyEscaped := k + keyEncoded := k + if escape { + keyEncoded = url.QueryEscape(k) + } for _, v := range vs { if buf.Len() > 0 { buf.WriteByte('&') } - buf.WriteString(keyEscaped) + buf.WriteString(keyEncoded) + + if len(v) == 0 { + continue // Skip `=` for empty values + } + buf.WriteByte('=') - buf.WriteString(v) + if escape { + buf.WriteString(url.QueryEscape(v)) + } else { + buf.WriteString(v) + } } } return buf.String() diff --git a/gno.land/pkg/gnoweb/url_test.go b/gno.land/pkg/gnoweb/weburl/url_test.go similarity index 87% rename from gno.land/pkg/gnoweb/url_test.go rename to gno.land/pkg/gnoweb/weburl/url_test.go index 7a491eaa149..682832f5b0d 100644 --- a/gno.land/pkg/gnoweb/url_test.go +++ b/gno.land/pkg/gnoweb/weburl/url_test.go @@ -1,4 +1,4 @@ -package gnoweb +package weburl import ( "net/url" @@ -301,7 +301,7 @@ func TestEncode(t *testing.T) { }, }, EncodeFlags: EncodeWebQuery | EncodeNoEscape, - Expected: "$fun$c=B$ ar&help=", + Expected: "$fun$c=B$ ar&help", }, { @@ -450,6 +450,69 @@ func TestEncode(t *testing.T) { EncodeFlags: EncodePath | EncodeArgs | EncodeQuery, Expected: "/r/demo/foo:example?hello=42", }, + + { + Name: "WebQuery with empty value", + GnoURL: GnoURL{ + Path: "/r/demo/foo", + WebQuery: url.Values{ + "source": {""}, + }, + }, + EncodeFlags: EncodePath | EncodeWebQuery | EncodeNoEscape, + Expected: "/r/demo/foo$source", + }, + + { + Name: "WebQuery with nil", + GnoURL: GnoURL{ + Path: "/r/demo/foo", + WebQuery: url.Values{ + "debug": nil, + }, + }, + EncodeFlags: EncodePath | EncodeWebQuery, + Expected: "/r/demo/foo$", + }, + + { + Name: "WebQuery with regular value", + GnoURL: GnoURL{ + Path: "/r/demo/foo", + WebQuery: url.Values{ + "key": {"value"}, + }, + }, + EncodeFlags: EncodePath | EncodeWebQuery, + Expected: "/r/demo/foo$key=value", + }, + + { + Name: "WebQuery mixing empty and nil and filled values", + GnoURL: GnoURL{ + Path: "/r/demo/foo", + WebQuery: url.Values{ + "source": {""}, + "debug": nil, + "user": {"Alice"}, + }, + }, + EncodeFlags: EncodePath | EncodeWebQuery, + Expected: "/r/demo/foo$source&user=Alice", + }, + + { + Name: "WebQuery mixing nil and filled values", + GnoURL: GnoURL{ + Path: "/r/demo/foo", + WebQuery: url.Values{ + "debug": nil, + "user": {"Alice"}, + }, + }, + EncodeFlags: EncodePath | EncodeWebQuery, + Expected: "/r/demo/foo$user=Alice", + }, } for _, tc := range testCases { diff --git a/gno.land/pkg/integration/node_testing.go b/gno.land/pkg/integration/node_testing.go index edcf53de5d3..1af699f014d 100644 --- a/gno.land/pkg/integration/node_testing.go +++ b/gno.land/pkg/integration/node_testing.go @@ -8,6 +8,8 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -103,10 +105,10 @@ func DefaultTestingGenesisConfig(gnoroot string, self crypto.PubKey, tmconfig *t ChainID: tmconfig.ChainID(), ConsensusParams: abci.ConsensusParams{ Block: &abci.BlockParams{ - MaxTxBytes: 1_000_000, // 1MB, - MaxDataBytes: 2_000_000, // 2MB, - MaxGas: 100_000_000, // 100M gas - TimeIotaMS: 100, // 100ms + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 3_000_000_000, // 3B gas + TimeIotaMS: 100, // 100ms }, }, Validators: []bft.GenesisValidator{ @@ -186,3 +188,30 @@ func DefaultTestingTMConfig(gnoroot string) *tmcfg.Config { tmconfig.P2P.ListenAddress = defaultListner return tmconfig } + +func GenerateTestingGenesisState(creator crypto.PrivKey, pkgs ...gnovm.MemPackage) gnoland.GnoGenesisState { + txs := make([]gnoland.TxWithMetadata, len(pkgs)) + for i, pkg := range pkgs { + // Create transaction + var tx std.Tx + tx.Fee = std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}} + tx.Msgs = []std.Msg{ + vmm.MsgAddPackage{ + Creator: creator.PubKey().Address(), + Package: &pkg, + }, + } + + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + txs[i] = gnoland.TxWithMetadata{Tx: tx} + } + + gnoland.SignGenesisTxs(txs, creator, "tendermint_test") + return gnoland.GnoGenesisState{ + Txs: txs, + Balances: []gnoland.Balance{{ + Address: creator.PubKey().Address(), + Amount: std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)), + }}, + } +} diff --git a/gno.land/pkg/integration/node_testing_test.go b/gno.land/pkg/integration/node_testing_test.go new file mode 100644 index 00000000000..96b40bc0ec7 --- /dev/null +++ b/gno.land/pkg/integration/node_testing_test.go @@ -0,0 +1,75 @@ +package integration + +import ( + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateTestingGenesisState(t *testing.T) { + // Generate a test private key and address + privKey := secp256k1.GenPrivKey() + creatorAddr := privKey.PubKey().Address() + + // Create sample packages + pkg1 := gnovm.MemPackage{ + Name: "pkg1", + Path: "pkg1", + Files: []*gnovm.MemFile{ + {Name: "file.gno", Body: "package1"}, + }, + } + pkg2 := gnovm.MemPackage{ + Name: "pkg2", + Path: "pkg2", + Files: []*gnovm.MemFile{ + {Name: "file.gno", Body: "package2"}, + }, + } + + t.Run("single package genesis", func(t *testing.T) { + genesis := GenerateTestingGenesisState(privKey, pkg1) + + // Verify transactions + require.Len(t, genesis.Txs, 1) + tx := genesis.Txs[0].Tx + + // Check the transaction's message + require.Len(t, tx.Msgs, 1) + msg, ok := tx.Msgs[0].(vm.MsgAddPackage) + require.True(t, ok, "expected MsgAddPackage") + assert.Equal(t, pkg1, *msg.Package, "package mismatch") + + // Verify transaction signatures + require.Len(t, tx.Signatures, 1) + assert.NotEmpty(t, tx.Signatures[0], "signature should not be empty") + + // Verify balances + require.Len(t, genesis.Balances, 1) + balance := genesis.Balances[0] + assert.Equal(t, creatorAddr, balance.Address) + assert.Equal(t, std.MustParseCoins(ugnot.ValueString(10_000_000_000_000)), balance.Amount) + }) + + t.Run("multiple packages genesis", func(t *testing.T) { + genesis := GenerateTestingGenesisState(privKey, pkg1, pkg2) + + // Verify two transactions are created + require.Len(t, genesis.Txs, 2) + + // Check each transaction's package + for i, expectedPkg := range []gnovm.MemPackage{pkg1, pkg2} { + tx := genesis.Txs[i].Tx + require.Len(t, tx.Msgs, 1) + msg, ok := tx.Msgs[0].(vm.MsgAddPackage) + require.True(t, ok, "expected MsgAddPackage") + assert.Equal(t, expectedPkg, *msg.Package, "package mismatch in tx %d", i) + } + }) +} diff --git a/gno.land/pkg/keyscli/run.go b/gno.land/pkg/keyscli/run.go index 00b2be585c6..2d7f754203e 100644 --- a/gno.land/pkg/keyscli/run.go +++ b/gno.land/pkg/keyscli/run.go @@ -106,11 +106,12 @@ func execMakeRun(cfg *MakeRunCfg, args []string, cmdio commands.IO) error { } } } + + memPkg.Name = "main" if memPkg.IsEmpty() { panic(fmt.Sprintf("found an empty package %q", memPkg.Path)) } - memPkg.Name = "main" // Set to empty; this will be automatically set by the VM keeper. memPkg.Path = "" diff --git a/gno.land/pkg/sdk/vm/errors.go b/gno.land/pkg/sdk/vm/errors.go index c8d6da98970..208fb074f7e 100644 --- a/gno.land/pkg/sdk/vm/errors.go +++ b/gno.land/pkg/sdk/vm/errors.go @@ -16,6 +16,7 @@ func (abciError) AssertABCIError() {} // NOTE: these are meant to be used in conjunction with pkgs/errors. type ( InvalidPkgPathError struct{ abciError } + NoRenderDeclError struct{ abciError } PkgExistError struct{ abciError } InvalidStmtError struct{ abciError } InvalidExprError struct{ abciError } @@ -27,6 +28,7 @@ type ( ) func (e InvalidPkgPathError) Error() string { return "invalid package path" } +func (e NoRenderDeclError) Error() string { return "render function not declared" } func (e PkgExistError) Error() string { return "package already exists" } func (e InvalidStmtError) Error() string { return "invalid statement" } func (e InvalidExprError) Error() string { return "invalid expression" } diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index c484e07e887..5aebf1afe46 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -129,9 +129,13 @@ func (vh vmHandler) queryRender(ctx sdk.Context, req abci.RequestQuery) (res abc expr := fmt.Sprintf("Render(%q)", path) result, err := vh.vm.QueryEvalString(ctx, pkgPath, expr) if err != nil { + if strings.Contains(err.Error(), "Render not declared") { + err = NoRenderDeclError{} + } res = sdk.ABCIResponseQueryFromError(err) return } + res.Data = []byte(result) return } diff --git a/gno.land/pkg/sdk/vm/package.go b/gno.land/pkg/sdk/vm/package.go index 0359061ccea..95e97648dac 100644 --- a/gno.land/pkg/sdk/vm/package.go +++ b/gno.land/pkg/sdk/vm/package.go @@ -20,6 +20,7 @@ var Package = amino.RegisterPackage(amino.NewPackage( // errors InvalidPkgPathError{}, "InvalidPkgPathError", + NoRenderDeclError{}, "NoRenderDeclError", PkgExistError{}, "PkgExistError", InvalidStmtError{}, "InvalidStmtError", InvalidExprError{}, "InvalidExprError", diff --git a/gnovm/Makefile b/gnovm/Makefile index ce745e44aae..ec6c5b06967 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -27,11 +27,14 @@ GOTEST_FLAGS ?= -v -p 1 -timeout=30m GNOROOT_DIR ?= $(abspath $(lastword $(MAKEFILE_LIST))/../../) # We can't use '-trimpath' yet as amino use absolute path from call stack # to find some directory: see #1236 -GOBUILD_FLAGS ?= -ldflags "-X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT=$(GNOROOT_DIR)" +GOBUILD_FLAGS ?= -ldflags "-X github.com/gnolang/gno/gnovm/pkg/version.Version=$(VERSION) -X github.com/gnolang/gno/gnovm/pkg/gnoenv._GNOROOT=$(GNOROOT_DIR)" # file where to place cover profile; used for coverage commands which are # more complex than adding -coverprofile, like test.cmd.coverage. GOTEST_COVER_PROFILE ?= cmd-profile.out +# user for gno version [branch].[N]+[hash] +VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || echo "$(shell git rev-parse --abbrev-ref HEAD).$(shell git rev-list --count HEAD)+$(shell git rev-parse --short HEAD)") + ######################################## # Dev tools .PHONY: build @@ -122,10 +125,8 @@ _test.filetest:; ######################################## # Code gen -# TODO: move _dev.stringer to go:generate instructions, simplify generate -# to just go generate. .PHONY: generate -generate: _dev.stringer _dev.generate _dev.docs +generate: _dev.generate _dev.docs fmt imports .PHONY: _dev.docs _dev.docs: @@ -133,16 +134,6 @@ _dev.docs: (go run ./cmd/gno -h 2>&1 || true) | grep -v "exit status 1" > .tmp/gno-help.txt $(rundep) github.com/campoy/embedmd -w `find . -name "*.md"` -stringer_cmd=$(rundep) golang.org/x/tools/cmd/stringer -.PHONY: _dev.stringer -_dev.stringer: - $(stringer_cmd) -type=Kind ./pkg/gnolang - $(stringer_cmd) -type=Op ./pkg/gnolang - $(stringer_cmd) -type=TransCtrl ./pkg/gnolang - $(stringer_cmd) -type=TransField ./pkg/gnolang - $(stringer_cmd) -type=VPType ./pkg/gnolang - $(stringer_cmd) -type=Word ./pkg/gnolang - .PHONY: _dev.generate _dev.generate: go generate -x ./... diff --git a/gnovm/cmd/gno/README.md b/gnovm/cmd/gno/README.md index 81b45622c05..81d3de2cf62 100644 --- a/gnovm/cmd/gno/README.md +++ b/gnovm/cmd/gno/README.md @@ -14,15 +14,16 @@ USAGE gno [arguments] SUBCOMMANDS - bug start a bug report - clean remove generated and cached data - doc show documentation for package or symbol - env print gno environment information - fmt gnofmt (reformat) package sources - mod module maintenance - run run gno packages - test test packages - tool run specified gno tool + bug start a bug report + clean remove generated and cached data + doc show documentation for package or symbol + env print gno environment information + fmt gnofmt (reformat) package sources + mod module maintenance + run run gno packages + test test packages + tool run specified gno tool + version display installed gno version ``` diff --git a/gnovm/cmd/gno/bug.go b/gnovm/cmd/gno/bug.go index 7a4345fb1ed..6f4f78410d6 100644 --- a/gnovm/cmd/gno/bug.go +++ b/gnovm/cmd/gno/bug.go @@ -11,6 +11,7 @@ import ( "text/template" "time" + "github.com/gnolang/gno/gnovm/pkg/version" "github.com/gnolang/gno/tm2/pkg/commands" ) @@ -23,9 +24,10 @@ Describe your issue in as much detail as possible here ### Your environment -* Go version: {{.GoVersion}} -* OS and CPU architecture: {{.Os}}/{{.Arch}} -* Gno commit hash causing the issue: {{.Commit}} +* Gno version: {{ .GnoVersion }} +* Go version: {{ .GoVersion }} +* OS and CPU architecture: {{ .Os }}/{{ .Arch }} +* Gno commit hash causing the issue: {{ .Commit }} ### Steps to reproduce @@ -62,10 +64,11 @@ func newBugCmd(io commands.IO) *commands.Command { Name: "bug", ShortUsage: "bug", ShortHelp: "start a bug report", - LongHelp: `opens https://github.com/gnolang/gno/issues in a browser. + LongHelp: `opens https://github.com/gnolang/gno/issues in a browser. The new issue body is prefilled for you with the following information: +- Gno version (the output of "gno version") - Go version (example: go1.22.4) - OS and CPU architecture (example: linux/amd64) - Gno commit hash causing the issue (example: f24690e7ebf325bffcfaf9e328c3df8e6b21e50c) @@ -96,10 +99,11 @@ func execBug(cfg *bugCfg, args []string, io commands.IO) error { } bugReportEnv := struct { - Os, Arch, GoVersion, Commit string + Os, Arch, GnoVersion, GoVersion, Commit string }{ runtime.GOOS, runtime.GOARCH, + version.Version, runtime.Version(), getCommitHash(), } diff --git a/gnovm/cmd/gno/bug_test.go b/gnovm/cmd/gno/bug_test.go index 516bfd4081b..4b0073ce014 100644 --- a/gnovm/cmd/gno/bug_test.go +++ b/gnovm/cmd/gno/bug_test.go @@ -5,17 +5,21 @@ import "testing" func TestBugApp(t *testing.T) { tc := []testMainCase{ { - args: []string{"bug -h"}, - errShouldBe: "flag: help requested", + args: []string{"bug", "-h"}, + errShouldContain: "flag: help requested", }, { - args: []string{"bug unknown"}, + args: []string{"bug", "unknown"}, errShouldBe: "flag: help requested", }, { args: []string{"bug", "-skip-browser"}, stdoutShouldContain: "Go version: go1.", }, + { + args: []string{"bug", "-skip-browser"}, + stdoutShouldContain: "Gno version: develop", + }, } testMainCaseRun(t, tc) } diff --git a/gnovm/cmd/gno/main.go b/gnovm/cmd/gno/main.go index 5f8bb7b522e..b18e610d535 100644 --- a/gnovm/cmd/gno/main.go +++ b/gnovm/cmd/gno/main.go @@ -41,6 +41,7 @@ func newGnocliCmd(io commands.IO) *commands.Command { newTestCmd(io), newToolCmd(io), // version -- show cmd/gno, golang versions + newGnoVersionCmd(io), // vet ) diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index f303908d8ee..e394684561f 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -34,7 +34,7 @@ func newModCmd(io commands.IO) *commands.Command { cmd.AddSubCommands( newModDownloadCmd(io), // edit - // graph + newModGraphCmd(io), newModInitCmd(), newModTidy(io), // vendor @@ -61,6 +61,21 @@ func newModDownloadCmd(io commands.IO) *commands.Command { ) } +func newModGraphCmd(io commands.IO) *commands.Command { + cfg := &modGraphCfg{} + return commands.NewCommand( + commands.Metadata{ + Name: "graph", + ShortUsage: "graph [path]", + ShortHelp: "print module requirement graph", + }, + cfg, + func(_ context.Context, args []string) error { + return execModGraph(cfg, args, io) + }, + ) +} + func newModInitCmd() *commands.Command { return commands.NewCommand( commands.Metadata{ @@ -144,6 +159,38 @@ func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) { ) } +type modGraphCfg struct{} + +func (c *modGraphCfg) RegisterFlags(fs *flag.FlagSet) { + // /out std + // /out remote + // /out _test processing + // ... +} + +func execModGraph(cfg *modGraphCfg, args []string, io commands.IO) error { + // default to current directory if no args provided + if len(args) == 0 { + args = []string{"."} + } + if len(args) > 1 { + return flag.ErrHelp + } + + stdout := io.Out() + + pkgs, err := gnomod.ListPkgs(args[0]) + if err != nil { + return err + } + for _, pkg := range pkgs { + for _, dep := range pkg.Imports { + fmt.Fprintf(stdout, "%s %s\n", pkg.Name, dep) + } + } + return nil +} + func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error { if len(args) > 0 { return flag.ErrHelp diff --git a/gnovm/cmd/gno/mod_test.go b/gnovm/cmd/gno/mod_test.go index afce25597cd..e6fdce50a86 100644 --- a/gnovm/cmd/gno/mod_test.go +++ b/gnovm/cmd/gno/mod_test.go @@ -210,6 +210,34 @@ func TestModApp(t *testing.T) { # gno.land/p/demo/avl valid.gno +`, + }, + + // test `gno mod graph` + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/minimalist_gnomod", + simulateExternalRepo: true, + stdoutShouldBe: ``, + }, + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/valid1", + simulateExternalRepo: true, + stdoutShouldBe: ``, + }, + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/valid2", + simulateExternalRepo: true, + stdoutShouldBe: `gno.land/p/integ/valid gno.land/p/demo/avl +`, + }, + { + args: []string{"mod", "graph"}, + testDir: "../../tests/integ/require_remote_module", + simulateExternalRepo: true, + stdoutShouldBe: `gno.land/tests/importavl gno.land/p/demo/avl `, }, } diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index 489016aa3d4..34bf818e8f5 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -93,7 +93,7 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error { // init store and machine _, testStore := test.Store( - cfg.rootDir, false, + cfg.rootDir, stdin, stdout, stderr) if cfg.verbose { testStore.SetLogStoreOps(true) diff --git a/gnovm/cmd/gno/testdata/test/error_correct.txtar b/gnovm/cmd/gno/testdata/test/error_correct.txtar index f9ce4dd9028..bcd2c87da5c 100644 --- a/gnovm/cmd/gno/testdata/test/error_correct.txtar +++ b/gnovm/cmd/gno/testdata/test/error_correct.txtar @@ -3,8 +3,8 @@ gno test -v . stderr '=== RUN file/x_filetest.gno' -stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/x_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' -- x_filetest.gno -- package main diff --git a/gnovm/cmd/gno/testdata/test/filetest_events.txtar b/gnovm/cmd/gno/testdata/test/filetest_events.txtar index 34da5fe2ff0..87f873980d5 100644 --- a/gnovm/cmd/gno/testdata/test/filetest_events.txtar +++ b/gnovm/cmd/gno/testdata/test/filetest_events.txtar @@ -3,14 +3,14 @@ gno test -print-events . ! stdout .+ -stderr 'ok \. \d\.\d\ds' +stderr 'ok \. \d+\.\d\ds' gno test -print-events -v . stdout 'test' stderr '=== RUN file/valid_filetest.gno' -stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/valid_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/testdata/test/minim2.txtar b/gnovm/cmd/gno/testdata/test/minim2.txtar index 3c4d1d085f0..d66d5076ef0 100644 --- a/gnovm/cmd/gno/testdata/test/minim2.txtar +++ b/gnovm/cmd/gno/testdata/test/minim2.txtar @@ -2,8 +2,8 @@ gno test . -! stdout .+ -stderr 'ok \. \d\.\d\ds' +! stdout .+ +stderr 'ok \. \d+\.\d\ds' -- minim.gno -- package minim diff --git a/gnovm/cmd/gno/testdata/test/minim3.txtar b/gnovm/cmd/gno/testdata/test/minim3.txtar index ac8ae0c41d4..ba1847a21df 100644 --- a/gnovm/cmd/gno/testdata/test/minim3.txtar +++ b/gnovm/cmd/gno/testdata/test/minim3.txtar @@ -2,8 +2,8 @@ gno test . -! stdout .+ -stderr 'ok \. \d\.\d\ds' +! stdout .+ +stderr 'ok \. \d+\.\d\ds' -- minim.gno -- package minim diff --git a/gnovm/cmd/gno/testdata/test/multitest_events.txtar b/gnovm/cmd/gno/testdata/test/multitest_events.txtar index 321c790561a..5cb134f46a1 100644 --- a/gnovm/cmd/gno/testdata/test/multitest_events.txtar +++ b/gnovm/cmd/gno/testdata/test/multitest_events.txtar @@ -2,10 +2,10 @@ gno test -print-events . -! stdout .+ +! stdout .+ stderr 'EVENTS: \[{\"type\":\"EventA\",\"attrs\":\[\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestA\"}\]' stderr 'EVENTS: \[{\"type\":\"EventB\",\"attrs\":\[{\"key\":\"keyA\",\"value\":\"valA\"}\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestB\"},{\"type\":\"EventC\",\"attrs\":\[{\"key\":\"keyD\",\"value\":\"valD\"}\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestB\"}\]' -stderr 'ok \. \d\.\d\ds' +stderr 'ok \. \d+\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/testdata/test/output_correct.txtar b/gnovm/cmd/gno/testdata/test/output_correct.txtar index a8aa878e0a4..3a829e66bee 100644 --- a/gnovm/cmd/gno/testdata/test/output_correct.txtar +++ b/gnovm/cmd/gno/testdata/test/output_correct.txtar @@ -5,8 +5,8 @@ gno test -v . stdout 'hey' stdout 'hru?' stderr '=== RUN file/x_filetest.gno' -stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/x_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' -- x_filetest.gno -- package main diff --git a/gnovm/cmd/gno/testdata/test/output_sync.txtar b/gnovm/cmd/gno/testdata/test/output_sync.txtar index 45385a7eef9..1d701cc1c7f 100644 --- a/gnovm/cmd/gno/testdata/test/output_sync.txtar +++ b/gnovm/cmd/gno/testdata/test/output_sync.txtar @@ -6,8 +6,8 @@ stdout 'hey' stdout '^hru\?' stderr '=== RUN file/x_filetest.gno' -stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/x_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' cmp x_filetest.gno x_filetest.gno.golden diff --git a/gnovm/cmd/gno/testdata/test/realm_correct.txtar b/gnovm/cmd/gno/testdata/test/realm_correct.txtar index ae1212133fd..8b1478d0df7 100644 --- a/gnovm/cmd/gno/testdata/test/realm_correct.txtar +++ b/gnovm/cmd/gno/testdata/test/realm_correct.txtar @@ -4,8 +4,8 @@ gno test -v . ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' -stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/x_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' -- x_filetest.gno -- // PKGPATH: gno.land/r/xx @@ -18,4 +18,4 @@ func main() { } // Realm: -// switchrealm["gno.land/r/xx"] \ No newline at end of file +// switchrealm["gno.land/r/xx"] diff --git a/gnovm/cmd/gno/testdata/test/realm_sync.txtar b/gnovm/cmd/gno/testdata/test/realm_sync.txtar index 65a930b2f03..91c83235d15 100644 --- a/gnovm/cmd/gno/testdata/test/realm_sync.txtar +++ b/gnovm/cmd/gno/testdata/test/realm_sync.txtar @@ -4,8 +4,8 @@ gno test -v . -update-golden-tests ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' -stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/x_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' cmp x_filetest.gno x_filetest.gno.golden diff --git a/gnovm/cmd/gno/testdata/test/valid_filetest.txtar b/gnovm/cmd/gno/testdata/test/valid_filetest.txtar index 4e24ad9ab08..bd73ce3dc99 100644 --- a/gnovm/cmd/gno/testdata/test/valid_filetest.txtar +++ b/gnovm/cmd/gno/testdata/test/valid_filetest.txtar @@ -3,14 +3,14 @@ gno test . ! stdout .+ -stderr 'ok \. \d\.\d\ds' +stderr 'ok \. \d+\.\d\ds' gno test -v . stdout 'test' stderr '=== RUN file/valid_filetest.gno' -stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \. \d\.\d\ds' +stderr '--- PASS: file/valid_filetest.gno \(\d+\.\d\ds\)' +stderr 'ok \. \d+\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/testdata/test/valid_test.txtar b/gnovm/cmd/gno/testdata/test/valid_test.txtar index 9590626776c..5a9fe37a4b0 100644 --- a/gnovm/cmd/gno/testdata/test/valid_test.txtar +++ b/gnovm/cmd/gno/testdata/test/valid_test.txtar @@ -2,13 +2,13 @@ gno test . -! stdout .+ -stderr 'ok \. \d\.\d\ds' +! stdout .+ +stderr 'ok \. \d+\.\d\ds' gno test ./... -! stdout .+ -stderr 'ok \. \d\.\d\ds' +! stdout .+ +stderr 'ok \. \d+\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar index 906c43cc41b..5e1f1a5ff0f 100644 --- a/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar +++ b/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar @@ -36,7 +36,7 @@ package main import "std" func hello() { - std.AssertOriginCall() + std.GetChainID() } -- main.gno.gen.go.golden -- @@ -61,5 +61,5 @@ package main import "github.com/gnolang/gno/gnovm/stdlibs/std" func hello() { - std.AssertOriginCall(nil) + std.GetChainID(nil) } diff --git a/gnovm/cmd/gno/tool_lint.go b/gnovm/cmd/gno/tool_lint.go index ce3465b484e..6983175cea0 100644 --- a/gnovm/cmd/gno/tool_lint.go +++ b/gnovm/cmd/gno/tool_lint.go @@ -97,9 +97,9 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { hasError := false - bs, ts := test.Store( - rootDir, false, - nopReader{}, goio.Discard, goio.Discard, + bs, ts := test.StoreWithOptions( + rootDir, nopReader{}, goio.Discard, goio.Discard, + test.StoreOptions{PreprocessOnly: true}, ) for _, pkgPath := range pkgPaths { @@ -162,13 +162,10 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { tm := test.Machine(gs, goio.Discard, memPkg.Path) defer tm.Release() - // Check package - tm.RunMemPackage(memPkg, true) - // Check test files - testFiles := lintTestFiles(memPkg) + packageFiles := sourceAndTestFileset(memPkg) - tm.RunFiles(testFiles.Files...) + tm.PreprocessFiles(memPkg.Name, memPkg.Path, packageFiles, false, false) }) if hasRuntimeErr { hasError = true @@ -221,20 +218,21 @@ func lintTypeCheck(io commands.IO, memPkg *gnovm.MemPackage, testStore gno.Store return true, nil } -func lintTestFiles(memPkg *gnovm.MemPackage) *gno.FileSet { +func sourceAndTestFileset(memPkg *gnovm.MemPackage) *gno.FileSet { testfiles := &gno.FileSet{} for _, mfile := range memPkg.Files { if !strings.HasSuffix(mfile.Name, ".gno") { continue // Skip non-GNO files } - n, _ := gno.ParseFile(mfile.Name, mfile.Body) + n := gno.MustParseFile(mfile.Name, mfile.Body) if n == nil { continue // Skip empty files } // XXX: package ending with `_test` is not supported yet - if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") { + if !strings.HasSuffix(mfile.Name, "_filetest.gno") && + !strings.HasSuffix(string(n.PkgName), "_test") { // Keep only test files testfiles.AddFiles(n) } diff --git a/gnovm/cmd/gno/tool_lint_test.go b/gnovm/cmd/gno/tool_lint_test.go index 85b625fa367..3f9e5cd59ba 100644 --- a/gnovm/cmd/gno/tool_lint_test.go +++ b/gnovm/cmd/gno/tool_lint_test.go @@ -10,49 +10,63 @@ func TestLintApp(t *testing.T) { { args: []string{"tool", "lint"}, errShouldBe: "flag: help requested", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/run_main/"}, stderrShouldContain: "./../../tests/integ/run_main: gno.mod file not found in current or any parent directory (code=1)", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"}, stderrShouldContain: "undefined_variables_test.gno:6:28: name toto not declared (code=2)", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/package_not_declared/main.gno"}, stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2)", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/several-lint-errors/main.gno"}, - stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2)\n../../tests/integ/several-lint-errors/main.gno:6", + stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=3)\n../../tests/integ/several-lint-errors/main.gno:6", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/several-files-multiple-errors/main.gno"}, stderrShouldContain: func() string { lines := []string{ - "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)", - "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2)", - "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2)", - "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2)", + "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=3)", + "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=3)", + "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=3)", + "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=3)", } return strings.Join(lines, "\n") + "\n" }(), errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/minimalist_gnomod/"}, // TODO: raise an error because there is a gno.mod, but no .gno files - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/invalid_module_name/"}, // TODO: raise an error because gno.mod is invalid - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/invalid_gno_file/"}, stderrShouldContain: "../../tests/integ/invalid_gno_file/invalid.gno:1:1: expected 'package', found packag (code=2)", errShouldBe: "exit code: 1", - }, { + }, + { args: []string{"tool", "lint", "../../tests/integ/typecheck_missing_return/"}, stderrShouldContain: "../../tests/integ/typecheck_missing_return/main.gno:5:1: missing return (code=4)", errShouldBe: "exit code: 1", }, + { + args: []string{"tool", "lint", "../../tests/integ/init/"}, + // stderr / stdout should be empty; the init function and statements + // should not be executed + }, // TODO: 'gno mod' is valid? // TODO: are dependencies valid? diff --git a/gnovm/cmd/gno/version.go b/gnovm/cmd/gno/version.go new file mode 100644 index 00000000000..f9b967d1c40 --- /dev/null +++ b/gnovm/cmd/gno/version.go @@ -0,0 +1,24 @@ +package main + +import ( + "context" + + "github.com/gnolang/gno/gnovm/pkg/version" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// newVersionCmd creates a new version command +func newGnoVersionCmd(io commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "version", + ShortUsage: "version", + ShortHelp: "display installed gno version", + }, + nil, + func(_ context.Context, args []string) error { + io.Println("gno version:", version.Version) + return nil + }, + ) +} diff --git a/gnovm/cmd/gno/version_test.go b/gnovm/cmd/gno/version_test.go new file mode 100644 index 00000000000..fab47319297 --- /dev/null +++ b/gnovm/cmd/gno/version_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "testing" + + "github.com/gnolang/gno/gnovm/pkg/version" +) + +func TestVersionApp(t *testing.T) { + originalVersion := version.Version + + t.Cleanup(func() { + version.Version = originalVersion + }) + + versionValues := []string{"chain/test4.2", "develop", "master"} + + testCases := make([]testMainCase, len(versionValues)) + for i, v := range versionValues { + testCases[i] = testMainCase{ + args: []string{"version"}, + stdoutShouldContain: "gno version: " + v, + } + } + + for i, testCase := range testCases { + t.Run(versionValues[i], func(t *testing.T) { + version.Version = versionValues[i] + testMainCaseRun(t, []testMainCase{testCase}) + }) + } +} diff --git a/gnovm/memfile.go b/gnovm/memfile.go index 6988c893dd7..a08e89579ad 100644 --- a/gnovm/memfile.go +++ b/gnovm/memfile.go @@ -34,7 +34,7 @@ func (mempkg *MemPackage) GetFile(name string) *MemFile { } func (mempkg *MemPackage) IsEmpty() bool { - return len(mempkg.Files) == 0 + return mempkg.Name == "" || len(mempkg.Files) == 0 } const pathLengthLimit = 256 diff --git a/gnovm/pkg/gnolang/README.md b/gnovm/pkg/gnolang/README.md index 56ac6e0baba..2cd7c2b27d2 100644 --- a/gnovm/pkg/gnolang/README.md +++ b/gnovm/pkg/gnolang/README.md @@ -1,3 +1,4 @@ # Gnolang -TODO: dedicated README +## Declarations +* Gno is only available for 64-bit architectures! diff --git a/gnovm/pkg/gnolang/debug.go b/gnovm/pkg/gnolang/debug.go index c7f9311ffe4..0f9cb9a1f9c 100644 --- a/gnovm/pkg/gnolang/debug.go +++ b/gnovm/pkg/gnolang/debug.go @@ -3,6 +3,8 @@ package gnolang import ( "fmt" "net/http" + "path" + "runtime" "strings" "time" @@ -48,7 +50,10 @@ var enabled bool = true func (debugging) Println(args ...interface{}) { if debug { if enabled { - fmt.Println(append([]interface{}{"DEBUG:"}, args...)...) + _, file, line, _ := runtime.Caller(2) + caller := fmt.Sprintf("%-.12s:%-4d", path.Base(file), line) + prefix := fmt.Sprintf("DEBUG: %17s: ", caller) + fmt.Println(append([]interface{}{prefix}, args...)...) } } } @@ -56,7 +61,10 @@ func (debugging) Println(args ...interface{}) { func (debugging) Printf(format string, args ...interface{}) { if debug { if enabled { - fmt.Printf("DEBUG: "+format, args...) + _, file, line, _ := runtime.Caller(2) + caller := fmt.Sprintf("%.12s:%-4d", path.Base(file), line) + prefix := fmt.Sprintf("DEBUG: %17s: ", caller) + fmt.Printf(prefix+format, args...) } } } diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index 926ff0595e6..a9e0a4834d5 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -39,7 +39,7 @@ func evalTest(debugAddr, in, file string) (out, err string) { err = strings.TrimSpace(strings.ReplaceAll(err, "../../tests/files/", "files/")) }() - _, testStore := test.Store(gnoenv.RootDir(), false, stdin, stdout, stderr) + _, testStore := test.Store(gnoenv.RootDir(), stdin, stdout, stderr) f := gnolang.MustReadFile(file) diff --git a/gnovm/pkg/gnolang/doc.go b/gnovm/pkg/gnolang/doc.go index e52d54470e9..4fe9536d47f 100644 --- a/gnovm/pkg/gnolang/doc.go +++ b/gnovm/pkg/gnolang/doc.go @@ -1,2 +1,7 @@ // SPDX-License-Identifier: GNO License Version 1.0 + +// Package gnolang contains the implementation of the Gno Virtual Machine. package gnolang + +//go:generate -command stringer go run -modfile ../../../misc/devdeps/go.mod golang.org/x/tools/cmd/stringer +//go:generate stringer -type=Kind,Op,TransCtrl,TransField,VPType,Word -output string_methods.go . diff --git a/gnovm/pkg/gnolang/files_test.go b/gnovm/pkg/gnolang/files_test.go index 2c82f6d8f29..31f04087855 100644 --- a/gnovm/pkg/gnolang/files_test.go +++ b/gnovm/pkg/gnolang/files_test.go @@ -45,9 +45,9 @@ func TestFiles(t *testing.T) { Error: io.Discard, Sync: *withSync, } - o.BaseStore, o.TestStore = test.Store( - rootDir, true, - nopReader{}, o.WriterForStore(), io.Discard, + o.BaseStore, o.TestStore = test.StoreWithOptions( + rootDir, nopReader{}, o.WriterForStore(), io.Discard, + test.StoreOptions{WithExtern: true}, ) return o } diff --git a/gnovm/pkg/gnolang/frame.go b/gnovm/pkg/gnolang/frame.go index 60f19979b7a..bfd0d42229e 100644 --- a/gnovm/pkg/gnolang/frame.go +++ b/gnovm/pkg/gnolang/frame.go @@ -216,3 +216,30 @@ func toConstExpTrace(cte *ConstExpr) string { return tv.T.String() } + +//---------------------------------------- +// Exception + +// Exception represents a panic that originates from a gno program. +type Exception struct { + // Value is the value passed to panic. + Value TypedValue + // Frame is used to reference the frame a panic occurred in so that recover() knows if the + // currently executing deferred function is able to recover from the panic. + Frame *Frame + + Stacktrace Stacktrace +} + +func (e Exception) Sprint(m *Machine) string { + return e.Value.Sprint(m) +} + +// UnhandledPanicError represents an error thrown when a panic is not handled in the realm. +type UnhandledPanicError struct { + Descriptor string // Description of the unhandled panic. +} + +func (e UnhandledPanicError) Error() string { + return e.Descriptor +} diff --git a/gnovm/pkg/gnolang/internal/softfloat/copy.sh b/gnovm/pkg/gnolang/internal/softfloat/copy.sh deleted file mode 100644 index 6d2a8f80462..00000000000 --- a/gnovm/pkg/gnolang/internal/softfloat/copy.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh - -# softfloat64.go: -# - add header -# - change package name -cat > runtime_softfloat64.go << EOF -// Code generated by copy.sh. DO NOT EDIT. -// This file is copied from \$GOROOT/src/runtime/softfloat64.go. -// It is the software floating point implementation used by the Go runtime. - -EOF -cat "$GOROOT/src/runtime/softfloat64.go" >> ./runtime_softfloat64.go -sed -i 's/^package runtime$/package softfloat/' runtime_softfloat64.go - -# softfloat64_test.go: -# - add header -# - change package name -# - change import to right package -# - change GOARCH to runtime.GOARCH, and import the "runtime" package -cat > runtime_softfloat64_test.go << EOF -// Code generated by copy.sh. DO NOT EDIT. -// This file is copied from \$GOROOT/src/runtime/softfloat64_test.go. -// It is the tests for the software floating point implementation -// used by the Go runtime. - -EOF -cat "$GOROOT/src/runtime/softfloat64_test.go" >> ./runtime_softfloat64_test.go -sed -i 's/^package runtime_test$/package softfloat_test/ -s#^\t\. "runtime"$#\t. "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat"# -s/GOARCH/runtime.GOARCH/g -16a\ - "runtime"' runtime_softfloat64_test.go \ No newline at end of file diff --git a/gnovm/pkg/gnolang/internal/softfloat/gen/main.go b/gnovm/pkg/gnolang/internal/softfloat/gen/main.go new file mode 100644 index 00000000000..7c89ff9b5a9 --- /dev/null +++ b/gnovm/pkg/gnolang/internal/softfloat/gen/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +func main() { + // Process softfloat64.go file + processSoftFloat64File() + + // Process softfloat64_test.go file + processSoftFloat64TestFile() + + // Run mvdan.cc/gofumpt + gofumpt() + + fmt.Println("Files processed successfully.") +} + +func processSoftFloat64File() { + // Read source file + content, err := os.ReadFile(fmt.Sprintf("%s/src/runtime/softfloat64.go", runtime.GOROOT())) + if err != nil { + log.Fatal("Error reading source file:", err) + } + + // Prepare header + header := `// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. +// This file is copied from $GOROOT/src/runtime/softfloat64.go. +// It is the software floating point implementation used by the Go runtime. + +` + + // Combine header with content + newContent := header + string(content) + + // Replace package name + newContent = strings.Replace(newContent, "package runtime", "package softfloat", 1) + + // Write to destination file + err = os.WriteFile("runtime_softfloat64.go", []byte(newContent), 0o644) + if err != nil { + log.Fatal("Error writing to destination file:", err) + } +} + +func processSoftFloat64TestFile() { + // Read source test file + content, err := os.ReadFile(fmt.Sprintf("%s/src/runtime/softfloat64_test.go", runtime.GOROOT())) + if err != nil { + log.Fatal("Error reading source test file:", err) + } + + // Prepare header + header := `// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. +// This file is copied from $GOROOT/src/runtime/softfloat64_test.go. +// It is the tests for the software floating point implementation +// used by the Go runtime. + +` + + // Combine header with content + newContent := header + string(content) + + // Replace package name and imports + newContent = strings.Replace(newContent, "package runtime_test", "package softfloat_test", 1) + newContent = strings.Replace(newContent, "\t. \"runtime\"", "\t\"runtime\"", 1) + newContent = strings.Replace(newContent, "GOARCH", "runtime.GOARCH", 1) + + newContent = strings.Replace(newContent, "import (", "import (\n\t. \"github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat\"", 1) + + // Write to destination file + err = os.WriteFile("runtime_softfloat64_test.go", []byte(newContent), 0o644) + if err != nil { + log.Fatal("Error writing to destination test file:", err) + } +} + +func gitRoot() (string, error) { + wd, err := os.Getwd() + if err != nil { + return "", err + } + p := wd + for { + if s, e := os.Stat(filepath.Join(p, ".git")); e == nil && s.IsDir() { + return p, nil + } + + if strings.HasSuffix(p, string(filepath.Separator)) { + return "", errors.New("root git not found") + } + + p = filepath.Dir(p) + } +} + +func gofumpt() { + rootPath, err := gitRoot() + if err != nil { + log.Fatal("error finding git root:", err) + } + + cmd := exec.Command("go", "run", "-modfile", filepath.Join(strings.TrimSpace(rootPath), "misc/devdeps/go.mod"), "mvdan.cc/gofumpt", "-w", ".") + _, err = cmd.Output() + if err != nil { + log.Fatal("error gofumpt:", err) + } +} diff --git a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go index cf2ad5afd8a..7623b9c2077 100644 --- a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go +++ b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64.go @@ -1,4 +1,4 @@ -// Code generated by copy.sh. DO NOT EDIT. +// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. // This file is copied from $GOROOT/src/runtime/softfloat64.go. // It is the software floating point implementation used by the Go runtime. diff --git a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go index c57fe08b0ef..70c76655a97 100644 --- a/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go +++ b/gnovm/pkg/gnolang/internal/softfloat/runtime_softfloat64_test.go @@ -1,4 +1,4 @@ -// Code generated by copy.sh. DO NOT EDIT. +// Code generated by github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen. DO NOT EDIT. // This file is copied from $GOROOT/src/runtime/softfloat64_test.go. // It is the tests for the software floating point implementation // used by the Go runtime. @@ -12,9 +12,10 @@ package softfloat_test import ( "math" "math/rand" - . "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat" + "runtime" "testing" - "runtime" + + . "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat" ) // turn uint64 op into float64 op diff --git a/gnovm/pkg/gnolang/internal/softfloat/softfloat.go b/gnovm/pkg/gnolang/internal/softfloat/softfloat.go index 30f66dff620..89dcd04d8fb 100644 --- a/gnovm/pkg/gnolang/internal/softfloat/softfloat.go +++ b/gnovm/pkg/gnolang/internal/softfloat/softfloat.go @@ -17,7 +17,7 @@ package softfloat // This file mostly exports the functions from runtime_softfloat64.go -//go:generate sh copy.sh +//go:generate go run github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat/gen const ( mask = 0x7FF diff --git a/gnovm/pkg/gnolang/kind_string.go b/gnovm/pkg/gnolang/kind_string.go deleted file mode 100644 index 12e95829b20..00000000000 --- a/gnovm/pkg/gnolang/kind_string.go +++ /dev/null @@ -1,53 +0,0 @@ -// Code generated by "stringer -type=Kind ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[InvalidKind-0] - _ = x[BoolKind-1] - _ = x[StringKind-2] - _ = x[IntKind-3] - _ = x[Int8Kind-4] - _ = x[Int16Kind-5] - _ = x[Int32Kind-6] - _ = x[Int64Kind-7] - _ = x[UintKind-8] - _ = x[Uint8Kind-9] - _ = x[Uint16Kind-10] - _ = x[Uint32Kind-11] - _ = x[Uint64Kind-12] - _ = x[Float32Kind-13] - _ = x[Float64Kind-14] - _ = x[BigintKind-15] - _ = x[BigdecKind-16] - _ = x[ArrayKind-17] - _ = x[SliceKind-18] - _ = x[PointerKind-19] - _ = x[StructKind-20] - _ = x[PackageKind-21] - _ = x[InterfaceKind-22] - _ = x[ChanKind-23] - _ = x[FuncKind-24] - _ = x[MapKind-25] - _ = x[TypeKind-26] - _ = x[BlockKind-27] - _ = x[HeapItemKind-28] - _ = x[TupleKind-29] - _ = x[RefTypeKind-30] -} - -const _Kind_name = "InvalidKindBoolKindStringKindIntKindInt8KindInt16KindInt32KindInt64KindUintKindUint8KindUint16KindUint32KindUint64KindFloat32KindFloat64KindBigintKindBigdecKindArrayKindSliceKindPointerKindStructKindPackageKindInterfaceKindChanKindFuncKindMapKindTypeKindBlockKindHeapItemKindTupleKindRefTypeKind" - -var _Kind_index = [...]uint16{0, 11, 19, 29, 36, 44, 53, 62, 71, 79, 88, 98, 108, 118, 129, 140, 150, 160, 169, 178, 189, 199, 210, 223, 231, 239, 246, 254, 263, 275, 284, 295} - -func (i Kind) String() string { - if i >= Kind(len(_Kind_index)-1) { - return "Kind(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Kind_name[_Kind_index[i]:_Kind_index[i+1]] -} diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 75d12ac5402..7b89e98eb6d 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -1,11 +1,11 @@ package gnolang -// XXX rename file to machine.go. - import ( "fmt" "io" + "path" "reflect" + "runtime" "slices" "strconv" "strings" @@ -18,30 +18,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/store" ) -// Exception represents a panic that originates from a gno program. -type Exception struct { - // Value is the value passed to panic. - Value TypedValue - // Frame is used to reference the frame a panic occurred in so that recover() knows if the - // currently executing deferred function is able to recover from the panic. - Frame *Frame - - Stacktrace Stacktrace -} - -func (e Exception) Sprint(m *Machine) string { - return e.Value.Sprint(m) -} - -// UnhandledPanicError represents an error thrown when a panic is not handled in the realm. -type UnhandledPanicError struct { - Descriptor string // Description of the unhandled panic. -} - -func (e UnhandledPanicError) Error() string { - return e.Descriptor -} - //---------------------------------------- // Machine @@ -454,6 +430,51 @@ func (m *Machine) RunFiles(fns ...*FileNode) { m.runInitFromUpdates(pv, updates) } +// PreprocessFiles runs Preprocess on the given files. It is used to detect +// compile-time errors in the package. +func (m *Machine) PreprocessFiles(pkgName, pkgPath string, fset *FileSet, save, withOverrides bool) (*PackageNode, *PackageValue) { + if !withOverrides { + if err := checkDuplicates(fset); err != nil { + panic(fmt.Errorf("running package %q: %w", pkgName, err)) + } + } + pn := NewPackageNode(Name(pkgName), pkgPath, fset) + pv := pn.NewPackage() + pb := pv.GetBlock(m.Store) + m.SetActivePackage(pv) + m.Store.SetBlockNode(pn) + PredefineFileSet(m.Store, pn, fset) + for _, fn := range fset.Files { + fn = Preprocess(m.Store, pn, fn).(*FileNode) + // After preprocessing, save blocknodes to store. + SaveBlockNodes(m.Store, fn) + // Make block for fn. + // Each file for each *PackageValue gets its own file *Block, + // with values copied over from each file's + // *FileNode.StaticBlock. + fb := m.Alloc.NewBlock(fn, pb) + fb.Values = make([]TypedValue, len(fn.StaticBlock.Values)) + copy(fb.Values, fn.StaticBlock.Values) + pv.AddFileBlock(fn.Name, fb) + } + // Get new values across all files in package. + pn.PrepareNewValues(pv) + // save package value. + var throwaway *Realm + if save { + // store new package values and types + throwaway = m.saveNewPackageValuesAndTypes() + if throwaway != nil { + m.Realm = throwaway + } + m.resavePackageValues(throwaway) + if throwaway != nil { + m.Realm = nil + } + } + return pn, pv +} + // Add files to the package's *FileSet and run decls in them. // This will also run each init function encountered. // Returns the updated typed values of package. @@ -2101,8 +2122,11 @@ func (m *Machine) Panic(ex TypedValue) { func (m *Machine) Println(args ...interface{}) { if debug { if enabled { - s := strings.Repeat("|", m.NumOps) - debug.Println(append([]interface{}{s}, args...)...) + _, file, line, _ := runtime.Caller(2) // get caller info + caller := fmt.Sprintf("%-.12s:%-4d", path.Base(file), line) + prefix := fmt.Sprintf("DEBUG: %17s: ", caller) + s := prefix + strings.Repeat("|", m.NumOps) + fmt.Println(append([]interface{}{s}, args...)...) } } } @@ -2110,8 +2134,11 @@ func (m *Machine) Println(args ...interface{}) { func (m *Machine) Printf(format string, args ...interface{}) { if debug { if enabled { - s := strings.Repeat("|", m.NumOps) - debug.Printf(s+" "+format, args...) + _, file, line, _ := runtime.Caller(2) // get caller info + caller := fmt.Sprintf("%-.12s:%-4d", path.Base(file), line) + prefix := fmt.Sprintf("DEBUG: %17s: ", caller) + s := prefix + strings.Repeat("|", m.NumOps) + fmt.Printf(s+" "+format, args...) } } } diff --git a/gnovm/pkg/gnolang/nocompile_on_32bits.go b/gnovm/pkg/gnolang/nocompile_on_32bits.go new file mode 100644 index 00000000000..03cb0855876 --- /dev/null +++ b/gnovm/pkg/gnolang/nocompile_on_32bits.go @@ -0,0 +1,10 @@ +package gnolang + +import "strconv" + +func _() { + // Restricting Gno to compile only on 64-bit architectures. + // Please see https://github.com/gnolang/gno/issues/3288 + var x [1]struct{} + _ = x[strconv.IntSize-64] +} diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 95451f738cc..dbc5b51caea 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -1391,12 +1391,13 @@ func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, e }) } + memPkg.Name = string(pkgName) + // If no .gno files are present, package simply does not exist. if !memPkg.IsEmpty() { if err := validatePkgName(string(pkgName)); err != nil { return nil, err } - memPkg.Name = string(pkgName) } return memPkg, nil diff --git a/gnovm/pkg/gnolang/op_string.go b/gnovm/pkg/gnolang/op_string.go deleted file mode 100644 index b13bb8f278e..00000000000 --- a/gnovm/pkg/gnolang/op_string.go +++ /dev/null @@ -1,178 +0,0 @@ -// Code generated by "stringer -type=Op ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[OpInvalid-0] - _ = x[OpHalt-1] - _ = x[OpNoop-2] - _ = x[OpExec-3] - _ = x[OpPrecall-4] - _ = x[OpCall-5] - _ = x[OpCallNativeBody-6] - _ = x[OpReturn-7] - _ = x[OpReturnFromBlock-8] - _ = x[OpReturnToBlock-9] - _ = x[OpDefer-10] - _ = x[OpCallDeferNativeBody-11] - _ = x[OpGo-12] - _ = x[OpSelect-13] - _ = x[OpSwitchClause-14] - _ = x[OpSwitchClauseCase-15] - _ = x[OpTypeSwitch-16] - _ = x[OpIfCond-17] - _ = x[OpPopValue-18] - _ = x[OpPopResults-19] - _ = x[OpPopBlock-20] - _ = x[OpPopFrameAndReset-21] - _ = x[OpPanic1-22] - _ = x[OpPanic2-23] - _ = x[OpUpos-32] - _ = x[OpUneg-33] - _ = x[OpUnot-34] - _ = x[OpUxor-35] - _ = x[OpUrecv-37] - _ = x[OpLor-38] - _ = x[OpLand-39] - _ = x[OpEql-40] - _ = x[OpNeq-41] - _ = x[OpLss-42] - _ = x[OpLeq-43] - _ = x[OpGtr-44] - _ = x[OpGeq-45] - _ = x[OpAdd-46] - _ = x[OpSub-47] - _ = x[OpBor-48] - _ = x[OpXor-49] - _ = x[OpMul-50] - _ = x[OpQuo-51] - _ = x[OpRem-52] - _ = x[OpShl-53] - _ = x[OpShr-54] - _ = x[OpBand-55] - _ = x[OpBandn-56] - _ = x[OpEval-64] - _ = x[OpBinary1-65] - _ = x[OpIndex1-66] - _ = x[OpIndex2-67] - _ = x[OpSelector-68] - _ = x[OpSlice-69] - _ = x[OpStar-70] - _ = x[OpRef-71] - _ = x[OpTypeAssert1-72] - _ = x[OpTypeAssert2-73] - _ = x[OpStaticTypeOf-74] - _ = x[OpCompositeLit-75] - _ = x[OpArrayLit-76] - _ = x[OpSliceLit-77] - _ = x[OpSliceLit2-78] - _ = x[OpMapLit-79] - _ = x[OpStructLit-80] - _ = x[OpFuncLit-81] - _ = x[OpConvert-82] - _ = x[OpArrayLitGoNative-96] - _ = x[OpSliceLitGoNative-97] - _ = x[OpStructLitGoNative-98] - _ = x[OpCallGoNative-99] - _ = x[OpFieldType-112] - _ = x[OpArrayType-113] - _ = x[OpSliceType-114] - _ = x[OpPointerType-115] - _ = x[OpInterfaceType-116] - _ = x[OpChanType-117] - _ = x[OpFuncType-118] - _ = x[OpMapType-119] - _ = x[OpStructType-120] - _ = x[OpMaybeNativeType-121] - _ = x[OpAssign-128] - _ = x[OpAddAssign-129] - _ = x[OpSubAssign-130] - _ = x[OpMulAssign-131] - _ = x[OpQuoAssign-132] - _ = x[OpRemAssign-133] - _ = x[OpBandAssign-134] - _ = x[OpBandnAssign-135] - _ = x[OpBorAssign-136] - _ = x[OpXorAssign-137] - _ = x[OpShlAssign-138] - _ = x[OpShrAssign-139] - _ = x[OpDefine-140] - _ = x[OpInc-141] - _ = x[OpDec-142] - _ = x[OpValueDecl-144] - _ = x[OpTypeDecl-145] - _ = x[OpSticky-208] - _ = x[OpBody-209] - _ = x[OpForLoop-210] - _ = x[OpRangeIter-211] - _ = x[OpRangeIterString-212] - _ = x[OpRangeIterMap-213] - _ = x[OpRangeIterArrayPtr-214] - _ = x[OpReturnCallDefers-215] - _ = x[OpVoid-255] -} - -const ( - _Op_name_0 = "OpInvalidOpHaltOpNoopOpExecOpPrecallOpCallOpCallNativeBodyOpReturnOpReturnFromBlockOpReturnToBlockOpDeferOpCallDeferNativeBodyOpGoOpSelectOpSwitchClauseOpSwitchClauseCaseOpTypeSwitchOpIfCondOpPopValueOpPopResultsOpPopBlockOpPopFrameAndResetOpPanic1OpPanic2" - _Op_name_1 = "OpUposOpUnegOpUnotOpUxor" - _Op_name_2 = "OpUrecvOpLorOpLandOpEqlOpNeqOpLssOpLeqOpGtrOpGeqOpAddOpSubOpBorOpXorOpMulOpQuoOpRemOpShlOpShrOpBandOpBandn" - _Op_name_3 = "OpEvalOpBinary1OpIndex1OpIndex2OpSelectorOpSliceOpStarOpRefOpTypeAssert1OpTypeAssert2OpStaticTypeOfOpCompositeLitOpArrayLitOpSliceLitOpSliceLit2OpMapLitOpStructLitOpFuncLitOpConvert" - _Op_name_4 = "OpArrayLitGoNativeOpSliceLitGoNativeOpStructLitGoNativeOpCallGoNative" - _Op_name_5 = "OpFieldTypeOpArrayTypeOpSliceTypeOpPointerTypeOpInterfaceTypeOpChanTypeOpFuncTypeOpMapTypeOpStructTypeOpMaybeNativeType" - _Op_name_6 = "OpAssignOpAddAssignOpSubAssignOpMulAssignOpQuoAssignOpRemAssignOpBandAssignOpBandnAssignOpBorAssignOpXorAssignOpShlAssignOpShrAssignOpDefineOpIncOpDec" - _Op_name_7 = "OpValueDeclOpTypeDecl" - _Op_name_8 = "OpStickyOpBodyOpForLoopOpRangeIterOpRangeIterStringOpRangeIterMapOpRangeIterArrayPtrOpReturnCallDefers" - _Op_name_9 = "OpVoid" -) - -var ( - _Op_index_0 = [...]uint16{0, 9, 15, 21, 27, 36, 42, 58, 66, 83, 98, 105, 126, 130, 138, 152, 170, 182, 190, 200, 212, 222, 240, 248, 256} - _Op_index_1 = [...]uint8{0, 6, 12, 18, 24} - _Op_index_2 = [...]uint8{0, 7, 12, 18, 23, 28, 33, 38, 43, 48, 53, 58, 63, 68, 73, 78, 83, 88, 93, 99, 106} - _Op_index_3 = [...]uint8{0, 6, 15, 23, 31, 41, 48, 54, 59, 72, 85, 99, 113, 123, 133, 144, 152, 163, 172, 181} - _Op_index_4 = [...]uint8{0, 18, 36, 55, 69} - _Op_index_5 = [...]uint8{0, 11, 22, 33, 46, 61, 71, 81, 90, 102, 119} - _Op_index_6 = [...]uint8{0, 8, 19, 30, 41, 52, 63, 75, 88, 99, 110, 121, 132, 140, 145, 150} - _Op_index_7 = [...]uint8{0, 11, 21} - _Op_index_8 = [...]uint8{0, 8, 14, 23, 34, 51, 65, 84, 102} -) - -func (i Op) String() string { - switch { - case i <= 23: - return _Op_name_0[_Op_index_0[i]:_Op_index_0[i+1]] - case 32 <= i && i <= 35: - i -= 32 - return _Op_name_1[_Op_index_1[i]:_Op_index_1[i+1]] - case 37 <= i && i <= 56: - i -= 37 - return _Op_name_2[_Op_index_2[i]:_Op_index_2[i+1]] - case 64 <= i && i <= 82: - i -= 64 - return _Op_name_3[_Op_index_3[i]:_Op_index_3[i+1]] - case 96 <= i && i <= 99: - i -= 96 - return _Op_name_4[_Op_index_4[i]:_Op_index_4[i+1]] - case 112 <= i && i <= 121: - i -= 112 - return _Op_name_5[_Op_index_5[i]:_Op_index_5[i+1]] - case 128 <= i && i <= 142: - i -= 128 - return _Op_name_6[_Op_index_6[i]:_Op_index_6[i+1]] - case 144 <= i && i <= 145: - i -= 144 - return _Op_name_7[_Op_index_7[i]:_Op_index_7[i+1]] - case 208 <= i && i <= 215: - i -= 208 - return _Op_name_8[_Op_index_8[i]:_Op_index_8[i+1]] - case i == 255: - return _Op_name_9 - default: - return "Op(" + strconv.FormatInt(int64(i), 10) + ")" - } -} diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index ca5834aa44e..0b86449b235 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -3366,18 +3366,42 @@ func getResultTypedValues(cx *CallExpr) []TypedValue { // NOTE: Generally, conversion happens in a separate step while leaving // composite exprs/nodes that contain constant expression nodes (e.g. const // exprs in the rhs of AssignStmts). +// +// Array-related expressions like `len` and `cap` are manually evaluated +// as constants, even if the array itself is not a constant. This evaluation +// is handled independently of the rest of the constant evaluation process, +// bypassing machine.EvalStatic. func evalConst(store Store, last BlockNode, x Expr) *ConstExpr { // TODO: some check or verification for ensuring x - // is constant? From the machine? - m := NewMachine(".dontcare", store) - m.PreprocessorMode = true + var cx *ConstExpr + if clx, ok := x.(*CallExpr); ok { + t := evalStaticTypeOf(store, last, clx.Args[0]) + if ar, ok := unwrapPointerType(baseOf(t)).(*ArrayType); ok { + fv := clx.Func.(*ConstExpr).V.(*FuncValue) + switch fv.Name { + case "cap", "len": + tv := TypedValue{T: IntType} + tv.SetInt(ar.Len) + cx = &ConstExpr{ + Source: x, + TypedValue: tv, + } + default: + panic(fmt.Sprintf("unexpected const func %s", fv.Name)) + } + } + } - cv := m.EvalStatic(last, x) - m.PreprocessorMode = false - m.Release() - cx := &ConstExpr{ - Source: x, - TypedValue: cv, + if cx == nil { + // is constant? From the machine? + m := NewMachine(".dontcare", store) + cv := m.EvalStatic(last, x) + m.PreprocessorMode = false + m.Release() + cx = &ConstExpr{ + Source: x, + TypedValue: cv, + } } cx.SetLine(x.GetLine()) cx.SetAttribute(ATTR_PREPROCESSED, true) diff --git a/gnovm/pkg/gnolang/string_methods.go b/gnovm/pkg/gnolang/string_methods.go new file mode 100644 index 00000000000..565b5860708 --- /dev/null +++ b/gnovm/pkg/gnolang/string_methods.go @@ -0,0 +1,466 @@ +// Code generated by "stringer -type=Kind,Op,TransCtrl,TransField,VPType,Word -output string_methods.go ."; DO NOT EDIT. + +package gnolang + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[InvalidKind-0] + _ = x[BoolKind-1] + _ = x[StringKind-2] + _ = x[IntKind-3] + _ = x[Int8Kind-4] + _ = x[Int16Kind-5] + _ = x[Int32Kind-6] + _ = x[Int64Kind-7] + _ = x[UintKind-8] + _ = x[Uint8Kind-9] + _ = x[Uint16Kind-10] + _ = x[Uint32Kind-11] + _ = x[Uint64Kind-12] + _ = x[Float32Kind-13] + _ = x[Float64Kind-14] + _ = x[BigintKind-15] + _ = x[BigdecKind-16] + _ = x[ArrayKind-17] + _ = x[SliceKind-18] + _ = x[PointerKind-19] + _ = x[StructKind-20] + _ = x[PackageKind-21] + _ = x[InterfaceKind-22] + _ = x[ChanKind-23] + _ = x[FuncKind-24] + _ = x[MapKind-25] + _ = x[TypeKind-26] + _ = x[BlockKind-27] + _ = x[HeapItemKind-28] + _ = x[TupleKind-29] + _ = x[RefTypeKind-30] +} + +const _Kind_name = "InvalidKindBoolKindStringKindIntKindInt8KindInt16KindInt32KindInt64KindUintKindUint8KindUint16KindUint32KindUint64KindFloat32KindFloat64KindBigintKindBigdecKindArrayKindSliceKindPointerKindStructKindPackageKindInterfaceKindChanKindFuncKindMapKindTypeKindBlockKindHeapItemKindTupleKindRefTypeKind" + +var _Kind_index = [...]uint16{0, 11, 19, 29, 36, 44, 53, 62, 71, 79, 88, 98, 108, 118, 129, 140, 150, 160, 169, 178, 189, 199, 210, 223, 231, 239, 246, 254, 263, 275, 284, 295} + +func (i Kind) String() string { + if i >= Kind(len(_Kind_index)-1) { + return "Kind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Kind_name[_Kind_index[i]:_Kind_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[OpInvalid-0] + _ = x[OpHalt-1] + _ = x[OpNoop-2] + _ = x[OpExec-3] + _ = x[OpPrecall-4] + _ = x[OpCall-5] + _ = x[OpCallNativeBody-6] + _ = x[OpReturn-7] + _ = x[OpReturnFromBlock-8] + _ = x[OpReturnToBlock-9] + _ = x[OpDefer-10] + _ = x[OpCallDeferNativeBody-11] + _ = x[OpGo-12] + _ = x[OpSelect-13] + _ = x[OpSwitchClause-14] + _ = x[OpSwitchClauseCase-15] + _ = x[OpTypeSwitch-16] + _ = x[OpIfCond-17] + _ = x[OpPopValue-18] + _ = x[OpPopResults-19] + _ = x[OpPopBlock-20] + _ = x[OpPopFrameAndReset-21] + _ = x[OpPanic1-22] + _ = x[OpPanic2-23] + _ = x[OpUpos-32] + _ = x[OpUneg-33] + _ = x[OpUnot-34] + _ = x[OpUxor-35] + _ = x[OpUrecv-37] + _ = x[OpLor-38] + _ = x[OpLand-39] + _ = x[OpEql-40] + _ = x[OpNeq-41] + _ = x[OpLss-42] + _ = x[OpLeq-43] + _ = x[OpGtr-44] + _ = x[OpGeq-45] + _ = x[OpAdd-46] + _ = x[OpSub-47] + _ = x[OpBor-48] + _ = x[OpXor-49] + _ = x[OpMul-50] + _ = x[OpQuo-51] + _ = x[OpRem-52] + _ = x[OpShl-53] + _ = x[OpShr-54] + _ = x[OpBand-55] + _ = x[OpBandn-56] + _ = x[OpEval-64] + _ = x[OpBinary1-65] + _ = x[OpIndex1-66] + _ = x[OpIndex2-67] + _ = x[OpSelector-68] + _ = x[OpSlice-69] + _ = x[OpStar-70] + _ = x[OpRef-71] + _ = x[OpTypeAssert1-72] + _ = x[OpTypeAssert2-73] + _ = x[OpStaticTypeOf-74] + _ = x[OpCompositeLit-75] + _ = x[OpArrayLit-76] + _ = x[OpSliceLit-77] + _ = x[OpSliceLit2-78] + _ = x[OpMapLit-79] + _ = x[OpStructLit-80] + _ = x[OpFuncLit-81] + _ = x[OpConvert-82] + _ = x[OpArrayLitGoNative-96] + _ = x[OpSliceLitGoNative-97] + _ = x[OpStructLitGoNative-98] + _ = x[OpCallGoNative-99] + _ = x[OpFieldType-112] + _ = x[OpArrayType-113] + _ = x[OpSliceType-114] + _ = x[OpPointerType-115] + _ = x[OpInterfaceType-116] + _ = x[OpChanType-117] + _ = x[OpFuncType-118] + _ = x[OpMapType-119] + _ = x[OpStructType-120] + _ = x[OpMaybeNativeType-121] + _ = x[OpAssign-128] + _ = x[OpAddAssign-129] + _ = x[OpSubAssign-130] + _ = x[OpMulAssign-131] + _ = x[OpQuoAssign-132] + _ = x[OpRemAssign-133] + _ = x[OpBandAssign-134] + _ = x[OpBandnAssign-135] + _ = x[OpBorAssign-136] + _ = x[OpXorAssign-137] + _ = x[OpShlAssign-138] + _ = x[OpShrAssign-139] + _ = x[OpDefine-140] + _ = x[OpInc-141] + _ = x[OpDec-142] + _ = x[OpValueDecl-144] + _ = x[OpTypeDecl-145] + _ = x[OpSticky-208] + _ = x[OpBody-209] + _ = x[OpForLoop-210] + _ = x[OpRangeIter-211] + _ = x[OpRangeIterString-212] + _ = x[OpRangeIterMap-213] + _ = x[OpRangeIterArrayPtr-214] + _ = x[OpReturnCallDefers-215] + _ = x[OpVoid-255] +} + +const ( + _Op_name_0 = "OpInvalidOpHaltOpNoopOpExecOpPrecallOpCallOpCallNativeBodyOpReturnOpReturnFromBlockOpReturnToBlockOpDeferOpCallDeferNativeBodyOpGoOpSelectOpSwitchClauseOpSwitchClauseCaseOpTypeSwitchOpIfCondOpPopValueOpPopResultsOpPopBlockOpPopFrameAndResetOpPanic1OpPanic2" + _Op_name_1 = "OpUposOpUnegOpUnotOpUxor" + _Op_name_2 = "OpUrecvOpLorOpLandOpEqlOpNeqOpLssOpLeqOpGtrOpGeqOpAddOpSubOpBorOpXorOpMulOpQuoOpRemOpShlOpShrOpBandOpBandn" + _Op_name_3 = "OpEvalOpBinary1OpIndex1OpIndex2OpSelectorOpSliceOpStarOpRefOpTypeAssert1OpTypeAssert2OpStaticTypeOfOpCompositeLitOpArrayLitOpSliceLitOpSliceLit2OpMapLitOpStructLitOpFuncLitOpConvert" + _Op_name_4 = "OpArrayLitGoNativeOpSliceLitGoNativeOpStructLitGoNativeOpCallGoNative" + _Op_name_5 = "OpFieldTypeOpArrayTypeOpSliceTypeOpPointerTypeOpInterfaceTypeOpChanTypeOpFuncTypeOpMapTypeOpStructTypeOpMaybeNativeType" + _Op_name_6 = "OpAssignOpAddAssignOpSubAssignOpMulAssignOpQuoAssignOpRemAssignOpBandAssignOpBandnAssignOpBorAssignOpXorAssignOpShlAssignOpShrAssignOpDefineOpIncOpDec" + _Op_name_7 = "OpValueDeclOpTypeDecl" + _Op_name_8 = "OpStickyOpBodyOpForLoopOpRangeIterOpRangeIterStringOpRangeIterMapOpRangeIterArrayPtrOpReturnCallDefers" + _Op_name_9 = "OpVoid" +) + +var ( + _Op_index_0 = [...]uint16{0, 9, 15, 21, 27, 36, 42, 58, 66, 83, 98, 105, 126, 130, 138, 152, 170, 182, 190, 200, 212, 222, 240, 248, 256} + _Op_index_1 = [...]uint8{0, 6, 12, 18, 24} + _Op_index_2 = [...]uint8{0, 7, 12, 18, 23, 28, 33, 38, 43, 48, 53, 58, 63, 68, 73, 78, 83, 88, 93, 99, 106} + _Op_index_3 = [...]uint8{0, 6, 15, 23, 31, 41, 48, 54, 59, 72, 85, 99, 113, 123, 133, 144, 152, 163, 172, 181} + _Op_index_4 = [...]uint8{0, 18, 36, 55, 69} + _Op_index_5 = [...]uint8{0, 11, 22, 33, 46, 61, 71, 81, 90, 102, 119} + _Op_index_6 = [...]uint8{0, 8, 19, 30, 41, 52, 63, 75, 88, 99, 110, 121, 132, 140, 145, 150} + _Op_index_7 = [...]uint8{0, 11, 21} + _Op_index_8 = [...]uint8{0, 8, 14, 23, 34, 51, 65, 84, 102} +) + +func (i Op) String() string { + switch { + case i <= 23: + return _Op_name_0[_Op_index_0[i]:_Op_index_0[i+1]] + case 32 <= i && i <= 35: + i -= 32 + return _Op_name_1[_Op_index_1[i]:_Op_index_1[i+1]] + case 37 <= i && i <= 56: + i -= 37 + return _Op_name_2[_Op_index_2[i]:_Op_index_2[i+1]] + case 64 <= i && i <= 82: + i -= 64 + return _Op_name_3[_Op_index_3[i]:_Op_index_3[i+1]] + case 96 <= i && i <= 99: + i -= 96 + return _Op_name_4[_Op_index_4[i]:_Op_index_4[i+1]] + case 112 <= i && i <= 121: + i -= 112 + return _Op_name_5[_Op_index_5[i]:_Op_index_5[i+1]] + case 128 <= i && i <= 142: + i -= 128 + return _Op_name_6[_Op_index_6[i]:_Op_index_6[i+1]] + case 144 <= i && i <= 145: + i -= 144 + return _Op_name_7[_Op_index_7[i]:_Op_index_7[i+1]] + case 208 <= i && i <= 215: + i -= 208 + return _Op_name_8[_Op_index_8[i]:_Op_index_8[i+1]] + case i == 255: + return _Op_name_9 + default: + return "Op(" + strconv.FormatInt(int64(i), 10) + ")" + } +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TRANS_CONTINUE-0] + _ = x[TRANS_SKIP-1] + _ = x[TRANS_EXIT-2] +} + +const _TransCtrl_name = "TRANS_CONTINUETRANS_SKIPTRANS_EXIT" + +var _TransCtrl_index = [...]uint8{0, 14, 24, 34} + +func (i TransCtrl) String() string { + if i >= TransCtrl(len(_TransCtrl_index)-1) { + return "TransCtrl(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _TransCtrl_name[_TransCtrl_index[i]:_TransCtrl_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TRANS_ROOT-0] + _ = x[TRANS_BINARY_LEFT-1] + _ = x[TRANS_BINARY_RIGHT-2] + _ = x[TRANS_CALL_FUNC-3] + _ = x[TRANS_CALL_ARG-4] + _ = x[TRANS_INDEX_X-5] + _ = x[TRANS_INDEX_INDEX-6] + _ = x[TRANS_SELECTOR_X-7] + _ = x[TRANS_SLICE_X-8] + _ = x[TRANS_SLICE_LOW-9] + _ = x[TRANS_SLICE_HIGH-10] + _ = x[TRANS_SLICE_MAX-11] + _ = x[TRANS_STAR_X-12] + _ = x[TRANS_REF_X-13] + _ = x[TRANS_TYPEASSERT_X-14] + _ = x[TRANS_TYPEASSERT_TYPE-15] + _ = x[TRANS_UNARY_X-16] + _ = x[TRANS_COMPOSITE_TYPE-17] + _ = x[TRANS_COMPOSITE_KEY-18] + _ = x[TRANS_COMPOSITE_VALUE-19] + _ = x[TRANS_FUNCLIT_TYPE-20] + _ = x[TRANS_FUNCLIT_HEAP_CAPTURE-21] + _ = x[TRANS_FUNCLIT_BODY-22] + _ = x[TRANS_FIELDTYPE_TYPE-23] + _ = x[TRANS_FIELDTYPE_TAG-24] + _ = x[TRANS_ARRAYTYPE_LEN-25] + _ = x[TRANS_ARRAYTYPE_ELT-26] + _ = x[TRANS_SLICETYPE_ELT-27] + _ = x[TRANS_INTERFACETYPE_METHOD-28] + _ = x[TRANS_CHANTYPE_VALUE-29] + _ = x[TRANS_FUNCTYPE_PARAM-30] + _ = x[TRANS_FUNCTYPE_RESULT-31] + _ = x[TRANS_MAPTYPE_KEY-32] + _ = x[TRANS_MAPTYPE_VALUE-33] + _ = x[TRANS_STRUCTTYPE_FIELD-34] + _ = x[TRANS_MAYBENATIVETYPE_TYPE-35] + _ = x[TRANS_ASSIGN_LHS-36] + _ = x[TRANS_ASSIGN_RHS-37] + _ = x[TRANS_BLOCK_BODY-38] + _ = x[TRANS_DECL_BODY-39] + _ = x[TRANS_DEFER_CALL-40] + _ = x[TRANS_EXPR_X-41] + _ = x[TRANS_FOR_INIT-42] + _ = x[TRANS_FOR_COND-43] + _ = x[TRANS_FOR_POST-44] + _ = x[TRANS_FOR_BODY-45] + _ = x[TRANS_GO_CALL-46] + _ = x[TRANS_IF_INIT-47] + _ = x[TRANS_IF_COND-48] + _ = x[TRANS_IF_BODY-49] + _ = x[TRANS_IF_ELSE-50] + _ = x[TRANS_IF_CASE_BODY-51] + _ = x[TRANS_INCDEC_X-52] + _ = x[TRANS_RANGE_X-53] + _ = x[TRANS_RANGE_KEY-54] + _ = x[TRANS_RANGE_VALUE-55] + _ = x[TRANS_RANGE_BODY-56] + _ = x[TRANS_RETURN_RESULT-57] + _ = x[TRANS_PANIC_EXCEPTION-58] + _ = x[TRANS_SELECT_CASE-59] + _ = x[TRANS_SELECTCASE_COMM-60] + _ = x[TRANS_SELECTCASE_BODY-61] + _ = x[TRANS_SEND_CHAN-62] + _ = x[TRANS_SEND_VALUE-63] + _ = x[TRANS_SWITCH_INIT-64] + _ = x[TRANS_SWITCH_X-65] + _ = x[TRANS_SWITCH_CASE-66] + _ = x[TRANS_SWITCHCASE_CASE-67] + _ = x[TRANS_SWITCHCASE_BODY-68] + _ = x[TRANS_FUNC_RECV-69] + _ = x[TRANS_FUNC_TYPE-70] + _ = x[TRANS_FUNC_BODY-71] + _ = x[TRANS_IMPORT_PATH-72] + _ = x[TRANS_CONST_TYPE-73] + _ = x[TRANS_CONST_VALUE-74] + _ = x[TRANS_VAR_NAME-75] + _ = x[TRANS_VAR_TYPE-76] + _ = x[TRANS_VAR_VALUE-77] + _ = x[TRANS_TYPE_TYPE-78] + _ = x[TRANS_FILE_BODY-79] +} + +const _TransField_name = "TRANS_ROOTTRANS_BINARY_LEFTTRANS_BINARY_RIGHTTRANS_CALL_FUNCTRANS_CALL_ARGTRANS_INDEX_XTRANS_INDEX_INDEXTRANS_SELECTOR_XTRANS_SLICE_XTRANS_SLICE_LOWTRANS_SLICE_HIGHTRANS_SLICE_MAXTRANS_STAR_XTRANS_REF_XTRANS_TYPEASSERT_XTRANS_TYPEASSERT_TYPETRANS_UNARY_XTRANS_COMPOSITE_TYPETRANS_COMPOSITE_KEYTRANS_COMPOSITE_VALUETRANS_FUNCLIT_TYPETRANS_FUNCLIT_HEAP_CAPTURETRANS_FUNCLIT_BODYTRANS_FIELDTYPE_TYPETRANS_FIELDTYPE_TAGTRANS_ARRAYTYPE_LENTRANS_ARRAYTYPE_ELTTRANS_SLICETYPE_ELTTRANS_INTERFACETYPE_METHODTRANS_CHANTYPE_VALUETRANS_FUNCTYPE_PARAMTRANS_FUNCTYPE_RESULTTRANS_MAPTYPE_KEYTRANS_MAPTYPE_VALUETRANS_STRUCTTYPE_FIELDTRANS_MAYBENATIVETYPE_TYPETRANS_ASSIGN_LHSTRANS_ASSIGN_RHSTRANS_BLOCK_BODYTRANS_DECL_BODYTRANS_DEFER_CALLTRANS_EXPR_XTRANS_FOR_INITTRANS_FOR_CONDTRANS_FOR_POSTTRANS_FOR_BODYTRANS_GO_CALLTRANS_IF_INITTRANS_IF_CONDTRANS_IF_BODYTRANS_IF_ELSETRANS_IF_CASE_BODYTRANS_INCDEC_XTRANS_RANGE_XTRANS_RANGE_KEYTRANS_RANGE_VALUETRANS_RANGE_BODYTRANS_RETURN_RESULTTRANS_PANIC_EXCEPTIONTRANS_SELECT_CASETRANS_SELECTCASE_COMMTRANS_SELECTCASE_BODYTRANS_SEND_CHANTRANS_SEND_VALUETRANS_SWITCH_INITTRANS_SWITCH_XTRANS_SWITCH_CASETRANS_SWITCHCASE_CASETRANS_SWITCHCASE_BODYTRANS_FUNC_RECVTRANS_FUNC_TYPETRANS_FUNC_BODYTRANS_IMPORT_PATHTRANS_CONST_TYPETRANS_CONST_VALUETRANS_VAR_NAMETRANS_VAR_TYPETRANS_VAR_VALUETRANS_TYPE_TYPETRANS_FILE_BODY" + +var _TransField_index = [...]uint16{0, 10, 27, 45, 60, 74, 87, 104, 120, 133, 148, 164, 179, 191, 202, 220, 241, 254, 274, 293, 314, 332, 358, 376, 396, 415, 434, 453, 472, 498, 518, 538, 559, 576, 595, 617, 643, 659, 675, 691, 706, 722, 734, 748, 762, 776, 790, 803, 816, 829, 842, 855, 873, 887, 900, 915, 932, 948, 967, 988, 1005, 1026, 1047, 1062, 1078, 1095, 1109, 1126, 1147, 1168, 1183, 1198, 1213, 1230, 1246, 1263, 1277, 1291, 1306, 1321, 1336} + +func (i TransField) String() string { + if i >= TransField(len(_TransField_index)-1) { + return "TransField(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _TransField_name[_TransField_index[i]:_TransField_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[VPUverse-0] + _ = x[VPBlock-1] + _ = x[VPField-2] + _ = x[VPValMethod-3] + _ = x[VPPtrMethod-4] + _ = x[VPInterface-5] + _ = x[VPSubrefField-6] + _ = x[VPDerefField-18] + _ = x[VPDerefValMethod-19] + _ = x[VPDerefPtrMethod-20] + _ = x[VPDerefInterface-21] + _ = x[VPNative-32] +} + +const ( + _VPType_name_0 = "VPUverseVPBlockVPFieldVPValMethodVPPtrMethodVPInterfaceVPSubrefField" + _VPType_name_1 = "VPDerefFieldVPDerefValMethodVPDerefPtrMethodVPDerefInterface" + _VPType_name_2 = "VPNative" +) + +var ( + _VPType_index_0 = [...]uint8{0, 8, 15, 22, 33, 44, 55, 68} + _VPType_index_1 = [...]uint8{0, 12, 28, 44, 60} +) + +func (i VPType) String() string { + switch { + case i <= 6: + return _VPType_name_0[_VPType_index_0[i]:_VPType_index_0[i+1]] + case 18 <= i && i <= 21: + i -= 18 + return _VPType_name_1[_VPType_index_1[i]:_VPType_index_1[i+1]] + case i == 32: + return _VPType_name_2 + default: + return "VPType(" + strconv.FormatInt(int64(i), 10) + ")" + } +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ILLEGAL-0] + _ = x[NAME-1] + _ = x[INT-2] + _ = x[FLOAT-3] + _ = x[IMAG-4] + _ = x[CHAR-5] + _ = x[STRING-6] + _ = x[ADD-7] + _ = x[SUB-8] + _ = x[MUL-9] + _ = x[QUO-10] + _ = x[REM-11] + _ = x[BAND-12] + _ = x[BOR-13] + _ = x[XOR-14] + _ = x[SHL-15] + _ = x[SHR-16] + _ = x[BAND_NOT-17] + _ = x[ADD_ASSIGN-18] + _ = x[SUB_ASSIGN-19] + _ = x[MUL_ASSIGN-20] + _ = x[QUO_ASSIGN-21] + _ = x[REM_ASSIGN-22] + _ = x[BAND_ASSIGN-23] + _ = x[BOR_ASSIGN-24] + _ = x[XOR_ASSIGN-25] + _ = x[SHL_ASSIGN-26] + _ = x[SHR_ASSIGN-27] + _ = x[BAND_NOT_ASSIGN-28] + _ = x[LAND-29] + _ = x[LOR-30] + _ = x[ARROW-31] + _ = x[INC-32] + _ = x[DEC-33] + _ = x[EQL-34] + _ = x[LSS-35] + _ = x[GTR-36] + _ = x[ASSIGN-37] + _ = x[NOT-38] + _ = x[NEQ-39] + _ = x[LEQ-40] + _ = x[GEQ-41] + _ = x[DEFINE-42] + _ = x[BREAK-43] + _ = x[CASE-44] + _ = x[CHAN-45] + _ = x[CONST-46] + _ = x[CONTINUE-47] + _ = x[DEFAULT-48] + _ = x[DEFER-49] + _ = x[ELSE-50] + _ = x[FALLTHROUGH-51] + _ = x[FOR-52] + _ = x[FUNC-53] + _ = x[GO-54] + _ = x[GOTO-55] + _ = x[IF-56] + _ = x[IMPORT-57] + _ = x[INTERFACE-58] + _ = x[MAP-59] + _ = x[PACKAGE-60] + _ = x[RANGE-61] + _ = x[RETURN-62] + _ = x[SELECT-63] + _ = x[STRUCT-64] + _ = x[SWITCH-65] + _ = x[TYPE-66] + _ = x[VAR-67] +} + +const _Word_name = "ILLEGALNAMEINTFLOATIMAGCHARSTRINGADDSUBMULQUOREMBANDBORXORSHLSHRBAND_NOTADD_ASSIGNSUB_ASSIGNMUL_ASSIGNQUO_ASSIGNREM_ASSIGNBAND_ASSIGNBOR_ASSIGNXOR_ASSIGNSHL_ASSIGNSHR_ASSIGNBAND_NOT_ASSIGNLANDLORARROWINCDECEQLLSSGTRASSIGNNOTNEQLEQGEQDEFINEBREAKCASECHANCONSTCONTINUEDEFAULTDEFERELSEFALLTHROUGHFORFUNCGOGOTOIFIMPORTINTERFACEMAPPACKAGERANGERETURNSELECTSTRUCTSWITCHTYPEVAR" + +var _Word_index = [...]uint16{0, 7, 11, 14, 19, 23, 27, 33, 36, 39, 42, 45, 48, 52, 55, 58, 61, 64, 72, 82, 92, 102, 112, 122, 133, 143, 153, 163, 173, 188, 192, 195, 200, 203, 206, 209, 212, 215, 221, 224, 227, 230, 233, 239, 244, 248, 252, 257, 265, 272, 277, 281, 292, 295, 299, 301, 305, 307, 313, 322, 325, 332, 337, 343, 349, 355, 361, 365, 368} + +func (i Word) String() string { + if i < 0 || i >= Word(len(_Word_index)-1) { + return "Word(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Word_name[_Word_index[i]:_Word_index[i+1]] +} diff --git a/gnovm/pkg/gnolang/transcribe.go b/gnovm/pkg/gnolang/transcribe.go index dab539a8707..572810e9668 100644 --- a/gnovm/pkg/gnolang/transcribe.go +++ b/gnovm/pkg/gnolang/transcribe.go @@ -14,7 +14,6 @@ type ( const ( TRANS_CONTINUE TransCtrl = iota TRANS_SKIP - TRANS_BREAK TRANS_EXIT ) @@ -101,7 +100,7 @@ const ( TRANS_IMPORT_PATH TRANS_CONST_TYPE TRANS_CONST_VALUE - TRANS_VAR_NAME // XXX stringer + TRANS_VAR_NAME TRANS_VAR_TYPE TRANS_VAR_VALUE TRANS_TYPE_TYPE @@ -113,8 +112,6 @@ const ( // - TRANS_SKIP to break out of the // ENTER,CHILDS1,[BLOCK,CHILDS2]?,LEAVE sequence for that node, // i.e. skipping (the rest of) it; -// - TRANS_BREAK to break out of looping in CHILDS1 or CHILDS2, -// but still perform TRANS_LEAVE. // - TRANS_EXIT to stop traversing altogether. // // Do not mutate ns. @@ -168,9 +165,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Args { cnn.Args[idx] = transcribe(t, nns, TRANS_CALL_ARG, idx, cnn.Args[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -248,16 +243,12 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc k, v := kvx.Key, kvx.Value if k != nil { k = transcribe(t, nns, TRANS_COMPOSITE_KEY, idx, k, &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } v = transcribe(t, nns, TRANS_COMPOSITE_VALUE, idx, v, &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } cnn.Elts[idx] = KeyValueExpr{Key: k, Value: v} @@ -269,9 +260,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.HeapCaptures { cnn.HeapCaptures[idx] = *(transcribe(t, nns, TRANS_FUNCLIT_HEAP_CAPTURE, idx, &cnn.HeapCaptures[idx], &c).(*NameExpr)) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -285,9 +274,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_FUNCLIT_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -321,9 +308,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc case *InterfaceTypeExpr: for idx := range cnn.Methods { cnn.Methods[idx] = *transcribe(t, nns, TRANS_INTERFACETYPE_METHOD, idx, &cnn.Methods[idx], &c).(*FieldTypeExpr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -341,9 +326,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Results { cnn.Results[idx] = *transcribe(t, nns, TRANS_FUNCTYPE_RESULT, idx, &cnn.Results[idx], &c).(*FieldTypeExpr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -359,9 +342,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc case *StructTypeExpr: for idx := range cnn.Fields { cnn.Fields[idx] = *transcribe(t, nns, TRANS_STRUCTTYPE_FIELD, idx, &cnn.Fields[idx], &c).(*FieldTypeExpr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -373,17 +354,13 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc case *AssignStmt: for idx := range cnn.Lhs { cnn.Lhs[idx] = transcribe(t, nns, TRANS_ASSIGN_LHS, idx, cnn.Lhs[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } for idx := range cnn.Rhs { cnn.Rhs[idx] = transcribe(t, nns, TRANS_ASSIGN_RHS, idx, cnn.Rhs[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -398,9 +375,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_BLOCK_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -409,9 +384,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_DECL_BODY, idx, cnn.Body[idx], &c).(SimpleDeclStmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -455,9 +428,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_FOR_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -506,9 +477,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_IF_CASE_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -544,18 +513,14 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_RANGE_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } case *ReturnStmt: for idx := range cnn.Results { cnn.Results[idx] = transcribe(t, nns, TRANS_RETURN_RESULT, idx, cnn.Results[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -564,9 +529,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc case *SelectStmt: for idx := range cnn.Cases { cnn.Cases[idx] = *transcribe(t, nns, TRANS_SELECT_CASE, idx, &cnn.Cases[idx], &c).(*SelectCaseStmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -585,9 +548,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_SELECTCASE_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -632,9 +593,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Clauses { cnn.Clauses[idx] = *transcribe(t, nns, TRANS_SWITCH_CASE, idx, &cnn.Clauses[idx], &c).(*SwitchClauseStmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -652,18 +611,14 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Cases { cnn.Cases[idx] = transcribe(t, nns, TRANS_SWITCHCASE_CASE, idx, cnn.Cases[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_SWITCHCASE_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -688,9 +643,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc // iterate over Body; its length can change if a statement is decomposed. for idx := 0; idx < len(cnn.Body); idx++ { cnn.Body[idx] = transcribe(t, nns, TRANS_FUNC_BODY, idx, cnn.Body[idx], &c).(Stmt) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -709,9 +662,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Values { cnn.Values[idx] = transcribe(t, nns, TRANS_VAR_VALUE, idx, cnn.Values[idx], &c).(Expr) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -730,9 +681,7 @@ func transcribe(t Transform, ns []Node, ftype TransField, index int, n Node, nc } for idx := range cnn.Decls { cnn.Decls[idx] = transcribe(t, nns, TRANS_FILE_BODY, idx, cnn.Decls[idx], &c).(Decl) - if isBreak(c) { - break - } else if isStopOrSkip(nc, c) { + if isStopOrSkip(nc, c) { return } } @@ -768,12 +717,3 @@ func isStopOrSkip(oldnc *TransCtrl, nc TransCtrl) (stop bool) { panic("should not happen") } } - -// returns true if transcribe() should break (a loop). -func isBreak(nc TransCtrl) (brek bool) { - if nc == TRANS_BREAK { - return true - } else { - return false - } -} diff --git a/gnovm/pkg/gnolang/transctrl_string.go b/gnovm/pkg/gnolang/transctrl_string.go deleted file mode 100644 index 92d33c65da5..00000000000 --- a/gnovm/pkg/gnolang/transctrl_string.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by "stringer -type=TransCtrl ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[TRANS_CONTINUE-0] - _ = x[TRANS_SKIP-1] - _ = x[TRANS_BREAK-2] - _ = x[TRANS_EXIT-3] -} - -const _TransCtrl_name = "TRANS_CONTINUETRANS_SKIPTRANS_BREAKTRANS_EXIT" - -var _TransCtrl_index = [...]uint8{0, 14, 24, 35, 45} - -func (i TransCtrl) String() string { - if i >= TransCtrl(len(_TransCtrl_index)-1) { - return "TransCtrl(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _TransCtrl_name[_TransCtrl_index[i]:_TransCtrl_index[i+1]] -} diff --git a/gnovm/pkg/gnolang/transfield_string.go b/gnovm/pkg/gnolang/transfield_string.go deleted file mode 100644 index 31afcf2be0d..00000000000 --- a/gnovm/pkg/gnolang/transfield_string.go +++ /dev/null @@ -1,102 +0,0 @@ -// Code generated by "stringer -type=TransField ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[TRANS_ROOT-0] - _ = x[TRANS_BINARY_LEFT-1] - _ = x[TRANS_BINARY_RIGHT-2] - _ = x[TRANS_CALL_FUNC-3] - _ = x[TRANS_CALL_ARG-4] - _ = x[TRANS_INDEX_X-5] - _ = x[TRANS_INDEX_INDEX-6] - _ = x[TRANS_SELECTOR_X-7] - _ = x[TRANS_SLICE_X-8] - _ = x[TRANS_SLICE_LOW-9] - _ = x[TRANS_SLICE_HIGH-10] - _ = x[TRANS_SLICE_MAX-11] - _ = x[TRANS_STAR_X-12] - _ = x[TRANS_REF_X-13] - _ = x[TRANS_TYPEASSERT_X-14] - _ = x[TRANS_TYPEASSERT_TYPE-15] - _ = x[TRANS_UNARY_X-16] - _ = x[TRANS_COMPOSITE_TYPE-17] - _ = x[TRANS_COMPOSITE_KEY-18] - _ = x[TRANS_COMPOSITE_VALUE-19] - _ = x[TRANS_FUNCLIT_TYPE-20] - _ = x[TRANS_FUNCLIT_HEAP_CAPTURE-21] - _ = x[TRANS_FUNCLIT_BODY-22] - _ = x[TRANS_FIELDTYPE_TYPE-23] - _ = x[TRANS_FIELDTYPE_TAG-24] - _ = x[TRANS_ARRAYTYPE_LEN-25] - _ = x[TRANS_ARRAYTYPE_ELT-26] - _ = x[TRANS_SLICETYPE_ELT-27] - _ = x[TRANS_INTERFACETYPE_METHOD-28] - _ = x[TRANS_CHANTYPE_VALUE-29] - _ = x[TRANS_FUNCTYPE_PARAM-30] - _ = x[TRANS_FUNCTYPE_RESULT-31] - _ = x[TRANS_MAPTYPE_KEY-32] - _ = x[TRANS_MAPTYPE_VALUE-33] - _ = x[TRANS_STRUCTTYPE_FIELD-34] - _ = x[TRANS_MAYBENATIVETYPE_TYPE-35] - _ = x[TRANS_ASSIGN_LHS-36] - _ = x[TRANS_ASSIGN_RHS-37] - _ = x[TRANS_BLOCK_BODY-38] - _ = x[TRANS_DECL_BODY-39] - _ = x[TRANS_DEFER_CALL-40] - _ = x[TRANS_EXPR_X-41] - _ = x[TRANS_FOR_INIT-42] - _ = x[TRANS_FOR_COND-43] - _ = x[TRANS_FOR_POST-44] - _ = x[TRANS_FOR_BODY-45] - _ = x[TRANS_GO_CALL-46] - _ = x[TRANS_IF_INIT-47] - _ = x[TRANS_IF_COND-48] - _ = x[TRANS_IF_BODY-49] - _ = x[TRANS_IF_ELSE-50] - _ = x[TRANS_IF_CASE_BODY-51] - _ = x[TRANS_INCDEC_X-52] - _ = x[TRANS_RANGE_X-53] - _ = x[TRANS_RANGE_KEY-54] - _ = x[TRANS_RANGE_VALUE-55] - _ = x[TRANS_RANGE_BODY-56] - _ = x[TRANS_RETURN_RESULT-57] - _ = x[TRANS_PANIC_EXCEPTION-58] - _ = x[TRANS_SELECT_CASE-59] - _ = x[TRANS_SELECTCASE_COMM-60] - _ = x[TRANS_SELECTCASE_BODY-61] - _ = x[TRANS_SEND_CHAN-62] - _ = x[TRANS_SEND_VALUE-63] - _ = x[TRANS_SWITCH_INIT-64] - _ = x[TRANS_SWITCH_X-65] - _ = x[TRANS_SWITCH_CASE-66] - _ = x[TRANS_SWITCHCASE_CASE-67] - _ = x[TRANS_SWITCHCASE_BODY-68] - _ = x[TRANS_FUNC_RECV-69] - _ = x[TRANS_FUNC_TYPE-70] - _ = x[TRANS_FUNC_BODY-71] - _ = x[TRANS_IMPORT_PATH-72] - _ = x[TRANS_CONST_TYPE-73] - _ = x[TRANS_CONST_VALUE-74] - _ = x[TRANS_VAR_NAME-75] - _ = x[TRANS_VAR_TYPE-76] - _ = x[TRANS_VAR_VALUE-77] - _ = x[TRANS_TYPE_TYPE-78] - _ = x[TRANS_FILE_BODY-79] -} - -const _TransField_name = "TRANS_ROOTTRANS_BINARY_LEFTTRANS_BINARY_RIGHTTRANS_CALL_FUNCTRANS_CALL_ARGTRANS_INDEX_XTRANS_INDEX_INDEXTRANS_SELECTOR_XTRANS_SLICE_XTRANS_SLICE_LOWTRANS_SLICE_HIGHTRANS_SLICE_MAXTRANS_STAR_XTRANS_REF_XTRANS_TYPEASSERT_XTRANS_TYPEASSERT_TYPETRANS_UNARY_XTRANS_COMPOSITE_TYPETRANS_COMPOSITE_KEYTRANS_COMPOSITE_VALUETRANS_FUNCLIT_TYPETRANS_FUNCLIT_HEAP_CAPTURETRANS_FUNCLIT_BODYTRANS_FIELDTYPE_TYPETRANS_FIELDTYPE_TAGTRANS_ARRAYTYPE_LENTRANS_ARRAYTYPE_ELTTRANS_SLICETYPE_ELTTRANS_INTERFACETYPE_METHODTRANS_CHANTYPE_VALUETRANS_FUNCTYPE_PARAMTRANS_FUNCTYPE_RESULTTRANS_MAPTYPE_KEYTRANS_MAPTYPE_VALUETRANS_STRUCTTYPE_FIELDTRANS_MAYBENATIVETYPE_TYPETRANS_ASSIGN_LHSTRANS_ASSIGN_RHSTRANS_BLOCK_BODYTRANS_DECL_BODYTRANS_DEFER_CALLTRANS_EXPR_XTRANS_FOR_INITTRANS_FOR_CONDTRANS_FOR_POSTTRANS_FOR_BODYTRANS_GO_CALLTRANS_IF_INITTRANS_IF_CONDTRANS_IF_BODYTRANS_IF_ELSETRANS_IF_CASE_BODYTRANS_INCDEC_XTRANS_RANGE_XTRANS_RANGE_KEYTRANS_RANGE_VALUETRANS_RANGE_BODYTRANS_RETURN_RESULTTRANS_PANIC_EXCEPTIONTRANS_SELECT_CASETRANS_SELECTCASE_COMMTRANS_SELECTCASE_BODYTRANS_SEND_CHANTRANS_SEND_VALUETRANS_SWITCH_INITTRANS_SWITCH_XTRANS_SWITCH_CASETRANS_SWITCHCASE_CASETRANS_SWITCHCASE_BODYTRANS_FUNC_RECVTRANS_FUNC_TYPETRANS_FUNC_BODYTRANS_IMPORT_PATHTRANS_CONST_TYPETRANS_CONST_VALUETRANS_VAR_NAMETRANS_VAR_TYPETRANS_VAR_VALUETRANS_TYPE_TYPETRANS_FILE_BODY" - -var _TransField_index = [...]uint16{0, 10, 27, 45, 60, 74, 87, 104, 120, 133, 148, 164, 179, 191, 202, 220, 241, 254, 274, 293, 314, 332, 358, 376, 396, 415, 434, 453, 472, 498, 518, 538, 559, 576, 595, 617, 643, 659, 675, 691, 706, 722, 734, 748, 762, 776, 790, 803, 816, 829, 842, 855, 873, 887, 900, 915, 932, 948, 967, 988, 1005, 1026, 1047, 1062, 1078, 1095, 1109, 1126, 1147, 1168, 1183, 1198, 1213, 1230, 1246, 1263, 1277, 1291, 1306, 1321, 1336} - -func (i TransField) String() string { - if i >= TransField(len(_TransField_index)-1) { - return "TransField(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _TransField_name[_TransField_index[i]:_TransField_index[i+1]] -} diff --git a/gnovm/pkg/gnolang/type_check.go b/gnovm/pkg/gnolang/type_check.go index f96cb71e4b6..a79e9c43ecc 100644 --- a/gnovm/pkg/gnolang/type_check.go +++ b/gnovm/pkg/gnolang/type_check.go @@ -270,7 +270,7 @@ Main: switch { case fv.Name == "len": at := evalStaticTypeOf(store, last, currExpr.Args[0]) - if _, ok := baseOf(at).(*ArrayType); ok { + if _, ok := unwrapPointerType(baseOf(at)).(*ArrayType); ok { // ok break Main } @@ -278,7 +278,7 @@ Main: break Main case fv.Name == "cap": at := evalStaticTypeOf(store, last, currExpr.Args[0]) - if _, ok := baseOf(at).(*ArrayType); ok { + if _, ok := unwrapPointerType(baseOf(at)).(*ArrayType); ok { // ok break Main } diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index 374ac6d9150..8ac07162f10 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -1469,6 +1469,13 @@ func baseOf(t Type) Type { } } +func unwrapPointerType(t Type) Type { + if pt, ok := t.(*PointerType); ok { + return pt.Elem() + } + return t +} + // NOTE: it may be faster to switch on baseOf(). func (dt *DeclaredType) Kind() Kind { return dt.Base.Kind() diff --git a/gnovm/pkg/gnolang/vptype_string.go b/gnovm/pkg/gnolang/vptype_string.go deleted file mode 100644 index 62a51c3b256..00000000000 --- a/gnovm/pkg/gnolang/vptype_string.go +++ /dev/null @@ -1,48 +0,0 @@ -// Code generated by "stringer -type=VPType ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[VPUverse-0] - _ = x[VPBlock-1] - _ = x[VPField-2] - _ = x[VPValMethod-3] - _ = x[VPPtrMethod-4] - _ = x[VPInterface-5] - _ = x[VPSubrefField-6] - _ = x[VPDerefField-18] - _ = x[VPDerefValMethod-19] - _ = x[VPDerefPtrMethod-20] - _ = x[VPDerefInterface-21] - _ = x[VPNative-32] -} - -const ( - _VPType_name_0 = "VPUverseVPBlockVPFieldVPValMethodVPPtrMethodVPInterfaceVPSubrefField" - _VPType_name_1 = "VPDerefFieldVPDerefValMethodVPDerefPtrMethodVPDerefInterface" - _VPType_name_2 = "VPNative" -) - -var ( - _VPType_index_0 = [...]uint8{0, 8, 15, 22, 33, 44, 55, 68} - _VPType_index_1 = [...]uint8{0, 12, 28, 44, 60} -) - -func (i VPType) String() string { - switch { - case i <= 6: - return _VPType_name_0[_VPType_index_0[i]:_VPType_index_0[i+1]] - case 18 <= i && i <= 21: - i -= 18 - return _VPType_name_1[_VPType_index_1[i]:_VPType_index_1[i+1]] - case i == 32: - return _VPType_name_2 - default: - return "VPType(" + strconv.FormatInt(int64(i), 10) + ")" - } -} diff --git a/gnovm/pkg/gnolang/word_string.go b/gnovm/pkg/gnolang/word_string.go deleted file mode 100644 index da5fc3d7412..00000000000 --- a/gnovm/pkg/gnolang/word_string.go +++ /dev/null @@ -1,90 +0,0 @@ -// Code generated by "stringer -type=Word ./pkg/gnolang"; DO NOT EDIT. - -package gnolang - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[ILLEGAL-0] - _ = x[NAME-1] - _ = x[INT-2] - _ = x[FLOAT-3] - _ = x[IMAG-4] - _ = x[CHAR-5] - _ = x[STRING-6] - _ = x[ADD-7] - _ = x[SUB-8] - _ = x[MUL-9] - _ = x[QUO-10] - _ = x[REM-11] - _ = x[BAND-12] - _ = x[BOR-13] - _ = x[XOR-14] - _ = x[SHL-15] - _ = x[SHR-16] - _ = x[BAND_NOT-17] - _ = x[ADD_ASSIGN-18] - _ = x[SUB_ASSIGN-19] - _ = x[MUL_ASSIGN-20] - _ = x[QUO_ASSIGN-21] - _ = x[REM_ASSIGN-22] - _ = x[BAND_ASSIGN-23] - _ = x[BOR_ASSIGN-24] - _ = x[XOR_ASSIGN-25] - _ = x[SHL_ASSIGN-26] - _ = x[SHR_ASSIGN-27] - _ = x[BAND_NOT_ASSIGN-28] - _ = x[LAND-29] - _ = x[LOR-30] - _ = x[ARROW-31] - _ = x[INC-32] - _ = x[DEC-33] - _ = x[EQL-34] - _ = x[LSS-35] - _ = x[GTR-36] - _ = x[ASSIGN-37] - _ = x[NOT-38] - _ = x[NEQ-39] - _ = x[LEQ-40] - _ = x[GEQ-41] - _ = x[DEFINE-42] - _ = x[BREAK-43] - _ = x[CASE-44] - _ = x[CHAN-45] - _ = x[CONST-46] - _ = x[CONTINUE-47] - _ = x[DEFAULT-48] - _ = x[DEFER-49] - _ = x[ELSE-50] - _ = x[FALLTHROUGH-51] - _ = x[FOR-52] - _ = x[FUNC-53] - _ = x[GO-54] - _ = x[GOTO-55] - _ = x[IF-56] - _ = x[IMPORT-57] - _ = x[INTERFACE-58] - _ = x[MAP-59] - _ = x[PACKAGE-60] - _ = x[RANGE-61] - _ = x[RETURN-62] - _ = x[SELECT-63] - _ = x[STRUCT-64] - _ = x[SWITCH-65] - _ = x[TYPE-66] - _ = x[VAR-67] -} - -const _Word_name = "ILLEGALNAMEINTFLOATIMAGCHARSTRINGADDSUBMULQUOREMBANDBORXORSHLSHRBAND_NOTADD_ASSIGNSUB_ASSIGNMUL_ASSIGNQUO_ASSIGNREM_ASSIGNBAND_ASSIGNBOR_ASSIGNXOR_ASSIGNSHL_ASSIGNSHR_ASSIGNBAND_NOT_ASSIGNLANDLORARROWINCDECEQLLSSGTRASSIGNNOTNEQLEQGEQDEFINEBREAKCASECHANCONSTCONTINUEDEFAULTDEFERELSEFALLTHROUGHFORFUNCGOGOTOIFIMPORTINTERFACEMAPPACKAGERANGERETURNSELECTSTRUCTSWITCHTYPEVAR" - -var _Word_index = [...]uint16{0, 7, 11, 14, 19, 23, 27, 33, 36, 39, 42, 45, 48, 52, 55, 58, 61, 64, 72, 82, 92, 102, 112, 122, 133, 143, 153, 163, 173, 188, 192, 195, 200, 203, 206, 209, 212, 215, 221, 224, 227, 230, 233, 239, 244, 248, 252, 257, 265, 272, 277, 281, 292, 295, 299, 301, 305, 307, 313, 322, 325, 332, 337, 343, 349, 355, 361, 365, 368} - -func (i Word) String() string { - if i < 0 || i >= Word(len(_Word_index)-1) { - return "Word(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _Word_name[_Word_index[i]:_Word_index[i+1]] -} diff --git a/gnovm/pkg/repl/repl.go b/gnovm/pkg/repl/repl.go index b0944d21646..fff80d672dc 100644 --- a/gnovm/pkg/repl/repl.go +++ b/gnovm/pkg/repl/repl.go @@ -125,7 +125,7 @@ func NewRepl(opts ...ReplOption) *Repl { r.stderr = &b r.storeFunc = func() gno.Store { - _, st := test.Store(gnoenv.RootDir(), false, r.stdin, r.stdout, r.stderr) + _, st := test.Store(gnoenv.RootDir(), r.stdin, r.stdout, r.stderr) return st } diff --git a/gnovm/pkg/test/filetest.go b/gnovm/pkg/test/filetest.go index 1934f429568..c24c014a9ba 100644 --- a/gnovm/pkg/test/filetest.go +++ b/gnovm/pkg/test/filetest.go @@ -103,6 +103,11 @@ func (opts *TestOptions) runFiletest(filename string, source []byte) (string, er // The Error directive (and many others) will have one trailing newline, // which is not in the output - so add it there. match(errDirective, result.Error+"\n") + } else if result.Output != "" { + outputDirective := dirs.First(DirectiveOutput) + if outputDirective == nil { + return "", fmt.Errorf("unexpected output:\n%s", result.Output) + } } else { err = m.CheckEmpty() if err != nil { diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go index a8dd709e501..95302ecffb0 100644 --- a/gnovm/pkg/test/imports.go +++ b/gnovm/pkg/test/imports.go @@ -25,22 +25,56 @@ import ( storetypes "github.com/gnolang/gno/tm2/pkg/store/types" ) +type StoreOptions struct { + // WithExtern interprets imports of packages under "github.com/gnolang/gno/_test/" + // as imports under the directory in gnovm/tests/files/extern. + // This should only be used for GnoVM internal filetests (gnovm/tests/files). + WithExtern bool + + // PreprocessOnly instructs the PackageGetter to run the imported files using + // [gno.Machine.PreprocessFiles]. It avoids executing code for contexts + // which only intend to perform a type check, ie. `gno lint`. + PreprocessOnly bool +} + // NOTE: this isn't safe, should only be used for testing. func Store( rootDir string, - withExtern bool, stdin io.Reader, stdout, stderr io.Writer, ) ( baseStore storetypes.CommitStore, resStore gno.Store, ) { + return StoreWithOptions(rootDir, stdin, stdout, stderr, StoreOptions{}) +} + +// StoreWithOptions is a variant of [Store] which additionally accepts a +// [StoreOptions] argument. +func StoreWithOptions( + rootDir string, + stdin io.Reader, + stdout, stderr io.Writer, + opts StoreOptions, +) ( + baseStore storetypes.CommitStore, + resStore gno.Store, +) { + processMemPackage := func(m *gno.Machine, memPkg *gnovm.MemPackage, save bool) (*gno.PackageNode, *gno.PackageValue) { + return m.RunMemPackage(memPkg, save) + } + if opts.PreprocessOnly { + processMemPackage = func(m *gno.Machine, memPkg *gnovm.MemPackage, save bool) (*gno.PackageNode, *gno.PackageValue) { + m.Store.AddMemPackage(memPkg) + return m.PreprocessFiles(memPkg.Name, memPkg.Path, gno.ParseMemPackage(memPkg), save, false) + } + } getPackage := func(pkgPath string, store gno.Store) (pn *gno.PackageNode, pv *gno.PackageValue) { if pkgPath == "" { panic(fmt.Sprintf("invalid zero package path in testStore().pkgGetter")) } - if withExtern { + if opts.WithExtern { // if _test package... const testPath = "github.com/gnolang/gno/_test/" if strings.HasPrefix(pkgPath, testPath) { @@ -54,7 +88,7 @@ func Store( Store: store, Context: ctx, }) - return m2.RunMemPackage(memPkg, true) + return processMemPackage(m2, memPkg, true) } } @@ -129,7 +163,7 @@ func Store( } // Load normal stdlib. - pn, pv = loadStdlib(rootDir, pkgPath, store, stdout) + pn, pv = loadStdlib(rootDir, pkgPath, store, stdout, opts.PreprocessOnly) if pn != nil { return } @@ -150,8 +184,7 @@ func Store( Store: store, Context: ctx, }) - pn, pv = m2.RunMemPackage(memPkg, true) - return + return processMemPackage(m2, memPkg, true) } return nil, nil } @@ -164,7 +197,7 @@ func Store( return } -func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gno.PackageNode, *gno.PackageValue) { +func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer, preprocessOnly bool) (*gno.PackageNode, *gno.PackageValue) { dirs := [...]string{ // Normal stdlib path. filepath.Join(rootDir, "gnovm", "stdlibs", pkgPath), @@ -202,6 +235,11 @@ func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gn Output: stdout, Store: store, }) + if preprocessOnly { + m2.Store.AddMemPackage(memPkg) + return m2.PreprocessFiles(memPkg.Name, memPkg.Path, gno.ParseMemPackage(memPkg), true, true) + } + // TODO: make this work when using gno lint. return m2.RunMemPackageWithOverrides(memPkg, true) } diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go index d06540761d7..92a867e1886 100644 --- a/gnovm/pkg/test/test.go +++ b/gnovm/pkg/test/test.go @@ -139,10 +139,7 @@ func NewTestOptions(rootDir string, stdin io.Reader, stdout, stderr io.Writer) * Output: stdout, Error: stderr, } - opts.BaseStore, opts.TestStore = Store( - rootDir, false, - stdin, opts.WriterForStore(), stderr, - ) + opts.BaseStore, opts.TestStore = Store(rootDir, stdin, opts.WriterForStore(), stderr) return opts } diff --git a/gnovm/pkg/transpiler/transpiler_test.go b/gnovm/pkg/transpiler/transpiler_test.go index 2a0707f7f79..63b77e49446 100644 --- a/gnovm/pkg/transpiler/transpiler_test.go +++ b/gnovm/pkg/transpiler/transpiler_test.go @@ -344,13 +344,13 @@ func Float32bits(i float32) uint32 func testfunc() { println(Float32bits(3.14159)) - std.AssertOriginCall() + std.GetChainID() } func otherFunc() { std := 1 // This is (incorrectly) changed for now. - std.AssertOriginCall() + std.GetChainID() } `, expectedOutput: ` @@ -363,13 +363,13 @@ import "github.com/gnolang/gno/gnovm/stdlibs/std" func testfunc() { println(Float32bits(3.14159)) - std.AssertOriginCall(nil) + std.GetChainID(nil) } func otherFunc() { std := 1 // This is (incorrectly) changed for now. - std.AssertOriginCall(nil) + std.GetChainID(nil) } `, expectedImports: []*ast.ImportSpec{ @@ -388,11 +388,11 @@ func otherFunc() { source: ` package std -func AssertOriginCall() +func GetChainID() func origCaller() string func testfunc() { - AssertOriginCall() + GetChainID() println(origCaller()) } `, @@ -403,7 +403,7 @@ func testfunc() { package std func testfunc() { - AssertOriginCall(nil) + GetChainID(nil) println(X_origCaller(nil)) } `, diff --git a/gnovm/pkg/version/version.go b/gnovm/pkg/version/version.go new file mode 100644 index 00000000000..933d4fac3e5 --- /dev/null +++ b/gnovm/pkg/version/version.go @@ -0,0 +1,3 @@ +package version + +var Version = "develop" diff --git a/gnovm/stdlibs/generated.go b/gnovm/stdlibs/generated.go index 6bd45de3589..01e5d1831dd 100644 --- a/gnovm/stdlibs/generated.go +++ b/gnovm/stdlibs/generated.go @@ -1,4 +1,4 @@ -// This file is autogenerated from the genstd tool (@/misc/genstd); do not edit. +// Code generated by the genstd tool (@/misc/genstd); DO NOT EDIT. // To regenerate it, run `go generate` from this directory. package stdlibs @@ -428,26 +428,6 @@ var nativeFuncs = [...]NativeFunc{ ) }, }, - { - "std", - "IsOriginCall", - []gno.FieldTypeExpr{}, - []gno.FieldTypeExpr{ - {Name: gno.N("r0"), Type: gno.X("bool")}, - }, - true, - func(m *gno.Machine) { - r0 := libs_std.IsOriginCall( - m, - ) - - m.PushValue(gno.Go2GnoValue( - m.Alloc, - m.Store, - reflect.ValueOf(&r0).Elem(), - )) - }, - }, { "std", "GetChainID", diff --git a/gnovm/stdlibs/std/native.gno b/gnovm/stdlibs/std/native.gno index 9cf8808a07e..2baa1f92f48 100644 --- a/gnovm/stdlibs/std/native.gno +++ b/gnovm/stdlibs/std/native.gno @@ -1,15 +1,11 @@ package std -// AssertOriginCall panics if [IsOriginCall] returns false. -func AssertOriginCall() // injected - -// IsOriginCall returns true only if the calling method is invoked via a direct -// MsgCall. It returns false for all other cases, like if the calling method +// AssertOriginCall panics if the calling method is not invoked via a direct +// MsgCall. It panics for for other cases, like if the calling method // is invoked by another method (even from the same realm or package). -// It also returns false every time when the transaction is broadcasted via +// It also panic every time when the transaction is broadcasted via // MsgRun. -func IsOriginCall() bool // injected - +func AssertOriginCall() // injected func GetChainID() string // injected func GetChainDomain() string // injected func GetHeight() int64 // injected diff --git a/gnovm/stdlibs/std/native.go b/gnovm/stdlibs/std/native.go index 9e398e907a2..68f4542f689 100644 --- a/gnovm/stdlibs/std/native.go +++ b/gnovm/stdlibs/std/native.go @@ -7,12 +7,12 @@ import ( ) func AssertOriginCall(m *gno.Machine) { - if !IsOriginCall(m) { + if !isOriginCall(m) { m.Panic(typedString("invalid non-origin call")) } } -func IsOriginCall(m *gno.Machine) bool { +func isOriginCall(m *gno.Machine) bool { n := m.NumFrames() if n == 0 { return false diff --git a/gnovm/stdlibs/std/native_test.go b/gnovm/stdlibs/std/native_test.go index 851785575d7..acbd22055d6 100644 --- a/gnovm/stdlibs/std/native_test.go +++ b/gnovm/stdlibs/std/native_test.go @@ -184,7 +184,7 @@ func TestPrevRealmIsOrigin(t *testing.T) { assert := assert.New(t) addr, pkgPath := X_getRealm(tt.machine, 1) - isOrigin := IsOriginCall(tt.machine) + isOrigin := isOriginCall(tt.machine) assert.Equal(string(tt.expectedAddr), addr) assert.Equal(tt.expectedPkgPath, pkgPath) diff --git a/gnovm/tests/files/addressable_5.gno b/gnovm/tests/files/addressable_5.gno index 800cc744458..fa39ef42841 100644 --- a/gnovm/tests/files/addressable_5.gno +++ b/gnovm/tests/files/addressable_5.gno @@ -9,3 +9,6 @@ func main() { le := &binary.LittleEndian println(&le.AppendUint16(b, 0)[0]) } + +// Output: +// &(0 uint8) diff --git a/gnovm/tests/files/const51.gno b/gnovm/tests/files/const51.gno new file mode 100644 index 00000000000..b00748b0ec7 --- /dev/null +++ b/gnovm/tests/files/const51.gno @@ -0,0 +1,20 @@ +package main + +type T1 struct { + x [2]string +} + +type T2 struct { + x *[2]string +} + +func main() { + t1 := T1{x: [2]string{"a", "b"}} + t2 := T2{x: &[2]string{"a", "b"}} + const c1 = len(t1.x) + const c2 = len(t2.x) + println(c1, c2) +} + +// Output: +// 2 2 diff --git a/gnovm/tests/files/const52.gno b/gnovm/tests/files/const52.gno new file mode 100644 index 00000000000..c213faeb12b --- /dev/null +++ b/gnovm/tests/files/const52.gno @@ -0,0 +1,11 @@ +package main + +func main() { + s := make([][2]string, 1) // Slice with length 1 + s[0] = [2]string{"a", "b"} // Assign value to s[0] + const r = len(s[0]) + println(r) // Prints: 2 +} + +// Output: +// 2 diff --git a/gnovm/tests/files/std5.gno b/gnovm/tests/files/std5.gno index 2f9e98bb4ec..1f1d013c3df 100644 --- a/gnovm/tests/files/std5.gno +++ b/gnovm/tests/files/std5.gno @@ -13,10 +13,10 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(2) -// std/native.gno:45 +// std/native.gno:41 // main() // main/files/std5.gno:10 diff --git a/gnovm/tests/files/std8.gno b/gnovm/tests/files/std8.gno index dfc2b8ca5fd..3d0e4a7085e 100644 --- a/gnovm/tests/files/std8.gno +++ b/gnovm/tests/files/std8.gno @@ -23,10 +23,10 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(4) -// std/native.gno:45 +// std/native.gno:41 // fn() // main/files/std8.gno:16 // testutils.WrapCall(inner) diff --git a/gnovm/tests/files/type_alias.gno b/gnovm/tests/files/type_alias.gno index e95c54126ec..09918f6d591 100644 --- a/gnovm/tests/files/type_alias.gno +++ b/gnovm/tests/files/type_alias.gno @@ -6,7 +6,8 @@ import "gno.land/p/demo/uassert" type TestingT = uassert.TestingT func main() { - println(TestingT) + println("ok") } -// No need for output; not panicking is passing. +// Output: +// ok diff --git a/gnovm/tests/files/types/eql_0f49.gno b/gnovm/tests/files/types/eql_0f49.gno index b5a4bf4ed05..b4d6f7e3972 100644 --- a/gnovm/tests/files/types/eql_0f49.gno +++ b/gnovm/tests/files/types/eql_0f49.gno @@ -14,6 +14,8 @@ func main() { } +// Output: +// true // true // true // true diff --git a/gnovm/tests/integ/init/gno.mod b/gnovm/tests/integ/init/gno.mod new file mode 100644 index 00000000000..28c7e51b750 --- /dev/null +++ b/gnovm/tests/integ/init/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/init diff --git a/gnovm/tests/integ/init/main.gno b/gnovm/tests/integ/init/main.gno new file mode 100644 index 00000000000..88cfafb9f24 --- /dev/null +++ b/gnovm/tests/integ/init/main.gno @@ -0,0 +1,10 @@ +package main + +var _ = func() int { + println("HELLO HELLO!!") + return 1 +}() + +func init() { + println("HELLO WORLD!") +} diff --git a/gnovm/tests/stdlibs/README.md b/gnovm/tests/stdlibs/README.md index 8742447e59a..d264cf35c45 100644 --- a/gnovm/tests/stdlibs/README.md +++ b/gnovm/tests/stdlibs/README.md @@ -5,4 +5,4 @@ available when testing gno code in `_test.gno` and `_filetest.gno` files. Re-declarations of functions already existing override the definitions of the normal stdlibs directory. -Adding imports that don't exist in the corresponding normal stdlib is undefined behavior \ No newline at end of file +Adding imports that don't exist in the corresponding normal stdlib is undefined behavior diff --git a/gnovm/tests/stdlibs/generated.go b/gnovm/tests/stdlibs/generated.go index 4445d2467e8..d7417b0c77d 100644 --- a/gnovm/tests/stdlibs/generated.go +++ b/gnovm/tests/stdlibs/generated.go @@ -1,4 +1,4 @@ -// This file is autogenerated from the genstd tool (@/misc/genstd); do not edit. +// Code generated by the genstd tool (@/misc/genstd); DO NOT EDIT. // To regenerate it, run `go generate` from this directory. package stdlibs @@ -43,26 +43,6 @@ var nativeFuncs = [...]NativeFunc{ ) }, }, - { - "std", - "IsOriginCall", - []gno.FieldTypeExpr{}, - []gno.FieldTypeExpr{ - {Name: gno.N("r0"), Type: gno.X("bool")}, - }, - true, - func(m *gno.Machine) { - r0 := testlibs_std.IsOriginCall( - m, - ) - - m.PushValue(gno.Go2GnoValue( - m.Alloc, - m.Store, - reflect.ValueOf(&r0).Elem(), - )) - }, - }, { "std", "TestSkipHeights", diff --git a/gnovm/tests/stdlibs/std/std.gno b/gnovm/tests/stdlibs/std/std.gno index dcb5a64dbb3..c30071313fe 100644 --- a/gnovm/tests/stdlibs/std/std.gno +++ b/gnovm/tests/stdlibs/std/std.gno @@ -1,7 +1,6 @@ package std func AssertOriginCall() // injected -func IsOriginCall() bool // injected func TestSkipHeights(count int64) // injected func TestSetOrigCaller(addr Address) { testSetOrigCaller(string(addr)) } diff --git a/gnovm/tests/stdlibs/std/std.go b/gnovm/tests/stdlibs/std/std.go index 675194b252f..eac51c5fb0e 100644 --- a/gnovm/tests/stdlibs/std/std.go +++ b/gnovm/tests/stdlibs/std/std.go @@ -26,7 +26,7 @@ type RealmOverride struct { } func AssertOriginCall(m *gno.Machine) { - if !IsOriginCall(m) { + if !isOriginCall(m) { m.Panic(typedString("invalid non-origin call")) } } @@ -37,7 +37,7 @@ func typedString(s gno.StringValue) gno.TypedValue { return tv } -func IsOriginCall(m *gno.Machine) bool { +func isOriginCall(m *gno.Machine) bool { tname := m.Frames[0].Func.Name switch tname { case "main": // test is a _filetest diff --git a/go.mod b/go.mod index ce58b8f7998..027ba6359bc 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 + github.com/valyala/bytebufferpool v1.0.0 github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc go.etcd.io/bbolt v1.3.11 diff --git a/go.sum b/go.sum index 046d9c8c75a..5fd4cddd627 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= diff --git a/misc/autocounterd/go.mod b/misc/autocounterd/go.mod index 730a3d901b7..972975d4fb0 100644 --- a/misc/autocounterd/go.mod +++ b/misc/autocounterd/go.mod @@ -29,6 +29,7 @@ require ( github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/misc/autocounterd/go.sum b/misc/autocounterd/go.sum index 3d0eae7661b..6d6d87fa01a 100644 --- a/misc/autocounterd/go.sum +++ b/misc/autocounterd/go.sum @@ -133,6 +133,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 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= diff --git a/misc/genstd/template.tmpl b/misc/genstd/template.tmpl index 2d714589ef6..09dd7786268 100644 --- a/misc/genstd/template.tmpl +++ b/misc/genstd/template.tmpl @@ -1,4 +1,4 @@ -// This file is autogenerated from the genstd tool (@/misc/genstd); do not edit. +// Code generated by the genstd tool (@/misc/genstd); DO NOT EDIT. // To regenerate it, run `go generate` from this directory. package stdlibs diff --git a/misc/genstd/testdata/integration/generated.go.golden b/misc/genstd/testdata/integration/generated.go.golden index d0be334480f..049adf83e7c 100644 --- a/misc/genstd/testdata/integration/generated.go.golden +++ b/misc/genstd/testdata/integration/generated.go.golden @@ -1,4 +1,4 @@ -// This file is autogenerated from the genstd tool (@/misc/genstd); do not edit. +// Code generated by the genstd tool (@/misc/genstd); DO NOT EDIT. // To regenerate it, run `go generate` from this directory. package stdlibs diff --git a/misc/loop/cmd/snapshotter.go b/misc/loop/cmd/snapshotter.go index 0173f9aad03..eef4be36d2a 100644 --- a/misc/loop/cmd/snapshotter.go +++ b/misc/loop/cmd/snapshotter.go @@ -18,7 +18,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/gnolang/tx-archive/backup" - "github.com/gnolang/tx-archive/backup/client/http" + "github.com/gnolang/tx-archive/backup/client/rpc" "github.com/gnolang/tx-archive/backup/writer/standard" ) @@ -202,6 +202,10 @@ func (s snapshotter) backupTXs(ctx context.Context, rpcURL string) error { cfg.FromBlock = 1 cfg.Watch = false + // We want to skip failed txs on the Portal Loop reset, + // because they might (unexpectedly) succeed + cfg.SkipFailedTx = true + instanceBackupFile, err := os.Create(s.instanceBackupFile) if err != nil { return err @@ -211,7 +215,7 @@ func (s snapshotter) backupTXs(ctx context.Context, rpcURL string) error { w := standard.NewWriter(instanceBackupFile) // Create the tx-archive backup service - c, err := http.NewClient(rpcURL) + c, err := rpc.NewHTTPClient(rpcURL) if err != nil { return fmt.Errorf("could not create tx-archive client, %w", err) } diff --git a/misc/loop/go.mod b/misc/loop/go.mod index c72101c7c1e..4c5a3f41839 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -8,7 +8,7 @@ require ( github.com/docker/docker v25.0.6+incompatible github.com/docker/go-connections v0.4.0 github.com/gnolang/gno v0.1.0-nightly.20240627 - github.com/gnolang/tx-archive v0.4.2 + github.com/gnolang/tx-archive v0.5.0 github.com/prometheus/client_golang v1.17.0 github.com/sirupsen/logrus v1.9.3 ) @@ -56,6 +56,7 @@ require ( github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect go.etcd.io/bbolt v1.3.11 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 634dbdac082..c5aed820f5e 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -70,8 +70,8 @@ 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/tx-archive v0.4.2 h1:xBBqLLKY9riv9yxpQgVhItCWxIji2rX6xNFmCY1cEOQ= -github.com/gnolang/tx-archive v0.4.2/go.mod h1:AGUBGO+DCLuKL80a1GJRnpcJ5gxVd9L4jEJXQB9uXp4= +github.com/gnolang/tx-archive v0.5.0 h1:npM+TfM3ufF2nz1V6hq+RLkCklPbADRZXBjiyPxXVu4= +github.com/gnolang/tx-archive v0.5.0/go.mod h1:thbXpyYT57ITGABl3hH4ftLSdO8eXaPFPi5hl6jZ2UE= 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= @@ -179,6 +179,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= diff --git a/misc/stdlib_diff/Makefile b/misc/stdlib_diff/Makefile index 439af22c586..32dcf95a2ec 100644 --- a/misc/stdlib_diff/Makefile +++ b/misc/stdlib_diff/Makefile @@ -1,7 +1,9 @@ all: clean gen +GOROOT_SAVE ?= $(shell go env GOROOT) + gen: - go run . -src $(GOROOT)/src -dst ../../gnovm/stdlibs -out ./stdlib_diff + go run . -src $(GOROOT_SAVE)/src -dst ../../gnovm/stdlibs -out ./stdlib_diff clean: rm -rf stdlib_diff diff --git a/misc/stdlib_diff/README.md b/misc/stdlib_diff/README.md index 32c3cbcd93d..47d05a0373b 100644 --- a/misc/stdlib_diff/README.md +++ b/misc/stdlib_diff/README.md @@ -1,6 +1,6 @@ # stdlibs_diff -stdlibs_diff is a tool that generates an html report indicating differences between gno standard libraries and go standrad libraries +stdlibs_diff is a tool that generates an html report indicating differences between gno standard libraries and go standard libraries. ## Usage @@ -27,4 +27,4 @@ Compare the `gno` standard libraries the `go` standard libraries ## Tips -An index.html is generated at the root of the report location. Utilize it to navigate easily through the report. \ No newline at end of file +An index.html is generated at the root of the report location. Utilize it to navigate easily through the report. diff --git a/tm2/Makefile b/tm2/Makefile index 0aaa63e5285..fd3aede0d4c 100644 --- a/tm2/Makefile +++ b/tm2/Makefile @@ -63,3 +63,8 @@ _test.pkg.others:; go test $(GOTEST_FLAGS) `go list ./pkg/... | grep -Ev 'pkg/( _test.pkg.amino:; go test $(GOTEST_FLAGS) ./pkg/amino/... _test.pkg.bft:; go test $(GOTEST_FLAGS) ./pkg/bft/... _test.pkg.db:; go test $(GOTEST_FLAGS) ./pkg/db/... ./pkg/iavl/benchmarks/... + +.PHONY: generate +generate: + go generate -x ./... + $(MAKE) fmt diff --git a/tm2/pkg/amino/amino.go b/tm2/pkg/amino/amino.go index 262f5d9a54e..b8942c49029 100644 --- a/tm2/pkg/amino/amino.go +++ b/tm2/pkg/amino/amino.go @@ -219,7 +219,8 @@ func (cdc *Codec) MarshalSized(o interface{}) ([]byte, error) { cdc.doAutoseal() // Write the bytes here. - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) // Write the bz without length-prefixing. bz, err := cdc.Marshal(o) @@ -239,7 +240,7 @@ func (cdc *Codec) MarshalSized(o interface{}) ([]byte, error) { return nil, err } - return buf.Bytes(), nil + return copyBytes(buf.Bytes()), nil } // MarshalSizedWriter writes the bytes as would be returned from @@ -271,8 +272,8 @@ func (cdc *Codec) MarshalAnySized(o interface{}) ([]byte, error) { cdc.doAutoseal() // Write the bytes here. - buf := new(bytes.Buffer) - + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) // Write the bz without length-prefixing. bz, err := cdc.MarshalAny(o) if err != nil { @@ -291,7 +292,7 @@ func (cdc *Codec) MarshalAnySized(o interface{}) ([]byte, error) { return nil, err } - return buf.Bytes(), nil + return copyBytes(buf.Bytes()), nil } func (cdc *Codec) MustMarshalAnySized(o interface{}) []byte { @@ -357,7 +358,9 @@ func (cdc *Codec) MarshalReflect(o interface{}) ([]byte, error) { // Encode Amino:binary bytes. var bz []byte - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + rt := rv.Type() info, err := cdc.getTypeInfoWLock(rt) if err != nil { @@ -377,7 +380,7 @@ func (cdc *Codec) MarshalReflect(o interface{}) ([]byte, error) { if err = cdc.writeFieldIfNotEmpty(buf, 1, info, FieldOptions{}, FieldOptions{}, rv, writeEmpty); err != nil { return nil, err } - bz = buf.Bytes() + bz = copyBytes(buf.Bytes()) } else { // The passed in BinFieldNum is only relevant for when the type is to // be encoded unpacked (elements are Typ3_ByteLength). In that case, @@ -387,7 +390,7 @@ func (cdc *Codec) MarshalReflect(o interface{}) ([]byte, error) { if err != nil { return nil, err } - bz = buf.Bytes() + bz = copyBytes(buf.Bytes()) } // If bz is empty, prefer nil. if len(bz) == 0 { @@ -443,16 +446,23 @@ func (cdc *Codec) MarshalAny(o interface{}) ([]byte, error) { } // Encode as interface. - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) err = cdc.encodeReflectBinaryInterface(buf, iinfo, reflect.ValueOf(&ivar).Elem(), FieldOptions{}, true) if err != nil { return nil, err } - bz := buf.Bytes() + bz := copyBytes(buf.Bytes()) return bz, nil } +func copyBytes(bz []byte) []byte { + cp := make([]byte, len(bz)) + copy(cp, bz) + return cp +} + // Panics if error. func (cdc *Codec) MustMarshalAny(o interface{}) []byte { bz, err := cdc.MarshalAny(o) @@ -764,7 +774,8 @@ func (cdc *Codec) JSONMarshal(o interface{}) ([]byte, error) { return []byte("null"), nil } rt := rv.Type() - w := new(bytes.Buffer) + w := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(w) info, err := cdc.getTypeInfoWLock(rt) if err != nil { return nil, err @@ -772,7 +783,8 @@ func (cdc *Codec) JSONMarshal(o interface{}) ([]byte, error) { if err = cdc.encodeReflectJSON(w, info, rv, FieldOptions{}); err != nil { return nil, err } - return w.Bytes(), nil + + return copyBytes(w.Bytes()), nil } func (cdc *Codec) MarshalJSONAny(o interface{}) ([]byte, error) { @@ -802,12 +814,14 @@ func (cdc *Codec) MarshalJSONAny(o interface{}) ([]byte, error) { } // Encode as interface. - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + err = cdc.encodeReflectJSONInterface(buf, iinfo, reflect.ValueOf(&ivar).Elem(), FieldOptions{}) if err != nil { return nil, err } - bz := buf.Bytes() + bz := copyBytes(buf.Bytes()) return bz, nil } @@ -863,12 +877,12 @@ func (cdc *Codec) MarshalJSONIndent(o interface{}, prefix, indent string) ([]byt if err != nil { return nil, err } + var out bytes.Buffer - err = json.Indent(&out, bz, prefix, indent) - if err != nil { + if err := json.Indent(&out, bz, prefix, indent); err != nil { return nil, err } - return out.Bytes(), nil + return copyBytes(out.Bytes()), nil } // ---------------------------------------- diff --git a/tm2/pkg/amino/binary_encode.go b/tm2/pkg/amino/binary_encode.go index 426cc520604..45758329284 100644 --- a/tm2/pkg/amino/binary_encode.go +++ b/tm2/pkg/amino/binary_encode.go @@ -1,12 +1,13 @@ package amino import ( - "bytes" "encoding/binary" "errors" "fmt" "io" "reflect" + + "github.com/valyala/bytebufferpool" ) const beOptionByte = 0x01 @@ -209,6 +210,8 @@ func (cdc *Codec) encodeReflectBinary(w io.Writer, info *TypeInfo, rv reflect.Va return err } +var poolBytesBuffer = new(bytebufferpool.Pool) + func (cdc *Codec) encodeReflectBinaryInterface(w io.Writer, iinfo *TypeInfo, rv reflect.Value, fopts FieldOptions, bare bool, ) (err error) { @@ -250,7 +253,9 @@ func (cdc *Codec) encodeReflectBinaryInterface(w io.Writer, iinfo *TypeInfo, rv // For Proto3 compatibility, encode interfaces as google.protobuf.Any // Write field #1, TypeURL - buf := bytes.NewBuffer(nil) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + { fnum := uint32(1) err = encodeFieldNumberAndTyp3(buf, fnum, Typ3ByteLength) @@ -269,7 +274,9 @@ func (cdc *Codec) encodeReflectBinaryInterface(w io.Writer, iinfo *TypeInfo, rv { // google.protobuf.Any values must be a struct, or an unpacked list which // is indistinguishable from a struct. - buf2 := bytes.NewBuffer(nil) + buf2 := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf2) + if !cinfo.IsStructOrUnpacked(fopts) { writeEmpty := false // Encode with an implicit struct, with a single field with number 1. @@ -356,7 +363,8 @@ func (cdc *Codec) encodeReflectBinaryList(w io.Writer, info *TypeInfo, rv reflec // Proto3 byte-length prefixing incurs alloc cost on the encoder. // Here we incur it for unpacked form for ease of dev. - buf := bytes.NewBuffer(nil) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) // If elem is not already a ByteLength type, write in packed form. // This is a Proto wart due to Proto backwards compatibility issues. @@ -393,6 +401,9 @@ func (cdc *Codec) encodeReflectBinaryList(w io.Writer, info *TypeInfo, rv reflec einfo.Elem.ReprType.Type.Kind() != reflect.Uint8 && einfo.Elem.ReprType.GetTyp3(fopts) != Typ3ByteLength + elemBuf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(elemBuf) + // Write elems in unpacked form. for i := 0; i < rv.Len(); i++ { // Write elements as repeated fields of the parent struct. @@ -431,20 +442,21 @@ func (cdc *Codec) encodeReflectBinaryList(w io.Writer, info *TypeInfo, rv reflec // form) are represented as lists of implicit structs. if writeImplicit { // Write field key for Value field of implicit struct. - buf2 := new(bytes.Buffer) - err = encodeFieldNumberAndTyp3(buf2, 1, Typ3ByteLength) + + err = encodeFieldNumberAndTyp3(elemBuf, 1, Typ3ByteLength) if err != nil { return } // Write field value of implicit struct to buf2. efopts := fopts efopts.BinFieldNum = 0 // dontcare - err = cdc.encodeReflectBinary(buf2, einfo, derv, efopts, false, 0) + err = cdc.encodeReflectBinary(elemBuf, einfo, derv, efopts, false, 0) if err != nil { return } // Write implicit struct to buf. - err = EncodeByteSlice(buf, buf2.Bytes()) + err = EncodeByteSlice(buf, elemBuf.Bytes()) + elemBuf.Reset() if err != nil { return } @@ -497,7 +509,8 @@ func (cdc *Codec) encodeReflectBinaryStruct(w io.Writer, info *TypeInfo, rv refl // Proto3 incurs a cost in writing non-root structs. // Here we incur it for root structs as well for ease of dev. - buf := bytes.NewBuffer(nil) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) for _, field := range info.Fields { // Get type info for field. @@ -553,7 +566,7 @@ func encodeFieldNumberAndTyp3(w io.Writer, num uint32, typ Typ3) (err error) { } func (cdc *Codec) writeFieldIfNotEmpty( - buf *bytes.Buffer, + buf *bytebufferpool.ByteBuffer, fieldNum uint32, finfo *TypeInfo, structsFopts FieldOptions, // the wrapping struct's FieldOptions if any @@ -579,7 +592,7 @@ func (cdc *Codec) writeFieldIfNotEmpty( if !isWriteEmpty && lBeforeValue == lAfterValue-1 && buf.Bytes()[buf.Len()-1] == 0x00 { // rollback typ3/fieldnum and last byte if // not a pointer and empty: - buf.Truncate(lBeforeKey) + buf.Set(buf.Bytes()[:lBeforeKey]) } return nil } diff --git a/tm2/pkg/amino/codec.go b/tm2/pkg/amino/codec.go index 3fa7634e3ad..ba24f49a808 100644 --- a/tm2/pkg/amino/codec.go +++ b/tm2/pkg/amino/codec.go @@ -1,7 +1,6 @@ package amino import ( - "bytes" "fmt" "io" "reflect" @@ -113,7 +112,9 @@ func (info *TypeInfo) String() string { // before it's fully populated. return "" } - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + buf.Write([]byte("TypeInfo{")) buf.Write([]byte(fmt.Sprintf("Type:%v,", info.Type))) if info.ConcreteInfo.Registered { diff --git a/tm2/pkg/amino/json_encode.go b/tm2/pkg/amino/json_encode.go index 113c3486565..99e1b445917 100644 --- a/tm2/pkg/amino/json_encode.go +++ b/tm2/pkg/amino/json_encode.go @@ -1,7 +1,6 @@ package amino import ( - "bytes" "encoding/json" "fmt" "io" @@ -156,7 +155,9 @@ func (cdc *Codec) encodeReflectJSONInterface(w io.Writer, iinfo *TypeInfo, rv re } // Write Value to buffer - buf := new(bytes.Buffer) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + cdc.encodeReflectJSON(buf, cinfo, crv, fopts) value := buf.Bytes() if len(value) == 0 { diff --git a/tm2/pkg/amino/wellknown.go b/tm2/pkg/amino/wellknown.go index 7720c2894d9..4053c23e893 100644 --- a/tm2/pkg/amino/wellknown.go +++ b/tm2/pkg/amino/wellknown.go @@ -3,7 +3,6 @@ package amino // NOTE: We must not depend on protubuf libraries for serialization. import ( - "bytes" "fmt" "io" "reflect" @@ -342,7 +341,9 @@ func encodeReflectBinaryWellKnown(w io.Writer, info *TypeInfo, rv reflect.Value, } // Maybe recurse with length-prefixing. if !bare { - buf := bytes.NewBuffer(nil) + buf := poolBytesBuffer.Get() + defer poolBytesBuffer.Put(buf) + ok, err = encodeReflectBinaryWellKnown(buf, info, rv, fopts, true) if err != nil { return false, err diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index c1afb2996fa..7f16d6780c7 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -12,8 +12,6 @@ import ( "sync" "time" - goErrors "errors" - "github.com/gnolang/gno/tm2/pkg/bft/appconn" "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" "github.com/gnolang/gno/tm2/pkg/p2p/conn" @@ -604,12 +602,10 @@ func (n *Node) OnStart() error { } // Start the transport. - lAddr := n.config.P2P.ExternalAddress - if lAddr == "" { - lAddr = n.config.P2P.ListenAddress - } + // The listen address for the transport needs to be an address within reach of the machine NIC + listenAddress := p2pTypes.NetAddressString(n.nodeKey.ID(), n.config.P2P.ListenAddress) - addr, err := p2pTypes.NewNetAddressFromString(p2pTypes.NetAddressString(n.nodeKey.ID(), lAddr)) + addr, err := p2pTypes.NewNetAddressFromString(listenAddress) if err != nil { return fmt.Errorf("unable to parse network address, %w", err) } @@ -713,7 +709,17 @@ func (n *Node) configureRPC() { rpccore.SetConfig(*n.config.RPC) } -func (n *Node) startRPC() ([]net.Listener, error) { +func (n *Node) startRPC() (listeners []net.Listener, err error) { + defer func() { + if err != nil { + // Close all the created listeners on any error, instead of + // leaking them: https://github.com/gnolang/gno/issues/3639 + for _, ln := range listeners { + ln.Close() + } + } + }() + listenAddrs := splitAndTrimEmpty(n.config.RPC.ListenAddress, ",", " ") config := rpcserver.DefaultConfig() @@ -729,8 +735,8 @@ func (n *Node) startRPC() ([]net.Listener, error) { // we may expose the rpc over both a unix and tcp socket var rebuildAddresses bool - listeners := make([]net.Listener, len(listenAddrs)) - for i, listenAddr := range listenAddrs { + listeners = make([]net.Listener, 0, len(listenAddrs)) + for _, listenAddr := range listenAddrs { mux := http.NewServeMux() rpcLogger := n.Logger.With("module", "rpc-server") wmLogger := rpcLogger.With("protocol", "websocket") @@ -782,7 +788,7 @@ func (n *Node) startRPC() ([]net.Listener, error) { ) } - listeners[i] = listener + listeners = append(listeners, listener) } if rebuildAddresses { n.config.RPC.ListenAddress = joinListenerAddresses(listeners) @@ -893,7 +899,7 @@ func makeNodeInfo( nodeInfo := p2pTypes.NodeInfo{ VersionSet: vset, - PeerID: nodeKey.ID(), + NetAddress: nil, // The shared address depends on the configuration Network: genDoc.ChainID, Version: version.Version, Channels: []byte{ @@ -908,13 +914,44 @@ func makeNodeInfo( }, } + // Make sure the discovery channel is shared with peers + // in case peer discovery is enabled if config.P2P.PeerExchange { nodeInfo.Channels = append(nodeInfo.Channels, discovery.Channel) } + // Grab the supplied listen address. + // This address needs to be valid, but it can be unspecified. + // If the listen address is unspecified (port / IP unbound), + // then this address cannot be used by peers for dialing + addr, err := p2pTypes.NewNetAddressFromString( + p2pTypes.NetAddressString(nodeKey.ID(), config.P2P.ListenAddress), + ) + if err != nil { + return p2pTypes.NodeInfo{}, fmt.Errorf("unable to parse network address, %w", err) + } + + // Use the transport listen address as the advertised address + nodeInfo.NetAddress = addr + + // Prepare the advertised dial address (if any) + // for the node, which other peers can use to dial + if config.P2P.ExternalAddress != "" { + addr, err = p2pTypes.NewNetAddressFromString( + p2pTypes.NetAddressString( + nodeKey.ID(), + config.P2P.ExternalAddress, + ), + ) + if err != nil { + return p2pTypes.NodeInfo{}, fmt.Errorf("invalid p2p external address: %w", err) + } + + nodeInfo.NetAddress = addr + } + // Validate the node info - err := nodeInfo.Validate() - if err != nil && !goErrors.Is(err, p2pTypes.ErrUnspecifiedIP) { + if err := nodeInfo.Validate(); err != nil { return p2pTypes.NodeInfo{}, fmt.Errorf("unable to validate node info, %w", err) } diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go index 9e10596a975..8269bc8bf8a 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers.go @@ -140,61 +140,84 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger *slog.Logger) http.H return } - // first try to unmarshal the incoming request as an array of RPC requests - var ( - requests types.RPCRequests - responses types.RPCResponses - ) - if err := json.Unmarshal(b, &requests); err != nil { - // next, try to unmarshal as a single request - var request types.RPCRequest - if err := json.Unmarshal(b, &request); err != nil { - WriteRPCResponseHTTP(w, types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshalling request"))) + // --- Branch 1: Attempt to Unmarshal as a Batch (Slice) of Requests --- + var requests types.RPCRequests + if err := json.Unmarshal(b, &requests); err == nil { + var responses types.RPCResponses + for _, req := range requests { + if resp := processRequest(r, req, funcMap, logger); resp != nil { + responses = append(responses, *resp) + } + } + + if len(responses) > 0 { + WriteRPCResponseArrayHTTP(w, responses) return } - requests = []types.RPCRequest{request} } - for _, request := range requests { - request := request - // A Notification is a Request object without an "id" member. - // The Server MUST NOT reply to a Notification, including those that are within a batch request. - if request.ID == types.JSONRPCStringID("") { - logger.Debug("HTTPJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") - continue - } - if len(r.URL.Path) > 1 { - responses = append(responses, types.RPCInvalidRequestError(request.ID, errors.New("path %s is invalid", r.URL.Path))) - continue - } - rpcFunc, ok := funcMap[request.Method] - if !ok || rpcFunc.ws { - responses = append(responses, types.RPCMethodNotFoundError(request.ID)) - continue - } - ctx := &types.Context{JSONReq: &request, HTTPReq: r} - args := []reflect.Value{reflect.ValueOf(ctx)} - if len(request.Params) > 0 { - fnArgs, err := jsonParamsToArgs(rpcFunc, request.Params) - if err != nil { - responses = append(responses, types.RPCInvalidParamsError(request.ID, errors.Wrap(err, "error converting json params to arguments"))) - continue - } - args = append(args, fnArgs...) - } - returns := rpcFunc.f.Call(args) - logger.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) - result, err := unreflectResult(returns) - if err != nil { - responses = append(responses, types.RPCInternalError(request.ID, err)) - continue + // --- Branch 2: Attempt to Unmarshal as a Single Request --- + var request types.RPCRequest + if err := json.Unmarshal(b, &request); err == nil { + if resp := processRequest(r, request, funcMap, logger); resp != nil { + WriteRPCResponseHTTP(w, *resp) + return } - responses = append(responses, types.NewRPCSuccessResponse(request.ID, result)) + } else { + WriteRPCResponseHTTP(w, types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshalling request"))) + return } - if len(responses) > 0 { - WriteRPCResponseArrayHTTP(w, responses) + } +} + +// processRequest checks and processes a single JSON-RPC request. +// If the request should produce a response, it returns a pointer to that response. +// Otherwise (e.g. if the request is a notification or fails validation), it returns nil. +func processRequest(r *http.Request, req types.RPCRequest, funcMap map[string]*RPCFunc, logger *slog.Logger) *types.RPCResponse { + // Skip notifications (an empty ID indicates no response should be sent) + if req.ID == types.JSONRPCStringID("") { + logger.Debug("Skipping notification (empty ID)") + return nil + } + + // Check that the URL path is valid (assume only "/" is acceptable) + if len(r.URL.Path) > 1 { + resp := types.RPCInvalidRequestError(req.ID, fmt.Errorf("invalid path: %s", r.URL.Path)) + return &resp + } + + // Look up the requested method in the function map. + rpcFunc, ok := funcMap[req.Method] + if !ok || rpcFunc.ws { + resp := types.RPCMethodNotFoundError(req.ID) + return &resp + } + + ctx := &types.Context{JSONReq: &req, HTTPReq: r} + args := []reflect.Value{reflect.ValueOf(ctx)} + if len(req.Params) > 0 { + fnArgs, err := jsonParamsToArgs(rpcFunc, req.Params) + if err != nil { + resp := types.RPCInvalidParamsError(req.ID, errors.Wrap(err, "error converting json params to arguments")) + return &resp } + args = append(args, fnArgs...) + } + + // Call the RPC function using reflection. + returns := rpcFunc.f.Call(args) + logger.Info("HTTPJSONRPC", "method", req.Method, "args", args, "returns", returns) + + // Convert the reflection return values into a result value for JSON serialization. + result, err := unreflectResult(returns) + if err != nil { + resp := types.RPCInternalError(req.ID, err) + return &resp } + + // Build and return a successful response. + resp := types.NewRPCSuccessResponse(req.ID, result) + return &resp } func handleInvalidJSONRPCPaths(next http.HandlerFunc) http.HandlerFunc { diff --git a/tm2/pkg/bft/rpc/lib/server/handlers_test.go b/tm2/pkg/bft/rpc/lib/server/handlers_test.go index f6572be7e0a..dde2cf1e327 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers_test.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers_test.go @@ -171,6 +171,12 @@ func TestRPCNotificationInBatch(t *testing.T) { ]`, 1, }, + { + `[ + {"jsonrpc": "2.0","method":"c","id":"abc","params":["a","10"]} + ]`, + 1, + }, { `[ {"jsonrpc": "2.0","id": ""}, @@ -198,21 +204,8 @@ func TestRPCNotificationInBatch(t *testing.T) { // try to unmarshal an array first err = json.Unmarshal(blob, &responses) if err != nil { - // if we were actually expecting an array, but got an error - if tt.expectCount > 1 { - t.Errorf("#%d: expected an array, couldn't unmarshal it\nblob: %s", i, blob) - continue - } else { - // we were expecting an error here, so let's unmarshal a single response - var response types.RPCResponse - err = json.Unmarshal(blob, &response) - if err != nil { - t.Errorf("#%d: expected successful parsing of an RPCResponse\nblob: %s", i, blob) - continue - } - // have a single-element result - responses = types.RPCResponses{response} - } + t.Errorf("#%d: expected an array, couldn't unmarshal it\nblob: %s", i, blob) + continue } if tt.expectCount != len(responses) { t.Errorf("#%d: expected %d response(s), but got %d\nblob: %s", i, tt.expectCount, len(responses), blob) diff --git a/tm2/pkg/bft/rpc/lib/server/http_server.go b/tm2/pkg/bft/rpc/lib/server/http_server.go index a4e535160b5..a5cec3d5c81 100644 --- a/tm2/pkg/bft/rpc/lib/server/http_server.go +++ b/tm2/pkg/bft/rpc/lib/server/http_server.go @@ -119,18 +119,14 @@ func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { // can write arrays of responses for batched request/response interactions via // the JSON RPC. func WriteRPCResponseArrayHTTP(w http.ResponseWriter, res types.RPCResponses) { - if len(res) == 1 { - WriteRPCResponseHTTP(w, res[0]) - } else { - jsonBytes, err := json.MarshalIndent(res, "", " ") - if err != nil { - panic(err) - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - if _, err := w.Write(jsonBytes); err != nil { - panic(err) - } + jsonBytes, err := json.MarshalIndent(res, "", " ") + if err != nil { + panic(err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + if _, err := w.Write(jsonBytes); err != nil { + panic(err) } } diff --git a/tm2/pkg/bft/types/params.go b/tm2/pkg/bft/types/params.go index c2e8f304698..323e12c25cd 100644 --- a/tm2/pkg/bft/types/params.go +++ b/tm2/pkg/bft/types/params.go @@ -24,7 +24,7 @@ const ( MaxBlockDataBytes int64 = 2000000 // 2MB // MaxBlockMaxGas is the max gas limit for the block - MaxBlockMaxGas int64 = 100000000 // 100M gas + MaxBlockMaxGas int64 = 3000000000 // 3B gas // BlockTimeIotaMS is the block time iota (in ms) BlockTimeIotaMS int64 = 100 // ms diff --git a/tm2/pkg/cmap/cmap_test.go b/tm2/pkg/cmap/cmap_test.go index d9051ea18d6..ebeb601633d 100644 --- a/tm2/pkg/cmap/cmap_test.go +++ b/tm2/pkg/cmap/cmap_test.go @@ -2,7 +2,9 @@ package cmap import ( "fmt" + "runtime" "strings" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -56,6 +58,61 @@ func TestContains(t *testing.T) { assert.Nil(t, cmap.Get("key2")) } +var sink any = nil + +func BenchmarkCMapConcurrentInsertsDeletesHas(b *testing.B) { + cm := NewCMap() + keys := make([]string, 100000) + for i := range keys { + keys[i] = fmt.Sprintf("key%d", i) + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + var wg sync.WaitGroup + semaCh := make(chan bool) + nCPU := runtime.NumCPU() + for j := 0; j < nCPU; j++ { + wg.Add(1) + go func() { + defer wg.Done() + + // Make sure that all the goroutines run at the + // exact same time for true concurrent tests. + <-semaCh + + for i, key := range keys { + if (j+i)%2 == 0 { + cm.Has(key) + } else { + cm.Set(key, j) + } + _ = cm.Size() + if (i+1)%3 == 0 { + cm.Delete(key) + } + + if (i+1)%327 == 0 { + cm.Clear() + } + _ = cm.Size() + _ = cm.Keys() + } + _ = cm.Values() + }() + } + close(semaCh) + wg.Wait() + + sink = semaCh + } + + if sink == nil { + b.Fatal("Benchmark did not run!") + } + sink = nil +} + func BenchmarkCMapHas(b *testing.B) { m := NewCMap() for i := 0; i < 1000; i++ { diff --git a/tm2/pkg/commands/command.go b/tm2/pkg/commands/command.go index aa717b62ad9..a7f80b69a70 100644 --- a/tm2/pkg/commands/command.go +++ b/tm2/pkg/commands/command.go @@ -5,6 +5,7 @@ import ( "errors" "flag" "fmt" + "io" "os" "strings" "text/tabwriter" @@ -31,26 +32,28 @@ func HelpExec(_ context.Context, _ []string) error { // Metadata contains basic help // information about a command type Metadata struct { - Name string - ShortUsage string - ShortHelp string - LongHelp string - Options []ff.Option + Name string + ShortUsage string + ShortHelp string + LongHelp string + Options []ff.Option + NoParentFlags bool } // Command is a simple wrapper for gnoland commands. type Command struct { - name string - shortUsage string - shortHelp string - longHelp string - options []ff.Option - cfg Config - flagSet *flag.FlagSet - subcommands []*Command - exec ExecMethod - selected *Command - args []string + name string + shortUsage string + shortHelp string + longHelp string + options []ff.Option + cfg Config + flagSet *flag.FlagSet + subcommands []*Command + exec ExecMethod + selected *Command + args []string + noParentFlags bool } func NewCommand( @@ -59,14 +62,15 @@ func NewCommand( exec ExecMethod, ) *Command { command := &Command{ - name: meta.Name, - shortUsage: meta.ShortUsage, - shortHelp: meta.ShortHelp, - longHelp: meta.LongHelp, - options: meta.Options, - flagSet: flag.NewFlagSet(meta.Name, flag.ContinueOnError), - exec: exec, - cfg: config, + name: meta.Name, + shortUsage: meta.ShortUsage, + shortHelp: meta.ShortHelp, + longHelp: meta.LongHelp, + options: meta.Options, + noParentFlags: meta.NoParentFlags, + flagSet: flag.NewFlagSet(meta.Name, flag.ContinueOnError), + exec: exec, + cfg: config, } if config != nil { @@ -77,11 +81,17 @@ func NewCommand( return command } +// SetOutput sets the destination for usage and error messages. +// If output is nil, [os.Stderr] is used. +func (c *Command) SetOutput(output io.Writer) { + c.flagSet.SetOutput(output) +} + // AddSubCommands adds a variable number of subcommands // and registers common flags using the flagset func (c *Command) AddSubCommands(cmds ...*Command) { for _, cmd := range cmds { - if c.cfg != nil { + if c.cfg != nil && !cmd.noParentFlags { // Register the parent flagset with the child. // The syntax is not intuitive, but the flagset being // modified is the subcommand's, using the flags defined diff --git a/tm2/pkg/crypto/mock/mock.go b/tm2/pkg/crypto/mock/mock.go index 9ea1c5d66dc..b3fe8c5e69f 100644 --- a/tm2/pkg/crypto/mock/mock.go +++ b/tm2/pkg/crypto/mock/mock.go @@ -42,7 +42,7 @@ func (privKey PrivKeyMock) Equals(other crypto.PrivKey) bool { func GenPrivKey() PrivKeyMock { randstr := random.RandStr(12) - return PrivKeyMock([]byte(randstr)) + return []byte(randstr) } // ------------------------------------- diff --git a/tm2/pkg/iavl/tree_fuzz_test.go b/tm2/pkg/iavl/tree_fuzz_test.go index 08645414fbf..ba709cc9da2 100644 --- a/tm2/pkg/iavl/tree_fuzz_test.go +++ b/tm2/pkg/iavl/tree_fuzz_test.go @@ -1,8 +1,14 @@ package iavl import ( + "encoding/json" "fmt" + "io" + "io/fs" "math/rand" + "os" + "path/filepath" + "strings" "testing" "github.com/gnolang/gno/tm2/pkg/db/memdb" @@ -14,28 +20,36 @@ import ( // A program is a list of instructions. type program struct { - instructions []instruction + Instructions []instruction `json:"instructions"` } func (p *program) Execute(tree *MutableTree) (err error) { var errLine int defer func() { - if r := recover(); r != nil { - var str string - - for i, instr := range p.instructions { - prefix := " " - if i == errLine { - prefix = ">> " - } - str += prefix + instr.String() + "\n" + r := recover() + if r == nil { + return + } + + // These are simply input errors and shouldn't be reported as actual logical issues. + if containsAny(fmt.Sprint(r), "Unrecognized op:", "Attempt to store nil value at key") { + return + } + + var str string + + for i, instr := range p.Instructions { + prefix := " " + if i == errLine { + prefix = ">> " } - err = fmt.Errorf("Program panicked with: %s\n%s", r, str) + str += prefix + instr.String() + "\n" } + err = fmt.Errorf("Program panicked with: %s\n%s", r, str) }() - for i, instr := range p.instructions { + for i, instr := range p.Instructions { errLine = i instr.Execute(tree) } @@ -43,39 +57,39 @@ func (p *program) Execute(tree *MutableTree) (err error) { } func (p *program) addInstruction(i instruction) { - p.instructions = append(p.instructions, i) + p.Instructions = append(p.Instructions, i) } func (p *program) size() int { - return len(p.instructions) + return len(p.Instructions) } type instruction struct { - op string - k, v []byte - version int64 + Op string + K, V []byte + Version int64 } func (i instruction) Execute(tree *MutableTree) { - switch i.op { + switch i.Op { case "SET": - tree.Set(i.k, i.v) + tree.Set(i.K, i.V) case "REMOVE": - tree.Remove(i.k) + tree.Remove(i.K) case "SAVE": tree.SaveVersion() case "DELETE": - tree.DeleteVersion(i.version) + tree.DeleteVersion(i.Version) default: - panic("Unrecognized op: " + i.op) + panic("Unrecognized op: " + i.Op) } } func (i instruction) String() string { - if i.version > 0 { - return fmt.Sprintf("%-8s %-8s %-8s %-8d", i.op, i.k, i.v, i.version) + if i.Version > 0 { + return fmt.Sprintf("%-8s %-8s %-8s %-8d", i.Op, i.K, i.V, i.Version) } - return fmt.Sprintf("%-8s %-8s %-8s", i.op, i.k, i.v) + return fmt.Sprintf("%-8s %-8s %-8s", i.Op, i.K, i.V) } // Generate a random program of the given size. @@ -88,15 +102,15 @@ func genRandomProgram(size int) *program { switch rand.Int() % 7 { case 0, 1, 2: - p.addInstruction(instruction{op: "SET", k: k, v: v}) + p.addInstruction(instruction{Op: "SET", K: k, V: v}) case 3, 4: - p.addInstruction(instruction{op: "REMOVE", k: k}) + p.addInstruction(instruction{Op: "REMOVE", K: k}) case 5: - p.addInstruction(instruction{op: "SAVE", version: int64(nextVersion)}) + p.addInstruction(instruction{Op: "SAVE", Version: int64(nextVersion)}) nextVersion++ case 6: if rv := rand.Int() % nextVersion; rv < nextVersion && rv > 0 { - p.addInstruction(instruction{op: "DELETE", version: int64(rv)}) + p.addInstruction(instruction{Op: "DELETE", Version: int64(rv)}) } } } @@ -107,19 +121,174 @@ func genRandomProgram(size int) *program { func TestMutableTreeFuzz(t *testing.T) { t.Parallel() + runThenGenerateMutableTreeFuzzSeeds(t, false) +} + +var pathForMutableTreeProgramSeeds = filepath.Join("testdata", "corpora", "mutable_tree_programs") + +func runThenGenerateMutableTreeFuzzSeeds(tb testing.TB, writeSeedsToFileSystem bool) { + tb.Helper() + + if testing.Short() { + tb.Skip("Running in -short mode") + } + maxIterations := testFuzzIterations progsPerIteration := 100000 iterations := 0 + if writeSeedsToFileSystem { + if err := os.MkdirAll(pathForMutableTreeProgramSeeds, 0o755); err != nil { + tb.Fatal(err) + } + } + for size := 5; iterations < maxIterations; size++ { for i := 0; i < progsPerIteration/size; i++ { tree := NewMutableTree(memdb.NewMemDB(), 0) program := genRandomProgram(size) err := program.Execute(tree) if err != nil { - t.Fatalf("Error after %d iterations (size %d): %s\n%s", iterations, size, err.Error(), tree.String()) + tb.Fatalf("Error after %d iterations (size %d): %s\n%s", iterations, size, err.Error(), tree.String()) } iterations++ + + if !writeSeedsToFileSystem { + continue + } + + // Otherwise write them to the testdata/corpra directory. + programJSON, err := json.Marshal(program) + if err != nil { + tb.Fatal(err) + } + path := filepath.Join(pathForMutableTreeProgramSeeds, fmt.Sprintf("%d", i+1)) + if err := os.WriteFile(path, programJSON, 0o755); err != nil { + tb.Fatal(err) + } + } + } +} + +type treeRange struct { + Start []byte + End []byte + Forward bool +} + +var basicRecords = []struct { + key, value string +}{ + {"abc", "123"}, + {"low", "high"}, + {"fan", "456"}, + {"foo", "a"}, + {"foobaz", "c"}, + {"good", "bye"}, + {"foobang", "d"}, + {"foobar", "b"}, + {"food", "e"}, + {"foml", "f"}, +} + +// Allows hooking into Go's fuzzers and then for continuous fuzzing +// enriched with coverage guided mutations, instead of naive mutations. +func FuzzIterateRange(f *testing.F) { + if testing.Short() { + f.Skip("Skipping in -short mode") + } + + // 1. Add the seeds. + seeds := []*treeRange{ + {[]byte("foo"), []byte("goo"), true}, + {[]byte("aaa"), []byte("abb"), true}, + {nil, []byte("flap"), true}, + {[]byte("foob"), nil, true}, + {[]byte("very"), nil, true}, + {[]byte("very"), nil, false}, + {[]byte("fooba"), []byte("food"), true}, + {[]byte("fooba"), []byte("food"), false}, + {[]byte("g"), nil, false}, + } + for _, seed := range seeds { + blob, err := json.Marshal(seed) + if err != nil { + f.Fatal(err) + } + f.Add(blob) + } + + db := memdb.NewMemDB() + tree := NewMutableTree(db, 0) + for _, br := range basicRecords { + tree.Set([]byte(br.key), []byte(br.value)) + } + + var trav traverser + + // 2. Run the fuzzer. + f.Fuzz(func(t *testing.T, rangeJSON []byte) { + tr := new(treeRange) + if err := json.Unmarshal(rangeJSON, tr); err != nil { + return + } + + tree.IterateRange(tr.Start, tr.End, tr.Forward, trav.view) + }) +} + +func containsAny(s string, anyOf ...string) bool { + for _, q := range anyOf { + if strings.Contains(s, q) { + return true + } + } + return false +} + +func FuzzMutableTreeInstructions(f *testing.F) { + if testing.Short() { + f.Skip("Skipping in -short mode") + } + + // 0. Generate then add the seeds. + runThenGenerateMutableTreeFuzzSeeds(f, true) + + // 1. Add the seeds. + dir := os.DirFS("testdata") + err := fs.WalkDir(dir, ".", func(path string, de fs.DirEntry, err error) error { + if de.IsDir() { + return err + } + + ff, err := dir.Open(path) + if err != nil { + return err + } + defer ff.Close() + + blob, err := io.ReadAll(ff) + if err != nil { + return err } + f.Add(blob) + return nil + }) + if err != nil { + f.Fatal(err) } + + // 2. Run the fuzzer. + f.Fuzz(func(t *testing.T, programJSON []byte) { + program := new(program) + if err := json.Unmarshal(programJSON, program); err != nil { + return + } + + tree := NewMutableTree(memdb.NewMemDB(), 0) + err := program.Execute(tree) + if err != nil { + t.Fatal(err) + } + }) } diff --git a/tm2/pkg/internal/p2p/p2p.go b/tm2/pkg/internal/p2p/p2p.go index 1e650e0cd25..0c8f1529b85 100644 --- a/tm2/pkg/internal/p2p/p2p.go +++ b/tm2/pkg/internal/p2p/p2p.go @@ -70,12 +70,12 @@ func MakeConnectedPeers( VersionSet: versionset.VersionSet{ versionset.VersionInfo{Name: "p2p", Version: "v0.0.0"}, }, - PeerID: key.ID(), - Network: "testing", - Software: "p2ptest", - Version: "v1.2.3-rc.0-deadbeef", - Channels: cfg.Channels, - Moniker: fmt.Sprintf("node-%d", index), + NetAddress: addr, + Network: "testing", + Software: "p2ptest", + Version: "v1.2.3-rc.0-deadbeef", + Channels: cfg.Channels, + Moniker: fmt.Sprintf("node-%d", index), Other: p2pTypes.NodeInfoOther{ TxIndex: "off", RPCAddress: fmt.Sprintf("127.0.0.1:%d", 0), @@ -231,7 +231,7 @@ func (mp *Peer) TrySend(_ byte, _ []byte) bool { return true } func (mp *Peer) Send(_ byte, _ []byte) bool { return true } func (mp *Peer) NodeInfo() p2pTypes.NodeInfo { return p2pTypes.NodeInfo{ - PeerID: mp.id, + NetAddress: mp.addr, } } func (mp *Peer) Status() conn.ConnectionStatus { return conn.ConnectionStatus{} } diff --git a/tm2/pkg/overflow/overflow_impl.go b/tm2/pkg/overflow/overflow_impl.go index 0f057f65387..ab9f13c163d 100644 --- a/tm2/pkg/overflow/overflow_impl.go +++ b/tm2/pkg/overflow/overflow_impl.go @@ -84,10 +84,9 @@ func Quotient8(a, b int8) (int8, int8, bool) { } c := a / b status := (c < 0) == ((a < 0) != (b < 0)) || (c == 0) // no sign check for 0 quotient - return c, a%b, status + return c, a % b, status } - // Add16 performs + operation on two int16 operands, returning a result and status. func Add16(a, b int16) (int16, bool) { c := a + b @@ -170,10 +169,9 @@ func Quotient16(a, b int16) (int16, int16, bool) { } c := a / b status := (c < 0) == ((a < 0) != (b < 0)) || (c == 0) // no sign check for 0 quotient - return c, a%b, status + return c, a % b, status } - // Add32 performs + operation on two int32 operands, returning a result and status. func Add32(a, b int32) (int32, bool) { c := a + b @@ -256,10 +254,9 @@ func Quotient32(a, b int32) (int32, int32, bool) { } c := a / b status := (c < 0) == ((a < 0) != (b < 0)) || (c == 0) // no sign check for 0 quotient - return c, a%b, status + return c, a % b, status } - // Add64 performs + operation on two int64 operands, returning a result and status. func Add64(a, b int64) (int64, bool) { c := a + b @@ -342,6 +339,5 @@ func Quotient64(a, b int64) (int64, int64, bool) { } c := a / b status := (c < 0) == ((a < 0) != (b < 0)) || (c == 0) // no sign check for 0 quotient - return c, a%b, status + return c, a % b, status } - diff --git a/tm2/pkg/p2p/discovery/discovery.go b/tm2/pkg/p2p/discovery/discovery.go index d884b118c75..7a9da3726c0 100644 --- a/tm2/pkg/p2p/discovery/discovery.go +++ b/tm2/pkg/p2p/discovery/discovery.go @@ -160,7 +160,7 @@ func (r *Reactor) Receive(chID byte, peer p2p.PeerConn, msgBytes []byte) { // Validate the message if err := msg.ValidateBasic(); err != nil { - r.Logger.Error("unable to validate discovery message", "err", err) + r.Logger.Warn("unable to validate discovery message", "err", err) return } @@ -168,7 +168,7 @@ func (r *Reactor) Receive(chID byte, peer p2p.PeerConn, msgBytes []byte) { switch msg := msg.(type) { case *Request: if err := r.handleDiscoveryRequest(peer); err != nil { - r.Logger.Error("unable to handle discovery request", "err", err) + r.Logger.Warn("unable to handle discovery request", "err", err) } case *Response: // Make the peers available for dialing on the switch @@ -186,9 +186,21 @@ func (r *Reactor) handleDiscoveryRequest(peer p2p.PeerConn) error { peers = make([]*types.NetAddress, 0, len(localPeers)) ) - // Exclude the private peers from being shared + // Exclude the private peers from being shared, + // as well as peers who are not dialable localPeers = slices.DeleteFunc(localPeers, func(p p2p.PeerConn) bool { - return p.IsPrivate() + var ( + // Private peers are peers whose information is kept private to the node + privatePeer = p.IsPrivate() + // The reason we don't validate the net address with .Routable() + // is because of legacy logic that supports local loopbacks as advertised + // peer addresses. Introducing a .Routable() constraint will filter all + // local loopback addresses shared by peers, and will cause local deployments + // (and unit test deployments) to break and require additional setup + invalidDialAddress = p.NodeInfo().DialAddress().Validate() != nil + ) + + return privatePeer || invalidDialAddress }) // Check if there is anything to share, @@ -207,7 +219,8 @@ func (r *Reactor) handleDiscoveryRequest(peer p2p.PeerConn) error { } for _, p := range localPeers { - peers = append(peers, p.SocketAddr()) + // Make sure only routable peers are shared + peers = append(peers, p.NodeInfo().DialAddress()) } // Create the response, and marshal diff --git a/tm2/pkg/p2p/discovery/discovery_test.go b/tm2/pkg/p2p/discovery/discovery_test.go index 17404e6039a..91741c648db 100644 --- a/tm2/pkg/p2p/discovery/discovery_test.go +++ b/tm2/pkg/p2p/discovery/discovery_test.go @@ -166,7 +166,7 @@ func TestReactor_DiscoveryResponse(t *testing.T) { slices.ContainsFunc(resp.Peers, func(addr *types.NetAddress) bool { for _, localP := range peers { - if localP.SocketAddr().Equals(*addr) { + if localP.NodeInfo().DialAddress().Equals(*addr) { return true } } @@ -317,7 +317,7 @@ func TestReactor_DiscoveryResponse(t *testing.T) { slices.ContainsFunc(resp.Peers, func(addr *types.NetAddress) bool { for _, localP := range peers { - if localP.SocketAddr().Equals(*addr) { + if localP.NodeInfo().DialAddress().Equals(*addr) { return true } } @@ -373,7 +373,7 @@ func TestReactor_DiscoveryResponse(t *testing.T) { peerAddrs := make([]*types.NetAddress, 0, len(peers)) for _, p := range peers { - peerAddrs = append(peerAddrs, p.SocketAddr()) + peerAddrs = append(peerAddrs, p.NodeInfo().DialAddress()) } // Prepare the message diff --git a/tm2/pkg/p2p/mock/peer.go b/tm2/pkg/p2p/mock/peer.go index e5a01952831..5be34121924 100644 --- a/tm2/pkg/p2p/mock/peer.go +++ b/tm2/pkg/p2p/mock/peer.go @@ -57,7 +57,7 @@ func GeneratePeers(t *testing.T, count int) []*Peer { }, NodeInfoFn: func() types.NodeInfo { return types.NodeInfo{ - PeerID: key.ID(), + NetAddress: addr, } }, SocketAddrFn: func() *types.NetAddress { diff --git a/tm2/pkg/p2p/peer.go b/tm2/pkg/p2p/peer.go index 135bf4b250c..dcca81ca097 100644 --- a/tm2/pkg/p2p/peer.go +++ b/tm2/pkg/p2p/peer.go @@ -160,7 +160,7 @@ func (p *peer) OnStop() { // ID returns the peer's ID - the hex encoded hash of its pubkey. func (p *peer) ID() types.ID { - return p.nodeInfo.PeerID + return p.nodeInfo.ID() } // NodeInfo returns a copy of the peer's NodeInfo. diff --git a/tm2/pkg/p2p/peer_test.go b/tm2/pkg/p2p/peer_test.go index a74ea9e96a4..75f5172ee66 100644 --- a/tm2/pkg/p2p/peer_test.go +++ b/tm2/pkg/p2p/peer_test.go @@ -243,7 +243,9 @@ func TestPeer_Properties(t *testing.T) { }, }, nodeInfo: types.NodeInfo{ - PeerID: id, + NetAddress: &types.NetAddress{ + ID: id, + }, }, connInfo: &ConnInfo{ Outbound: testCase.outbound, diff --git a/tm2/pkg/p2p/switch.go b/tm2/pkg/p2p/switch.go index 0dd087026dd..c96e429973e 100644 --- a/tm2/pkg/p2p/switch.go +++ b/tm2/pkg/p2p/switch.go @@ -1,11 +1,13 @@ package p2p import ( + "bytes" "context" "crypto/rand" + "encoding/binary" + "errors" "fmt" "math" - "math/big" "sync" "time" @@ -356,7 +358,7 @@ func (sw *MultiplexSwitch) runRedialLoop(ctx context.Context) { type backoffItem struct { lastDialTime time.Time - attempts int + attempts uint } var ( @@ -405,57 +407,76 @@ func (sw *MultiplexSwitch) runRedialLoop(ctx context.Context) { peersToDial = make([]*types.NetAddress, 0) ) + // Gather addresses of persistent peers that are missing or + // not already in the dial queue sw.persistentPeers.Range(func(key, value any) bool { var ( id = key.(types.ID) addr = value.(*types.NetAddress) ) - // Check if the peer is part of the peer set - // or is scheduled for dialing - if peers.Has(id) || sw.dialQueue.Has(addr) { - return true + if !peers.Has(id) && !sw.dialQueue.Has(addr) { + peersToDial = append(peersToDial, addr) } - peersToDial = append(peersToDial, addr) - return true }) if len(peersToDial) == 0 { - // No persistent peers are missing + // No persistent peers need dialing return } - // Calculate the dial items + // Prepare dial items with the appropriate backoff dialItems := make([]dial.Item, 0, len(peersToDial)) - for _, p := range peersToDial { - item := getBackoffItem(p.ID) + for _, addr := range peersToDial { + item := getBackoffItem(addr.ID) + if item == nil { - dialItem := dial.Item{ - Time: time.Now(), - Address: p, - } + // First attempt + now := time.Now() + + dialItems = append(dialItems, + dial.Item{ + Time: now, + Address: addr, + }, + ) - dialItems = append(dialItems, dialItem) - setBackoffItem(p.ID, &backoffItem{dialItem.Time, 0}) + setBackoffItem(addr.ID, &backoffItem{ + lastDialTime: now, + attempts: 0, + }) continue } - setBackoffItem(p.ID, &backoffItem{ - lastDialTime: time.Now().Add( + // Subsequent attempt: apply backoff + var ( + attempts = item.attempts + 1 + dialTime = time.Now().Add( calculateBackoff( item.attempts, time.Second, 10*time.Minute, ), - ), - attempts: item.attempts + 1, + ) + ) + + dialItems = append(dialItems, + dial.Item{ + Time: dialTime, + Address: addr, + }, + ) + + setBackoffItem(addr.ID, &backoffItem{ + lastDialTime: dialTime, + attempts: attempts, }) } - // Add the peers to the dial queue + // Add these items to the dial queue sw.dialItems(dialItems...) } @@ -482,65 +503,68 @@ func (sw *MultiplexSwitch) runRedialLoop(ctx context.Context) { } } -// calculateBackoff calculates a backoff time, -// based on the number of attempts and range limits +// calculateBackoff calculates the backoff interval by exponentiating the base interval +// by the number of attempts. The returned interval is capped at maxInterval and has a +// jitter factor applied to it (+/- 10% of interval, max 10 sec). func calculateBackoff( - attempts int, - minTimeout time.Duration, - maxTimeout time.Duration, + attempts uint, + baseInterval time.Duration, + maxInterval time.Duration, ) time.Duration { - var ( - minTime = time.Second * 1 - maxTime = time.Second * 60 - multiplier = float64(2) // exponential + const ( + defaultBaseInterval = time.Second * 1 + defaultMaxInterval = time.Second * 60 ) - // Check the min limit - if minTimeout > 0 { - minTime = minTimeout + // Sanitize base interval parameter. + if baseInterval <= 0 { + baseInterval = defaultBaseInterval } - // Check the max limit - if maxTimeout > 0 { - maxTime = maxTimeout + // Sanitize max interval parameter. + if maxInterval <= 0 { + maxInterval = defaultMaxInterval } - // Sanity check the range - if minTime >= maxTime { - return maxTime + // Calculate the interval by exponentiating the base interval by the number of attempts. + interval := baseInterval << attempts + + // Cap the interval to the maximum interval. + if interval > maxInterval { + interval = maxInterval } - // Calculate the backoff duration - var ( - base = float64(minTime) - calculated = base * math.Pow(multiplier, float64(attempts)) - ) + // Below is the code to add a jitter factor to the interval. + // Read random bytes into an 8 bytes buffer (size of an int64). + var randBytes [8]byte + if _, err := rand.Read(randBytes[:]); err != nil { + return interval + } - // Attempt to calculate the jitter factor - n, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) - if err == nil { - jitterFactor := float64(n.Int64()) / float64(math.MaxInt64) // range [0, 1] + // Convert the random bytes to an int64. + var randInt64 int64 + _ = binary.Read(bytes.NewReader(randBytes[:]), binary.NativeEndian, &randInt64) - calculated = jitterFactor*(calculated-base) + base - } + // Calculate the random jitter multiplier (float between -1 and 1). + jitterMultiplier := float64(randInt64) / float64(math.MaxInt64) - // Prevent overflow for int64 (duration) cast - if calculated > float64(math.MaxInt64) { - return maxTime - } + const ( + maxJitterDuration = 10 * time.Second + maxJitterPercentage = 10 // 10% + ) - duration := time.Duration(calculated) + // Calculate the maximum jitter based on interval percentage. + maxJitter := interval * maxJitterPercentage / 100 - // Clamp the duration within bounds - if duration < minTime { - return minTime + // Cap the maximum jitter to the maximum duration. + if maxJitter > maxJitterDuration { + maxJitter = maxJitterDuration } - if duration > maxTime { - return maxTime - } + // Calculate the jitter. + jitter := time.Duration(float64(maxJitter) * jitterMultiplier) - return duration + return interval + jitter } // DialPeers adds the peers to the dial queue for async dialing. @@ -618,50 +642,50 @@ func (sw *MultiplexSwitch) isPrivatePeer(id types.ID) bool { // and persisting them func (sw *MultiplexSwitch) runAcceptLoop(ctx context.Context) { for { - select { - case <-ctx.Done(): - sw.Logger.Debug("switch context close received") + p, err := sw.transport.Accept(ctx, sw.peerBehavior) - return + switch { + case err == nil: // ok + case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded): + // Upper context as been canceled/timeout + sw.Logger.Debug("switch context close received") + return // exit + case errors.As(err, &errTransportClosed): + // Underlaying transport as been closed + sw.Logger.Warn("cannot accept connection on closed transport, exiting") + return // exit default: - p, err := sw.transport.Accept(ctx, sw.peerBehavior) - if err != nil { - sw.Logger.Error( - "error encountered during peer connection accept", - "err", err, - ) + // An error occurred during accept, report and continue + sw.Logger.Error("error encountered during peer connection accept", "err", err) + continue + } - continue - } + // Ignore connection if we already have enough peers. + if in := sw.Peers().NumInbound(); in >= sw.maxInboundPeers { + sw.Logger.Info( + "Ignoring inbound connection: already have enough inbound peers", + "address", p.SocketAddr(), + "have", in, + "max", sw.maxInboundPeers, + ) - // Ignore connection if we already have enough peers. - if in := sw.Peers().NumInbound(); in >= sw.maxInboundPeers { - sw.Logger.Info( - "Ignoring inbound connection: already have enough inbound peers", - "address", p.SocketAddr(), - "have", in, - "max", sw.maxInboundPeers, - ) + sw.transport.Remove(p) + continue + } - sw.transport.Remove(p) + // There are open peer slots, add peers + if err := sw.addPeer(p); err != nil { + sw.transport.Remove(p) - continue + if p.IsRunning() { + _ = p.Stop() } - // There are open peer slots, add peers - if err := sw.addPeer(p); err != nil { - sw.transport.Remove(p) - - if p.IsRunning() { - _ = p.Stop() - } - - sw.Logger.Info( - "Ignoring inbound connection: error while adding peer", - "err", err, - "id", p.ID(), - ) - } + sw.Logger.Info( + "Ignoring inbound connection: error while adding peer", + "err", err, + "id", p.ID(), + ) } } } diff --git a/tm2/pkg/p2p/switch_test.go b/tm2/pkg/p2p/switch_test.go index 19a5db2efa5..e5f472cc28e 100644 --- a/tm2/pkg/p2p/switch_test.go +++ b/tm2/pkg/p2p/switch_test.go @@ -727,7 +727,7 @@ func TestMultiplexSwitch_DialPeers(t *testing.T) { // as the transport (node) p.NodeInfoFn = func() types.NodeInfo { return types.NodeInfo{ - PeerID: addr.ID, + NetAddress: &addr, } } @@ -823,3 +823,101 @@ func TestMultiplexSwitch_DialPeers(t *testing.T) { } }) } + +func TestCalculateBackoff(t *testing.T) { + t.Parallel() + + checkJitterRange := func(t *testing.T, expectedAbs, actual time.Duration) { + t.Helper() + require.LessOrEqual(t, actual, expectedAbs) + require.GreaterOrEqual(t, actual, expectedAbs*-1) + } + + // Test that the default jitter factor is 10% of the backoff duration. + t.Run("percentage jitter", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 100*time.Millisecond, calculateBackoff(0, time.Second, 10*time.Minute)-time.Second) + checkJitterRange(t, 200*time.Millisecond, calculateBackoff(1, time.Second, 10*time.Minute)-2*time.Second) + checkJitterRange(t, 400*time.Millisecond, calculateBackoff(2, time.Second, 10*time.Minute)-4*time.Second) + checkJitterRange(t, 800*time.Millisecond, calculateBackoff(3, time.Second, 10*time.Minute)-8*time.Second) + checkJitterRange(t, 1600*time.Millisecond, calculateBackoff(4, time.Second, 10*time.Minute)-16*time.Second) + } + }) + + // Test that the jitter factor is capped at 10 sec. + t.Run("capped jitter", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 10*time.Second, calculateBackoff(7, time.Second, 10*time.Minute)-128*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(10, time.Second, 20*time.Minute)-1024*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(20, time.Second, 300*time.Hour)-1048576*time.Second) + } + }) + + // Test that the backoff interval is based on the baseInterval. + t.Run("base interval", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 4800*time.Millisecond, calculateBackoff(4, 3*time.Second, 10*time.Minute)-48*time.Second) + checkJitterRange(t, 8*time.Second, calculateBackoff(3, 10*time.Second, 10*time.Minute)-80*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(5, 3*time.Hour, 100*time.Hour)-96*time.Hour) + } + }) + + // Test that the backoff interval is capped at maxInterval +/- jitter factor. + t.Run("max interval", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 100*time.Millisecond, calculateBackoff(10, 10*time.Hour, time.Second)-time.Second) + checkJitterRange(t, 1600*time.Millisecond, calculateBackoff(10, 10*time.Hour, 16*time.Second)-16*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(10, 10*time.Hour, 128*time.Second)-128*time.Second) + } + }) + + // Test parameters sanitization for base and max intervals. + t.Run("parameters sanitization", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 1000; i++ { + checkJitterRange(t, 100*time.Millisecond, calculateBackoff(0, -10, -10)-time.Second) + checkJitterRange(t, 1600*time.Millisecond, calculateBackoff(4, -10, -10)-16*time.Second) + checkJitterRange(t, 10*time.Second, calculateBackoff(7, -10, 10*time.Minute)-128*time.Second) + } + }) +} + +func TestSwitchAcceptLoopTransportClosed(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var transportClosed bool + mockTransport := &mockTransport{ + acceptFn: func(context.Context, PeerBehavior) (PeerConn, error) { + transportClosed = true + return nil, errTransportClosed + }, + } + + sw := NewMultiplexSwitch(mockTransport) + + // Run the accept loop + done := make(chan struct{}) + go func() { + sw.runAcceptLoop(ctx) + close(done) // signal that accept loop as ended + }() + + select { + case <-time.After(time.Second * 2): + require.FailNow(t, "timeout while waiting for running loop to stop") + case <-done: + assert.True(t, transportClosed) + } +} diff --git a/tm2/pkg/p2p/transport.go b/tm2/pkg/p2p/transport.go index 150072ad5eb..3d64a48f437 100644 --- a/tm2/pkg/p2p/transport.go +++ b/tm2/pkg/p2p/transport.go @@ -2,6 +2,7 @@ package p2p import ( "context" + goerrors "errors" "fmt" "io" "log/slog" @@ -22,7 +23,6 @@ const defaultHandshakeTimeout = 3 * time.Second var ( errTransportClosed = errors.New("transport is closed") - errTransportInactive = errors.New("transport is inactive") errDuplicateConnection = errors.New("duplicate peer connection") errPeerIDNodeInfoMismatch = errors.New("connection ID does not match node info ID") errPeerIDDialMismatch = errors.New("connection ID does not match dialed ID") @@ -75,7 +75,10 @@ func NewMultiplexTransport( mConfig conn.MConnConfig, logger *slog.Logger, ) *MultiplexTransport { + ctx, cancel := context.WithCancel(context.Background()) return &MultiplexTransport{ + ctx: ctx, + cancelFn: cancel, peerCh: make(chan peerInfo, 1), mConfig: mConfig, nodeInfo: nodeInfo, @@ -92,12 +95,6 @@ func (mt *MultiplexTransport) NetAddress() types.NetAddress { // Accept waits for a verified inbound Peer to connect, and returns it [BLOCKING] func (mt *MultiplexTransport) Accept(ctx context.Context, behavior PeerBehavior) (PeerConn, error) { - // Sanity check, no need to wait - // on an inactive transport - if mt.listener == nil { - return nil, errTransportInactive - } - select { case <-ctx.Done(): return nil, ctx.Err() @@ -147,39 +144,31 @@ func (mt *MultiplexTransport) Close() error { } // Listen starts an active process of listening for incoming connections [NON-BLOCKING] -func (mt *MultiplexTransport) Listen(addr types.NetAddress) (rerr error) { +func (mt *MultiplexTransport) Listen(addr types.NetAddress) error { // Reserve a port, and start listening ln, err := net.Listen("tcp", addr.DialString()) if err != nil { return fmt.Errorf("unable to listen on address, %w", err) } - defer func() { - if rerr != nil { - ln.Close() - } - }() - if addr.Port == 0 { // net.Listen on port 0 means the kernel will auto-allocate a port // - find out which one has been given to us. tcpAddr, ok := ln.Addr().(*net.TCPAddr) if !ok { + ln.Close() return fmt.Errorf("error finding port (after listening on port 0): %w", err) } addr.Port = uint16(tcpAddr.Port) } - // Set up the context - mt.ctx, mt.cancelFn = context.WithCancel(context.Background()) - mt.netAddr = addr mt.listener = ln // Run the routine for accepting // incoming peer connections - go mt.runAcceptLoop() + go mt.runAcceptLoop(mt.ctx) return nil } @@ -189,60 +178,58 @@ func (mt *MultiplexTransport) Listen(addr types.NetAddress) (rerr error) { // 1. accepted by the transport // 2. filtered // 3. upgraded (handshaked + verified) -func (mt *MultiplexTransport) runAcceptLoop() { +func (mt *MultiplexTransport) runAcceptLoop(ctx context.Context) { var wg sync.WaitGroup - defer func() { wg.Wait() // Wait for all process routines - close(mt.peerCh) }() - for { - select { - case <-mt.ctx.Done(): - mt.logger.Debug("transport accept context closed") + ctx, cancel := context.WithCancel(ctx) + defer cancel() // cancel sub-connection process - return + for { + // Accept an incoming peer connection + c, err := mt.listener.Accept() + + switch { + case err == nil: // ok + case goerrors.Is(err, net.ErrClosed): + // Listener has been closed, this is not recoverable. + mt.logger.Debug("listener has been closed") + return // exit default: - // Accept an incoming peer connection - c, err := mt.listener.Accept() + // An error occurred during accept, report and continue + mt.logger.Warn("accept p2p connection error", "err", err) + continue + } + + // Process the new connection asynchronously + wg.Add(1) + + go func(c net.Conn) { + defer wg.Done() + + info, err := mt.processConn(c, "") if err != nil { mt.logger.Error( - "unable to accept p2p connection", + "unable to process p2p connection", "err", err, ) - continue - } - - // Process the new connection asynchronously - wg.Add(1) + // Close the connection + _ = c.Close() - go func(c net.Conn) { - defer wg.Done() - - info, err := mt.processConn(c, "") - if err != nil { - mt.logger.Error( - "unable to process p2p connection", - "err", err, - ) - - // Close the connection - _ = c.Close() - - return - } + return + } - select { - case mt.peerCh <- info: - case <-mt.ctx.Done(): - // Give up if the transport was closed. - _ = c.Close() - } - }(c) - } + select { + case mt.peerCh <- info: + case <-ctx.Done(): + // Give up if the transport was closed. + _ = c.Close() + } + }(c) } } diff --git a/tm2/pkg/p2p/transport_test.go b/tm2/pkg/p2p/transport_test.go index 3eb3264ec2b..840eb974e76 100644 --- a/tm2/pkg/p2p/transport_test.go +++ b/tm2/pkg/p2p/transport_test.go @@ -122,14 +122,12 @@ func TestMultiplexTransport_Accept(t *testing.T) { transport := NewMultiplexTransport(ni, nk, mCfg, logger) - p, err := transport.Accept(context.Background(), nil) + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + p, err := transport.Accept(ctx, nil) assert.Nil(t, p) - assert.ErrorIs( - t, - err, - errTransportInactive, - ) + assert.ErrorIs(t, err, context.DeadlineExceeded) }) t.Run("transport closed", func(t *testing.T) { @@ -239,7 +237,7 @@ func TestMultiplexTransport_Accept(t *testing.T) { ni := types.NodeInfo{ Network: network, // common network - PeerID: id, + NetAddress: na, Version: "v1.0.0-rc.0", Moniker: fmt.Sprintf("node-%d", index), VersionSet: make(versionset.VersionSet, 0), // compatible version set @@ -319,7 +317,7 @@ func TestMultiplexTransport_Accept(t *testing.T) { ni := types.NodeInfo{ Network: chainID, - PeerID: id, + NetAddress: na, Version: "v1.0.0-rc.0", Moniker: fmt.Sprintf("node-%d", index), VersionSet: make(versionset.VersionSet, 0), // compatible version set @@ -391,7 +389,7 @@ func TestMultiplexTransport_Accept(t *testing.T) { ni := types.NodeInfo{ Network: network, // common network - PeerID: key.ID(), + NetAddress: na, Version: "v1.0.0-rc.0", Moniker: fmt.Sprintf("node-%d", index), VersionSet: make(versionset.VersionSet, 0), // compatible version set @@ -469,7 +467,7 @@ func TestMultiplexTransport_Accept(t *testing.T) { ni := types.NodeInfo{ Network: network, // common network - PeerID: key.ID(), + NetAddress: na, Version: "v1.0.0-rc.0", Moniker: fmt.Sprintf("node-%d", index), VersionSet: make(versionset.VersionSet, 0), // compatible version set diff --git a/tm2/pkg/p2p/types/node_info.go b/tm2/pkg/p2p/types/node_info.go index 8452cb43cb8..4080ff2d8aa 100644 --- a/tm2/pkg/p2p/types/node_info.go +++ b/tm2/pkg/p2p/types/node_info.go @@ -14,7 +14,6 @@ const ( ) var ( - ErrInvalidPeerID = errors.New("invalid peer ID") ErrInvalidVersion = errors.New("invalid node version") ErrInvalidMoniker = errors.New("invalid node moniker") ErrInvalidRPCAddress = errors.New("invalid node RPC address") @@ -30,8 +29,8 @@ type NodeInfo struct { // Set of protocol versions VersionSet versionset.VersionSet `json:"version_set"` - // Unique peer identifier - PeerID ID `json:"id"` + // The advertised net address of the peer + NetAddress *NetAddress `json:"net_address"` // Check compatibility. // Channels are HexBytes so easier to read as JSON @@ -54,12 +53,27 @@ type NodeInfoOther struct { // Validate checks the self-reported NodeInfo is safe. // It returns an error if there // are too many Channels, if there are any duplicate Channels, -// if the ListenAddr is malformed, or if the ListenAddr is a host name +// if the NetAddress is malformed, or if the NetAddress is a host name // that can not be resolved to some IP func (info NodeInfo) Validate() error { - // Validate the ID - if err := info.PeerID.Validate(); err != nil { - return fmt.Errorf("%w, %w", ErrInvalidPeerID, err) + // There are a few checks that need to be performed when validating + // the node info's net address: + // - the ID needs to be valid + // - the FORMAT of the net address needs to be valid + // + // The key nuance here is that the net address is not being validated + // for its "dialability", but whether it's of the correct format. + // + // Unspecified IPs are tolerated (ex. 0.0.0.0 or ::), + // because of legacy logic that assumes node info + // can have unspecified IPs (ex. no external address is set, use + // the listen address which is bound to 0.0.0.0). + // + // These types of IPs are caught during the + // real peer info sharing process, since they are undialable + _, err := NewNetAddressFromString(NetAddressString(info.NetAddress.ID, info.NetAddress.DialString())) + if err != nil { + return fmt.Errorf("invalid net address in node info, %w", err) } // Validate Version @@ -100,7 +114,12 @@ func (info NodeInfo) Validate() error { // ID returns the local node ID func (info NodeInfo) ID() ID { - return info.PeerID + return info.NetAddress.ID +} + +// DialAddress is the advertised peer dial address (share-able) +func (info NodeInfo) DialAddress() *NetAddress { + return info.NetAddress } // CompatibleWith checks if two NodeInfo are compatible with each other. diff --git a/tm2/pkg/p2p/types/node_info_test.go b/tm2/pkg/p2p/types/node_info_test.go index d03d77e608f..575d8ae5fbd 100644 --- a/tm2/pkg/p2p/types/node_info_test.go +++ b/tm2/pkg/p2p/types/node_info_test.go @@ -2,23 +2,43 @@ package types import ( "fmt" + "net" "testing" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/versionset" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNodeInfo_Validate(t *testing.T) { t.Parallel() + generateNetAddress := func() *NetAddress { + var ( + key = GenerateNodeKey() + address = "127.0.0.1:8080" + ) + + tcpAddr, err := net.ResolveTCPAddr("tcp", address) + require.NoError(t, err) + + addr, err := NewNetAddress(key.ID(), tcpAddr) + require.NoError(t, err) + + return addr + } + t.Run("invalid peer ID", func(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: "", // zero + NetAddress: &NetAddress{ + ID: "", // zero + }, } - assert.ErrorIs(t, info.Validate(), ErrInvalidPeerID) + assert.ErrorIs(t, info.Validate(), crypto.ErrZeroID) }) t.Run("invalid version", func(t *testing.T) { @@ -47,8 +67,8 @@ func TestNodeInfo_Validate(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: GenerateNodeKey().ID(), - Version: testCase.version, + NetAddress: generateNetAddress(), + Version: testCase.version, } assert.ErrorIs(t, info.Validate(), ErrInvalidVersion) @@ -86,8 +106,8 @@ func TestNodeInfo_Validate(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: GenerateNodeKey().ID(), - Moniker: testCase.moniker, + NetAddress: generateNetAddress(), + Moniker: testCase.moniker, } assert.ErrorIs(t, info.Validate(), ErrInvalidMoniker) @@ -121,8 +141,8 @@ func TestNodeInfo_Validate(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: GenerateNodeKey().ID(), - Moniker: "valid moniker", + NetAddress: generateNetAddress(), + Moniker: "valid moniker", Other: NodeInfoOther{ RPCAddress: testCase.rpcAddress, }, @@ -162,9 +182,9 @@ func TestNodeInfo_Validate(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: GenerateNodeKey().ID(), - Moniker: "valid moniker", - Channels: testCase.channels, + NetAddress: generateNetAddress(), + Moniker: "valid moniker", + Channels: testCase.channels, } assert.ErrorIs(t, info.Validate(), testCase.expectedErr) @@ -176,9 +196,9 @@ func TestNodeInfo_Validate(t *testing.T) { t.Parallel() info := &NodeInfo{ - PeerID: GenerateNodeKey().ID(), - Moniker: "valid moniker", - Channels: []byte{10, 20, 30}, + NetAddress: generateNetAddress(), + Moniker: "valid moniker", + Channels: []byte{10, 20, 30}, Other: NodeInfoOther{ RPCAddress: "0.0.0.0:26657", }, diff --git a/tm2/pkg/service/service.go b/tm2/pkg/service/service.go index 05f7a4f4ae6..c93eb06b298 100644 --- a/tm2/pkg/service/service.go +++ b/tm2/pkg/service/service.go @@ -159,7 +159,7 @@ func (bs *BaseService) OnStart() error { return nil } func (bs *BaseService) Stop() error { if atomic.CompareAndSwapUint32(&bs.stopped, 0, 1) { if atomic.LoadUint32(&bs.started) == 0 { - bs.Logger.Error(fmt.Sprintf("Not stopping %v -- have not been started yet", bs.name), "impl", bs.impl) + bs.Logger.Warn(fmt.Sprintf("Not stopping %v -- have not been started yet", bs.name), "impl", bs.impl) // revert flag atomic.StoreUint32(&bs.stopped, 0) return ErrNotStarted