diff --git a/.gitattributes b/.gitattributes index adc4144ffa3..13825940056 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,7 @@ go.sum linguist-generated text gnovm/stdlibs/generated.go linguist-generated gnovm/tests/stdlibs/generated.go linguist-generated +*.gen.gno linguist-generated +*.gen_test.gno linguist-generated +*.gen.go linguist-generated +*.gen_test.go linguist-generated \ No newline at end of file diff --git a/.github/.editorconfig b/.github/.editorconfig new file mode 100644 index 00000000000..751cd705457 --- /dev/null +++ b/.github/.editorconfig @@ -0,0 +1,8 @@ +# Make sure this is the top-level editorconfig +# https://editorconfig.org/ +root = true + +# GitHub Actions Workflows +[workflows/**.yml] +indent_style = space +indent_size = 2 diff --git a/.github/codecov.yml b/.github/codecov.yml index d1ecba7ade3..f0cb9583cf2 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -4,7 +4,7 @@ codecov: wait_for_ci: true comment: - require_changes: false + require_changes: true coverage: round: down @@ -13,7 +13,7 @@ coverage: project: default: target: auto - threshold: 10 # Let's decrease this later. + threshold: 5 # Let's decrease this later. base: parent if_no_uploads: error if_not_found: success @@ -22,12 +22,12 @@ coverage: patch: default: target: auto - threshold: 10 # Let's decrease this later. + threshold: 5 # Let's decrease this later. base: auto if_no_uploads: error if_not_found: success if_ci_failed: error - only_pulls: false + only_pulls: true # Only check patch coverage on PRs flag_management: default_rules: diff --git a/.github/golangci.yml b/.github/golangci.yml index b8bd5537135..afc581d2ec5 100644 --- a/.github/golangci.yml +++ b/.github/golangci.yml @@ -44,9 +44,11 @@ linters: linters-settings: gofmt: simplify: true + goconst: min-len: 3 min-occurrences: 3 + gosec: excludes: - G204 # Subprocess launched with a potential tainted input or cmd arguments @@ -56,6 +58,7 @@ linters-settings: checks: [ "all", "-ST1022", "-ST1003" ] errorlint: asserts: false + gocritic: enabled-tags: - diagnostic @@ -63,6 +66,7 @@ linters-settings: - opinionated - performance - style + forbidigo: forbid: - p: '^regexp\.(Match|MatchString)$' @@ -74,12 +78,14 @@ issues: max-same-issues: 0 new: false fix: false + exclude-rules: - path: _test\.go linters: - gosec # Disabled linting of weak number generators - makezero # Disabled linting of intentional slice appends - goconst # Disabled linting of common mnemonics and test case strings + - unused # Disabled linting of unused mock methods - path: _\.gno linters: - errorlint # Disabled linting of error comparisons, because of lacking std lib support diff --git a/.github/workflows/auto-author-assign.yml b/.github/workflows/auto-author-assign.yml index 06dfb4ab903..890e70da9ae 100644 --- a/.github/workflows/auto-author-assign.yml +++ b/.github/workflows/auto-author-assign.yml @@ -1,4 +1,4 @@ -name: auto-author-assign +name: Auto Assign PR Author on: pull_request_target: diff --git a/.github/workflows/autocounterd.yml b/.github/workflows/autocounterd.yml index 9217fe2eef2..dcba56178bd 100644 --- a/.github/workflows/autocounterd.yml +++ b/.github/workflows/autocounterd.yml @@ -1,19 +1,13 @@ -name: autocounterd +name: Portal Loop - autocounterd on: - pull_request: - branches: - - master push: + branches: + - "master" paths: - misc/autocounterd - misc/loop - .github/workflows/autocounterd.yml - branches: - - "master" - - "misc/autocounterd" - tags: - - "v*" permissions: contents: read diff --git a/.github/workflows/benchmark-master-push.yml b/.github/workflows/benchmark-master-push.yml index 622baefc0de..1c054077a3a 100644 --- a/.github/workflows/benchmark-master-push.yml +++ b/.github/workflows/benchmark-master-push.yml @@ -1,14 +1,14 @@ -name: run benchmarks when pushing on main branch +name: Run and Save Benchmarks on: push: branches: - master paths: - - contribs/** - - gno.land/** - - gnovm/** - - tm2/** + - contribs/**/*.go + - gno.land/**/*.go + - gnovm/**/*.go + - tm2/**/*.go permissions: # deployments permission to deploy GitHub pages website @@ -22,7 +22,7 @@ env: jobs: benchmarks: if: ${{ github.repository == 'gnolang/gno' }} - runs-on: [self-hosted, Linux, X64, benchmarks] + runs-on: [ self-hosted, Linux, X64, benchmarks ] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d2eef9d7445..4745788714d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,13 +9,17 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "CodeQL" +name: CodeQL on: push: branches: [ "master", "chain/*" ] pull_request: branches: [ "master", "chain/*" ] + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' schedule: - cron: '22 17 * * 3' @@ -41,8 +45,8 @@ jobs: fail-fast: false matrix: include: - - language: go - build-mode: autobuild + - language: go + build-mode: autobuild # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -52,38 +56,38 @@ jobs: # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - - name: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. - # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/contribs.yml b/.github/workflows/contribs.yml index 3739339f7be..c1de5e78c35 100644 --- a/.github/workflows/contribs.yml +++ b/.github/workflows/contribs.yml @@ -1,30 +1,54 @@ -name: contribs +name: Contribs on: push: branches: - master - workflow_dispatch: pull_request: + paths: + - contribs/** + workflow_dispatch: jobs: setup: runs-on: ubuntu-latest outputs: programs: ${{ steps.set-matrix.outputs.programs }} + go-versions: ${{ steps.get-go-versions.outputs.go-versions }} steps: - uses: actions/checkout@v4 + - id: set-matrix - run: echo "::set-output name=programs::$(ls -d contribs/*/ | cut -d/ -f2 | jq -R -s -c 'split("\n")[:-1]')" + run: | + echo "::set-output name=programs::$(ls -d contribs/*/ | cut -d/ -f2 | jq -R -s -c 'split("\n")[:-1]')" + + - id: get-go-versions + run: | + contribs_programs=$(ls -d contribs/*/ | cut -d/ -f2) + versions_map="{" + + for p in $contribs_programs; do + # Fetch the go version of the contribs entry, and save it + # to a versions map we can reference later in the workflow + go_version=$(grep "^go [0-9]" contribs/$p/go.mod | cut -d ' ' -f2) + versions_map="$versions_map\"$p\":\"$go_version\"," + done + + # Close out the JSON + versions_map="${versions_map%,}" + versions_map="$versions_map}" + echo "::set-output name=go-versions::$versions_map" + main: needs: setup strategy: - fail-fast: false - matrix: - program: ${{ fromJson(needs.setup.outputs.programs) }} + fail-fast: false + matrix: + program: ${{ fromJson(needs.setup.outputs.programs) }} name: Run Main uses: ./.github/workflows/main_template.yml with: modulepath: contribs/${{ matrix.program }} + go-version: ${{ (fromJson(needs.setup.outputs.go-versions))[matrix.program] }} secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/dependabot-validate.yml b/.github/workflows/dependabot-validate.yml index b1387dc0bb2..3d7b2c315c6 100644 --- a/.github/workflows/dependabot-validate.yml +++ b/.github/workflows/dependabot-validate.yml @@ -1,10 +1,11 @@ -name: dependabot validate +name: Validate Dependabot Config on: pull_request: paths: - '.github/dependabot.yml' - '.github/workflows/dependabot-validate.yml' + jobs: validate: runs-on: ubuntu-latest diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index d800147a498..f180f1679b1 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,4 +1,6 @@ -name: deploy docs on gnolang/docs.gno.land repository +# This workflow triggers a cross-repo workflow call, +# that deploys the monorepo docs on Netlify, using Docusaurus +name: Deploy the Documentation on: push: branches: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs-linter.yml similarity index 95% rename from .github/workflows/docs.yml rename to .github/workflows/docs-linter.yml index c9d9af0fb6f..d603d796ae9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs-linter.yml @@ -1,8 +1,8 @@ -name: "docs / lint" +name: Docs Linter on: push: - paths: + branches: - master pull_request: paths: diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 41d579c4567..5d606a2a663 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -1,9 +1,14 @@ -name: examples +name: Gno Examples on: - pull_request: push: - branches: ["master"] + branches: + - master + pull_request: + paths: + - gnovm/**/*.gno + - examples/**/*.gno + - examples/**/gno.mod concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -66,27 +71,25 @@ jobs: - run: make lint -C ./examples # TODO: consider running lint on every other directories, maybe in "warning" mode? # TODO: track coverage + fmt: - strategy: - fail-fast: false - matrix: - goversion: ["1.22.x"] - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: ${{ matrix.goversion }} - - run: | - make fmt -C ./examples - # Check if there are changes after running make fmt - git diff --exit-code || (echo "Some gno files are not formatted, please run 'make fmt'." && exit 1) + name: Run gno fmt on examples + uses: ./.github/workflows/gnofmt_template.yml + with: + path: "examples/..." + + generate: + name: Check generated files are up to date + uses: ./.github/workflows/build_template.yml + with: + modulepath: "examples" + go-version: "1.22.x" + mod-tidy: strategy: fail-fast: false matrix: - go-version: ["1.22.x"] + go-version: [ "1.22.x" ] # unittests: TODO: matrix with contracts runs-on: ubuntu-latest timeout-minutes: 10 diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 41d9a2cba94..0a94211cb90 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -1,6 +1,18 @@ name: Dependency License Scanning on: + push: + branches: + - master + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' + pull_request_target: + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' workflow_dispatch: permissions: diff --git a/.github/workflows/genesis-verify.yml b/.github/workflows/genesis-verify.yml index 1288d588100..acc41cc99ad 100644 --- a/.github/workflows/genesis-verify.yml +++ b/.github/workflows/genesis-verify.yml @@ -1,9 +1,10 @@ -name: genesis-verify +name: Deployment genesis.json Verification on: - pull_request: + push: branches: - master + pull_request: paths: - "misc/deployments/**/genesis.json" - ".github/workflows/genesis-verify.yml" @@ -13,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - testnet: ["test5.gno.land"] + testnet: [ "test5.gno.land" ] runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 993c11b5941..a293469bb5d 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -1,9 +1,11 @@ -# generate docs and publish on gh-pages branch -name: gh-pages +# generate Go docs and publish on gh-pages branch +# Live at: https://gnolang.github.io/gno +name: Go Reference Docs Deployment on: push: - branches: [ "master" ] + branches: + - master workflow_dispatch: permissions: @@ -28,10 +30,10 @@ jobs: - run: echo $GOROOT - run: "cd misc/stdlib_diff && make gen" - run: "cd misc/gendocs && make install gen" - - run: "mkdir -p pages_ouput/stdlib_diff" + - run: "mkdir -p pages_output/stdlib_diff" - run: | - mv misc/gendocs/godoc pages_output - mv misc/stdlib_diff/stdlib_diff pages_ouput/stdlib_diff + cp -r misc/gendocs/godoc/* pages_output/ + cp -r misc/stdlib_diff/stdlib_diff/* pages_output/stdlib_diff/ - uses: actions/configure-pages@v5 id: pages - uses: actions/upload-pages-artifact@v3 diff --git a/.github/workflows/gnofmt_template.yml b/.github/workflows/gnofmt_template.yml index 1ba66d0fbe3..096dbaa1b5d 100644 --- a/.github/workflows/gnofmt_template.yml +++ b/.github/workflows/gnofmt_template.yml @@ -1,12 +1,15 @@ on: workflow_call: - inputs: - path: - required: true - type: string - go-version: - required: true - type: string + inputs: + path: + description: "Path to run gno fmt on" + required: true + type: string + go-version: + description: "Go version to use" + required: false + type: string + default: "1.22.x" jobs: fmt: @@ -16,9 +19,15 @@ jobs: uses: actions/setup-go@v5 with: go-version: ${{ inputs.go-version }} + - name: Checkout code uses: actions/checkout@v4 - - name: Fmt + + - name: Format code with gno fmt env: GNOFMT_PATH: ${{ inputs.path }} run: go run ./gnovm/cmd/gno fmt -v -diff $GNOFMT_PATH + + - name: Check for unformatted code + run: | + git diff --exit-code || (echo "Some gno files are not formatted, please run 'make fmt'." && exit 1) \ No newline at end of file diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 59050f1baa4..0d3a7a10516 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -4,12 +4,18 @@ on: push: branches: - master - workflow_dispatch: pull_request: + paths: + - gno.land/** + # We trigger the testing workflow for gno.land on the following, + # since there are integration suites that cover the gnovm / tm2 + - gnovm/** + - tm2/** + workflow_dispatch: jobs: main: - name: Run Main + name: Run gno.land suite uses: ./.github/workflows/main_template.yml with: modulepath: "gno.land" diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 7e7586b23d9..7a015b74e09 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -1,23 +1,26 @@ -name: gnovm +name: GnoVM on: push: branches: - master - workflow_dispatch: pull_request: + paths: + - gnovm/** + - tm2/** # GnoVM has a dependency on TM2 types + workflow_dispatch: jobs: main: - name: Run Main + name: Run GnoVM suite uses: ./.github/workflows/main_template.yml with: modulepath: "gnovm" + tests-extra-args: "-coverpkg=github.com/gnolang/gno/gnovm/..." secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} fmt: - name: Run Gno Fmt + name: Run gno fmt on stdlibs uses: ./.github/workflows/gnofmt_template.yml with: path: "gnovm/stdlibs/..." - go-version: "1.22.x" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 06b2daa1d3d..56075c31db3 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,6 +1,6 @@ -name: "Pull Request Labeler" +name: Pull Request Labeler on: -- pull_request_target + - pull_request_target jobs: triage: @@ -9,5 +9,5 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/labeler@v5 + - uses: actions/checkout@v4 + - uses: actions/labeler@v5 diff --git a/.github/workflows/lint-pr-title.yml b/.github/workflows/lint-pr-title.yml index 631f764c37f..3c7236b264f 100644 --- a/.github/workflows/lint-pr-title.yml +++ b/.github/workflows/lint-pr-title.yml @@ -1,4 +1,4 @@ -name: "lint-pr-title" +name: PR Title Linter on: pull_request_target: diff --git a/.github/workflows/lint_template.yml b/.github/workflows/lint_template.yml index b7568d19c41..43246572daa 100644 --- a/.github/workflows/lint_template.yml +++ b/.github/workflows/lint_template.yml @@ -8,7 +8,6 @@ on: required: true type: string - jobs: lint: runs-on: ubuntu-latest diff --git a/.github/workflows/main_template.yml b/.github/workflows/main_template.yml index 5b3437b54a1..a463bb330ea 100644 --- a/.github/workflows/main_template.yml +++ b/.github/workflows/main_template.yml @@ -1,41 +1,42 @@ on: - workflow_call: - inputs: - modulepath: - required: true - type: string - tests-extra-args: - required: false - type: string - secrets: - codecov-token: - required: true - -# TODO: environment variables cannot be sent to reusable workflows: https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations -# env: -# GO_VERSION: "1.22.x" + workflow_call: + inputs: + modulepath: + required: true + type: string + tests-extra-args: + required: false + type: string + go-version: + description: "Go version to use" + required: false + type: string + default: "1.22.x" + secrets: + codecov-token: + required: true jobs: - lint: - name: Go Linter - uses: ./.github/workflows/lint_template.yml - with: - modulepath: ${{ inputs.modulepath }} - go-version: "1.22.x" - build: - name: Go Build - uses: ./.github/workflows/build_template.yml - with: - modulepath: ${{ inputs.modulepath }} - go-version: "1.22.x" - test: - name: Go Test - uses: ./.github/workflows/test_template.yml - with: - modulepath: ${{ inputs.modulepath }} - tests-timeout: "30m" - go-version: "1.22.x" - tests-extra-args: ${{ inputs.tests-extra-args }} - secrets: - codecov-token: ${{ secrets.codecov-token }} + lint: + name: Go Lint + uses: ./.github/workflows/lint_template.yml + with: + modulepath: ${{ inputs.modulepath }} + go-version: ${{ inputs.go-version }} + build: + name: Go Build + uses: ./.github/workflows/build_template.yml + with: + modulepath: ${{ inputs.modulepath }} + go-version: ${{ inputs.go-version }} + test: + name: Go Test + uses: ./.github/workflows/test_template.yml + with: + modulepath: ${{ inputs.modulepath }} + tests-timeout: "30m" + go-version: ${{ inputs.go-version }} + tests-extra-args: ${{ inputs.tests-extra-args }} + secrets: + codecov-token: ${{ secrets.codecov-token }} diff --git a/.github/workflows/misc.yml b/.github/workflows/misc.yml index ad2c886e2ac..1116a87c300 100644 --- a/.github/workflows/misc.yml +++ b/.github/workflows/misc.yml @@ -6,8 +6,10 @@ on: push: branches: - master - workflow_dispatch: pull_request: + paths: + - misc/** + workflow_dispatch: jobs: main: @@ -21,9 +23,10 @@ jobs: - genstd - goscan - loop - name: Run Main + name: Run misc suite uses: ./.github/workflows/main_template.yml with: modulepath: misc/${{ matrix.program }} + tests-extra-args: "-coverpkg=github.com/gnolang/gno/misc/..." secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/mod-tidy.yml b/.github/workflows/mod-tidy.yml index 24eab553d19..5b6401b0d13 100644 --- a/.github/workflows/mod-tidy.yml +++ b/.github/workflows/mod-tidy.yml @@ -1,11 +1,19 @@ -name: Ensure go.mods are tidied +name: go.mod Tidy Checker on: push: branches: - master - workflow_dispatch: + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' pull_request: + paths: + - '**/*.go' + - 'go.mod' + - 'go.sum' + workflow_dispatch: jobs: main: diff --git a/.github/workflows/portal-loop.yml b/.github/workflows/portal-loop.yml index b898a149e9d..b5cafa459a7 100644 --- a/.github/workflows/portal-loop.yml +++ b/.github/workflows/portal-loop.yml @@ -1,19 +1,13 @@ -name: portal-loop +name: Portal Loop on: - pull_request: - branches: - - master push: + branches: + - "master" + pull_request: paths: - "misc/loop/**" - ".github/workflows/portal-loop.yml" - branches: - - "master" - # NOTE(albttx): branch name to simplify tests for this workflow - - "ci/portal-loop" - tags: - - "v*" permissions: contents: read @@ -69,14 +63,14 @@ jobs: while block_height=$(curl -s localhost:26657/status | jq -r '.result.sync_info.latest_block_height') echo "Current block height: $block_height" - [[ "$block_height" -lt 10 ]] + [[ "$block_height" -lt 2 ]] do sleep 1 done curl -s localhost:26657/status | jq - - name: "Buid new gnolang/gno image" + - name: "Build new gnolang/gno image" run: | docker build -t ghcr.io/gnolang/gno/gnoland:master -f Dockerfile --target gnoland . @@ -96,7 +90,7 @@ jobs: while block_height=$(curl -s localhost:26657/status | jq -r '.result.sync_info.latest_block_height') echo "Current block height: $block_height" - [[ "$block_height" -lt 10 ]] + [[ "$block_height" -lt 2 ]] do sleep 5 done diff --git a/.github/workflows/releaser-master.yml b/.github/workflows/releaser-master.yml index 3d194e2cb4c..6e3eed31914 100644 --- a/.github/workflows/releaser-master.yml +++ b/.github/workflows/releaser-master.yml @@ -1,9 +1,9 @@ -name: Trigger master build +name: Master Releases on: push: branches: - - "master" + - master workflow_dispatch: permissions: diff --git a/.github/workflows/releaser-nightly.yml b/.github/workflows/releaser-nightly.yml index 4308f1c4a7d..4f6e636af1b 100644 --- a/.github/workflows/releaser-nightly.yml +++ b/.github/workflows/releaser-nightly.yml @@ -1,4 +1,4 @@ -name: Trigger nightly build +name: Nightly Releases on: schedule: diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml deleted file mode 100644 index 309664bdcce..00000000000 --- a/.github/workflows/releaser.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Go Releaser - -on: - push: - tags: - - "v*" - -permissions: - contents: write # needed to write releases - id-token: write # needed for keyless signing - packages: write # needed for ghcr access - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - cache: true - - - uses: sigstore/cosign-installer@v3.7.0 - - uses: anchore/sbom-action/download-syft@v0.17.8 - - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - uses: goreleaser/goreleaser-action@v6 - with: - distribution: goreleaser-pro - version: ~> v2 - args: release --clean --config ./.github/goreleaser.yaml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 55a17ac60a8..6eb38ac5728 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -1,4 +1,4 @@ -name: "Close stale PRs" +name: Stale PR Bot on: schedule: - cron: "30 1 * * *" diff --git a/.github/workflows/test_template.yml b/.github/workflows/test_template.yml index c7956b4caf4..a1bc58ecebb 100644 --- a/.github/workflows/test_template.yml +++ b/.github/workflows/test_template.yml @@ -1,84 +1,70 @@ on: - workflow_call: - inputs: - modulepath: - required: true - type: string - tests-timeout: - required: true - type: string - go-version: - required: true - type: string - tests-extra-args: - required: false - type: string - secrets: - codecov-token: - required: true + workflow_call: + inputs: + modulepath: + required: true + type: string + tests-timeout: + required: true + type: string + go-version: + required: true + type: string + tests-extra-args: + required: false + type: string + secrets: + codecov-token: + required: true jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: ${{ inputs.go-version }} - - name: Go test - working-directory: ${{ inputs.modulepath }} - env: - TXTARCOVERDIR: /tmp/txtarcoverdir # txtar cover output - GOCOVERDIR: /tmp/gocoverdir # go cover output - COVERDIR: /tmp/coverdir # final output - run: | - set -x # print commands + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + - name: Go test + working-directory: ${{ inputs.modulepath }} + env: + TXTARCOVERDIR: /tmp/txtarcoverdir # txtar cover output + GOCOVERDIR: /tmp/gocoverdir # go cover output + COVERDIR: /tmp/coverdir # final output + run: | + set -x # print commands + + mkdir -p "$GOCOVERDIR" "$TXTARCOVERDIR" "$COVERDIR" + + # Craft a filter flag based on the module path to avoid expanding coverage on unrelated tags. + export filter="-pkg=github.com/gnolang/gno/${{ inputs.modulepath }}/..." + + # codecov only supports "boolean" coverage (whether a line is + # covered or not); so using -covermode=count or atomic would be + # pointless here. + # XXX: Simplify coverage of txtar - the current setup is a bit + # confusing and meticulous. There will be some improvements in Go + # 1.23 regarding coverage, so we can use this as a workaround until + # then. + go test -covermode=set -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR + + # Print results + (set +x; echo 'go coverage results:') + go tool covdata percent $filter -i=$GOCOVERDIR + (set +x; echo 'txtar coverage results:') + go tool covdata percent $filter -i=$TXTARCOVERDIR + + # Generate final coverage output + go tool covdata textfmt -v 1 $filter -i=$GOCOVERDIR,$TXTARCOVERDIR -o gocoverage.out - mkdir -p "$GOCOVERDIR" "$TXTARCOVERDIR" "$COVERDIR" - - # Craft a filter flag based on the module path to avoid expanding coverage on unrelated tags. - export filter="-pkg=github.com/gnolang/gno/${{ inputs.modulepath }}/..." - - # codecov only supports "boolean" coverage (whether a line is - # covered or not); so using -covermode=count or atomic would be - # pointless here. - # XXX: Simplify coverage of txtar - the current setup is a bit - # confusing and meticulous. There will be some improvements in Go - # 1.23 regarding coverage, so we can use this as a workaround until - # then. - go test -covermode=set -timeout ${{ inputs.tests-timeout }} ${{ inputs.tests-extra-args }} ./... -test.gocoverdir=$GOCOVERDIR - - # Print results - (set +x; echo 'go coverage results:') - go tool covdata percent $filter -i=$GOCOVERDIR - (set +x; echo 'txtar coverage results:') - go tool covdata percent $filter -i=$TXTARCOVERDIR - - # Generate final coverage output - go tool covdata textfmt -v 1 $filter -i=$GOCOVERDIR,$TXTARCOVERDIR -o gocoverage.out - - - name: Upload go coverage to Codecov - uses: codecov/codecov-action@v5 - with: - disable_search: true - fail_ci_if_error: true - files: ${{ inputs.modulepath }}/gocoverage.out - flags: ${{ inputs.modulepath }} - token: ${{ secrets.codecov-token }} - verbose: true # keep this enable as it help debugging when coverage fail randomly on the CI - - # TODO: We have to fix race conditions before running this job - # test-with-race: - # runs-on: ubuntu-latest - # steps: - # - name: Install Go - # uses: actions/setup-go@v5 - # with: - # go-version: ${{ inputs.go-version }} - # - name: Checkout code - # uses: actions/checkout@v4 - # - name: Go race test - # run: go test -race -timeout ${{ inputs.tests-timeout }} ./... - # working-directory: ${{ inputs.modulepath }} + - name: Upload go coverage to Codecov + uses: codecov/codecov-action@v5 + with: + disable_search: true + fail_ci_if_error: true + files: ${{ inputs.modulepath }}/gocoverage.out + flags: ${{ inputs.modulepath }} + token: ${{ secrets.codecov-token }} + verbose: true # keep this enable as it help debugging when coverage fails randomly on the CI diff --git a/.github/workflows/tm2.yml b/.github/workflows/tm2.yml index 57e84793c94..757391eab8c 100644 --- a/.github/workflows/tm2.yml +++ b/.github/workflows/tm2.yml @@ -1,17 +1,20 @@ -name: tm2 +name: TM2 on: push: branches: - master - workflow_dispatch: pull_request: + paths: + - tm2/** + workflow_dispatch: jobs: main: - name: Run Main + name: Run TM2 suite uses: ./.github/workflows/main_template.yml with: modulepath: "tm2" + tests-extra-args: "-coverpkg=github.com/gnolang/gno/tm2/..." secrets: codecov-token: ${{ secrets.CODECOV_TOKEN }} diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index b5b5a402c2a..92d8494fa40 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -77,10 +77,12 @@ require ( github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.2 // indirect github.com/yuin/goldmark-emoji v1.0.2 // indirect + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect go.etcd.io/bbolt v1.3.11 // indirect @@ -93,7 +95,7 @@ require ( go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap/exp v0.2.0 // indirect + go.uber.org/zap/exp v0.3.0 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index bab6e5364e8..3f22e4f2f00 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -3,8 +3,10 @@ dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= +github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -91,6 +93,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeC github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -133,8 +137,6 @@ github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gotuna/gotuna v0.6.0 h1:N1lQKXEi/lwRp8u3sccTYLhzOffA4QasExz/1M5Riws= -github.com/gotuna/gotuna v0.6.0/go.mod h1:F/ecRt29ChB6Ycy1AFIBpBiMNK0j7Heq+gFbLWquhjc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= @@ -211,6 +213,8 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -225,10 +229,13 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.2 h1:NjGd7lO7zrUn/A7eKwn5PEOt4ONYGqpxSEeZuduvgxc= github.com/yuin/goldmark v1.7.2/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= +github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= @@ -257,8 +264,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= -go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index fa9e2d11e29..12a88490515 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -489,6 +489,8 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) // Speed up stdlib loading after first start (saves about 2-3 seconds on each reload). nodeConfig.CacheStdlibLoad = true nodeConfig.Genesis.ConsensusParams.Block.MaxGas = n.config.MaxGasPerBlock + // Genesis verification is always false with Gnodev + nodeConfig.SkipGenesisVerification = true // recoverFromError handles panics and converts them to errors. recoverFromError := func() { diff --git a/contribs/gnofaucet/go.mod b/contribs/gnofaucet/go.mod index eab9fc90c50..3d1e5f54c54 100644 --- a/contribs/gnofaucet/go.mod +++ b/contribs/gnofaucet/go.mod @@ -31,6 +31,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.29.0 // indirect @@ -40,7 +41,7 @@ require ( go.opentelemetry.io/otel/trace v1.29.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap/exp v0.2.0 // indirect + go.uber.org/zap/exp v0.3.0 // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum index aabe858e893..10e2c19b408 100644 --- a/contribs/gnofaucet/go.sum +++ b/contribs/gnofaucet/go.sum @@ -111,6 +111,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -146,8 +148,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= -go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= +go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= +go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod index f1b316c2bee..3056af1d4cc 100644 --- a/contribs/gnogenesis/go.mod +++ b/contribs/gnogenesis/go.mod @@ -32,6 +32,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect @@ -49,6 +50,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum index 7ba3aede534..7e4a683cad1 100644 --- a/contribs/gnogenesis/go.sum +++ b/contribs/gnogenesis/go.sum @@ -120,6 +120,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages.go b/contribs/gnogenesis/internal/txs/txs_add_packages.go index cf863c72116..53c0bb4b686 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages.go @@ -5,8 +5,9 @@ import ( "errors" "flag" "fmt" + "os" - "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" @@ -15,28 +16,44 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" ) -var ( - errInvalidPackageDir = errors.New("invalid package directory") - errInvalidDeployerAddr = errors.New("invalid deployer address") +const ( + defaultAccount_Name = "test1" + defaultAccount_Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" + defaultAccount_publicKey = "gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj" ) +var errInvalidPackageDir = errors.New("invalid package directory") + // Keep in sync with gno.land/cmd/start.go -var ( - defaultCreator = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1 - genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) -) +var genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) type addPkgCfg struct { - txsCfg *txsCfg - deployerAddress string + txsCfg *txsCfg + keyName string + gnoHome string // default GNOHOME env var, just here to ease testing with parallel tests + insecurePasswordStdin bool } func (c *addPkgCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( - &c.deployerAddress, - "deployer-address", - defaultCreator.String(), - "the address that will be used to deploy the package", + &c.keyName, + "key-name", + "", + "The package deployer key name or address contained on gnokey", + ) + + fs.StringVar( + &c.gnoHome, + "gno-home", + os.Getenv("GNOHOME"), + "the gno home directory", + ) + + fs.BoolVar( + &c.insecurePasswordStdin, + "insecure-password-stdin", + false, + "the gno home directory", ) } @@ -65,10 +82,15 @@ func execTxsAddPackages( io commands.IO, args []string, ) error { + var ( + keyname = defaultAccount_Name + keybase keys.Keybase + pass string + ) // Load the genesis - genesis, loadErr := types.GenesisDocFromFile(cfg.txsCfg.GenesisPath) - if loadErr != nil { - return fmt.Errorf("unable to load genesis, %w", loadErr) + genesis, err := types.GenesisDocFromFile(cfg.txsCfg.GenesisPath) + if err != nil { + return fmt.Errorf("unable to load genesis, %w", err) } // Make sure the package dir is set @@ -76,19 +98,30 @@ func execTxsAddPackages( return errInvalidPackageDir } - var ( - creator = defaultCreator - err error - ) - - // Check if the deployer address is set - if cfg.deployerAddress != defaultCreator.String() { - creator, err = crypto.AddressFromString(cfg.deployerAddress) + if cfg.keyName != "" { + keyname = cfg.keyName + keybase, err = keys.NewKeyBaseFromDir(cfg.gnoHome) + if err != nil { + return fmt.Errorf("unable to load keybase: %w", err) + } + pass, err = io.GetPassword("Enter password.", cfg.insecurePasswordStdin) + if err != nil { + return fmt.Errorf("cannot read password: %w", err) + } + } else { + keybase = keys.NewInMemory() + _, err := keybase.CreateAccount(defaultAccount_Name, defaultAccount_Seed, "", "", 0, 0) if err != nil { - return fmt.Errorf("%w, %w", errInvalidDeployerAddr, err) + return fmt.Errorf("unable to create account: %w", err) } } + info, err := keybase.GetByNameOrAddress(keyname) + if err != nil { + return fmt.Errorf("unable to find key in keybase: %w", err) + } + + creator := info.GetAddress() parsedTxs := make([]gnoland.TxWithMetadata, 0) for _, path := range args { // Generate transactions from the packages (recursively) @@ -97,6 +130,10 @@ func execTxsAddPackages( return fmt.Errorf("unable to load txs from directory, %w", err) } + if err := signTxs(txs, keybase, genesis.ChainID, keyname, pass); err != nil { + return fmt.Errorf("unable to sign txs, %w", err) + } + parsedTxs = append(parsedTxs, txs...) } @@ -117,3 +154,25 @@ func execTxsAddPackages( return nil } + +func signTxs(txs []gnoland.TxWithMetadata, keybase keys.Keybase, chainID, keyname string, password string) error { + for index, tx := range txs { + // Here accountNumber and sequenceNumber are set to 0 because they are considered as 0 on genesis transactions. + signBytes, err := tx.Tx.GetSignBytes(chainID, 0, 0) + if err != nil { + return fmt.Errorf("unable to load txs from directory, %w", err) + } + signature, publicKey, err := keybase.Sign(keyname, password, signBytes) + if err != nil { + return fmt.Errorf("unable sign tx %w", err) + } + txs[index].Tx.Signatures = []std.Signature{ + { + PubKey: publicKey, + Signature: signature, + }, + } + } + + return nil +} diff --git a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go index c3405d6ff8d..38d930401e8 100644 --- a/contribs/gnogenesis/internal/txs/txs_add_packages_test.go +++ b/contribs/gnogenesis/internal/txs/txs_add_packages_test.go @@ -2,9 +2,11 @@ package txs import ( "context" + "encoding/hex" "fmt" "os" "path/filepath" + "strings" "testing" "github.com/gnolang/contribs/gnogenesis/internal/common" @@ -12,6 +14,8 @@ import ( vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" "github.com/gnolang/gno/tm2/pkg/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,6 +23,7 @@ import ( func TestGenesis_Txs_Add_Packages(t *testing.T) { t.Parallel() + const addPkgExpectedSignature = "cfe5a15d8def04cbdaf9d08e2511db7928152b26419c4577cbfa282c83118852411f3de5d045ce934555572c21bda8042ce5c64b793a01748e49cf2cff7c2983" t.Run("invalid genesis file", func(t *testing.T) { t.Parallel() @@ -60,8 +65,10 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { assert.ErrorContains(t, cmdErr, errInvalidPackageDir.Error()) }) - t.Run("invalid deployer address", func(t *testing.T) { + t.Run("non existent key", func(t *testing.T) { t.Parallel() + keybaseDir := t.TempDir() + keyname := "beep-boop" tempGenesis, cleanup := testutils.NewTestFile(t) t.Cleanup(cleanup) @@ -69,24 +76,36 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { genesis := common.GetDefaultGenesis() require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + io := commands.NewTestIO() + io.SetIn( + strings.NewReader( + fmt.Sprintf( + "%s\n", + "password", + ), + ), + ) // Create the command - cmd := NewTxsCmd(commands.NewTestIO()) + cmd := NewTxsCmd(io) args := []string{ "add", "packages", "--genesis-path", tempGenesis.Name(), t.TempDir(), // package dir - "--deployer-address", - "beep-boop", // invalid address + "--key-name", + keyname, // non-existent key name + "--gno-home", + keybaseDir, // temporaryDir for keybase + "--insecure-password-stdin", } // Run the command cmdErr := cmd.ParseAndRun(context.Background(), args) - assert.ErrorIs(t, cmdErr, errInvalidDeployerAddr) + assert.ErrorContains(t, cmdErr, "Key "+keyname+" not found") }) - t.Run("valid package", func(t *testing.T) { + t.Run("existent key wrong password", func(t *testing.T) { t.Parallel() tempGenesis, cleanup := testutils.NewTestFile(t) @@ -94,32 +113,189 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { genesis := common.GetDefaultGenesis() require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + // Prepare the package + var ( + packagePath = "gno.land/p/demo/cuttlas" + dir = t.TempDir() + keybaseDir = t.TempDir() + keyname = "beep-boop" + password = "somepass" + ) + createValidFile(t, dir, packagePath) + // Create key + kb, err := keys.NewKeyBaseFromDir(keybaseDir) + require.NoError(t, err) + mnemonic, err := client.GenerateMnemonic(256) + require.NoError(t, err) + _, err = kb.CreateAccount(keyname, mnemonic, "", password+"wrong", 0, 0) + require.NoError(t, err) + + io := commands.NewTestIO() + io.SetIn( + strings.NewReader( + fmt.Sprintf( + "%s\n", + password, + ), + ), + ) + + // Create the command + cmd := NewTxsCmd(io) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + "--key-name", + keyname, // non-existent key name + "--gno-home", + keybaseDir, // temporaryDir for keybase + "--insecure-password-stdin", + dir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "unable to sign txs") + }) + + t.Run("existent key correct password", func(t *testing.T) { + t.Parallel() + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) // Prepare the package var ( packagePath = "gno.land/p/demo/cuttlas" dir = t.TempDir() + keybaseDir = t.TempDir() + keyname = "beep-boop" + password = "somepass" ) + createValidFile(t, dir, packagePath) + // Create key + kb, err := keys.NewKeyBaseFromDir(keybaseDir) + require.NoError(t, err) + info, err := kb.CreateAccount(keyname, defaultAccount_Seed, "", password, 0, 0) + require.NoError(t, err) - createFile := func(path, data string) { - file, err := os.Create(path) - require.NoError(t, err) + io := commands.NewTestIO() + io.SetIn( + strings.NewReader( + fmt.Sprintf( + "%s\n", + password, + ), + ), + ) - _, err = file.WriteString(data) - require.NoError(t, err) + // Create the command + cmd := NewTxsCmd(io) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + "--key-name", + keyname, // non-existent key name + "--gno-home", + keybaseDir, // temporaryDir for keybase + "--insecure-password-stdin", + dir, } - // Create the gno.mod file - createFile( - filepath.Join(dir, "gno.mod"), - fmt.Sprintf("module %s\n", packagePath), + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + require.Equal(t, 1, len(state.Txs)) + require.Equal(t, 1, len(state.Txs[0].Tx.Msgs)) + + msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage) + require.True(t, ok) + require.Equal(t, info.GetPubKey(), state.Txs[0].Tx.Signatures[0].PubKey) + require.Equal(t, addPkgExpectedSignature, hex.EncodeToString(state.Txs[0].Tx.Signatures[0].Signature)) + + assert.Equal(t, packagePath, msgAddPkg.Package.Path) + }) + + t.Run("ok default key", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + // Prepare the package + var ( + packagePath = "gno.land/p/demo/cuttlas" + dir = t.TempDir() + keybaseDir = t.TempDir() ) + createValidFile(t, dir, packagePath) + + // Create the command + cmd := NewTxsCmd(commands.NewTestIO()) + args := []string{ + "add", + "packages", + "--genesis-path", + tempGenesis.Name(), + "--gno-home", + keybaseDir, // temporaryDir for keybase + dir, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) - // Create a simple main.gno - createFile( - filepath.Join(dir, "main.gno"), - "package cuttlas\n\nfunc Example() string {\nreturn \"Manos arriba!\"\n}", + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + require.Equal(t, 1, len(state.Txs)) + require.Equal(t, 1, len(state.Txs[0].Tx.Msgs)) + + msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage) + require.True(t, ok) + require.Equal(t, defaultAccount_publicKey, state.Txs[0].Tx.Signatures[0].PubKey.String()) + require.Equal(t, addPkgExpectedSignature, hex.EncodeToString(state.Txs[0].Tx.Signatures[0].Signature)) + + assert.Equal(t, packagePath, msgAddPkg.Package.Path) + }) + + t.Run("valid package", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := common.GetDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + // Prepare the package + var ( + packagePath = "gno.land/p/demo/cuttlas" + dir = t.TempDir() ) + createValidFile(t, dir, packagePath) // Create the command cmd := NewTxsCmd(commands.NewTestIO()) @@ -148,7 +324,32 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) { msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage) require.True(t, ok) + require.Equal(t, defaultAccount_publicKey, state.Txs[0].Tx.Signatures[0].PubKey.String()) + require.Equal(t, addPkgExpectedSignature, hex.EncodeToString(state.Txs[0].Tx.Signatures[0].Signature)) assert.Equal(t, packagePath, msgAddPkg.Package.Path) }) } + +func createValidFile(t *testing.T, dir string, packagePath string) { + t.Helper() + createFile := func(path, data string) { + file, err := os.Create(path) + require.NoError(t, err) + + _, err = file.WriteString(data) + require.NoError(t, err) + } + + // Create the gno.mod file + createFile( + filepath.Join(dir, "gno.mod"), + fmt.Sprintf("module %s\n", packagePath), + ) + + // Create a simple main.gno + createFile( + filepath.Join(dir, "main.gno"), + "package cuttlas\n\nfunc Example() string {\nreturn \"Manos arriba!\"\n}", + ) +} diff --git a/contribs/gnohealth/go.mod b/contribs/gnohealth/go.mod index 4f5862a0d2e..4a3f6392804 100644 --- a/contribs/gnohealth/go.mod +++ b/contribs/gnohealth/go.mod @@ -21,6 +21,7 @@ require ( github.com/peterbourgon/ff/v3 v3.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.9.0 // indirect go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.29.0 // indirect @@ -34,6 +35,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/contribs/gnohealth/go.sum b/contribs/gnohealth/go.sum index dd287d9ca84..02e8893406a 100644 --- a/contribs/gnohealth/go.sum +++ b/contribs/gnohealth/go.sum @@ -103,6 +103,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -149,6 +151,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/contribs/gnokeykc/go.mod b/contribs/gnokeykc/go.mod index 479daed22f6..157b5585828 100644 --- a/contribs/gnokeykc/go.mod +++ b/contribs/gnokeykc/go.mod @@ -35,6 +35,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/zondax/hid v0.9.2 // indirect @@ -52,6 +53,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum index cacf6788d45..7aac05b84a0 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -124,6 +124,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -180,6 +182,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/contribs/gnomd/go.mod b/contribs/gnomd/go.mod index 423e4414a79..57c07621324 100644 --- a/contribs/gnomd/go.mod +++ b/contribs/gnomd/go.mod @@ -21,7 +21,7 @@ require ( github.com/mattn/go-isatty v0.0.11 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect github.com/rivo/uniseg v0.1.0 // indirect - golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 // indirect + golang.org/x/image v0.18.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.18.0 // indirect ) diff --git a/contribs/gnomd/go.sum b/contribs/gnomd/go.sum index 0ff70dd99fb..3d4666530b1 100644 --- a/contribs/gnomd/go.sum +++ b/contribs/gnomd/go.sum @@ -55,8 +55,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 h1:gQ6GUSD102fPgli+Yb4cR/cGaHF7tNBt+GYoRCpGC7s= golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod index cd31adc4f6f..49f40eb79af 100644 --- a/contribs/gnomigrate/go.mod +++ b/contribs/gnomigrate/go.mod @@ -29,6 +29,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/xid v1.6.0 // indirect + github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect go.etcd.io/bbolt v1.3.11 // indirect go.opentelemetry.io/otel v1.29.0 // indirect @@ -44,6 +45,7 @@ require ( golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum index 7ba3aede534..7e4a683cad1 100644 --- a/contribs/gnomigrate/go.sum +++ b/contribs/gnomigrate/go.sum @@ -120,6 +120,8 @@ github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b h1:oV47z+jotrLVvhiLRNzACVe7/qZ8DcRlMlDucR/FARo= +github.com/sig-0/insertion-queue v0.0.0-20241004125609-6b3ca841346b/go.mod h1:JprPCeMgYyLKJoAy9nxpVScm7NwFSwpibdrUKm4kcw0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/contribs/gnomigrate/internal/txs/txs.go b/contribs/gnomigrate/internal/txs/txs.go index 4c65ca6ef0b..231428d5064 100644 --- a/contribs/gnomigrate/internal/txs/txs.go +++ b/contribs/gnomigrate/internal/txs/txs.go @@ -184,7 +184,7 @@ func processFile(ctx context.Context, io commands.IO, source, destination string continue } - if _, err = outputFile.WriteString(fmt.Sprintf("%s\n", string(marshaledData))); err != nil { + if _, err = fmt.Fprintf(outputFile, "%s\n", marshaledData); err != nil { io.ErrPrintfln("unable to save to output file, %s", err) } } diff --git a/examples/gno.land/p/demo/avl/list/gno.mod b/examples/gno.land/p/demo/avl/list/gno.mod new file mode 100644 index 00000000000..c05923b7708 --- /dev/null +++ b/examples/gno.land/p/demo/avl/list/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/avl/list diff --git a/examples/gno.land/p/demo/avl/list/list.gno b/examples/gno.land/p/demo/avl/list/list.gno new file mode 100644 index 00000000000..0875eb66e01 --- /dev/null +++ b/examples/gno.land/p/demo/avl/list/list.gno @@ -0,0 +1,298 @@ +// Package list implements a dynamic list data structure backed by an AVL tree. +// It provides O(log n) operations for most list operations while maintaining +// order stability. +// +// The list supports various operations including append, get, set, delete, +// range queries, and iteration. It can store values of any type. +// +// Example usage: +// +// // Create a new list and add elements +// var l list.List +// l.Append(1, 2, 3) +// +// // Get and set elements +// value := l.Get(1) // returns 2 +// l.Set(1, 42) // updates index 1 to 42 +// +// // Delete elements +// l.Delete(0) // removes first element +// +// // Iterate over elements +// l.ForEach(func(index int, value interface{}) bool { +// ufmt.Printf("index %d: %v\n", index, value) +// return false // continue iteration +// }) +// // Output: +// // index 0: 42 +// // index 1: 3 +// +// // Create a list of specific size +// l = list.Make(3, "default") // creates [default, default, default] +// +// // Create a list using a variable declaration +// var l2 list.List +// l2.Append(4, 5, 6) +// println(l2.Len()) // Output: 3 +package list + +import ( + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" +) + +// List represents an ordered sequence of items backed by an AVL tree +type List struct { + tree avl.Tree + idGen seqid.ID +} + +// Len returns the number of elements in the list. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3) +// println(l.Len()) // Output: 3 +func (l *List) Len() int { + return l.tree.Size() +} + +// Append adds one or more values to the end of the list. +// +// Example: +// +// l := list.New() +// l.Append(1) // adds single value +// l.Append(2, 3, 4) // adds multiple values +// println(l.Len()) // Output: 4 +func (l *List) Append(values ...interface{}) { + for _, v := range values { + l.tree.Set(l.idGen.Next().String(), v) + } +} + +// Get returns the value at the specified index. +// Returns nil if index is out of bounds. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3) +// println(l.Get(1)) // Output: 2 +// println(l.Get(-1)) // Output: nil +// println(l.Get(999)) // Output: nil +func (l *List) Get(index int) interface{} { + if index < 0 || index >= l.tree.Size() { + return nil + } + _, value := l.tree.GetByIndex(index) + return value +} + +// Set updates or appends a value at the specified index. +// Returns true if the operation was successful, false otherwise. +// For empty lists, only index 0 is valid (append case). +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3) +// +// l.Set(1, 42) // updates existing index +// println(l.Get(1)) // Output: 42 +// +// l.Set(3, 4) // appends at end +// println(l.Get(3)) // Output: 4 +// +// l.Set(-1, 5) // invalid index +// println(l.Len()) // Output: 4 (list unchanged) +func (l *List) Set(index int, value interface{}) bool { + size := l.tree.Size() + + // Handle empty list case - only allow index 0 + if size == 0 { + if index == 0 { + l.Append(value) + return true + } + return false + } + + if index < 0 || index > size { + return false + } + + // If setting at the end (append case) + if index == size { + l.Append(value) + return true + } + + // Get the key at the specified index + key, _ := l.tree.GetByIndex(index) + if key == "" { + return false + } + + // Update the value at the existing key + l.tree.Set(key, value) + return true +} + +// Delete removes the element at the specified index. +// Returns the deleted value and true if successful, nil and false otherwise. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3) +// +// val, ok := l.Delete(1) +// println(val, ok) // Output: 2 true +// println(l.Len()) // Output: 2 +// +// val, ok = l.Delete(-1) +// println(val, ok) // Output: nil false +func (l *List) Delete(index int) (interface{}, bool) { + size := l.tree.Size() + // Always return nil, false for empty list + if size == 0 { + return nil, false + } + + if index < 0 || index >= size { + return nil, false + } + + key, value := l.tree.GetByIndex(index) + if key == "" { + return nil, false + } + + l.tree.Remove(key) + return value, true +} + +// Slice returns a slice of values from startIndex (inclusive) to endIndex (exclusive). +// Returns nil if the range is invalid. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3, 4, 5) +// +// println(l.Slice(1, 4)) // Output: [2 3 4] +// println(l.Slice(-1, 2)) // Output: [1 2] +// println(l.Slice(3, 999)) // Output: [4 5] +// println(l.Slice(3, 2)) // Output: nil +func (l *List) Slice(startIndex, endIndex int) []interface{} { + size := l.tree.Size() + + // Normalize bounds + if startIndex < 0 { + startIndex = 0 + } + if endIndex > size { + endIndex = size + } + if startIndex >= endIndex { + return nil + } + + count := endIndex - startIndex + result := make([]interface{}, count) + + i := 0 + l.tree.IterateByOffset(startIndex, count, func(_ string, value interface{}) bool { + result[i] = value + i++ + return false + }) + return result +} + +// ForEach iterates through all elements in the list. +func (l *List) ForEach(fn func(index int, value interface{}) bool) { + if l.tree.Size() == 0 { + return + } + + index := 0 + l.tree.IterateByOffset(0, l.tree.Size(), func(_ string, value interface{}) bool { + result := fn(index, value) + index++ + return result + }) +} + +// Clone creates a shallow copy of the list. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3) +// +// clone := l.Clone() +// clone.Set(0, 42) +// +// println(l.Get(0)) // Output: 1 +// println(clone.Get(0)) // Output: 42 +func (l *List) Clone() *List { + newList := &List{ + tree: avl.Tree{}, + idGen: l.idGen, + } + + size := l.tree.Size() + if size == 0 { + return newList + } + + l.tree.IterateByOffset(0, size, func(_ string, value interface{}) bool { + newList.Append(value) + return false + }) + + return newList +} + +// DeleteRange removes elements from startIndex (inclusive) to endIndex (exclusive). +// Returns the number of elements deleted. +// +// Example: +// +// l := list.New() +// l.Append(1, 2, 3, 4, 5) +// +// deleted := l.DeleteRange(1, 4) +// println(deleted) // Output: 3 +// println(l.Range(0, l.Len())) // Output: [1 5] +func (l *List) DeleteRange(startIndex, endIndex int) int { + size := l.tree.Size() + + // Normalize bounds + if startIndex < 0 { + startIndex = 0 + } + if endIndex > size { + endIndex = size + } + if startIndex >= endIndex { + return 0 + } + + // Collect keys to delete + keysToDelete := make([]string, 0, endIndex-startIndex) + l.tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, _ interface{}) bool { + keysToDelete = append(keysToDelete, key) + return false + }) + + // Delete collected keys + for _, key := range keysToDelete { + l.tree.Remove(key) + } + + return len(keysToDelete) +} diff --git a/examples/gno.land/p/demo/avl/list/list_test.gno b/examples/gno.land/p/demo/avl/list/list_test.gno new file mode 100644 index 00000000000..265fbdb5eb1 --- /dev/null +++ b/examples/gno.land/p/demo/avl/list/list_test.gno @@ -0,0 +1,409 @@ +package list + +import ( + "testing" +) + +func TestList_Basic(t *testing.T) { + var l List + + // Test empty list + if l.Len() != 0 { + t.Errorf("new list should be empty, got len %d", l.Len()) + } + + // Test append and length + l.Append(1, 2, 3) + if l.Len() != 3 { + t.Errorf("expected len 3, got %d", l.Len()) + } + + // Test get + if v := l.Get(0); v != 1 { + t.Errorf("expected 1 at index 0, got %v", v) + } + if v := l.Get(1); v != 2 { + t.Errorf("expected 2 at index 1, got %v", v) + } + if v := l.Get(2); v != 3 { + t.Errorf("expected 3 at index 2, got %v", v) + } + + // Test out of bounds + if v := l.Get(-1); v != nil { + t.Errorf("expected nil for negative index, got %v", v) + } + if v := l.Get(3); v != nil { + t.Errorf("expected nil for out of bounds index, got %v", v) + } +} + +func TestList_Set(t *testing.T) { + var l List + l.Append(1, 2, 3) + + // Test valid set within bounds + if ok := l.Set(1, 42); !ok { + t.Error("Set should return true for valid index") + } + if v := l.Get(1); v != 42 { + t.Errorf("expected 42 after Set, got %v", v) + } + + // Test set at size (append) + if ok := l.Set(3, 4); !ok { + t.Error("Set should return true when appending at size") + } + if v := l.Get(3); v != 4 { + t.Errorf("expected 4 after Set at size, got %v", v) + } + + // Test invalid sets + if ok := l.Set(-1, 10); ok { + t.Error("Set should return false for negative index") + } + if ok := l.Set(5, 10); ok { + t.Error("Set should return false for index > size") + } + + // Verify list state hasn't changed after invalid operations + expected := []interface{}{1, 42, 3, 4} + for i, want := range expected { + if got := l.Get(i); got != want { + t.Errorf("index %d = %v; want %v", i, got, want) + } + } +} + +func TestList_Delete(t *testing.T) { + var l List + l.Append(1, 2, 3) + + // Test valid delete + if v, ok := l.Delete(1); !ok || v != 2 { + t.Errorf("Delete(1) = %v, %v; want 2, true", v, ok) + } + if l.Len() != 2 { + t.Errorf("expected len 2 after delete, got %d", l.Len()) + } + if v := l.Get(1); v != 3 { + t.Errorf("expected 3 at index 1 after delete, got %v", v) + } + + // Test invalid delete + if v, ok := l.Delete(-1); ok || v != nil { + t.Errorf("Delete(-1) = %v, %v; want nil, false", v, ok) + } + if v, ok := l.Delete(2); ok || v != nil { + t.Errorf("Delete(2) = %v, %v; want nil, false", v, ok) + } +} + +func TestList_Slice(t *testing.T) { + var l List + l.Append(1, 2, 3, 4, 5) + + // Test valid ranges + values := l.Slice(1, 4) + expected := []interface{}{2, 3, 4} + if !sliceEqual(values, expected) { + t.Errorf("Slice(1,4) = %v; want %v", values, expected) + } + + // Test edge cases + if values := l.Slice(-1, 2); !sliceEqual(values, []interface{}{1, 2}) { + t.Errorf("Slice(-1,2) = %v; want [1 2]", values) + } + if values := l.Slice(3, 10); !sliceEqual(values, []interface{}{4, 5}) { + t.Errorf("Slice(3,10) = %v; want [4 5]", values) + } + if values := l.Slice(3, 2); values != nil { + t.Errorf("Slice(3,2) = %v; want nil", values) + } +} + +func TestList_ForEach(t *testing.T) { + var l List + l.Append(1, 2, 3) + + sum := 0 + l.ForEach(func(index int, value interface{}) bool { + sum += value.(int) + return false + }) + + if sum != 6 { + t.Errorf("ForEach sum = %d; want 6", sum) + } + + // Test early termination + count := 0 + l.ForEach(func(index int, value interface{}) bool { + count++ + return true // stop after first item + }) + + if count != 1 { + t.Errorf("ForEach early termination count = %d; want 1", count) + } +} + +func TestList_Clone(t *testing.T) { + var l List + l.Append(1, 2, 3) + + clone := l.Clone() + + // Test same length + if clone.Len() != l.Len() { + t.Errorf("clone.Len() = %d; want %d", clone.Len(), l.Len()) + } + + // Test same values + for i := 0; i < l.Len(); i++ { + if clone.Get(i) != l.Get(i) { + t.Errorf("clone.Get(%d) = %v; want %v", i, clone.Get(i), l.Get(i)) + } + } + + // Test independence + l.Set(0, 42) + if clone.Get(0) == l.Get(0) { + t.Error("clone should be independent of original") + } +} + +func TestList_DeleteRange(t *testing.T) { + var l List + l.Append(1, 2, 3, 4, 5) + + // Test valid range delete + deleted := l.DeleteRange(1, 4) + if deleted != 3 { + t.Errorf("DeleteRange(1,4) deleted %d elements; want 3", deleted) + } + if l.Len() != 2 { + t.Errorf("after DeleteRange(1,4) len = %d; want 2", l.Len()) + } + expected := []interface{}{1, 5} + for i, want := range expected { + if got := l.Get(i); got != want { + t.Errorf("after DeleteRange(1,4) index %d = %v; want %v", i, got, want) + } + } + + // Test edge cases + l = List{} + l.Append(1, 2, 3) + + // Delete with negative start + if deleted := l.DeleteRange(-1, 2); deleted != 2 { + t.Errorf("DeleteRange(-1,2) deleted %d elements; want 2", deleted) + } + + // Delete with end > length + l = List{} + l.Append(1, 2, 3) + if deleted := l.DeleteRange(1, 5); deleted != 2 { + t.Errorf("DeleteRange(1,5) deleted %d elements; want 2", deleted) + } + + // Delete invalid range + if deleted := l.DeleteRange(2, 1); deleted != 0 { + t.Errorf("DeleteRange(2,1) deleted %d elements; want 0", deleted) + } + + // Delete empty range + if deleted := l.DeleteRange(1, 1); deleted != 0 { + t.Errorf("DeleteRange(1,1) deleted %d elements; want 0", deleted) + } +} + +func TestList_EmptyOperations(t *testing.T) { + var l List + + // Operations on empty list + if v := l.Get(0); v != nil { + t.Errorf("Get(0) on empty list = %v; want nil", v) + } + + // Set should work at index 0 for empty list (append case) + if ok := l.Set(0, 1); !ok { + t.Error("Set(0,1) on empty list = false; want true") + } + if v := l.Get(0); v != 1 { + t.Errorf("Get(0) after Set = %v; want 1", v) + } + + l = List{} // Reset to empty list + if v, ok := l.Delete(0); ok || v != nil { + t.Errorf("Delete(0) on empty list = %v, %v; want nil, false", v, ok) + } + if values := l.Slice(0, 1); values != nil { + t.Errorf("Range(0,1) on empty list = %v; want nil", values) + } +} + +func TestList_DifferentTypes(t *testing.T) { + var l List + + // Test with different types + l.Append(42, "hello", true, 3.14) + + if v := l.Get(0).(int); v != 42 { + t.Errorf("Get(0) = %v; want 42", v) + } + if v := l.Get(1).(string); v != "hello" { + t.Errorf("Get(1) = %v; want 'hello'", v) + } + if v := l.Get(2).(bool); !v { + t.Errorf("Get(2) = %v; want true", v) + } + if v := l.Get(3).(float64); v != 3.14 { + t.Errorf("Get(3) = %v; want 3.14", v) + } +} + +func TestList_LargeOperations(t *testing.T) { + var l List + + // Test with larger number of elements + n := 1000 + for i := 0; i < n; i++ { + l.Append(i) + } + + if l.Len() != n { + t.Errorf("Len() = %d; want %d", l.Len(), n) + } + + // Test range on large list + values := l.Slice(n-3, n) + expected := []interface{}{n - 3, n - 2, n - 1} + if !sliceEqual(values, expected) { + t.Errorf("Range(%d,%d) = %v; want %v", n-3, n, values, expected) + } + + // Test large range deletion + deleted := l.DeleteRange(100, 900) + if deleted != 800 { + t.Errorf("DeleteRange(100,900) = %d; want 800", deleted) + } + if l.Len() != 200 { + t.Errorf("Len() after large delete = %d; want 200", l.Len()) + } +} + +func TestList_ChainedOperations(t *testing.T) { + var l List + + // Test sequence of operations + l.Append(1, 2, 3) + l.Delete(1) + l.Append(4) + l.Set(1, 5) + + expected := []interface{}{1, 5, 4} + for i, want := range expected { + if got := l.Get(i); got != want { + t.Errorf("index %d = %v; want %v", i, got, want) + } + } +} + +func TestList_RangeEdgeCases(t *testing.T) { + var l List + l.Append(1, 2, 3, 4, 5) + + // Test various edge cases for Range + cases := []struct { + start, end int + want []interface{} + }{ + {-10, 2, []interface{}{1, 2}}, + {3, 10, []interface{}{4, 5}}, + {0, 0, nil}, + {5, 5, nil}, + {4, 3, nil}, + {-1, -1, nil}, + } + + for _, tc := range cases { + got := l.Slice(tc.start, tc.end) + if !sliceEqual(got, tc.want) { + t.Errorf("Slice(%d,%d) = %v; want %v", tc.start, tc.end, got, tc.want) + } + } +} + +func TestList_IndexConsistency(t *testing.T) { + var l List + + // Initial additions + l.Append(1, 2, 3, 4, 5) // [1,2,3,4,5] + + // Delete from middle + l.Delete(2) // [1,2,4,5] + + // Add more elements + l.Append(6, 7) // [1,2,4,5,6,7] + + // Delete range from middle + l.DeleteRange(1, 4) // [1,6,7] + + // Add more elements + l.Append(8, 9, 10) // [1,6,7,8,9,10] + + // Verify sequence is continuous + expected := []interface{}{1, 6, 7, 8, 9, 10} + for i, want := range expected { + if got := l.Get(i); got != want { + t.Errorf("index %d = %v; want %v", i, got, want) + } + } + + // Verify no extra elements exist + if l.Len() != len(expected) { + t.Errorf("length = %d; want %d", l.Len(), len(expected)) + } + + // Verify all indices are accessible + allValues := l.Slice(0, l.Len()) + if !sliceEqual(allValues, expected) { + t.Errorf("Slice(0, Len()) = %v; want %v", allValues, expected) + } + + // Verify no gaps in iteration + var iteratedValues []interface{} + var indices []int + l.ForEach(func(index int, value interface{}) bool { + iteratedValues = append(iteratedValues, value) + indices = append(indices, index) + return false + }) + + // Check values from iteration + if !sliceEqual(iteratedValues, expected) { + t.Errorf("ForEach values = %v; want %v", iteratedValues, expected) + } + + // Check indices are sequential + for i, idx := range indices { + if idx != i { + t.Errorf("ForEach index %d = %d; want %d", i, idx, i) + } + } +} + +// Helper function to compare slices +func sliceEqual(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/examples/gno.land/p/demo/avl/node.gno b/examples/gno.land/p/demo/avl/node.gno index 7308e163768..7d4ddffff02 100644 --- a/examples/gno.land/p/demo/avl/node.gno +++ b/examples/gno.land/p/demo/avl/node.gno @@ -384,7 +384,7 @@ func (node *Node) TraverseInRange(start, end string, ascending bool, leavesOnly // TraverseByOffset traverses all nodes, including inner nodes. // A limit of math.MaxInt means no limit. -func (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { +func (node *Node) TraverseByOffset(offset, limit int, ascending bool, leavesOnly bool, cb func(*Node) bool) bool { if node == nil { return false } @@ -401,21 +401,21 @@ func (node *Node) TraverseByOffset(offset, limit int, descending bool, leavesOnl } // go to the actual recursive function. - return node.traverseByOffset(offset, limit, descending, leavesOnly, cb) + return node.traverseByOffset(offset, limit, ascending, leavesOnly, cb) } // TraverseByOffset traverses the subtree rooted at the node by offset and limit, // in either ascending or descending order, and applies the callback function to each traversed node. // If leavesOnly is true, only leaf nodes are visited. -func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnly bool, cb func(*Node) bool) bool { +func (node *Node) traverseByOffset(offset, limit int, ascending bool, leavesOnly bool, cb func(*Node) bool) bool { // caller guarantees: offset < node.size; limit > 0. if !leavesOnly { if cb(node) { - return true + return true // Stop traversal if callback returns true } } first, second := node.getLeftNode(), node.getRightNode() - if descending { + if !ascending { first, second = second, first } if first.IsLeaf() { @@ -423,10 +423,12 @@ func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnl if offset > 0 { offset-- } else { - cb(first) + if cb(first) { + return true // Stop traversal if callback returns true + } limit-- if limit <= 0 { - return false + return true // Stop traversal when limit is reached } } } else { @@ -437,7 +439,7 @@ func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnl if offset >= first.size { offset -= first.size // 1 } else { - if first.traverseByOffset(offset, limit, descending, leavesOnly, cb) { + if first.traverseByOffset(offset, limit, ascending, leavesOnly, cb) { return true } // number of leaves which could actually be called from inside @@ -460,7 +462,7 @@ func (node *Node) traverseByOffset(offset, limit int, descending bool, leavesOnl } // => if it is not a leaf, it will still be enough to recursively call this // function with the updated offset and limit - return second.traverseByOffset(offset, limit, descending, leavesOnly, cb) + return second.traverseByOffset(offset, limit, ascending, leavesOnly, cb) } // Only used in testing... diff --git a/examples/gno.land/p/demo/avl/node_test.gno b/examples/gno.land/p/demo/avl/node_test.gno index f24217625ea..3682cbc7c80 100644 --- a/examples/gno.land/p/demo/avl/node_test.gno +++ b/examples/gno.land/p/demo/avl/node_test.gno @@ -17,36 +17,34 @@ Book Browser` tt := []struct { name string - desc bool + asc bool }{ - {"ascending", false}, - {"descending", true}, + {"ascending", true}, + {"descending", false}, } for _, tt := range tt { t.Run(tt.name, func(t *testing.T) { + // use sl to insert the values, and reversed to match the values + // we do this to ensure that the order of TraverseByOffset is independent + // from the insertion order sl := strings.Split(testStrings, "\n") - - // sort a first time in the order opposite to how we'll be traversing - // the tree, to ensure that we are not just iterating through with - // insertion order. sort.Strings(sl) - if !tt.desc { - reverseSlice(sl) + reversed := append([]string{}, sl...) + reverseSlice(reversed) + + if !tt.asc { + sl, reversed = reversed, sl } - r := NewNode(sl[0], nil) - for _, v := range sl[1:] { + r := NewNode(reversed[0], nil) + for _, v := range reversed[1:] { r, _ = r.Set(v, nil) } - // then sort sl in the order we'll be traversing it, so that we can - // compare the result with sl. - reverseSlice(sl) - var result []string for i := 0; i < len(sl); i++ { - r.TraverseByOffset(i, 1, tt.desc, true, func(n *Node) bool { + r.TraverseByOffset(i, 1, tt.asc, true, func(n *Node) bool { result = append(result, n.Key()) return false }) @@ -66,7 +64,7 @@ Browser` exp := sl[i:max] actual := []string{} - r.TraverseByOffset(i, l, tt.desc, true, func(tr *Node) bool { + r.TraverseByOffset(i, l, tt.asc, true, func(tr *Node) bool { actual = append(actual, tr.Key()) return false }) @@ -422,6 +420,30 @@ func TestTraverse(t *testing.T) { t.Errorf("want %v got %v", expected, result) } }) + + t.Run("early termination", func(t *testing.T) { + if len(tt.input) == 0 { + return // Skip for empty tree + } + + var result []string + var count int + tree.Iterate("", "", func(n *Node) bool { + count++ + result = append(result, n.Key()) + return true // Stop after first item + }) + + if count != 1 { + t.Errorf("Expected callback to be called exactly once, got %d calls", count) + } + if len(result) != 1 { + t.Errorf("Expected exactly one result, got %d items", len(result)) + } + if len(result) > 0 && result[0] != tt.expected[0] { + t.Errorf("Expected first item to be %v, got %v", tt.expected[0], result[0]) + } + }) }) } } @@ -435,7 +457,7 @@ func TestRotateWhenHeightDiffers(t *testing.T) { { "right rotation when left subtree is higher", []string{"E", "C", "A", "B", "D"}, - []string{"A", "B", "C", "E", "D"}, + []string{"A", "B", "C", "D", "E"}, }, { "left rotation when right subtree is higher", @@ -445,12 +467,12 @@ func TestRotateWhenHeightDiffers(t *testing.T) { { "left-right rotation", []string{"E", "A", "C", "B", "D"}, - []string{"A", "B", "C", "E", "D"}, + []string{"A", "B", "C", "D", "E"}, }, { "right-left rotation", []string{"A", "E", "C", "B", "D"}, - []string{"A", "B", "C", "E", "D"}, + []string{"A", "B", "C", "D", "E"}, }, } @@ -533,7 +555,7 @@ func slicesEqual(w1, w2 []string) bool { return false } for i := 0; i < len(w1); i++ { - if w1[0] != w2[0] { + if w1[i] != w2[i] { return false } } diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index ca3eadde032..f5f909a473d 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -90,12 +90,12 @@ func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page { items := []Item{} if p.Reversed { - p.Tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { items = append(items, Item{Key: key, Value: value}) return false }) } else { - p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + p.Tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { items = append(items, Item{Key: key, Value: value}) return false }) diff --git a/examples/gno.land/p/demo/avlhelpers/avlhelpers.gno b/examples/gno.land/p/demo/avlhelpers/avlhelpers.gno index 27842932dd3..e5fe33cacad 100644 --- a/examples/gno.land/p/demo/avlhelpers/avlhelpers.gno +++ b/examples/gno.land/p/demo/avlhelpers/avlhelpers.gno @@ -8,7 +8,7 @@ import ( // It calls the provided callback function for each key-value pair encountered. // If the callback returns true, the iteration is stopped. // The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes. -func IterateByteStringKeysByPrefix(tree avl.Tree, prefix string, cb avl.IterCbFn) { +func IterateByteStringKeysByPrefix(tree avl.ITree, prefix string, cb avl.IterCbFn) { end := "" n := len(prefix) // To make the end of the search, increment the final character ASCII by one. @@ -28,7 +28,7 @@ func IterateByteStringKeysByPrefix(tree avl.Tree, prefix string, cb avl.IterCbFn // Get a list of keys starting from the given prefix. Limit the // number of results to maxResults. // The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes. -func ListByteStringKeysByPrefix(tree avl.Tree, prefix string, maxResults int) []string { +func ListByteStringKeysByPrefix(tree avl.ITree, prefix string, maxResults int) []string { result := []string{} IterateByteStringKeysByPrefix(tree, prefix, func(key string, value interface{}) bool { result = append(result, key) diff --git a/examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno b/examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno index 1c7873e297a..5ecda41d1a6 100644 --- a/examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno +++ b/examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno @@ -10,7 +10,7 @@ import ( ) func main() { - tree := avl.Tree{} + tree := avl.NewTree() { // Empty tree. @@ -44,7 +44,7 @@ func main() { println("match: " + matches[0]) } - tree = avl.Tree{} + tree = avl.NewTree() tree.Set("a\xff", "") tree.Set("a\xff\xff", "") tree.Set("b", "") diff --git a/examples/gno.land/p/demo/grc/grc20/token.gno b/examples/gno.land/p/demo/grc/grc20/token.gno index 4634bae933b..4986eaebf04 100644 --- a/examples/gno.land/p/demo/grc/grc20/token.gno +++ b/examples/gno.land/p/demo/grc/grc20/token.gno @@ -1,7 +1,6 @@ package grc20 import ( - "math/overflow" "std" "strconv" @@ -65,6 +64,14 @@ func (tok *Token) RenderHome() string { return str } +// Getter returns a TokenGetter function that returns this token. This allows +// storing indirect pointers to a token in a remote realm. +func (tok *Token) Getter() TokenGetter { + return func() *Token { + return tok + } +} + // SpendAllowance decreases the allowance of the specified owner and spender. func (led *PrivateLedger) SpendAllowance(owner, spender std.Address, amount uint64) error { if !owner.IsValid() { @@ -162,17 +169,24 @@ func (led *PrivateLedger) Approve(owner, spender std.Address, amount uint64) err } // Mint increases the total supply of the token and adds the specified amount to the specified address. -func (led *PrivateLedger) Mint(address std.Address, amount uint64) error { +func (led *PrivateLedger) Mint(address std.Address, amount uint64) (err error) { if !address.IsValid() { return ErrInvalidAddress } - // XXX: math/overflow is not supporting uint64. - // This checks prevents overflow but makes the totalSupply limited to a uint63. - sum, ok := overflow.Add64(int64(led.totalSupply), int64(amount)) - if !ok { - return ErrOverflow - } + defer func() { + if r := recover(); r != nil { + if r != "addition overflow" { + panic(r) + } + err = ErrOverflow + } + }() + + // Convert amount and totalSupply to signed integers to enable + // overflow checking (not occuring on unsigned) when computing the sum. + // The maximum value for totalSupply is therefore 1<<63. + sum := int64(led.totalSupply) + int64(amount) led.totalSupply = uint64(sum) currentBalance := led.balanceOf(address) diff --git a/examples/gno.land/p/moul/addrset/addrset.gno b/examples/gno.land/p/moul/addrset/addrset.gno new file mode 100644 index 00000000000..0bb8165f9fe --- /dev/null +++ b/examples/gno.land/p/moul/addrset/addrset.gno @@ -0,0 +1,100 @@ +// Package addrset provides a specialized set data structure for managing unique Gno addresses. +// +// It is built on top of an AVL tree for efficient operations and maintains addresses in sorted order. +// This package is particularly useful when you need to: +// - Track a collection of unique addresses (e.g., for whitelists, participants, etc.) +// - Efficiently check address membership +// - Support pagination when displaying addresses +// +// Example usage: +// +// import ( +// "std" +// "gno.land/p/moul/addrset" +// ) +// +// func MyHandler() { +// // Create a new address set +// var set addrset.Set +// +// // Add some addresses +// addr1 := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") +// addr2 := std.Address("g1sss5g0rkqr88k4u648yd5d3l9t4d8vvqwszqth") +// +// set.Add(addr1) // returns true (newly added) +// set.Add(addr2) // returns true (newly added) +// set.Add(addr1) // returns false (already exists) +// +// // Check membership +// if set.Has(addr1) { +// // addr1 is in the set +// } +// +// // Get size +// size := set.Size() // returns 2 +// +// // Iterate with pagination (10 items per page, starting at offset 0) +// set.IterateByOffset(0, 10, func(addr std.Address) bool { +// // Process addr +// return false // continue iteration +// }) +// +// // Remove an address +// set.Remove(addr1) // returns true (was present) +// set.Remove(addr1) // returns false (not present) +// } +package addrset + +import ( + "std" + + "gno.land/p/demo/avl" +) + +type Set struct { + tree avl.Tree +} + +// Add inserts an address into the set. +// Returns true if the address was newly added, false if it already existed. +func (s *Set) Add(addr std.Address) bool { + return !s.tree.Set(string(addr), nil) +} + +// Remove deletes an address from the set. +// Returns true if the address was found and removed, false if it didn't exist. +func (s *Set) Remove(addr std.Address) bool { + _, removed := s.tree.Remove(string(addr)) + return removed +} + +// Has checks if an address exists in the set. +func (s *Set) Has(addr std.Address) bool { + return s.tree.Has(string(addr)) +} + +// Size returns the number of addresses in the set. +func (s *Set) Size() int { + return s.tree.Size() +} + +// IterateByOffset walks through addresses starting at the given offset. +// The callback should return true to stop iteration. +func (s *Set) IterateByOffset(offset int, count int, cb func(addr std.Address) bool) { + s.tree.IterateByOffset(offset, count, func(key string, _ interface{}) bool { + return cb(std.Address(key)) + }) +} + +// ReverseIterateByOffset walks through addresses in reverse order starting at the given offset. +// The callback should return true to stop iteration. +func (s *Set) ReverseIterateByOffset(offset int, count int, cb func(addr std.Address) bool) { + s.tree.ReverseIterateByOffset(offset, count, func(key string, _ interface{}) bool { + return cb(std.Address(key)) + }) +} + +// Tree returns the underlying AVL tree for advanced usage. +func (s *Set) Tree() avl.ITree { + return &s.tree +} diff --git a/examples/gno.land/p/moul/addrset/addrset_test.gno b/examples/gno.land/p/moul/addrset/addrset_test.gno new file mode 100644 index 00000000000..c3e27eab1df --- /dev/null +++ b/examples/gno.land/p/moul/addrset/addrset_test.gno @@ -0,0 +1,174 @@ +package addrset + +import ( + "std" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestSet(t *testing.T) { + addr1 := std.Address("addr1") + addr2 := std.Address("addr2") + addr3 := std.Address("addr3") + + tests := []struct { + name string + actions func(s *Set) + size int + has map[std.Address]bool + addrs []std.Address // for iteration checks + }{ + { + name: "empty set", + actions: func(s *Set) {}, + size: 0, + has: map[std.Address]bool{addr1: false}, + }, + { + name: "single address", + actions: func(s *Set) { + s.Add(addr1) + }, + size: 1, + has: map[std.Address]bool{ + addr1: true, + addr2: false, + }, + addrs: []std.Address{addr1}, + }, + { + name: "multiple addresses", + actions: func(s *Set) { + s.Add(addr1) + s.Add(addr2) + s.Add(addr3) + }, + size: 3, + has: map[std.Address]bool{ + addr1: true, + addr2: true, + addr3: true, + }, + addrs: []std.Address{addr1, addr2, addr3}, + }, + { + name: "remove address", + actions: func(s *Set) { + s.Add(addr1) + s.Add(addr2) + s.Remove(addr1) + }, + size: 1, + has: map[std.Address]bool{ + addr1: false, + addr2: true, + }, + addrs: []std.Address{addr2}, + }, + { + name: "duplicate adds", + actions: func(s *Set) { + uassert.True(t, s.Add(addr1)) // first add returns true + uassert.False(t, s.Add(addr1)) // second add returns false + uassert.True(t, s.Remove(addr1)) // remove existing returns true + uassert.False(t, s.Remove(addr1)) // remove non-existing returns false + }, + size: 0, + has: map[std.Address]bool{ + addr1: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var set Set + + // Execute test actions + tt.actions(&set) + + // Check size + uassert.Equal(t, tt.size, set.Size()) + + // Check existence + for addr, expected := range tt.has { + uassert.Equal(t, expected, set.Has(addr)) + } + + // Check iteration if addresses are specified + if tt.addrs != nil { + collected := []std.Address{} + set.IterateByOffset(0, 10, func(addr std.Address) bool { + collected = append(collected, addr) + return false + }) + + // Check length + uassert.Equal(t, len(tt.addrs), len(collected)) + + // Check each address + for i, addr := range tt.addrs { + uassert.Equal(t, addr, collected[i]) + } + } + }) + } +} + +func TestSetIterationLimits(t *testing.T) { + tests := []struct { + name string + addrs []std.Address + offset int + limit int + expected int + }{ + { + name: "zero offset full list", + addrs: []std.Address{"a1", "a2", "a3"}, + offset: 0, + limit: 10, + expected: 3, + }, + { + name: "offset with limit", + addrs: []std.Address{"a1", "a2", "a3", "a4"}, + offset: 1, + limit: 2, + expected: 2, + }, + { + name: "offset beyond size", + addrs: []std.Address{"a1", "a2"}, + offset: 3, + limit: 1, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var set Set + for _, addr := range tt.addrs { + set.Add(addr) + } + + // Test forward iteration + count := 0 + set.IterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool { + count++ + return false + }) + uassert.Equal(t, tt.expected, count) + + // Test reverse iteration + count = 0 + set.ReverseIterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool { + count++ + return false + }) + uassert.Equal(t, tt.expected, count) + }) + } +} diff --git a/examples/gno.land/p/moul/addrset/gno.mod b/examples/gno.land/p/moul/addrset/gno.mod new file mode 100644 index 00000000000..45bb53b399c --- /dev/null +++ b/examples/gno.land/p/moul/addrset/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/addrset diff --git a/examples/gno.land/p/moul/fp/fp.gno b/examples/gno.land/p/moul/fp/fp.gno new file mode 100644 index 00000000000..b2811c77d5a --- /dev/null +++ b/examples/gno.land/p/moul/fp/fp.gno @@ -0,0 +1,270 @@ +// Package fp provides functional programming utilities for Gno, enabling +// transformations, filtering, and other operations on slices of interface{}. +// +// Example of chaining operations: +// +// numbers := []interface{}{1, 2, 3, 4, 5, 6} +// +// // Define predicates, mappers and reducers +// isEven := func(v interface{}) bool { return v.(int)%2 == 0 } +// double := func(v interface{}) interface{} { return v.(int) * 2 } +// sum := func(a, b interface{}) interface{} { return a.(int) + b.(int) } +// +// // Chain operations: filter even numbers, double them, then sum +// evenNums := Filter(numbers, isEven) // [2, 4, 6] +// doubled := Map(evenNums, double) // [4, 8, 12] +// result := Reduce(doubled, sum, 0) // 24 +// +// // Alternative: group by even/odd, then get even numbers +// byMod2 := func(v interface{}) interface{} { return v.(int) % 2 } +// grouped := GroupBy(numbers, byMod2) // {0: [2,4,6], 1: [1,3,5]} +// evens := grouped[0] // [2,4,6] +package fp + +// Mapper is a function type that maps an element to another element. +type Mapper func(interface{}) interface{} + +// Predicate is a function type that evaluates a condition on an element. +type Predicate func(interface{}) bool + +// Reducer is a function type that reduces two elements to a single value. +type Reducer func(interface{}, interface{}) interface{} + +// Filter filters elements from the slice that satisfy the given predicate. +// +// Example: +// +// numbers := []interface{}{-1, 0, 1, 2} +// isPositive := func(v interface{}) bool { return v.(int) > 0 } +// result := Filter(numbers, isPositive) // [1, 2] +func Filter(values []interface{}, fn Predicate) []interface{} { + result := []interface{}{} + for _, v := range values { + if fn(v) { + result = append(result, v) + } + } + return result +} + +// Map applies a function to each element in the slice. +// +// Example: +// +// numbers := []interface{}{1, 2, 3} +// toString := func(v interface{}) interface{} { return fmt.Sprintf("%d", v) } +// result := Map(numbers, toString) // ["1", "2", "3"] +func Map(values []interface{}, fn Mapper) []interface{} { + result := make([]interface{}, len(values)) + for i, v := range values { + result[i] = fn(v) + } + return result +} + +// Reduce reduces a slice to a single value by applying a function. +// +// Example: +// +// numbers := []interface{}{1, 2, 3, 4} +// sum := func(a, b interface{}) interface{} { return a.(int) + b.(int) } +// result := Reduce(numbers, sum, 0) // 10 +func Reduce(values []interface{}, fn Reducer, initial interface{}) interface{} { + acc := initial + for _, v := range values { + acc = fn(acc, v) + } + return acc +} + +// FlatMap maps each element to a collection and flattens the results. +// +// Example: +// +// words := []interface{}{"hello", "world"} +// split := func(v interface{}) interface{} { +// chars := []interface{}{} +// for _, c := range v.(string) { +// chars = append(chars, string(c)) +// } +// return chars +// } +// result := FlatMap(words, split) // ["h","e","l","l","o","w","o","r","l","d"] +func FlatMap(values []interface{}, fn Mapper) []interface{} { + result := []interface{}{} + for _, v := range values { + inner := fn(v).([]interface{}) + result = append(result, inner...) + } + return result +} + +// All returns true if all elements satisfy the predicate. +// +// Example: +// +// numbers := []interface{}{2, 4, 6, 8} +// isEven := func(v interface{}) bool { return v.(int)%2 == 0 } +// result := All(numbers, isEven) // true +func All(values []interface{}, fn Predicate) bool { + for _, v := range values { + if !fn(v) { + return false + } + } + return true +} + +// Any returns true if at least one element satisfies the predicate. +// +// Example: +// +// numbers := []interface{}{1, 3, 4, 7} +// isEven := func(v interface{}) bool { return v.(int)%2 == 0 } +// result := Any(numbers, isEven) // true (4 is even) +func Any(values []interface{}, fn Predicate) bool { + for _, v := range values { + if fn(v) { + return true + } + } + return false +} + +// None returns true if no elements satisfy the predicate. +// +// Example: +// +// numbers := []interface{}{1, 3, 5, 7} +// isEven := func(v interface{}) bool { return v.(int)%2 == 0 } +// result := None(numbers, isEven) // true (no even numbers) +func None(values []interface{}, fn Predicate) bool { + for _, v := range values { + if fn(v) { + return false + } + } + return true +} + +// Chunk splits a slice into chunks of the given size. +// +// Example: +// +// numbers := []interface{}{1, 2, 3, 4, 5} +// result := Chunk(numbers, 2) // [[1,2], [3,4], [5]] +func Chunk(values []interface{}, size int) [][]interface{} { + if size <= 0 { + return nil + } + var chunks [][]interface{} + for i := 0; i < len(values); i += size { + end := i + size + if end > len(values) { + end = len(values) + } + chunks = append(chunks, values[i:end]) + } + return chunks +} + +// Find returns the first element that satisfies the predicate and a boolean indicating if an element was found. +// +// Example: +// +// numbers := []interface{}{1, 2, 3, 4} +// isEven := func(v interface{}) bool { return v.(int)%2 == 0 } +// result, found := Find(numbers, isEven) // 2, true +func Find(values []interface{}, fn Predicate) (interface{}, bool) { + for _, v := range values { + if fn(v) { + return v, true + } + } + return nil, false +} + +// Reverse reverses the order of elements in a slice. +// +// Example: +// +// numbers := []interface{}{1, 2, 3} +// result := Reverse(numbers) // [3, 2, 1] +func Reverse(values []interface{}) []interface{} { + result := make([]interface{}, len(values)) + for i, v := range values { + result[len(values)-1-i] = v + } + return result +} + +// Zip combines two slices into a slice of pairs. If the slices have different lengths, +// extra elements from the longer slice are ignored. +// +// Example: +// +// a := []interface{}{1, 2, 3} +// b := []interface{}{"a", "b", "c"} +// result := Zip(a, b) // [[1,"a"], [2,"b"], [3,"c"]] +func Zip(a, b []interface{}) [][2]interface{} { + length := min(len(a), len(b)) + result := make([][2]interface{}, length) + for i := 0; i < length; i++ { + result[i] = [2]interface{}{a[i], b[i]} + } + return result +} + +// Unzip splits a slice of pairs into two separate slices. +// +// Example: +// +// pairs := [][2]interface{}{{1,"a"}, {2,"b"}, {3,"c"}} +// numbers, letters := Unzip(pairs) // [1,2,3], ["a","b","c"] +func Unzip(pairs [][2]interface{}) ([]interface{}, []interface{}) { + a := make([]interface{}, len(pairs)) + b := make([]interface{}, len(pairs)) + for i, pair := range pairs { + a[i] = pair[0] + b[i] = pair[1] + } + return a, b +} + +// GroupBy groups elements based on a key returned by a Mapper. +// +// Example: +// +// numbers := []interface{}{1, 2, 3, 4, 5, 6} +// byMod3 := func(v interface{}) interface{} { return v.(int) % 3 } +// result := GroupBy(numbers, byMod3) // {0: [3,6], 1: [1,4], 2: [2,5]} +func GroupBy(values []interface{}, fn Mapper) map[interface{}][]interface{} { + result := make(map[interface{}][]interface{}) + for _, v := range values { + key := fn(v) + result[key] = append(result[key], v) + } + return result +} + +// Flatten flattens a slice of slices into a single slice. +// +// Example: +// +// nested := [][]interface{}{{1,2}, {3,4}, {5}} +// result := Flatten(nested) // [1,2,3,4,5] +func Flatten(values [][]interface{}) []interface{} { + result := []interface{}{} + for _, v := range values { + result = append(result, v...) + } + return result +} + +// Helper functions +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/examples/gno.land/p/moul/fp/fp_test.gno b/examples/gno.land/p/moul/fp/fp_test.gno new file mode 100644 index 00000000000..00957486fe9 --- /dev/null +++ b/examples/gno.land/p/moul/fp/fp_test.gno @@ -0,0 +1,666 @@ +package fp + +import ( + "fmt" + "testing" +) + +func TestMap(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) interface{} + expected []interface{} + }{ + { + name: "multiply numbers by 2", + input: []interface{}{1, 2, 3}, + fn: func(v interface{}) interface{} { return v.(int) * 2 }, + expected: []interface{}{2, 4, 6}, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(v interface{}) interface{} { return v.(int) * 2 }, + expected: []interface{}{}, + }, + { + name: "convert numbers to strings", + input: []interface{}{1, 2, 3}, + fn: func(v interface{}) interface{} { return fmt.Sprintf("%d", v.(int)) }, + expected: []interface{}{"1", "2", "3"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Map(tt.input, tt.fn) + if !equalSlices(result, tt.expected) { + t.Errorf("Map failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestFilter(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) bool + expected []interface{} + }{ + { + name: "filter even numbers", + input: []interface{}{1, 2, 3, 4}, + fn: func(v interface{}) bool { return v.(int)%2 == 0 }, + expected: []interface{}{2, 4}, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(v interface{}) bool { return v.(int)%2 == 0 }, + expected: []interface{}{}, + }, + { + name: "no matches", + input: []interface{}{1, 3, 5}, + fn: func(v interface{}) bool { return v.(int)%2 == 0 }, + expected: []interface{}{}, + }, + { + name: "all matches", + input: []interface{}{2, 4, 6}, + fn: func(v interface{}) bool { return v.(int)%2 == 0 }, + expected: []interface{}{2, 4, 6}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Filter(tt.input, tt.fn) + if !equalSlices(result, tt.expected) { + t.Errorf("Filter failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestReduce(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}, interface{}) interface{} + initial interface{} + expected interface{} + }{ + { + name: "sum numbers", + input: []interface{}{1, 2, 3}, + fn: func(a, b interface{}) interface{} { return a.(int) + b.(int) }, + initial: 0, + expected: 6, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(a, b interface{}) interface{} { return a.(int) + b.(int) }, + initial: 0, + expected: 0, + }, + { + name: "concatenate strings", + input: []interface{}{"a", "b", "c"}, + fn: func(a, b interface{}) interface{} { return a.(string) + b.(string) }, + initial: "", + expected: "abc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Reduce(tt.input, tt.fn, tt.initial) + if result != tt.expected { + t.Errorf("Reduce failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestFlatMap(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) interface{} + expected []interface{} + }{ + { + name: "split words into chars", + input: []interface{}{"go", "fn"}, + fn: func(word interface{}) interface{} { + chars := []interface{}{} + for _, c := range word.(string) { + chars = append(chars, string(c)) + } + return chars + }, + expected: []interface{}{"g", "o", "f", "n"}, + }, + { + name: "empty string handling", + input: []interface{}{"", "a", ""}, + fn: func(word interface{}) interface{} { + chars := []interface{}{} + for _, c := range word.(string) { + chars = append(chars, string(c)) + } + return chars + }, + expected: []interface{}{"a"}, + }, + { + name: "nil handling", + input: []interface{}{nil, "a", nil}, + fn: func(word interface{}) interface{} { + if word == nil { + return []interface{}{} + } + return []interface{}{word} + }, + expected: []interface{}{"a"}, + }, + { + name: "empty slice result", + input: []interface{}{"", "", ""}, + fn: func(word interface{}) interface{} { + return []interface{}{} + }, + expected: []interface{}{}, + }, + { + name: "nested array flattening", + input: []interface{}{1, 2, 3}, + fn: func(n interface{}) interface{} { + return []interface{}{n, n} + }, + expected: []interface{}{1, 1, 2, 2, 3, 3}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FlatMap(tt.input, tt.fn) + if !equalSlices(result, tt.expected) { + t.Errorf("FlatMap failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestAllAnyNone(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) bool + expectedAll bool + expectedAny bool + expectedNone bool + }{ + { + name: "all even numbers", + input: []interface{}{2, 4, 6, 8}, + fn: func(x interface{}) bool { return x.(int)%2 == 0 }, + expectedAll: true, + expectedAny: true, + expectedNone: false, + }, + { + name: "no even numbers", + input: []interface{}{1, 3, 5, 7}, + fn: func(x interface{}) bool { return x.(int)%2 == 0 }, + expectedAll: false, + expectedAny: false, + expectedNone: true, + }, + { + name: "mixed even/odd numbers", + input: []interface{}{1, 2, 3, 4}, + fn: func(x interface{}) bool { return x.(int)%2 == 0 }, + expectedAll: false, + expectedAny: true, + expectedNone: false, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(x interface{}) bool { return x.(int)%2 == 0 }, + expectedAll: true, // vacuously true + expectedAny: false, // vacuously false + expectedNone: true, // vacuously true + }, + { + name: "nil predicate handling", + input: []interface{}{nil, nil, nil}, + fn: func(x interface{}) bool { return x == nil }, + expectedAll: true, + expectedAny: true, + expectedNone: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resultAll := All(tt.input, tt.fn) + if resultAll != tt.expectedAll { + t.Errorf("All failed, expected %v, got %v", tt.expectedAll, resultAll) + } + + resultAny := Any(tt.input, tt.fn) + if resultAny != tt.expectedAny { + t.Errorf("Any failed, expected %v, got %v", tt.expectedAny, resultAny) + } + + resultNone := None(tt.input, tt.fn) + if resultNone != tt.expectedNone { + t.Errorf("None failed, expected %v, got %v", tt.expectedNone, resultNone) + } + }) + } +} + +func TestChunk(t *testing.T) { + tests := []struct { + name string + input []interface{} + size int + expected [][]interface{} + }{ + { + name: "normal chunks", + input: []interface{}{1, 2, 3, 4, 5}, + size: 2, + expected: [][]interface{}{{1, 2}, {3, 4}, {5}}, + }, + { + name: "empty slice", + input: []interface{}{}, + size: 2, + expected: [][]interface{}{}, + }, + { + name: "chunk size equals length", + input: []interface{}{1, 2, 3}, + size: 3, + expected: [][]interface{}{{1, 2, 3}}, + }, + { + name: "chunk size larger than length", + input: []interface{}{1, 2}, + size: 3, + expected: [][]interface{}{{1, 2}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Chunk(tt.input, tt.size) + if !equalNestedSlices(result, tt.expected) { + t.Errorf("Chunk failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestFind(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) bool + expected interface{} + shouldFound bool + }{ + { + name: "find first number greater than 2", + input: []interface{}{1, 2, 3, 4}, + fn: func(v interface{}) bool { return v.(int) > 2 }, + expected: 3, + shouldFound: true, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(v interface{}) bool { return v.(int) > 2 }, + expected: nil, + shouldFound: false, + }, + { + name: "no match", + input: []interface{}{1, 2}, + fn: func(v interface{}) bool { return v.(int) > 10 }, + expected: nil, + shouldFound: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, found := Find(tt.input, tt.fn) + if found != tt.shouldFound { + t.Errorf("Find failed, expected found=%v, got found=%v", tt.shouldFound, found) + } + if found && result != tt.expected { + t.Errorf("Find failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestReverse(t *testing.T) { + tests := []struct { + name string + input []interface{} + expected []interface{} + }{ + { + name: "normal sequence", + input: []interface{}{1, 2, 3, 4}, + expected: []interface{}{4, 3, 2, 1}, + }, + { + name: "empty slice", + input: []interface{}{}, + expected: []interface{}{}, + }, + { + name: "single element", + input: []interface{}{1}, + expected: []interface{}{1}, + }, + { + name: "mixed types", + input: []interface{}{1, "a", true, 2.5}, + expected: []interface{}{2.5, true, "a", 1}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Reverse(tt.input) + if !equalSlices(result, tt.expected) { + t.Errorf("Reverse failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestZipUnzip(t *testing.T) { + tests := []struct { + name string + a []interface{} + b []interface{} + expectedZip [][2]interface{} + expectedA []interface{} + expectedB []interface{} + }{ + { + name: "normal case", + a: []interface{}{1, 2, 3}, + b: []interface{}{"a", "b", "c"}, + expectedZip: [][2]interface{}{{1, "a"}, {2, "b"}, {3, "c"}}, + expectedA: []interface{}{1, 2, 3}, + expectedB: []interface{}{"a", "b", "c"}, + }, + { + name: "empty slices", + a: []interface{}{}, + b: []interface{}{}, + expectedZip: [][2]interface{}{}, + expectedA: []interface{}{}, + expectedB: []interface{}{}, + }, + { + name: "different lengths - a shorter", + a: []interface{}{1, 2}, + b: []interface{}{"a", "b", "c"}, + expectedZip: [][2]interface{}{{1, "a"}, {2, "b"}}, + expectedA: []interface{}{1, 2}, + expectedB: []interface{}{"a", "b"}, + }, + { + name: "different lengths - b shorter", + a: []interface{}{1, 2, 3}, + b: []interface{}{"a"}, + expectedZip: [][2]interface{}{{1, "a"}}, + expectedA: []interface{}{1}, + expectedB: []interface{}{"a"}, + }, + { + name: "mixed types", + a: []interface{}{1, true, "x"}, + b: []interface{}{2.5, false, "y"}, + expectedZip: [][2]interface{}{{1, 2.5}, {true, false}, {"x", "y"}}, + expectedA: []interface{}{1, true, "x"}, + expectedB: []interface{}{2.5, false, "y"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + zipped := Zip(tt.a, tt.b) + if len(zipped) != len(tt.expectedZip) { + t.Errorf("Zip failed, expected length %v, got %v", len(tt.expectedZip), len(zipped)) + } + for i, pair := range zipped { + if pair[0] != tt.expectedZip[i][0] || pair[1] != tt.expectedZip[i][1] { + t.Errorf("Zip failed at index %d, expected %v, got %v", i, tt.expectedZip[i], pair) + } + } + + unzippedA, unzippedB := Unzip(zipped) + if !equalSlices(unzippedA, tt.expectedA) { + t.Errorf("Unzip failed for slice A, expected %v, got %v", tt.expectedA, unzippedA) + } + if !equalSlices(unzippedB, tt.expectedB) { + t.Errorf("Unzip failed for slice B, expected %v, got %v", tt.expectedB, unzippedB) + } + }) + } +} + +func TestGroupBy(t *testing.T) { + tests := []struct { + name string + input []interface{} + fn func(interface{}) interface{} + expected map[interface{}][]interface{} + }{ + { + name: "group by even/odd", + input: []interface{}{1, 2, 3, 4, 5, 6}, + fn: func(v interface{}) interface{} { return v.(int) % 2 }, + expected: map[interface{}][]interface{}{ + 0: {2, 4, 6}, + 1: {1, 3, 5}, + }, + }, + { + name: "empty slice", + input: []interface{}{}, + fn: func(v interface{}) interface{} { return v.(int) % 2 }, + expected: map[interface{}][]interface{}{}, + }, + { + name: "single group", + input: []interface{}{2, 4, 6}, + fn: func(v interface{}) interface{} { return v.(int) % 2 }, + expected: map[interface{}][]interface{}{ + 0: {2, 4, 6}, + }, + }, + { + name: "group by type", + input: []interface{}{1, "a", 2, "b", true}, + fn: func(v interface{}) interface{} { + switch v.(type) { + case int: + return "int" + case string: + return "string" + default: + return "other" + } + }, + expected: map[interface{}][]interface{}{ + "int": {1, 2}, + "string": {"a", "b"}, + "other": {true}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GroupBy(tt.input, tt.fn) + if len(result) != len(tt.expected) { + t.Errorf("GroupBy failed, expected %d groups, got %d", len(tt.expected), len(result)) + } + for k, v := range tt.expected { + if !equalSlices(result[k], v) { + t.Errorf("GroupBy failed for key %v, expected %v, got %v", k, v, result[k]) + } + } + }) + } +} + +func TestFlatten(t *testing.T) { + tests := []struct { + name string + input [][]interface{} + expected []interface{} + }{ + { + name: "normal nested slices", + input: [][]interface{}{{1, 2}, {3, 4}, {5}}, + expected: []interface{}{1, 2, 3, 4, 5}, + }, + { + name: "empty outer slice", + input: [][]interface{}{}, + expected: []interface{}{}, + }, + { + name: "empty inner slices", + input: [][]interface{}{{}, {}, {}}, + expected: []interface{}{}, + }, + { + name: "mixed types", + input: [][]interface{}{{1, "a"}, {true, 2.5}, {nil}}, + expected: []interface{}{1, "a", true, 2.5, nil}, + }, + { + name: "single element slices", + input: [][]interface{}{{1}, {2}, {3}}, + expected: []interface{}{1, 2, 3}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Flatten(tt.input) + if !equalSlices(result, tt.expected) { + t.Errorf("Flatten failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestContains(t *testing.T) { + tests := []struct { + name string + slice []interface{} + item interface{} + expected bool + }{ + { + name: "contains integer", + slice: []interface{}{1, 2, 3}, + item: 2, + expected: true, + }, + { + name: "does not contain integer", + slice: []interface{}{1, 2, 3}, + item: 4, + expected: false, + }, + { + name: "contains string", + slice: []interface{}{"a", "b", "c"}, + item: "b", + expected: true, + }, + { + name: "empty slice", + slice: []interface{}{}, + item: 1, + expected: false, + }, + { + name: "contains nil", + slice: []interface{}{1, nil, 3}, + item: nil, + expected: true, + }, + { + name: "mixed types", + slice: []interface{}{1, "a", true}, + item: true, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := contains(tt.slice, tt.item) + if result != tt.expected { + t.Errorf("contains failed, expected %v, got %v", tt.expected, result) + } + }) + } +} + +// Helper function for testing +func contains(slice []interface{}, item interface{}) bool { + for _, v := range slice { + if v == item { + return true + } + } + return false +} + +// Helper functions for comparing slices +func equalSlices(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func equalNestedSlices(a, b [][]interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !equalSlices(a[i], b[i]) { + return false + } + } + return true +} diff --git a/examples/gno.land/p/moul/fp/gno.mod b/examples/gno.land/p/moul/fp/gno.mod new file mode 100644 index 00000000000..905fa0f1c0e --- /dev/null +++ b/examples/gno.land/p/moul/fp/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/fp diff --git a/examples/gno.land/p/moul/memo/gno.mod b/examples/gno.land/p/moul/memo/gno.mod new file mode 100644 index 00000000000..4a9948c30f7 --- /dev/null +++ b/examples/gno.land/p/moul/memo/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/memo diff --git a/examples/gno.land/p/moul/memo/memo.gno b/examples/gno.land/p/moul/memo/memo.gno new file mode 100644 index 00000000000..e31f13aab15 --- /dev/null +++ b/examples/gno.land/p/moul/memo/memo.gno @@ -0,0 +1,134 @@ +// Package memo provides a simple memoization utility to cache function results. +// +// The package offers a Memoizer type that can cache function results based on keys, +// with optional validation of cached values. This is useful for expensive computations +// that need to be cached and potentially invalidated based on custom conditions. +// +// /!\ Important Warning for Gno Usage: +// In Gno, storage updates only persist during transactions. This means: +// - Cache entries created during queries will NOT persist +// - Creating cache entries during queries will actually decrease performance +// as it wastes resources trying to save data that won't be saved +// +// Best Practices: +// - Use this pattern in transaction-driven contexts rather than query/render scenarios +// - Consider controlled cache updates, e.g., by specific accounts (like oracles) +// - Ideal for cases where cache updates happen every N blocks or on specific events +// - Carefully evaluate if caching will actually improve performance in your use case +// +// Basic usage example: +// +// m := memo.New() +// +// // Cache expensive computation +// result := m.Memoize("key", func() interface{} { +// // expensive operation +// return "computed-value" +// }) +// +// // Subsequent calls with same key return cached result +// result = m.Memoize("key", func() interface{} { +// // function won't be called, cached value is returned +// return "computed-value" +// }) +// +// Example with validation: +// +// type TimestampedValue struct { +// Value string +// Timestamp time.Time +// } +// +// m := memo.New() +// +// // Cache value with timestamp +// result := m.MemoizeWithValidator( +// "key", +// func() interface{} { +// return TimestampedValue{ +// Value: "data", +// Timestamp: time.Now(), +// } +// }, +// func(cached interface{}) bool { +// // Validate that the cached value is not older than 1 hour +// if tv, ok := cached.(TimestampedValue); ok { +// return time.Since(tv.Timestamp) < time.Hour +// } +// return false +// }, +// ) +package memo + +import ( + "gno.land/p/demo/btree" + "gno.land/p/demo/ufmt" +) + +// Record implements the btree.Record interface for our cache entries +type cacheEntry struct { + key interface{} + value interface{} +} + +// Less implements btree.Record interface +func (e cacheEntry) Less(than btree.Record) bool { + // Convert the other record to cacheEntry + other := than.(cacheEntry) + // Compare string representations of keys for consistent ordering + return ufmt.Sprintf("%v", e.key) < ufmt.Sprintf("%v", other.key) +} + +// Memoizer is a structure to handle memoization of function results. +type Memoizer struct { + cache *btree.BTree +} + +// New creates a new Memoizer instance. +func New() *Memoizer { + return &Memoizer{ + cache: btree.New(), + } +} + +// Memoize ensures the result of the given function is cached for the specified key. +func (m *Memoizer) Memoize(key interface{}, fn func() interface{}) interface{} { + entry := cacheEntry{key: key} + if found := m.cache.Get(entry); found != nil { + return found.(cacheEntry).value + } + + value := fn() + m.cache.Insert(cacheEntry{key: key, value: value}) + return value +} + +// MemoizeWithValidator ensures the result is cached and valid according to the validator function. +func (m *Memoizer) MemoizeWithValidator(key interface{}, fn func() interface{}, isValid func(interface{}) bool) interface{} { + entry := cacheEntry{key: key} + if found := m.cache.Get(entry); found != nil { + cachedEntry := found.(cacheEntry) + if isValid(cachedEntry.value) { + return cachedEntry.value + } + } + + value := fn() + m.cache.Insert(cacheEntry{key: key, value: value}) + return value +} + +// Invalidate removes the cached value for the specified key. +func (m *Memoizer) Invalidate(key interface{}) { + m.cache.Delete(cacheEntry{key: key}) +} + +// Clear clears all cached values. +func (m *Memoizer) Clear() { + m.cache.Clear(true) +} + +// Size returns the number of items currently in the cache. +func (m *Memoizer) Size() int { + return m.cache.Len() +} diff --git a/examples/gno.land/p/moul/memo/memo_test.gno b/examples/gno.land/p/moul/memo/memo_test.gno new file mode 100644 index 00000000000..44dde5df640 --- /dev/null +++ b/examples/gno.land/p/moul/memo/memo_test.gno @@ -0,0 +1,449 @@ +package memo + +import ( + "std" + "testing" + "time" +) + +type timestampedValue struct { + value interface{} + timestamp time.Time +} + +// complexKey is used to test struct keys +type complexKey struct { + ID int + Name string +} + +func TestMemoize(t *testing.T) { + tests := []struct { + name string + key interface{} + value interface{} + callCount *int + }{ + { + name: "string key and value", + key: "test-key", + value: "test-value", + callCount: new(int), + }, + { + name: "int key and value", + key: 42, + value: 123, + callCount: new(int), + }, + { + name: "mixed types", + key: "number", + value: 42, + callCount: new(int), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := New() + if m.Size() != 0 { + t.Errorf("Initial size = %d, want 0", m.Size()) + } + + fn := func() interface{} { + *tt.callCount++ + return tt.value + } + + // First call should compute + result := m.Memoize(tt.key, fn) + if result != tt.value { + t.Errorf("Memoize() = %v, want %v", result, tt.value) + } + if *tt.callCount != 1 { + t.Errorf("Function called %d times, want 1", *tt.callCount) + } + if m.Size() != 1 { + t.Errorf("Size after first call = %d, want 1", m.Size()) + } + + // Second call should use cache + result = m.Memoize(tt.key, fn) + if result != tt.value { + t.Errorf("Memoize() second call = %v, want %v", result, tt.value) + } + if *tt.callCount != 1 { + t.Errorf("Function called %d times, want 1", *tt.callCount) + } + if m.Size() != 1 { + t.Errorf("Size after second call = %d, want 1", m.Size()) + } + }) + } +} + +func TestMemoizeWithValidator(t *testing.T) { + tests := []struct { + name string + key interface{} + value interface{} + validDuration time.Duration + waitDuration time.Duration + expectedCalls int + shouldRecompute bool + }{ + { + name: "valid cache", + key: "key1", + value: "value1", + validDuration: time.Hour, + waitDuration: time.Millisecond, + expectedCalls: 1, + shouldRecompute: false, + }, + { + name: "expired cache", + key: "key2", + value: "value2", + validDuration: time.Millisecond, + waitDuration: time.Millisecond * 2, + expectedCalls: 2, + shouldRecompute: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := New() + callCount := 0 + + fn := func() interface{} { + callCount++ + return timestampedValue{ + value: tt.value, + timestamp: time.Now(), + } + } + + isValid := func(cached interface{}) bool { + if tv, ok := cached.(timestampedValue); ok { + return time.Since(tv.timestamp) < tt.validDuration + } + return false + } + + // First call + result := m.MemoizeWithValidator(tt.key, fn, isValid) + if tv, ok := result.(timestampedValue); !ok || tv.value != tt.value { + t.Errorf("MemoizeWithValidator() = %v, want value %v", result, tt.value) + } + + // Wait + std.TestSkipHeights(10) + + // Second call + result = m.MemoizeWithValidator(tt.key, fn, isValid) + if tv, ok := result.(timestampedValue); !ok || tv.value != tt.value { + t.Errorf("MemoizeWithValidator() second call = %v, want value %v", result, tt.value) + } + + if callCount != tt.expectedCalls { + t.Errorf("Function called %d times, want %d", callCount, tt.expectedCalls) + } + }) + } +} + +func TestInvalidate(t *testing.T) { + tests := []struct { + name string + key interface{} + value interface{} + callCount *int + }{ + { + name: "invalidate existing key", + key: "test-key", + value: "test-value", + callCount: new(int), + }, + { + name: "invalidate non-existing key", + key: "missing-key", + value: "test-value", + callCount: new(int), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := New() + fn := func() interface{} { + *tt.callCount++ + return tt.value + } + + // First call + m.Memoize(tt.key, fn) + if m.Size() != 1 { + t.Errorf("Size after first call = %d, want 1", m.Size()) + } + + // Invalidate + m.Invalidate(tt.key) + if m.Size() != 0 { + t.Errorf("Size after invalidate = %d, want 0", m.Size()) + } + + // Call again should recompute + result := m.Memoize(tt.key, fn) + if result != tt.value { + t.Errorf("Memoize() after invalidate = %v, want %v", result, tt.value) + } + if *tt.callCount != 2 { + t.Errorf("Function called %d times, want 2", *tt.callCount) + } + if m.Size() != 1 { + t.Errorf("Size after recompute = %d, want 1", m.Size()) + } + }) + } +} + +func TestClear(t *testing.T) { + m := New() + callCount := 0 + + fn := func() interface{} { + callCount++ + return "value" + } + + // Cache some values + m.Memoize("key1", fn) + m.Memoize("key2", fn) + + if callCount != 2 { + t.Errorf("Initial calls = %d, want 2", callCount) + } + if m.Size() != 2 { + t.Errorf("Size after initial calls = %d, want 2", m.Size()) + } + + // Clear cache + m.Clear() + if m.Size() != 0 { + t.Errorf("Size after clear = %d, want 0", m.Size()) + } + + // Recompute values + m.Memoize("key1", fn) + m.Memoize("key2", fn) + + if callCount != 4 { + t.Errorf("Calls after clear = %d, want 4", callCount) + } + if m.Size() != 2 { + t.Errorf("Size after recompute = %d, want 2", m.Size()) + } +} + +func TestSize(t *testing.T) { + m := New() + + if m.Size() != 0 { + t.Errorf("Initial size = %d, want 0", m.Size()) + } + + callCount := 0 + fn := func() interface{} { + callCount++ + return "value" + } + + // Add items + m.Memoize("key1", fn) + if m.Size() != 1 { + t.Errorf("Size after first insert = %d, want 1", m.Size()) + } + + m.Memoize("key2", fn) + if m.Size() != 2 { + t.Errorf("Size after second insert = %d, want 2", m.Size()) + } + + // Duplicate key should not increase size + m.Memoize("key1", fn) + if m.Size() != 2 { + t.Errorf("Size after duplicate insert = %d, want 2", m.Size()) + } + + // Remove item + m.Invalidate("key1") + if m.Size() != 1 { + t.Errorf("Size after invalidate = %d, want 1", m.Size()) + } + + // Clear all + m.Clear() + if m.Size() != 0 { + t.Errorf("Size after clear = %d, want 0", m.Size()) + } +} + +func TestMemoizeWithDifferentKeyTypes(t *testing.T) { + tests := []struct { + name string + keys []interface{} // Now an array of keys + values []string // Corresponding values + callCount *int + }{ + { + name: "integer keys", + keys: []interface{}{42, 43}, + values: []string{"value-for-42", "value-for-43"}, + callCount: new(int), + }, + { + name: "float keys", + keys: []interface{}{3.14, 2.718}, + values: []string{"value-for-pi", "value-for-e"}, + callCount: new(int), + }, + { + name: "bool keys", + keys: []interface{}{true, false}, + values: []string{"value-for-true", "value-for-false"}, + callCount: new(int), + }, + /* + { + name: "struct keys", + keys: []interface{}{ + complexKey{ID: 1, Name: "test1"}, + complexKey{ID: 2, Name: "test2"}, + }, + values: []string{"value-for-struct1", "value-for-struct2"}, + callCount: new(int), + }, + { + name: "nil and empty interface keys", + keys: []interface{}{nil, interface{}(nil)}, + values: []string{"value-for-nil", "value-for-empty-interface"}, + callCount: new(int), + }, + */ + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := New() + + // Test both keys + for i, key := range tt.keys { + value := tt.values[i] + fn := func() interface{} { + *tt.callCount++ + return value + } + + // First call should compute + result := m.Memoize(key, fn) + if result != value { + t.Errorf("Memoize() for key %v = %v, want %v", key, result, value) + } + if *tt.callCount != i+1 { + t.Errorf("Function called %d times, want %d", *tt.callCount, i+1) + } + } + + // Verify size includes both entries + if m.Size() != 2 { + t.Errorf("Size after both inserts = %d, want 2", m.Size()) + } + + // Second call for each key should use cache + for i, key := range tt.keys { + initialCount := *tt.callCount + result := m.Memoize(key, func() interface{} { + *tt.callCount++ + return "should-not-be-called" + }) + + if result != tt.values[i] { + t.Errorf("Memoize() second call for key %v = %v, want %v", key, result, tt.values[i]) + } + if *tt.callCount != initialCount { + t.Errorf("Cache miss for key %v", key) + } + } + + // Test invalidate for each key + for i, key := range tt.keys { + m.Invalidate(key) + if m.Size() != 1-i { + t.Errorf("Size after invalidate %d = %d, want %d", i+1, m.Size(), 1-i) + } + } + }) + } +} + +func TestMultipleKeyTypes(t *testing.T) { + m := New() + callCount := 0 + + // Insert different key types simultaneously (two of each type) + keys := []interface{}{ + 42, 43, // ints + "string-key1", "string-key2", // strings + 3.14, 2.718, // floats + true, false, // bools + } + + for i, key := range keys { + value := i + m.Memoize(key, func() interface{} { + callCount++ + return value + }) + } + + // Verify size includes all entries + if m.Size() != len(keys) { + t.Errorf("Size = %d, want %d", m.Size(), len(keys)) + } + + // Verify all values are cached correctly + for i, key := range keys { + initialCount := callCount + result := m.Memoize(key, func() interface{} { + callCount++ + return -1 // Should never be returned if cache works + }) + + if result != i { + t.Errorf("Memoize(%v) = %v, want %v", key, result, i) + } + if callCount != initialCount { + t.Errorf("Cache miss for key %v", key) + } + } + + // Test invalidation of pairs + for i := 0; i < len(keys); i += 2 { + m.Invalidate(keys[i]) + m.Invalidate(keys[i+1]) + expectedSize := len(keys) - (i + 2) + if m.Size() != expectedSize { + t.Errorf("Size after invalidating pair %d = %d, want %d", i/2, m.Size(), expectedSize) + } + } + + // Clear and verify + m.Clear() + if m.Size() != 0 { + t.Errorf("Size after clear = %d, want 0", m.Size()) + } +} diff --git a/examples/gno.land/p/moul/typeutil/gno.mod b/examples/gno.land/p/moul/typeutil/gno.mod new file mode 100644 index 00000000000..4f9c432456b --- /dev/null +++ b/examples/gno.land/p/moul/typeutil/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/typeutil diff --git a/examples/gno.land/p/moul/typeutil/typeutil.gno b/examples/gno.land/p/moul/typeutil/typeutil.gno new file mode 100644 index 00000000000..1fa79b94549 --- /dev/null +++ b/examples/gno.land/p/moul/typeutil/typeutil.gno @@ -0,0 +1,715 @@ +// Package typeutil provides utility functions for converting between different types +// and checking their states. It aims to provide consistent behavior across different +// types while remaining lightweight and dependency-free. +package typeutil + +import ( + "errors" + "sort" + "std" + "strconv" + "strings" + "time" +) + +// stringer is the interface that wraps the String method. +type stringer interface { + String() string +} + +// ToString converts any value to its string representation. +// It supports a wide range of Go types including: +// - Basic: string, bool +// - Numbers: int, int8-64, uint, uint8-64, float32, float64 +// - Special: time.Time, std.Address, []byte +// - Slices: []T for most basic types +// - Maps: map[string]string, map[string]interface{} +// - Interface: types implementing String() string +// +// Example usage: +// +// str := typeutil.ToString(42) // "42" +// str = typeutil.ToString([]int{1, 2}) // "[1 2]" +// str = typeutil.ToString(map[string]string{ // "map[a:1 b:2]" +// "a": "1", +// "b": "2", +// }) +func ToString(val interface{}) string { + if val == nil { + return "" + } + + // First check if value implements Stringer interface + if s, ok := val.(interface{ String() string }); ok { + return s.String() + } + + switch v := val.(type) { + // Pointer types - dereference and recurse + case *string: + if v == nil { + return "" + } + return *v + case *int: + if v == nil { + return "" + } + return strconv.Itoa(*v) + case *bool: + if v == nil { + return "" + } + return strconv.FormatBool(*v) + case *time.Time: + if v == nil { + return "" + } + return v.String() + case *std.Address: + if v == nil { + return "" + } + return string(*v) + + // String types + case string: + return v + case stringer: + return v.String() + + // Special types + case time.Time: + return v.String() + case std.Address: + return string(v) + case []byte: + return string(v) + case struct{}: + return "{}" + + // Integer types + case int: + return strconv.Itoa(v) + case int8: + return strconv.FormatInt(int64(v), 10) + case int16: + return strconv.FormatInt(int64(v), 10) + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case uint: + return strconv.FormatUint(uint64(v), 10) + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint16: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + + // Float types + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + + // Boolean + case bool: + if v { + return "true" + } + return "false" + + // Slice types + case []string: + return join(v) + case []int: + return join(v) + case []int32: + return join(v) + case []int64: + return join(v) + case []float32: + return join(v) + case []float64: + return join(v) + case []interface{}: + return join(v) + case []time.Time: + return joinTimes(v) + case []stringer: + return join(v) + case []std.Address: + return joinAddresses(v) + case [][]byte: + return joinBytes(v) + + // Map types with various key types + case map[interface{}]interface{}, map[string]interface{}, map[string]string, map[string]int: + var b strings.Builder + b.WriteString("map[") + first := true + + switch m := v.(type) { + case map[interface{}]interface{}: + // Convert all keys to strings for consistent ordering + keys := make([]string, 0) + keyMap := make(map[string]interface{}) + + for k := range m { + keyStr := ToString(k) + keys = append(keys, keyStr) + keyMap[keyStr] = k + } + sort.Strings(keys) + + for _, keyStr := range keys { + if !first { + b.WriteString(" ") + } + origKey := keyMap[keyStr] + b.WriteString(keyStr) + b.WriteString(":") + b.WriteString(ToString(m[origKey])) + first = false + } + + case map[string]interface{}: + keys := make([]string, 0) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + if !first { + b.WriteString(" ") + } + b.WriteString(k) + b.WriteString(":") + b.WriteString(ToString(m[k])) + first = false + } + + case map[string]string: + keys := make([]string, 0) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + if !first { + b.WriteString(" ") + } + b.WriteString(k) + b.WriteString(":") + b.WriteString(m[k]) + first = false + } + + case map[string]int: + keys := make([]string, 0) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + if !first { + b.WriteString(" ") + } + b.WriteString(k) + b.WriteString(":") + b.WriteString(strconv.Itoa(m[k])) + first = false + } + } + b.WriteString("]") + return b.String() + + // Default + default: + return "" + } +} + +func join(slice interface{}) string { + if IsZero(slice) { + return "[]" + } + + items := ToInterfaceSlice(slice) + if items == nil { + return "[]" + } + + var b strings.Builder + b.WriteString("[") + for i, item := range items { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(ToString(item)) + } + b.WriteString("]") + return b.String() +} + +func joinTimes(slice []time.Time) string { + if len(slice) == 0 { + return "[]" + } + var b strings.Builder + b.WriteString("[") + for i, t := range slice { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(t.String()) + } + b.WriteString("]") + return b.String() +} + +func joinAddresses(slice []std.Address) string { + if len(slice) == 0 { + return "[]" + } + var b strings.Builder + b.WriteString("[") + for i, addr := range slice { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(string(addr)) + } + b.WriteString("]") + return b.String() +} + +func joinBytes(slice [][]byte) string { + if len(slice) == 0 { + return "[]" + } + var b strings.Builder + b.WriteString("[") + for i, bytes := range slice { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(string(bytes)) + } + b.WriteString("]") + return b.String() +} + +// ToBool converts any value to a boolean based on common programming conventions. +// For example: +// - Numbers: 0 is false, any other number is true +// - Strings: "", "0", "false", "f", "no", "n", "off" are false, others are true +// - Slices/Maps: empty is false, non-empty is true +// - nil: always false +// - bool: direct value +func ToBool(val interface{}) bool { + if IsZero(val) { + return false + } + + // Handle special string cases + if str, ok := val.(string); ok { + str = strings.ToLower(strings.TrimSpace(str)) + return str != "" && str != "0" && str != "false" && str != "f" && str != "no" && str != "n" && str != "off" + } + + return true +} + +// IsZero returns true if the value represents a "zero" or "empty" state for its type. +// For example: +// - Numbers: 0 +// - Strings: "" +// - Slices/Maps: empty +// - nil: true +// - bool: false +// - time.Time: IsZero() +// - std.Address: empty string +func IsZero(val interface{}) bool { + if val == nil { + return true + } + + switch v := val.(type) { + // Pointer types - nil pointer is zero, otherwise check pointed value + case *bool: + return v == nil || !*v + case *string: + return v == nil || *v == "" + case *int: + return v == nil || *v == 0 + case *time.Time: + return v == nil || v.IsZero() + case *std.Address: + return v == nil || string(*v) == "" + + // Bool + case bool: + return !v + + // String types + case string: + return v == "" + case stringer: + return v.String() == "" + + // Integer types + case int: + return v == 0 + case int8: + return v == 0 + case int16: + return v == 0 + case int32: + return v == 0 + case int64: + return v == 0 + case uint: + return v == 0 + case uint8: + return v == 0 + case uint16: + return v == 0 + case uint32: + return v == 0 + case uint64: + return v == 0 + + // Float types + case float32: + return v == 0 + case float64: + return v == 0 + + // Special types + case []byte: + return len(v) == 0 + case time.Time: + return v.IsZero() + case std.Address: + return string(v) == "" + + // Slices (check if empty) + case []string: + return len(v) == 0 + case []int: + return len(v) == 0 + case []int32: + return len(v) == 0 + case []int64: + return len(v) == 0 + case []float32: + return len(v) == 0 + case []float64: + return len(v) == 0 + case []interface{}: + return len(v) == 0 + case []time.Time: + return len(v) == 0 + case []std.Address: + return len(v) == 0 + case [][]byte: + return len(v) == 0 + case []stringer: + return len(v) == 0 + + // Maps (check if empty) + case map[string]string: + return len(v) == 0 + case map[string]interface{}: + return len(v) == 0 + + default: + return false // non-nil unknown types are considered non-zero + } +} + +// ToInterfaceSlice converts various slice types to []interface{} +func ToInterfaceSlice(val interface{}) []interface{} { + switch v := val.(type) { + case []interface{}: + return v + case []string: + result := make([]interface{}, len(v)) + for i, s := range v { + result[i] = s + } + return result + case []int: + result := make([]interface{}, len(v)) + for i, n := range v { + result[i] = n + } + return result + case []int32: + result := make([]interface{}, len(v)) + for i, n := range v { + result[i] = n + } + return result + case []int64: + result := make([]interface{}, len(v)) + for i, n := range v { + result[i] = n + } + return result + case []float32: + result := make([]interface{}, len(v)) + for i, n := range v { + result[i] = n + } + return result + case []float64: + result := make([]interface{}, len(v)) + for i, n := range v { + result[i] = n + } + return result + case []bool: + result := make([]interface{}, len(v)) + for i, b := range v { + result[i] = b + } + return result + default: + return nil + } +} + +// ToMapStringInterface converts a map with string keys and any value type to map[string]interface{} +func ToMapStringInterface(m interface{}) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + switch v := m.(type) { + case map[string]interface{}: + return v, nil + case map[string]string: + for k, val := range v { + result[k] = val + } + case map[string]int: + for k, val := range v { + result[k] = val + } + case map[string]int64: + for k, val := range v { + result[k] = val + } + case map[string]float64: + for k, val := range v { + result[k] = val + } + case map[string]bool: + for k, val := range v { + result[k] = val + } + case map[string][]string: + for k, val := range v { + result[k] = ToInterfaceSlice(val) + } + case map[string][]int: + for k, val := range v { + result[k] = ToInterfaceSlice(val) + } + case map[string][]interface{}: + for k, val := range v { + result[k] = val + } + case map[string]map[string]interface{}: + for k, val := range v { + result[k] = val + } + case map[string]map[string]string: + for k, val := range v { + if converted, err := ToMapStringInterface(val); err == nil { + result[k] = converted + } else { + return nil, errors.New("failed to convert nested map at key: " + k) + } + } + default: + return nil, errors.New("unsupported map type: " + ToString(m)) + } + + return result, nil +} + +// ToMapIntInterface converts a map with int keys and any value type to map[int]interface{} +func ToMapIntInterface(m interface{}) (map[int]interface{}, error) { + result := make(map[int]interface{}) + + switch v := m.(type) { + case map[int]interface{}: + return v, nil + case map[int]string: + for k, val := range v { + result[k] = val + } + case map[int]int: + for k, val := range v { + result[k] = val + } + case map[int]int64: + for k, val := range v { + result[k] = val + } + case map[int]float64: + for k, val := range v { + result[k] = val + } + case map[int]bool: + for k, val := range v { + result[k] = val + } + case map[int][]string: + for k, val := range v { + result[k] = ToInterfaceSlice(val) + } + case map[int][]int: + for k, val := range v { + result[k] = ToInterfaceSlice(val) + } + case map[int][]interface{}: + for k, val := range v { + result[k] = val + } + case map[int]map[string]interface{}: + for k, val := range v { + result[k] = val + } + case map[int]map[int]interface{}: + for k, val := range v { + result[k] = val + } + default: + return nil, errors.New("unsupported map type: " + ToString(m)) + } + + return result, nil +} + +// ToStringSlice converts various slice types to []string +func ToStringSlice(val interface{}) []string { + switch v := val.(type) { + case []string: + return v + case []interface{}: + result := make([]string, len(v)) + for i, item := range v { + result[i] = ToString(item) + } + return result + case []int: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.Itoa(n) + } + return result + case []int32: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatInt(int64(n), 10) + } + return result + case []int64: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatInt(n, 10) + } + return result + case []float32: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatFloat(float64(n), 'f', -1, 32) + } + return result + case []float64: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatFloat(n, 'f', -1, 64) + } + return result + case []bool: + result := make([]string, len(v)) + for i, b := range v { + result[i] = strconv.FormatBool(b) + } + return result + case []time.Time: + result := make([]string, len(v)) + for i, t := range v { + result[i] = t.String() + } + return result + case []std.Address: + result := make([]string, len(v)) + for i, addr := range v { + result[i] = string(addr) + } + return result + case [][]byte: + result := make([]string, len(v)) + for i, b := range v { + result[i] = string(b) + } + return result + case []stringer: + result := make([]string, len(v)) + for i, s := range v { + result[i] = s.String() + } + return result + case []uint: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatUint(uint64(n), 10) + } + return result + case []uint8: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatUint(uint64(n), 10) + } + return result + case []uint16: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatUint(uint64(n), 10) + } + return result + case []uint32: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatUint(uint64(n), 10) + } + return result + case []uint64: + result := make([]string, len(v)) + for i, n := range v { + result[i] = strconv.FormatUint(n, 10) + } + return result + default: + // Try to convert using reflection if it's a slice + if slice := ToInterfaceSlice(val); slice != nil { + result := make([]string, len(slice)) + for i, item := range slice { + result[i] = ToString(item) + } + return result + } + return nil + } +} diff --git a/examples/gno.land/p/moul/typeutil/typeutil_test.gno b/examples/gno.land/p/moul/typeutil/typeutil_test.gno new file mode 100644 index 00000000000..543ea1deec4 --- /dev/null +++ b/examples/gno.land/p/moul/typeutil/typeutil_test.gno @@ -0,0 +1,1075 @@ +package typeutil + +import ( + "std" + "strings" + "testing" + "time" +) + +type testStringer struct { + value string +} + +func (t testStringer) String() string { + return "test:" + t.value +} + +func TestToString(t *testing.T) { + // setup test data + str := "hello" + num := 42 + b := true + now := time.Now() + addr := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + stringer := testStringer{value: "hello"} + + type testCase struct { + name string + input interface{} + expected string + } + + tests := []testCase{ + // basic types + {"string", "hello", "hello"}, + {"empty_string", "", ""}, + {"nil", nil, ""}, + + // integer types + {"int", 42, "42"}, + {"int8", int8(8), "8"}, + {"int16", int16(16), "16"}, + {"int32", int32(32), "32"}, + {"int64", int64(64), "64"}, + {"uint", uint(42), "42"}, + {"uint8", uint8(8), "8"}, + {"uint16", uint16(16), "16"}, + {"uint32", uint32(32), "32"}, + {"uint64", uint64(64), "64"}, + + // float types + {"float32", float32(3.14), "3.14"}, + {"float64", 3.14159, "3.14159"}, + + // boolean + {"bool_true", true, "true"}, + {"bool_false", false, "false"}, + + // special types + {"time", now, now.String()}, + {"address", addr, string(addr)}, + {"bytes", []byte("hello"), "hello"}, + {"stringer", stringer, "test:hello"}, + + // slices + {"empty_slice", []string{}, "[]"}, + {"string_slice", []string{"a", "b"}, "[a b]"}, + {"int_slice", []int{1, 2}, "[1 2]"}, + {"int32_slice", []int32{1, 2}, "[1 2]"}, + {"int64_slice", []int64{1, 2}, "[1 2]"}, + {"float32_slice", []float32{1.1, 2.2}, "[1.1 2.2]"}, + {"float64_slice", []float64{1.1, 2.2}, "[1.1 2.2]"}, + {"bytes_slice", [][]byte{[]byte("a"), []byte("b")}, "[a b]"}, + {"time_slice", []time.Time{now, now}, "[" + now.String() + " " + now.String() + "]"}, + {"address_slice", []std.Address{addr, addr}, "[" + string(addr) + " " + string(addr) + "]"}, + {"interface_slice", []interface{}{1, "a", true}, "[1 a true]"}, + + // empty slices + {"empty_string_slice", []string{}, "[]"}, + {"empty_int_slice", []int{}, "[]"}, + {"empty_int32_slice", []int32{}, "[]"}, + {"empty_int64_slice", []int64{}, "[]"}, + {"empty_float32_slice", []float32{}, "[]"}, + {"empty_float64_slice", []float64{}, "[]"}, + {"empty_bytes_slice", [][]byte{}, "[]"}, + {"empty_time_slice", []time.Time{}, "[]"}, + {"empty_address_slice", []std.Address{}, "[]"}, + {"empty_interface_slice", []interface{}{}, "[]"}, + + // maps + {"empty_string_map", map[string]string{}, "map[]"}, + {"string_map", map[string]string{"a": "1", "b": "2"}, "map[a:1 b:2]"}, + {"empty_interface_map", map[string]interface{}{}, "map[]"}, + {"interface_map", map[string]interface{}{"a": 1, "b": "2"}, "map[a:1 b:2]"}, + + // edge cases + {"empty_bytes", []byte{}, ""}, + {"nil_interface", interface{}(nil), ""}, + {"empty_struct", struct{}{}, "{}"}, + {"unknown_type", struct{ foo string }{}, ""}, + + // pointer types + {"nil_string_ptr", (*string)(nil), ""}, + {"string_ptr", &str, "hello"}, + {"nil_int_ptr", (*int)(nil), ""}, + {"int_ptr", &num, "42"}, + {"nil_bool_ptr", (*bool)(nil), ""}, + {"bool_ptr", &b, "true"}, + // {"nil_time_ptr", (*time.Time)(nil), ""}, // TODO: fix this + {"time_ptr", &now, now.String()}, + // {"nil_address_ptr", (*std.Address)(nil), ""}, // TODO: fix this + {"address_ptr", &addr, string(addr)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToString(tt.input) + if got != tt.expected { + t.Errorf("%s: ToString(%v) = %q, want %q", tt.name, tt.input, got, tt.expected) + } + }) + } +} + +func TestToBool(t *testing.T) { + str := "true" + num := 42 + b := true + now := time.Now() + addr := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + zero := 0 + empty := "" + falseVal := false + + type testCase struct { + name string + input interface{} + expected bool + } + + tests := []testCase{ + // basic types + {"true", true, true}, + {"false", false, false}, + {"nil", nil, false}, + + // strings + {"empty_string", "", false}, + {"zero_string", "0", false}, + {"false_string", "false", false}, + {"f_string", "f", false}, + {"no_string", "no", false}, + {"n_string", "n", false}, + {"off_string", "off", false}, + {"space_string", " ", false}, + {"true_string", "true", true}, + {"yes_string", "yes", true}, + {"random_string", "hello", true}, + + // numbers + {"zero_int", 0, false}, + {"positive_int", 1, true}, + {"negative_int", -1, true}, + {"zero_float", 0.0, false}, + {"positive_float", 0.1, true}, + {"negative_float", -0.1, true}, + + // special types + {"empty_bytes", []byte{}, false}, + {"non_empty_bytes", []byte{1}, true}, + /*{"zero_time", time.Time{}, false},*/ // TODO: fix this + {"empty_address", std.Address(""), false}, + + // slices + {"empty_slice", []string{}, false}, + {"non_empty_slice", []string{"a"}, true}, + + // maps + {"empty_map", map[string]string{}, false}, + {"non_empty_map", map[string]string{"a": "b"}, true}, + + // pointer types + {"nil_bool_ptr", (*bool)(nil), false}, + {"true_ptr", &b, true}, + {"false_ptr", &falseVal, false}, + {"nil_string_ptr", (*string)(nil), false}, + {"string_ptr", &str, true}, + {"empty_string_ptr", &empty, false}, + {"nil_int_ptr", (*int)(nil), false}, + {"int_ptr", &num, true}, + {"zero_int_ptr", &zero, false}, + // {"nil_time_ptr", (*time.Time)(nil), false}, // TODO: fix this + {"time_ptr", &now, true}, + // {"nil_address_ptr", (*std.Address)(nil), false}, // TODO: fix this + {"address_ptr", &addr, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToBool(tt.input) + if got != tt.expected { + t.Errorf("%s: ToBool(%v) = %v, want %v", tt.name, tt.input, got, tt.expected) + } + }) + } +} + +func TestIsZero(t *testing.T) { + str := "hello" + num := 42 + b := true + now := time.Now() + addr := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + zero := 0 + empty := "" + falseVal := false + + type testCase struct { + name string + input interface{} + expected bool + } + + tests := []testCase{ + // basic types + {"true", true, false}, + {"false", false, true}, + {"nil", nil, true}, + + // strings + {"empty_string", "", true}, + {"non_empty_string", "hello", false}, + + // numbers + {"zero_int", 0, true}, + {"non_zero_int", 1, false}, + {"zero_float", 0.0, true}, + {"non_zero_float", 0.1, false}, + + // special types + {"empty_bytes", []byte{}, true}, + {"non_empty_bytes", []byte{1}, false}, + /*{"zero_time", time.Time{}, true},*/ // TODO: fix this + {"empty_address", std.Address(""), true}, + + // slices + {"empty_slice", []string{}, true}, + {"non_empty_slice", []string{"a"}, false}, + + // maps + {"empty_map", map[string]string{}, true}, + {"non_empty_map", map[string]string{"a": "b"}, false}, + + // pointer types + {"nil_bool_ptr", (*bool)(nil), true}, + {"false_ptr", &falseVal, true}, + {"true_ptr", &b, false}, + {"nil_string_ptr", (*string)(nil), true}, + {"empty_string_ptr", &empty, true}, + {"string_ptr", &str, false}, + {"nil_int_ptr", (*int)(nil), true}, + {"zero_int_ptr", &zero, true}, + {"int_ptr", &num, false}, + // {"nil_time_ptr", (*time.Time)(nil), true}, // TODO: fix this + {"time_ptr", &now, false}, + // {"nil_address_ptr", (*std.Address)(nil), true}, // TODO: fix this + {"address_ptr", &addr, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsZero(tt.input) + if got != tt.expected { + t.Errorf("%s: IsZero(%v) = %v, want %v", tt.name, tt.input, got, tt.expected) + } + }) + } +} + +func TestToInterfaceSlice(t *testing.T) { + now := time.Now() + addr := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + str := testStringer{value: "hello"} + + tests := []struct { + name string + input interface{} + expected []interface{} + compare func([]interface{}, []interface{}) bool + }{ + { + name: "nil", + input: nil, + expected: nil, + compare: compareNil, + }, + { + name: "empty_interface_slice", + input: []interface{}{}, + expected: []interface{}{}, + compare: compareEmpty, + }, + { + name: "interface_slice", + input: []interface{}{1, "two", true}, + expected: []interface{}{1, "two", true}, + compare: compareInterfaces, + }, + { + name: "string_slice", + input: []string{"a", "b", "c"}, + expected: []interface{}{"a", "b", "c"}, + compare: compareStrings, + }, + { + name: "int_slice", + input: []int{1, 2, 3}, + expected: []interface{}{1, 2, 3}, + compare: compareInts, + }, + { + name: "int32_slice", + input: []int32{1, 2, 3}, + expected: []interface{}{int32(1), int32(2), int32(3)}, + compare: compareInt32s, + }, + { + name: "int64_slice", + input: []int64{1, 2, 3}, + expected: []interface{}{int64(1), int64(2), int64(3)}, + compare: compareInt64s, + }, + { + name: "float32_slice", + input: []float32{1.1, 2.2, 3.3}, + expected: []interface{}{float32(1.1), float32(2.2), float32(3.3)}, + compare: compareFloat32s, + }, + { + name: "float64_slice", + input: []float64{1.1, 2.2, 3.3}, + expected: []interface{}{1.1, 2.2, 3.3}, + compare: compareFloat64s, + }, + { + name: "bool_slice", + input: []bool{true, false, true}, + expected: []interface{}{true, false, true}, + compare: compareBools, + }, + /* { + name: "time_slice", + input: []time.Time{now}, + expected: []interface{}{now}, + compare: compareTimes, + }, */ // TODO: fix this + /* { + name: "address_slice", + input: []std.Address{addr}, + expected: []interface{}{addr}, + compare: compareAddresses, + },*/ // TODO: fix this + /* { + name: "bytes_slice", + input: [][]byte{[]byte("hello"), []byte("world")}, + expected: []interface{}{[]byte("hello"), []byte("world")}, + compare: compareBytes, + },*/ // TODO: fix this + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToInterfaceSlice(tt.input) + if !tt.compare(got, tt.expected) { + t.Errorf("ToInterfaceSlice() = %v, want %v", got, tt.expected) + } + }) + } +} + +func compareNil(a, b []interface{}) bool { + return a == nil && b == nil +} + +func compareEmpty(a, b []interface{}) bool { + return len(a) == 0 && len(b) == 0 +} + +func compareInterfaces(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func compareStrings(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + as, ok1 := a[i].(string) + bs, ok2 := b[i].(string) + if !ok1 || !ok2 || as != bs { + return false + } + } + return true +} + +func compareInts(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ai, ok1 := a[i].(int) + bi, ok2 := b[i].(int) + if !ok1 || !ok2 || ai != bi { + return false + } + } + return true +} + +func compareInt32s(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ai, ok1 := a[i].(int32) + bi, ok2 := b[i].(int32) + if !ok1 || !ok2 || ai != bi { + return false + } + } + return true +} + +func compareInt64s(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ai, ok1 := a[i].(int64) + bi, ok2 := b[i].(int64) + if !ok1 || !ok2 || ai != bi { + return false + } + } + return true +} + +func compareFloat32s(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ai, ok1 := a[i].(float32) + bi, ok2 := b[i].(float32) + if !ok1 || !ok2 || ai != bi { + return false + } + } + return true +} + +func compareFloat64s(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ai, ok1 := a[i].(float64) + bi, ok2 := b[i].(float64) + if !ok1 || !ok2 || ai != bi { + return false + } + } + return true +} + +func compareBools(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ab, ok1 := a[i].(bool) + bb, ok2 := b[i].(bool) + if !ok1 || !ok2 || ab != bb { + return false + } + } + return true +} + +func compareTimes(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + at, ok1 := a[i].(time.Time) + bt, ok2 := b[i].(time.Time) + if !ok1 || !ok2 || !at.Equal(bt) { + return false + } + } + return true +} + +func compareAddresses(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + aa, ok1 := a[i].(std.Address) + ba, ok2 := b[i].(std.Address) + if !ok1 || !ok2 || aa != ba { + return false + } + } + return true +} + +func compareBytes(a, b []interface{}) bool { + if len(a) != len(b) { + return false + } + for i := range a { + ab, ok1 := a[i].([]byte) + bb, ok2 := b[i].([]byte) + if !ok1 || !ok2 || string(ab) != string(bb) { + return false + } + } + return true +} + +// compareStringInterfaceMaps compares two map[string]interface{} for equality +func compareStringInterfaceMaps(a, b map[string]interface{}) bool { + if len(a) != len(b) { + return false + } + for k, v1 := range a { + v2, ok := b[k] + if !ok { + return false + } + // Compare values based on their type + switch val1 := v1.(type) { + case string: + val2, ok := v2.(string) + if !ok || val1 != val2 { + return false + } + case int: + val2, ok := v2.(int) + if !ok || val1 != val2 { + return false + } + case float64: + val2, ok := v2.(float64) + if !ok || val1 != val2 { + return false + } + case bool: + val2, ok := v2.(bool) + if !ok || val1 != val2 { + return false + } + case []interface{}: + val2, ok := v2.([]interface{}) + if !ok || len(val1) != len(val2) { + return false + } + for i := range val1 { + if val1[i] != val2[i] { + return false + } + } + case map[string]interface{}: + val2, ok := v2.(map[string]interface{}) + if !ok || !compareStringInterfaceMaps(val1, val2) { + return false + } + default: + return false + } + } + return true +} + +func TestToMapStringInterface(t *testing.T) { + tests := []struct { + name string + input interface{} + expected map[string]interface{} + wantErr bool + }{ + { + name: "map[string]interface{}", + input: map[string]interface{}{ + "key1": "value1", + "key2": 42, + }, + expected: map[string]interface{}{ + "key1": "value1", + "key2": 42, + }, + wantErr: false, + }, + { + name: "map[string]string", + input: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + expected: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + wantErr: false, + }, + { + name: "map[string]int", + input: map[string]int{ + "key1": 1, + "key2": 2, + }, + expected: map[string]interface{}{ + "key1": 1, + "key2": 2, + }, + wantErr: false, + }, + { + name: "map[string]float64", + input: map[string]float64{ + "key1": 1.1, + "key2": 2.2, + }, + expected: map[string]interface{}{ + "key1": 1.1, + "key2": 2.2, + }, + wantErr: false, + }, + { + name: "map[string]bool", + input: map[string]bool{ + "key1": true, + "key2": false, + }, + expected: map[string]interface{}{ + "key1": true, + "key2": false, + }, + wantErr: false, + }, + { + name: "map[string][]string", + input: map[string][]string{ + "key1": {"a", "b"}, + "key2": {"c", "d"}, + }, + expected: map[string]interface{}{ + "key1": []interface{}{"a", "b"}, + "key2": []interface{}{"c", "d"}, + }, + wantErr: false, + }, + { + name: "nested map[string]map[string]string", + input: map[string]map[string]string{ + "key1": {"nested1": "value1"}, + "key2": {"nested2": "value2"}, + }, + expected: map[string]interface{}{ + "key1": map[string]interface{}{"nested1": "value1"}, + "key2": map[string]interface{}{"nested2": "value2"}, + }, + wantErr: false, + }, + { + name: "unsupported type", + input: 42, // not a map + expected: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ToMapStringInterface(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ToMapStringInterface() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if !compareStringInterfaceMaps(got, tt.expected) { + t.Errorf("ToMapStringInterface() = %v, expected %v", got, tt.expected) + } + } + }) + } +} + +// Test error messages +func TestToMapStringInterfaceErrors(t *testing.T) { + _, err := ToMapStringInterface(42) + if err == nil || !strings.Contains(err.Error(), "unsupported map type") { + t.Errorf("Expected error containing 'unsupported map type', got %v", err) + } +} + +// compareIntInterfaceMaps compares two map[int]interface{} for equality +func compareIntInterfaceMaps(a, b map[int]interface{}) bool { + if len(a) != len(b) { + return false + } + for k, v1 := range a { + v2, ok := b[k] + if !ok { + return false + } + // Compare values based on their type + switch val1 := v1.(type) { + case string: + val2, ok := v2.(string) + if !ok || val1 != val2 { + return false + } + case int: + val2, ok := v2.(int) + if !ok || val1 != val2 { + return false + } + case float64: + val2, ok := v2.(float64) + if !ok || val1 != val2 { + return false + } + case bool: + val2, ok := v2.(bool) + if !ok || val1 != val2 { + return false + } + case []interface{}: + val2, ok := v2.([]interface{}) + if !ok || len(val1) != len(val2) { + return false + } + for i := range val1 { + if val1[i] != val2[i] { + return false + } + } + case map[string]interface{}: + val2, ok := v2.(map[string]interface{}) + if !ok || !compareStringInterfaceMaps(val1, val2) { + return false + } + default: + return false + } + } + return true +} + +func TestToMapIntInterface(t *testing.T) { + tests := []struct { + name string + input interface{} + expected map[int]interface{} + wantErr bool + }{ + { + name: "map[int]interface{}", + input: map[int]interface{}{ + 1: "value1", + 2: 42, + }, + expected: map[int]interface{}{ + 1: "value1", + 2: 42, + }, + wantErr: false, + }, + { + name: "map[int]string", + input: map[int]string{ + 1: "value1", + 2: "value2", + }, + expected: map[int]interface{}{ + 1: "value1", + 2: "value2", + }, + wantErr: false, + }, + { + name: "map[int]int", + input: map[int]int{ + 1: 10, + 2: 20, + }, + expected: map[int]interface{}{ + 1: 10, + 2: 20, + }, + wantErr: false, + }, + { + name: "map[int]float64", + input: map[int]float64{ + 1: 1.1, + 2: 2.2, + }, + expected: map[int]interface{}{ + 1: 1.1, + 2: 2.2, + }, + wantErr: false, + }, + { + name: "map[int]bool", + input: map[int]bool{ + 1: true, + 2: false, + }, + expected: map[int]interface{}{ + 1: true, + 2: false, + }, + wantErr: false, + }, + { + name: "map[int][]string", + input: map[int][]string{ + 1: {"a", "b"}, + 2: {"c", "d"}, + }, + expected: map[int]interface{}{ + 1: []interface{}{"a", "b"}, + 2: []interface{}{"c", "d"}, + }, + wantErr: false, + }, + { + name: "map[int]map[string]interface{}", + input: map[int]map[string]interface{}{ + 1: {"nested1": "value1"}, + 2: {"nested2": "value2"}, + }, + expected: map[int]interface{}{ + 1: map[string]interface{}{"nested1": "value1"}, + 2: map[string]interface{}{"nested2": "value2"}, + }, + wantErr: false, + }, + { + name: "unsupported type", + input: 42, // not a map + expected: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ToMapIntInterface(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ToMapIntInterface() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if !compareIntInterfaceMaps(got, tt.expected) { + t.Errorf("ToMapIntInterface() = %v, expected %v", got, tt.expected) + } + } + }) + } +} + +func TestToStringSlice(t *testing.T) { + tests := []struct { + name string + input interface{} + expected []string + }{ + { + name: "nil input", + input: nil, + expected: nil, + }, + { + name: "empty slice", + input: []string{}, + expected: []string{}, + }, + { + name: "string slice", + input: []string{"a", "b", "c"}, + expected: []string{"a", "b", "c"}, + }, + { + name: "int slice", + input: []int{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "int32 slice", + input: []int32{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "int64 slice", + input: []int64{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "uint slice", + input: []uint{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "uint8 slice", + input: []uint8{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "uint16 slice", + input: []uint16{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "uint32 slice", + input: []uint32{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "uint64 slice", + input: []uint64{1, 2, 3}, + expected: []string{"1", "2", "3"}, + }, + { + name: "float32 slice", + input: []float32{1.1, 2.2, 3.3}, + expected: []string{"1.1", "2.2", "3.3"}, + }, + { + name: "float64 slice", + input: []float64{1.1, 2.2, 3.3}, + expected: []string{"1.1", "2.2", "3.3"}, + }, + { + name: "bool slice", + input: []bool{true, false, true}, + expected: []string{"true", "false", "true"}, + }, + { + name: "[]byte slice", + input: [][]byte{[]byte("hello"), []byte("world")}, + expected: []string{"hello", "world"}, + }, + { + name: "interface slice", + input: []interface{}{1, "hello", true}, + expected: []string{"1", "hello", "true"}, + }, + { + name: "time slice", + input: []time.Time{time.Time{}, time.Time{}}, + expected: []string{"0001-01-01 00:00:00 +0000 UTC", "0001-01-01 00:00:00 +0000 UTC"}, + }, + { + name: "address slice", + input: []std.Address{"addr1", "addr2"}, + expected: []string{"addr1", "addr2"}, + }, + { + name: "non-slice input", + input: 42, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ToStringSlice(tt.input) + if !slicesEqual(result, tt.expected) { + t.Errorf("ToStringSlice(%v) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} + +// Helper function to compare string slices +func slicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestToStringAdvanced(t *testing.T) { + tests := []struct { + name string + input interface{} + expected string + }{ + { + name: "slice with mixed basic types", + input: []interface{}{ + 42, + "hello", + true, + 3.14, + }, + expected: "[42 hello true 3.14]", + }, + { + name: "map with basic types", + input: map[string]interface{}{ + "int": 42, + "str": "hello", + "bool": true, + "float": 3.14, + }, + expected: "map[bool:true float:3.14 int:42 str:hello]", + }, + { + name: "mixed types map", + input: map[interface{}]interface{}{ + 42: "number", + "string": 123, + true: []int{1, 2, 3}, + struct{}{}: "empty", + }, + expected: "map[42:number string:123 true:[1 2 3] {}:empty]", + }, + { + name: "nested maps", + input: map[string]interface{}{ + "a": map[string]int{ + "x": 1, + "y": 2, + }, + "b": []interface{}{1, "two", true}, + }, + expected: "map[a:map[x:1 y:2] b:[1 two true]]", + }, + { + name: "empty struct", + input: struct{}{}, + expected: "{}", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ToString(tt.input) + if result != tt.expected { + t.Errorf("\nToString(%v) =\n%v\nwant:\n%v", tt.input, result, tt.expected) + } + }) + } +} diff --git a/examples/gno.land/p/moul/ulist/gno.mod b/examples/gno.land/p/moul/ulist/gno.mod new file mode 100644 index 00000000000..077f8c556f3 --- /dev/null +++ b/examples/gno.land/p/moul/ulist/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/ulist diff --git a/examples/gno.land/p/moul/ulist/ulist.gno b/examples/gno.land/p/moul/ulist/ulist.gno new file mode 100644 index 00000000000..507a02a4e45 --- /dev/null +++ b/examples/gno.land/p/moul/ulist/ulist.gno @@ -0,0 +1,437 @@ +// Package ulist provides an append-only list implementation using a binary tree structure, +// optimized for scenarios requiring sequential inserts with auto-incrementing indices. +// +// The implementation uses a binary tree where new elements are added by following a path +// determined by the binary representation of the index. This provides automatic balancing +// for append operations without requiring any balancing logic. +// +// Unlike the AVL tree-based list implementation (p/demo/avl/list), ulist is specifically +// designed for append-only operations and does not require rebalancing. This makes it more +// efficient for sequential inserts but less flexible for general-purpose list operations. +// +// Key differences from AVL list: +// * Append-only design (no arbitrary inserts) +// * No tree rebalancing needed +// * Simpler implementation +// * More memory efficient for sequential operations +// * Less flexible than AVL (no arbitrary inserts/reordering) +// +// Key characteristics: +// * O(log n) append and access operations +// * Perfect balance for power-of-2 sizes +// * No balancing needed +// * Memory efficient +// * Natural support for range queries +// * Support for soft deletion of elements +// * Forward and reverse iteration capabilities +// * Offset-based iteration with count control +package ulist + +// TODO: Use this ulist in moul/collection for the primary index. +// TODO: Benchmarks. + +import ( + "errors" +) + +// List represents an append-only binary tree list +type List struct { + root *treeNode + totalSize int + activeSize int +} + +// Entry represents a key-value pair in the list, where Index is the position +// and Value is the stored data +type Entry struct { + Index int + Value interface{} +} + +// treeNode represents a node in the binary tree +type treeNode struct { + data interface{} + left *treeNode + right *treeNode +} + +// Error variables +var ( + ErrOutOfBounds = errors.New("index out of bounds") + ErrDeleted = errors.New("element already deleted") +) + +// New creates a new empty List instance +func New() *List { + return &List{} +} + +// Append adds one or more values to the end of the list. +// Values are added sequentially, and the list grows automatically. +func (l *List) Append(values ...interface{}) { + for _, value := range values { + index := l.totalSize + node := l.findNode(index, true) + node.data = value + l.totalSize++ + l.activeSize++ + } +} + +// Get retrieves the value at the specified index. +// Returns nil if the index is out of bounds or if the element was deleted. +func (l *List) Get(index int) interface{} { + node := l.findNode(index, false) + if node == nil { + return nil + } + return node.data +} + +// Delete marks the elements at the specified indices as deleted. +// Returns ErrOutOfBounds if any index is invalid or ErrDeleted if +// the element was already deleted. +func (l *List) Delete(indices ...int) error { + if len(indices) == 0 { + return nil + } + if l == nil || l.totalSize == 0 { + return ErrOutOfBounds + } + + for _, index := range indices { + if index < 0 || index >= l.totalSize { + return ErrOutOfBounds + } + + node := l.findNode(index, false) + if node == nil || node.data == nil { + return ErrDeleted + } + node.data = nil + l.activeSize-- + } + + return nil +} + +// Set updates or restores a value at the specified index if within bounds +// Returns ErrOutOfBounds if the index is invalid +func (l *List) Set(index int, value interface{}) error { + if l == nil || index < 0 || index >= l.totalSize { + return ErrOutOfBounds + } + + node := l.findNode(index, false) + if node == nil { + return ErrOutOfBounds + } + + // If this is restoring a deleted element + if value != nil && node.data == nil { + l.activeSize++ + } + + // If this is deleting an element + if value == nil && node.data != nil { + l.activeSize-- + } + + node.data = value + return nil +} + +// Size returns the number of active (non-deleted) elements in the list +func (l *List) Size() int { + if l == nil { + return 0 + } + return l.activeSize +} + +// TotalSize returns the total number of elements ever added to the list, +// including deleted elements +func (l *List) TotalSize() int { + if l == nil { + return 0 + } + return l.totalSize +} + +// IterCbFn is a callback function type used in iteration methods. +// Return true to stop iteration, false to continue. +type IterCbFn func(index int, value interface{}) bool + +// Iterator performs iteration between start and end indices, calling cb for each entry. +// If start > end, iteration is performed in reverse order. +// Returns true if iteration was stopped early by the callback returning true. +// Skips deleted elements. +func (l *List) Iterator(start, end int, cb IterCbFn) bool { + // For empty list or invalid range + if l == nil || l.totalSize == 0 { + return false + } + if start < 0 && end < 0 { + return false + } + if start >= l.totalSize && end >= l.totalSize { + return false + } + + // Normalize indices + if start < 0 { + start = 0 + } + if end < 0 { + end = 0 + } + if end >= l.totalSize { + end = l.totalSize - 1 + } + if start >= l.totalSize { + start = l.totalSize - 1 + } + + // Handle reverse iteration + if start > end { + for i := start; i >= end; i-- { + val := l.Get(i) + if val != nil { + if cb(i, val) { + return true + } + } + } + return false + } + + // Handle forward iteration + for i := start; i <= end; i++ { + val := l.Get(i) + if val != nil { + if cb(i, val) { + return true + } + } + } + return false +} + +// IteratorByOffset performs iteration starting from offset for count elements. +// If count is positive, iterates forward; if negative, iterates backward. +// The iteration stops after abs(count) elements or when reaching list bounds. +// Skips deleted elements. +func (l *List) IteratorByOffset(offset int, count int, cb IterCbFn) bool { + if count == 0 || l == nil || l.totalSize == 0 { + return false + } + + // Normalize offset + if offset < 0 { + offset = 0 + } + if offset >= l.totalSize { + offset = l.totalSize - 1 + } + + // Determine end based on count direction + var end int + if count > 0 { + end = l.totalSize - 1 + } else { + end = 0 + } + + wrapperReturned := false + + // Wrap the callback to limit iterations + remaining := abs(count) + wrapper := func(index int, value interface{}) bool { + if remaining <= 0 { + wrapperReturned = true + return true + } + remaining-- + return cb(index, value) + } + ret := l.Iterator(offset, end, wrapper) + if wrapperReturned { + return false + } + return ret +} + +// abs returns the absolute value of x +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +// findNode locates or creates a node at the given index in the binary tree. +// The tree is structured such that the path to a node is determined by the binary +// representation of the index. For example, a tree with 15 elements would look like: +// +// 0 +// / \ +// 1 2 +// / \ / \ +// 3 4 5 6 +// / \ / \ / \ / \ +// 7 8 9 10 11 12 13 14 +// +// To find index 13 (binary 1101): +// 1. Start at root (0) +// 2. Calculate bits needed (4 bits for index 13) +// 3. Skip the highest bit position and start from bits-2 +// 4. Read bits from left to right: +// - 1 -> go right to 2 +// - 1 -> go right to 6 +// - 0 -> go left to 13 +// +// Special cases: +// - Index 0 always returns the root node +// - For create=true, missing nodes are created along the path +// - For create=false, returns nil if any node is missing +func (l *List) findNode(index int, create bool) *treeNode { + // For read operations, check bounds strictly + if !create && (l == nil || index < 0 || index >= l.totalSize) { + return nil + } + + // For create operations, allow index == totalSize for append + if create && (l == nil || index < 0 || index > l.totalSize) { + return nil + } + + // Initialize root if needed + if l.root == nil { + if !create { + return nil + } + l.root = &treeNode{} + return l.root + } + + node := l.root + + // Special case for root node + if index == 0 { + return node + } + + // Calculate the number of bits needed (inline highestBit logic) + bits := 0 + n := index + 1 + for n > 0 { + n >>= 1 + bits++ + } + + // Start from the second highest bit + for level := bits - 2; level >= 0; level-- { + bit := (index & (1 << uint(level))) != 0 + + if bit { + if node.right == nil { + if !create { + return nil + } + node.right = &treeNode{} + } + node = node.right + } else { + if node.left == nil { + if !create { + return nil + } + node.left = &treeNode{} + } + node = node.left + } + } + + return node +} + +// MustDelete deletes elements at the specified indices. +// Panics if any index is invalid or if any element was already deleted. +func (l *List) MustDelete(indices ...int) { + if err := l.Delete(indices...); err != nil { + panic(err) + } +} + +// MustGet retrieves the value at the specified index. +// Panics if the index is out of bounds or if the element was deleted. +func (l *List) MustGet(index int) interface{} { + if l == nil || index < 0 || index >= l.totalSize { + panic(ErrOutOfBounds) + } + value := l.Get(index) + if value == nil { + panic(ErrDeleted) + } + return value +} + +// MustSet updates or restores a value at the specified index. +// Panics if the index is out of bounds. +func (l *List) MustSet(index int, value interface{}) { + if err := l.Set(index, value); err != nil { + panic(err) + } +} + +// GetRange returns a slice of Entry containing elements between start and end indices. +// If start > end, elements are returned in reverse order. +// Deleted elements are skipped. +func (l *List) GetRange(start, end int) []Entry { + var entries []Entry + l.Iterator(start, end, func(index int, value interface{}) bool { + entries = append(entries, Entry{Index: index, Value: value}) + return false + }) + return entries +} + +// GetByOffset returns a slice of Entry starting from offset for count elements. +// If count is positive, returns elements forward; if negative, returns elements backward. +// The operation stops after abs(count) elements or when reaching list bounds. +// Deleted elements are skipped. +func (l *List) GetByOffset(offset int, count int) []Entry { + var entries []Entry + l.IteratorByOffset(offset, count, func(index int, value interface{}) bool { + entries = append(entries, Entry{Index: index, Value: value}) + return false + }) + return entries +} + +// IList defines the interface for an ulist.List compatible structure. +type IList interface { + // Basic operations + Append(values ...interface{}) + Get(index int) interface{} + Delete(indices ...int) error + Size() int + TotalSize() int + Set(index int, value interface{}) error + + // Must variants that panic instead of returning errors + MustDelete(indices ...int) + MustGet(index int) interface{} + MustSet(index int, value interface{}) + + // Range operations + GetRange(start, end int) []Entry + GetByOffset(offset int, count int) []Entry + + // Iterator operations + Iterator(start, end int, cb IterCbFn) bool + IteratorByOffset(offset int, count int, cb IterCbFn) bool +} + +// Verify that List implements IList +var _ IList = (*List)(nil) diff --git a/examples/gno.land/p/moul/ulist/ulist_test.gno b/examples/gno.land/p/moul/ulist/ulist_test.gno new file mode 100644 index 00000000000..f098731a7db --- /dev/null +++ b/examples/gno.land/p/moul/ulist/ulist_test.gno @@ -0,0 +1,1422 @@ +package ulist + +import ( + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/typeutil" +) + +func TestNew(t *testing.T) { + l := New() + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, 0, l.TotalSize()) +} + +func TestListAppendAndGet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + expected interface{} + }{ + { + name: "empty list", + setup: func() *List { + return New() + }, + index: 0, + expected: nil, + }, + { + name: "single append and get", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + expected: 42, + }, + { + name: "multiple appends and get first", + setup: func() *List { + l := New() + l.Append(1) + l.Append(2) + l.Append(3) + return l + }, + index: 0, + expected: 1, + }, + { + name: "multiple appends and get last", + setup: func() *List { + l := New() + l.Append(1) + l.Append(2) + l.Append(3) + return l + }, + index: 2, + expected: 3, + }, + { + name: "get with invalid index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + expected: nil, + }, + { + name: "31 items get first", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 0, + expected: 0, + }, + { + name: "31 items get last", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 30, + expected: 30, + }, + { + name: "31 items get middle", + setup: func() *List { + l := New() + for i := 0; i < 31; i++ { + l.Append(i) + } + return l + }, + index: 15, + expected: 15, + }, + { + name: "values around power of 2 boundary", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 15, + expected: 15, + }, + { + name: "values at power of 2", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 16, + expected: 16, + }, + { + name: "values after power of 2", + setup: func() *List { + l := New() + for i := 0; i < 18; i++ { + l.Append(i) + } + return l + }, + index: 17, + expected: 17, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + got := l.Get(tt.index) + if got != tt.expected { + t.Errorf("List.Get() = %v, want %v", got, tt.expected) + } + }) + } +} + +// generateSequence creates a slice of integers from 0 to n-1 +func generateSequence(n int) []interface{} { + result := make([]interface{}, n) + for i := 0; i < n; i++ { + result[i] = i + } + return result +} + +func TestListDelete(t *testing.T) { + tests := []struct { + name string + setup func() *List + deleteIndices []int + expectedErr error + expectedSize int + }{ + { + name: "delete single element", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + deleteIndices: []int{1}, + expectedErr: nil, + expectedSize: 2, + }, + { + name: "delete multiple elements", + setup: func() *List { + l := New() + l.Append(1, 2, 3, 4, 5) + return l + }, + deleteIndices: []int{0, 2, 4}, + expectedErr: nil, + expectedSize: 2, + }, + { + name: "delete with negative index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + deleteIndices: []int{-1}, + expectedErr: ErrOutOfBounds, + expectedSize: 1, + }, + { + name: "delete beyond size", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + deleteIndices: []int{1}, + expectedErr: ErrOutOfBounds, + expectedSize: 1, + }, + { + name: "delete already deleted element", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + deleteIndices: []int{0}, + expectedErr: ErrDeleted, + expectedSize: 0, + }, + { + name: "delete multiple elements in reverse", + setup: func() *List { + l := New() + l.Append(1, 2, 3, 4, 5) + return l + }, + deleteIndices: []int{4, 2, 0}, + expectedErr: nil, + expectedSize: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + initialSize := l.Size() + err := l.Delete(tt.deleteIndices...) + if err != nil && tt.expectedErr != nil { + uassert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + uassert.Equal(t, tt.expectedErr, err) + } + uassert.Equal(t, tt.expectedSize, l.Size(), + ufmt.Sprintf("Expected size %d after deleting %d elements from size %d, got %d", + tt.expectedSize, len(tt.deleteIndices), initialSize, l.Size())) + }) + } +} + +func TestListSizeAndTotalSize(t *testing.T) { + t.Run("empty list", func(t *testing.T) { + list := New() + uassert.Equal(t, 0, list.Size()) + uassert.Equal(t, 0, list.TotalSize()) + }) + + t.Run("list with elements", func(t *testing.T) { + list := New() + list.Append(1) + list.Append(2) + list.Append(3) + uassert.Equal(t, 3, list.Size()) + uassert.Equal(t, 3, list.TotalSize()) + }) + + t.Run("list with deleted elements", func(t *testing.T) { + list := New() + list.Append(1) + list.Append(2) + list.Append(3) + list.Delete(1) + uassert.Equal(t, 2, list.Size()) + uassert.Equal(t, 3, list.TotalSize()) + }) +} + +func TestIterator(t *testing.T) { + tests := []struct { + name string + values []interface{} + start int + end int + expected []Entry + wantStop bool + stopAfter int // stop after N elements, -1 for no stop + }{ + { + name: "empty list", + values: []interface{}{}, + start: 0, + end: 10, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "nil list", + values: nil, + start: 0, + end: 0, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "single element forward", + values: []interface{}{42}, + start: 0, + end: 0, + expected: []Entry{ + {Index: 0, Value: 42}, + }, + stopAfter: -1, + }, + { + name: "multiple elements forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + {Index: 4, Value: 5}, + }, + stopAfter: -1, + }, + { + name: "multiple elements reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 4, + end: 0, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "partial range forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 1, + end: 3, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + }, + stopAfter: -1, + }, + { + name: "partial range reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 3, + end: 1, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + stopAfter: -1, + }, + { + name: "stop iteration early", + values: []interface{}{1, 2, 3, 4, 5}, + start: 0, + end: 4, + wantStop: true, + stopAfter: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + }, + { + name: "negative start", + values: []interface{}{1, 2, 3}, + start: -1, + end: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + stopAfter: -1, + }, + { + name: "negative end", + values: []interface{}{1, 2, 3}, + start: 0, + end: -2, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "start beyond size", + values: []interface{}{1, 2, 3}, + start: 5, + end: 6, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "end beyond size", + values: []interface{}{1, 2, 3}, + start: 0, + end: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + stopAfter: -1, + }, + { + name: "with deleted elements", + values: []interface{}{1, 2, nil, 4, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 3, Value: 4}, + {Index: 4, Value: 5}, + }, + stopAfter: -1, + }, + { + name: "with deleted elements reverse", + values: []interface{}{1, nil, 3, nil, 5}, + start: 4, + end: 0, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 2, Value: 3}, + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "start equals end", + values: []interface{}{1, 2, 3}, + start: 1, + end: 1, + expected: []Entry{{Index: 1, Value: 2}}, + stopAfter: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + var result []Entry + stopped := list.Iterator(tt.start, tt.end, func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return tt.stopAfter >= 0 && len(result) >= tt.stopAfter + }) + + uassert.Equal(t, len(result), len(tt.expected), "comparing length") + + for i := range result { + uassert.Equal(t, result[i].Index, tt.expected[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(result[i].Value), typeutil.ToString(tt.expected[i].Value), "comparing value") + } + + uassert.Equal(t, stopped, tt.wantStop, "comparing stopped") + }) + } +} + +func TestLargeListAppendGetAndDelete(t *testing.T) { + l := New() + size := 100 + + // Append values from 0 to 99 + for i := 0; i < size; i++ { + l.Append(i) + val := l.Get(i) + uassert.Equal(t, i, val) + } + + // Verify size + uassert.Equal(t, size, l.Size()) + uassert.Equal(t, size, l.TotalSize()) + + // Get and verify each value + for i := 0; i < size; i++ { + val := l.Get(i) + uassert.Equal(t, i, val) + } + + // Get and verify each value + for i := 0; i < size; i++ { + err := l.Delete(i) + uassert.Equal(t, nil, err) + } + + // Verify size + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, size, l.TotalSize()) + + // Get and verify each value + for i := 0; i < size; i++ { + val := l.Get(i) + uassert.Equal(t, nil, val) + } +} + +func TestEdgeCases(t *testing.T) { + tests := []struct { + name string + test func(t *testing.T) + }{ + { + name: "nil list operations", + test: func(t *testing.T) { + var l *List + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, 0, l.TotalSize()) + uassert.Equal(t, nil, l.Get(0)) + err := l.Delete(0) + uassert.Equal(t, ErrOutOfBounds.Error(), err.Error()) + }, + }, + { + name: "delete empty indices slice", + test: func(t *testing.T) { + l := New() + l.Append(1) + err := l.Delete() + uassert.Equal(t, nil, err) + uassert.Equal(t, 1, l.Size()) + }, + }, + { + name: "append nil values", + test: func(t *testing.T) { + l := New() + l.Append(nil, nil) + uassert.Equal(t, 2, l.Size()) + uassert.Equal(t, nil, l.Get(0)) + uassert.Equal(t, nil, l.Get(1)) + }, + }, + { + name: "delete same index multiple times", + test: func(t *testing.T) { + l := New() + l.Append(1, 2, 3) + err := l.Delete(1) + uassert.Equal(t, nil, err) + err = l.Delete(1) + uassert.Equal(t, ErrDeleted.Error(), err.Error()) + }, + }, + { + name: "iterator with all deleted elements", + test: func(t *testing.T) { + l := New() + l.Append(1, 2, 3) + l.Delete(0, 1, 2) + var count int + l.Iterator(0, 2, func(index int, value interface{}) bool { + count++ + return false + }) + uassert.Equal(t, 0, count) + }, + }, + { + name: "append after delete", + test: func(t *testing.T) { + l := New() + l.Append(1, 2) + l.Delete(1) + l.Append(3) + uassert.Equal(t, 2, l.Size()) + uassert.Equal(t, 3, l.TotalSize()) + uassert.Equal(t, 1, l.Get(0)) + uassert.Equal(t, nil, l.Get(1)) + uassert.Equal(t, 3, l.Get(2)) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.test(t) + }) + } +} + +func TestIteratorByOffset(t *testing.T) { + tests := []struct { + name string + values []interface{} + offset int + count int + expected []Entry + wantStop bool + }{ + { + name: "empty list", + values: []interface{}{}, + offset: 0, + count: 5, + expected: []Entry{}, + wantStop: false, + }, + { + name: "positive count forward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 1, + count: 2, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "negative count backward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 3, + count: -2, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "count exceeds available elements forward", + values: []interface{}{1, 2, 3}, + offset: 1, + count: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + wantStop: false, + }, + { + name: "count exceeds available elements backward", + values: []interface{}{1, 2, 3}, + offset: 1, + count: -5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "zero count", + values: []interface{}{1, 2, 3}, + offset: 0, + count: 0, + expected: []Entry{}, + wantStop: false, + }, + { + name: "negative offset", + values: []interface{}{1, 2, 3}, + offset: -1, + count: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + wantStop: false, + }, + { + name: "offset beyond size", + values: []interface{}{1, 2, 3}, + offset: 5, + count: -2, + expected: []Entry{ + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + wantStop: false, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + offset: 0, + count: 3, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + wantStop: false, + }, + { + name: "early stop in forward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 0, + count: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + wantStop: true, // The callback will return true after 2 elements + }, + { + name: "early stop in backward iteration", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 4, + count: -5, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 3, Value: 4}, + }, + wantStop: true, // The callback will return true after 2 elements + }, + { + name: "nil list", + values: nil, + offset: 0, + count: 5, + expected: []Entry{}, + wantStop: false, + }, + { + name: "single element forward", + values: []interface{}{1}, + offset: 0, + count: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "single element backward", + values: []interface{}{1}, + offset: 0, + count: -5, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + wantStop: false, + }, + { + name: "all deleted elements", + values: []interface{}{nil, nil, nil}, + offset: 0, + count: 3, + expected: []Entry{}, + wantStop: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + var result []Entry + var cb IterCbFn + if tt.wantStop { + cb = func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return len(result) >= 2 // Stop after 2 elements for early stop tests + } + } else { + cb = func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return false + } + } + + stopped := list.IteratorByOffset(tt.offset, tt.count, cb) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + uassert.Equal(t, tt.wantStop, stopped, "comparing stopped") + }) + } +} + +func TestMustDelete(t *testing.T) { + tests := []struct { + name string + setup func() *List + indices []int + shouldPanic bool + panicMsg string + }{ + { + name: "successful delete", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + indices: []int{1}, + shouldPanic: false, + }, + { + name: "out of bounds", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + indices: []int{1}, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "already deleted", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + indices: []int{0}, + shouldPanic: true, + panicMsg: ErrDeleted.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + l.MustDelete(tt.indices...) + if tt.shouldPanic { + t.Error("Expected panic") + } + }) + } +} + +func TestMustGet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + expected interface{} + shouldPanic bool + panicMsg string + }{ + { + name: "successful get", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + expected: 42, + shouldPanic: false, + }, + { + name: "out of bounds negative", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: -1, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "out of bounds positive", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "deleted element", + setup: func() *List { + l := New() + l.Append(1) + l.Delete(0) + return l + }, + index: 0, + shouldPanic: true, + panicMsg: ErrDeleted.Error(), + }, + { + name: "nil list", + setup: func() *List { + return nil + }, + index: 0, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + result := l.MustGet(tt.index) + if tt.shouldPanic { + t.Error("Expected panic") + } + uassert.Equal(t, typeutil.ToString(tt.expected), typeutil.ToString(result)) + }) + } +} + +func TestGetRange(t *testing.T) { + tests := []struct { + name string + values []interface{} + start int + end int + expected []Entry + }{ + { + name: "empty list", + values: []interface{}{}, + start: 0, + end: 10, + expected: []Entry{}, + }, + { + name: "single element", + values: []interface{}{42}, + start: 0, + end: 0, + expected: []Entry{ + {Index: 0, Value: 42}, + }, + }, + { + name: "multiple elements forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 1, + end: 3, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + }, + }, + { + name: "multiple elements reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 3, + end: 1, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + }, + { + name: "nil list", + values: nil, + start: 0, + end: 5, + expected: []Entry{}, + }, + { + name: "negative indices", + values: []interface{}{1, 2, 3}, + start: -1, + end: -2, + expected: []Entry{}, + }, + { + name: "indices beyond size", + values: []interface{}{1, 2, 3}, + start: 1, + end: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + result := list.GetRange(tt.start, tt.end) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + }) + } +} + +func TestGetByOffset(t *testing.T) { + tests := []struct { + name string + values []interface{} + offset int + count int + expected []Entry + }{ + { + name: "empty list", + values: []interface{}{}, + offset: 0, + count: 5, + expected: []Entry{}, + }, + { + name: "positive count forward", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 1, + count: 2, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + { + name: "negative count backward", + values: []interface{}{1, 2, 3, 4, 5}, + offset: 3, + count: -2, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + }, + }, + { + name: "count exceeds available elements", + values: []interface{}{1, 2, 3}, + offset: 1, + count: 5, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + }, + { + name: "zero count", + values: []interface{}{1, 2, 3}, + offset: 0, + count: 0, + expected: []Entry{}, + }, + { + name: "with deleted elements", + values: []interface{}{1, nil, 3, nil, 5}, + offset: 0, + count: 3, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 2, Value: 3}, + {Index: 4, Value: 5}, + }, + }, + { + name: "negative offset", + values: []interface{}{1, 2, 3}, + offset: -1, + count: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + }, + }, + { + name: "offset beyond size", + values: []interface{}{1, 2, 3}, + offset: 5, + count: -2, + expected: []Entry{ + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + }, + }, + { + name: "nil list", + values: nil, + offset: 0, + count: 5, + expected: []Entry{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + list := New() + list.Append(tt.values...) + + result := list.GetByOffset(tt.offset, tt.count) + + uassert.Equal(t, len(tt.expected), len(result), "comparing length") + for i := range result { + uassert.Equal(t, tt.expected[i].Index, result[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(tt.expected[i].Value), typeutil.ToString(result[i].Value), "comparing value") + } + }) + } +} + +func TestMustSet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + value interface{} + shouldPanic bool + panicMsg string + }{ + { + name: "successful set", + setup: func() *List { + l := New() + l.Append(42) + return l + }, + index: 0, + value: 99, + shouldPanic: false, + }, + { + name: "restore deleted element", + setup: func() *List { + l := New() + l.Append(42) + l.Delete(0) + return l + }, + index: 0, + value: 99, + shouldPanic: false, + }, + { + name: "out of bounds negative", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: -1, + value: 99, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "out of bounds positive", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + value: 99, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + { + name: "nil list", + setup: func() *List { + return nil + }, + index: 0, + value: 99, + shouldPanic: true, + panicMsg: ErrOutOfBounds.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + if tt.shouldPanic { + defer func() { + r := recover() + if r == nil { + t.Error("Expected panic but got none") + } + err, ok := r.(error) + if !ok { + t.Errorf("Expected error but got %v", r) + } + uassert.Equal(t, tt.panicMsg, err.Error()) + }() + } + l.MustSet(tt.index, tt.value) + if tt.shouldPanic { + t.Error("Expected panic") + } + // Verify the value was set correctly for non-panic cases + if !tt.shouldPanic { + result := l.Get(tt.index) + uassert.Equal(t, typeutil.ToString(tt.value), typeutil.ToString(result)) + } + }) + } +} + +func TestSet(t *testing.T) { + tests := []struct { + name string + setup func() *List + index int + value interface{} + expectedErr error + verify func(t *testing.T, l *List) + }{ + { + name: "set value in empty list", + setup: func() *List { + return New() + }, + index: 0, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 0, l.Size()) + }, + }, + { + name: "set value at valid index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 0, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(0)) + uassert.Equal(t, 1, l.Size()) + uassert.Equal(t, 1, l.TotalSize()) + }, + }, + { + name: "set value at negative index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: -1, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 1, l.Get(0)) + }, + }, + { + name: "set value beyond size", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 1, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 1, l.Get(0)) + uassert.Equal(t, 1, l.Size()) + }, + }, + { + name: "set nil value", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 0, + value: nil, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, nil, l.Get(0)) + uassert.Equal(t, 0, l.Size()) + }, + }, + { + name: "set value at deleted index", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + l.Delete(1) + return l + }, + index: 1, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(1)) + uassert.Equal(t, 3, l.Size()) + uassert.Equal(t, 3, l.TotalSize()) + }, + }, + { + name: "set value in nil list", + setup: func() *List { + return nil + }, + index: 0, + value: 42, + expectedErr: ErrOutOfBounds, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 0, l.Size()) + }, + }, + { + name: "set multiple values at same index", + setup: func() *List { + l := New() + l.Append(1) + return l + }, + index: 0, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(0)) + err := l.Set(0, 99) + uassert.Equal(t, nil, err) + uassert.Equal(t, 99, l.Get(0)) + uassert.Equal(t, 1, l.Size()) + }, + }, + { + name: "set value at last index", + setup: func() *List { + l := New() + l.Append(1, 2, 3) + return l + }, + index: 2, + value: 42, + verify: func(t *testing.T, l *List) { + uassert.Equal(t, 42, l.Get(2)) + uassert.Equal(t, 3, l.Size()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + err := l.Set(tt.index, tt.value) + + if tt.expectedErr != nil { + uassert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + uassert.Equal(t, nil, err) + } + + tt.verify(t, l) + }) + } +} diff --git a/examples/gno.land/p/moul/xmath/generate.go b/examples/gno.land/p/moul/xmath/generate.go new file mode 100644 index 00000000000..ad70adb06bd --- /dev/null +++ b/examples/gno.land/p/moul/xmath/generate.go @@ -0,0 +1,3 @@ +package xmath + +//go:generate go run generator.go diff --git a/examples/gno.land/p/moul/xmath/generator.go b/examples/gno.land/p/moul/xmath/generator.go new file mode 100644 index 00000000000..afe5a4341fa --- /dev/null +++ b/examples/gno.land/p/moul/xmath/generator.go @@ -0,0 +1,184 @@ +//go:build ignore + +package main + +import ( + "bytes" + "fmt" + "go/format" + "log" + "os" + "strings" + "text/template" +) + +type Type struct { + Name string + ZeroValue string + Signed bool + Float bool +} + +var types = []Type{ + {"Int8", "0", true, false}, + {"Int16", "0", true, false}, + {"Int32", "0", true, false}, + {"Int64", "0", true, false}, + {"Int", "0", true, false}, + {"Uint8", "0", false, false}, + {"Uint16", "0", false, false}, + {"Uint32", "0", false, false}, + {"Uint64", "0", false, false}, + {"Uint", "0", false, false}, + {"Float32", "0.0", true, true}, + {"Float64", "0.0", true, true}, +} + +const sourceTpl = `// Code generated by generator.go; DO NOT EDIT. +package xmath + +{{ range .Types }} +// {{.Name}} helpers +func Max{{.Name}}(a, b {{.Name | lower}}) {{.Name | lower}} { + if a > b { + return a + } + return b +} + +func Min{{.Name}}(a, b {{.Name | lower}}) {{.Name | lower}} { + if a < b { + return a + } + return b +} + +func Clamp{{.Name}}(value, min, max {{.Name | lower}}) {{.Name | lower}} { + if value < min { + return min + } + if value > max { + return max + } + return value +} +{{if .Signed}} +func Abs{{.Name}}(x {{.Name | lower}}) {{.Name | lower}} { + if x < 0 { + return -x + } + return x +} + +func Sign{{.Name}}(x {{.Name | lower}}) {{.Name | lower}} { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} +{{end}} +{{end}} +` + +const testTpl = `package xmath + +import "testing" + +{{range .Types}} +func Test{{.Name}}Helpers(t *testing.T) { + // Test Max{{.Name}} + if Max{{.Name}}(1, 2) != 2 { + t.Error("Max{{.Name}}(1, 2) should be 2") + } + {{if .Signed}}if Max{{.Name}}(-1, -2) != -1 { + t.Error("Max{{.Name}}(-1, -2) should be -1") + }{{end}} + + // Test Min{{.Name}} + if Min{{.Name}}(1, 2) != 1 { + t.Error("Min{{.Name}}(1, 2) should be 1") + } + {{if .Signed}}if Min{{.Name}}(-1, -2) != -2 { + t.Error("Min{{.Name}}(-1, -2) should be -2") + }{{end}} + + // Test Clamp{{.Name}} + if Clamp{{.Name}}(5, 1, 3) != 3 { + t.Error("Clamp{{.Name}}(5, 1, 3) should be 3") + } + if Clamp{{.Name}}(0, 1, 3) != 1 { + t.Error("Clamp{{.Name}}(0, 1, 3) should be 1") + } + if Clamp{{.Name}}(2, 1, 3) != 2 { + t.Error("Clamp{{.Name}}(2, 1, 3) should be 2") + } + {{if .Signed}} + // Test Abs{{.Name}} + if Abs{{.Name}}(-5) != 5 { + t.Error("Abs{{.Name}}(-5) should be 5") + } + if Abs{{.Name}}(5) != 5 { + t.Error("Abs{{.Name}}(5) should be 5") + } + + // Test Sign{{.Name}} + if Sign{{.Name}}(-5) != -1 { + t.Error("Sign{{.Name}}(-5) should be -1") + } + if Sign{{.Name}}(5) != 1 { + t.Error("Sign{{.Name}}(5) should be 1") + } + if Sign{{.Name}}({{.ZeroValue}}) != 0 { + t.Error("Sign{{.Name}}({{.ZeroValue}}) should be 0") + } + {{end}} +} +{{end}} +` + +func main() { + funcMap := template.FuncMap{ + "lower": strings.ToLower, + } + + // Generate source file + sourceTmpl := template.Must(template.New("source").Funcs(funcMap).Parse(sourceTpl)) + var sourceOut bytes.Buffer + if err := sourceTmpl.Execute(&sourceOut, struct{ Types []Type }{types}); err != nil { + log.Fatal(err) + } + + // Format the generated code + formattedSource, err := format.Source(sourceOut.Bytes()) + if err != nil { + log.Fatal(err) + } + + // Write source file + if err := os.WriteFile("xmath.gen.gno", formattedSource, 0644); err != nil { + log.Fatal(err) + } + + // Generate test file + testTmpl := template.Must(template.New("test").Parse(testTpl)) + var testOut bytes.Buffer + if err := testTmpl.Execute(&testOut, struct{ Types []Type }{types}); err != nil { + log.Fatal(err) + } + + // Format the generated test code + formattedTest, err := format.Source(testOut.Bytes()) + if err != nil { + log.Fatal(err) + } + + // Write test file + if err := os.WriteFile("xmath.gen_test.gno", formattedTest, 0644); err != nil { + log.Fatal(err) + } + + fmt.Println("Generated xmath.gen.gno and xmath.gen_test.gno") +} diff --git a/examples/gno.land/p/moul/xmath/gno.mod b/examples/gno.land/p/moul/xmath/gno.mod new file mode 100644 index 00000000000..63b782c88f2 --- /dev/null +++ b/examples/gno.land/p/moul/xmath/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/xmath diff --git a/examples/gno.land/p/moul/xmath/xmath.gen.gno b/examples/gno.land/p/moul/xmath/xmath.gen.gno new file mode 100644 index 00000000000..266c77e1e84 --- /dev/null +++ b/examples/gno.land/p/moul/xmath/xmath.gen.gno @@ -0,0 +1,421 @@ +// Code generated by generator.go; DO NOT EDIT. +package xmath + +// Int8 helpers +func MaxInt8(a, b int8) int8 { + if a > b { + return a + } + return b +} + +func MinInt8(a, b int8) int8 { + if a < b { + return a + } + return b +} + +func ClampInt8(value, min, max int8) int8 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt8(x int8) int8 { + if x < 0 { + return -x + } + return x +} + +func SignInt8(x int8) int8 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int16 helpers +func MaxInt16(a, b int16) int16 { + if a > b { + return a + } + return b +} + +func MinInt16(a, b int16) int16 { + if a < b { + return a + } + return b +} + +func ClampInt16(value, min, max int16) int16 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt16(x int16) int16 { + if x < 0 { + return -x + } + return x +} + +func SignInt16(x int16) int16 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int32 helpers +func MaxInt32(a, b int32) int32 { + if a > b { + return a + } + return b +} + +func MinInt32(a, b int32) int32 { + if a < b { + return a + } + return b +} + +func ClampInt32(value, min, max int32) int32 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt32(x int32) int32 { + if x < 0 { + return -x + } + return x +} + +func SignInt32(x int32) int32 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int64 helpers +func MaxInt64(a, b int64) int64 { + if a > b { + return a + } + return b +} + +func MinInt64(a, b int64) int64 { + if a < b { + return a + } + return b +} + +func ClampInt64(value, min, max int64) int64 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt64(x int64) int64 { + if x < 0 { + return -x + } + return x +} + +func SignInt64(x int64) int64 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Int helpers +func MaxInt(a, b int) int { + if a > b { + return a + } + return b +} + +func MinInt(a, b int) int { + if a < b { + return a + } + return b +} + +func ClampInt(value, min, max int) int { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsInt(x int) int { + if x < 0 { + return -x + } + return x +} + +func SignInt(x int) int { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Uint8 helpers +func MaxUint8(a, b uint8) uint8 { + if a > b { + return a + } + return b +} + +func MinUint8(a, b uint8) uint8 { + if a < b { + return a + } + return b +} + +func ClampUint8(value, min, max uint8) uint8 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint16 helpers +func MaxUint16(a, b uint16) uint16 { + if a > b { + return a + } + return b +} + +func MinUint16(a, b uint16) uint16 { + if a < b { + return a + } + return b +} + +func ClampUint16(value, min, max uint16) uint16 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint32 helpers +func MaxUint32(a, b uint32) uint32 { + if a > b { + return a + } + return b +} + +func MinUint32(a, b uint32) uint32 { + if a < b { + return a + } + return b +} + +func ClampUint32(value, min, max uint32) uint32 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint64 helpers +func MaxUint64(a, b uint64) uint64 { + if a > b { + return a + } + return b +} + +func MinUint64(a, b uint64) uint64 { + if a < b { + return a + } + return b +} + +func ClampUint64(value, min, max uint64) uint64 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Uint helpers +func MaxUint(a, b uint) uint { + if a > b { + return a + } + return b +} + +func MinUint(a, b uint) uint { + if a < b { + return a + } + return b +} + +func ClampUint(value, min, max uint) uint { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +// Float32 helpers +func MaxFloat32(a, b float32) float32 { + if a > b { + return a + } + return b +} + +func MinFloat32(a, b float32) float32 { + if a < b { + return a + } + return b +} + +func ClampFloat32(value, min, max float32) float32 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsFloat32(x float32) float32 { + if x < 0 { + return -x + } + return x +} + +func SignFloat32(x float32) float32 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +// Float64 helpers +func MaxFloat64(a, b float64) float64 { + if a > b { + return a + } + return b +} + +func MinFloat64(a, b float64) float64 { + if a < b { + return a + } + return b +} + +func ClampFloat64(value, min, max float64) float64 { + if value < min { + return min + } + if value > max { + return max + } + return value +} + +func AbsFloat64(x float64) float64 { + if x < 0 { + return -x + } + return x +} + +func SignFloat64(x float64) float64 { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} diff --git a/examples/gno.land/p/moul/xmath/xmath.gen_test.gno b/examples/gno.land/p/moul/xmath/xmath.gen_test.gno new file mode 100644 index 00000000000..16c80fc983d --- /dev/null +++ b/examples/gno.land/p/moul/xmath/xmath.gen_test.gno @@ -0,0 +1,466 @@ +package xmath + +import "testing" + +func TestInt8Helpers(t *testing.T) { + // Test MaxInt8 + if MaxInt8(1, 2) != 2 { + t.Error("MaxInt8(1, 2) should be 2") + } + if MaxInt8(-1, -2) != -1 { + t.Error("MaxInt8(-1, -2) should be -1") + } + + // Test MinInt8 + if MinInt8(1, 2) != 1 { + t.Error("MinInt8(1, 2) should be 1") + } + if MinInt8(-1, -2) != -2 { + t.Error("MinInt8(-1, -2) should be -2") + } + + // Test ClampInt8 + if ClampInt8(5, 1, 3) != 3 { + t.Error("ClampInt8(5, 1, 3) should be 3") + } + if ClampInt8(0, 1, 3) != 1 { + t.Error("ClampInt8(0, 1, 3) should be 1") + } + if ClampInt8(2, 1, 3) != 2 { + t.Error("ClampInt8(2, 1, 3) should be 2") + } + + // Test AbsInt8 + if AbsInt8(-5) != 5 { + t.Error("AbsInt8(-5) should be 5") + } + if AbsInt8(5) != 5 { + t.Error("AbsInt8(5) should be 5") + } + + // Test SignInt8 + if SignInt8(-5) != -1 { + t.Error("SignInt8(-5) should be -1") + } + if SignInt8(5) != 1 { + t.Error("SignInt8(5) should be 1") + } + if SignInt8(0) != 0 { + t.Error("SignInt8(0) should be 0") + } + +} + +func TestInt16Helpers(t *testing.T) { + // Test MaxInt16 + if MaxInt16(1, 2) != 2 { + t.Error("MaxInt16(1, 2) should be 2") + } + if MaxInt16(-1, -2) != -1 { + t.Error("MaxInt16(-1, -2) should be -1") + } + + // Test MinInt16 + if MinInt16(1, 2) != 1 { + t.Error("MinInt16(1, 2) should be 1") + } + if MinInt16(-1, -2) != -2 { + t.Error("MinInt16(-1, -2) should be -2") + } + + // Test ClampInt16 + if ClampInt16(5, 1, 3) != 3 { + t.Error("ClampInt16(5, 1, 3) should be 3") + } + if ClampInt16(0, 1, 3) != 1 { + t.Error("ClampInt16(0, 1, 3) should be 1") + } + if ClampInt16(2, 1, 3) != 2 { + t.Error("ClampInt16(2, 1, 3) should be 2") + } + + // Test AbsInt16 + if AbsInt16(-5) != 5 { + t.Error("AbsInt16(-5) should be 5") + } + if AbsInt16(5) != 5 { + t.Error("AbsInt16(5) should be 5") + } + + // Test SignInt16 + if SignInt16(-5) != -1 { + t.Error("SignInt16(-5) should be -1") + } + if SignInt16(5) != 1 { + t.Error("SignInt16(5) should be 1") + } + if SignInt16(0) != 0 { + t.Error("SignInt16(0) should be 0") + } + +} + +func TestInt32Helpers(t *testing.T) { + // Test MaxInt32 + if MaxInt32(1, 2) != 2 { + t.Error("MaxInt32(1, 2) should be 2") + } + if MaxInt32(-1, -2) != -1 { + t.Error("MaxInt32(-1, -2) should be -1") + } + + // Test MinInt32 + if MinInt32(1, 2) != 1 { + t.Error("MinInt32(1, 2) should be 1") + } + if MinInt32(-1, -2) != -2 { + t.Error("MinInt32(-1, -2) should be -2") + } + + // Test ClampInt32 + if ClampInt32(5, 1, 3) != 3 { + t.Error("ClampInt32(5, 1, 3) should be 3") + } + if ClampInt32(0, 1, 3) != 1 { + t.Error("ClampInt32(0, 1, 3) should be 1") + } + if ClampInt32(2, 1, 3) != 2 { + t.Error("ClampInt32(2, 1, 3) should be 2") + } + + // Test AbsInt32 + if AbsInt32(-5) != 5 { + t.Error("AbsInt32(-5) should be 5") + } + if AbsInt32(5) != 5 { + t.Error("AbsInt32(5) should be 5") + } + + // Test SignInt32 + if SignInt32(-5) != -1 { + t.Error("SignInt32(-5) should be -1") + } + if SignInt32(5) != 1 { + t.Error("SignInt32(5) should be 1") + } + if SignInt32(0) != 0 { + t.Error("SignInt32(0) should be 0") + } + +} + +func TestInt64Helpers(t *testing.T) { + // Test MaxInt64 + if MaxInt64(1, 2) != 2 { + t.Error("MaxInt64(1, 2) should be 2") + } + if MaxInt64(-1, -2) != -1 { + t.Error("MaxInt64(-1, -2) should be -1") + } + + // Test MinInt64 + if MinInt64(1, 2) != 1 { + t.Error("MinInt64(1, 2) should be 1") + } + if MinInt64(-1, -2) != -2 { + t.Error("MinInt64(-1, -2) should be -2") + } + + // Test ClampInt64 + if ClampInt64(5, 1, 3) != 3 { + t.Error("ClampInt64(5, 1, 3) should be 3") + } + if ClampInt64(0, 1, 3) != 1 { + t.Error("ClampInt64(0, 1, 3) should be 1") + } + if ClampInt64(2, 1, 3) != 2 { + t.Error("ClampInt64(2, 1, 3) should be 2") + } + + // Test AbsInt64 + if AbsInt64(-5) != 5 { + t.Error("AbsInt64(-5) should be 5") + } + if AbsInt64(5) != 5 { + t.Error("AbsInt64(5) should be 5") + } + + // Test SignInt64 + if SignInt64(-5) != -1 { + t.Error("SignInt64(-5) should be -1") + } + if SignInt64(5) != 1 { + t.Error("SignInt64(5) should be 1") + } + if SignInt64(0) != 0 { + t.Error("SignInt64(0) should be 0") + } + +} + +func TestIntHelpers(t *testing.T) { + // Test MaxInt + if MaxInt(1, 2) != 2 { + t.Error("MaxInt(1, 2) should be 2") + } + if MaxInt(-1, -2) != -1 { + t.Error("MaxInt(-1, -2) should be -1") + } + + // Test MinInt + if MinInt(1, 2) != 1 { + t.Error("MinInt(1, 2) should be 1") + } + if MinInt(-1, -2) != -2 { + t.Error("MinInt(-1, -2) should be -2") + } + + // Test ClampInt + if ClampInt(5, 1, 3) != 3 { + t.Error("ClampInt(5, 1, 3) should be 3") + } + if ClampInt(0, 1, 3) != 1 { + t.Error("ClampInt(0, 1, 3) should be 1") + } + if ClampInt(2, 1, 3) != 2 { + t.Error("ClampInt(2, 1, 3) should be 2") + } + + // Test AbsInt + if AbsInt(-5) != 5 { + t.Error("AbsInt(-5) should be 5") + } + if AbsInt(5) != 5 { + t.Error("AbsInt(5) should be 5") + } + + // Test SignInt + if SignInt(-5) != -1 { + t.Error("SignInt(-5) should be -1") + } + if SignInt(5) != 1 { + t.Error("SignInt(5) should be 1") + } + if SignInt(0) != 0 { + t.Error("SignInt(0) should be 0") + } + +} + +func TestUint8Helpers(t *testing.T) { + // Test MaxUint8 + if MaxUint8(1, 2) != 2 { + t.Error("MaxUint8(1, 2) should be 2") + } + + // Test MinUint8 + if MinUint8(1, 2) != 1 { + t.Error("MinUint8(1, 2) should be 1") + } + + // Test ClampUint8 + if ClampUint8(5, 1, 3) != 3 { + t.Error("ClampUint8(5, 1, 3) should be 3") + } + if ClampUint8(0, 1, 3) != 1 { + t.Error("ClampUint8(0, 1, 3) should be 1") + } + if ClampUint8(2, 1, 3) != 2 { + t.Error("ClampUint8(2, 1, 3) should be 2") + } + +} + +func TestUint16Helpers(t *testing.T) { + // Test MaxUint16 + if MaxUint16(1, 2) != 2 { + t.Error("MaxUint16(1, 2) should be 2") + } + + // Test MinUint16 + if MinUint16(1, 2) != 1 { + t.Error("MinUint16(1, 2) should be 1") + } + + // Test ClampUint16 + if ClampUint16(5, 1, 3) != 3 { + t.Error("ClampUint16(5, 1, 3) should be 3") + } + if ClampUint16(0, 1, 3) != 1 { + t.Error("ClampUint16(0, 1, 3) should be 1") + } + if ClampUint16(2, 1, 3) != 2 { + t.Error("ClampUint16(2, 1, 3) should be 2") + } + +} + +func TestUint32Helpers(t *testing.T) { + // Test MaxUint32 + if MaxUint32(1, 2) != 2 { + t.Error("MaxUint32(1, 2) should be 2") + } + + // Test MinUint32 + if MinUint32(1, 2) != 1 { + t.Error("MinUint32(1, 2) should be 1") + } + + // Test ClampUint32 + if ClampUint32(5, 1, 3) != 3 { + t.Error("ClampUint32(5, 1, 3) should be 3") + } + if ClampUint32(0, 1, 3) != 1 { + t.Error("ClampUint32(0, 1, 3) should be 1") + } + if ClampUint32(2, 1, 3) != 2 { + t.Error("ClampUint32(2, 1, 3) should be 2") + } + +} + +func TestUint64Helpers(t *testing.T) { + // Test MaxUint64 + if MaxUint64(1, 2) != 2 { + t.Error("MaxUint64(1, 2) should be 2") + } + + // Test MinUint64 + if MinUint64(1, 2) != 1 { + t.Error("MinUint64(1, 2) should be 1") + } + + // Test ClampUint64 + if ClampUint64(5, 1, 3) != 3 { + t.Error("ClampUint64(5, 1, 3) should be 3") + } + if ClampUint64(0, 1, 3) != 1 { + t.Error("ClampUint64(0, 1, 3) should be 1") + } + if ClampUint64(2, 1, 3) != 2 { + t.Error("ClampUint64(2, 1, 3) should be 2") + } + +} + +func TestUintHelpers(t *testing.T) { + // Test MaxUint + if MaxUint(1, 2) != 2 { + t.Error("MaxUint(1, 2) should be 2") + } + + // Test MinUint + if MinUint(1, 2) != 1 { + t.Error("MinUint(1, 2) should be 1") + } + + // Test ClampUint + if ClampUint(5, 1, 3) != 3 { + t.Error("ClampUint(5, 1, 3) should be 3") + } + if ClampUint(0, 1, 3) != 1 { + t.Error("ClampUint(0, 1, 3) should be 1") + } + if ClampUint(2, 1, 3) != 2 { + t.Error("ClampUint(2, 1, 3) should be 2") + } + +} + +func TestFloat32Helpers(t *testing.T) { + // Test MaxFloat32 + if MaxFloat32(1, 2) != 2 { + t.Error("MaxFloat32(1, 2) should be 2") + } + if MaxFloat32(-1, -2) != -1 { + t.Error("MaxFloat32(-1, -2) should be -1") + } + + // Test MinFloat32 + if MinFloat32(1, 2) != 1 { + t.Error("MinFloat32(1, 2) should be 1") + } + if MinFloat32(-1, -2) != -2 { + t.Error("MinFloat32(-1, -2) should be -2") + } + + // Test ClampFloat32 + if ClampFloat32(5, 1, 3) != 3 { + t.Error("ClampFloat32(5, 1, 3) should be 3") + } + if ClampFloat32(0, 1, 3) != 1 { + t.Error("ClampFloat32(0, 1, 3) should be 1") + } + if ClampFloat32(2, 1, 3) != 2 { + t.Error("ClampFloat32(2, 1, 3) should be 2") + } + + // Test AbsFloat32 + if AbsFloat32(-5) != 5 { + t.Error("AbsFloat32(-5) should be 5") + } + if AbsFloat32(5) != 5 { + t.Error("AbsFloat32(5) should be 5") + } + + // Test SignFloat32 + if SignFloat32(-5) != -1 { + t.Error("SignFloat32(-5) should be -1") + } + if SignFloat32(5) != 1 { + t.Error("SignFloat32(5) should be 1") + } + if SignFloat32(0.0) != 0 { + t.Error("SignFloat32(0.0) should be 0") + } + +} + +func TestFloat64Helpers(t *testing.T) { + // Test MaxFloat64 + if MaxFloat64(1, 2) != 2 { + t.Error("MaxFloat64(1, 2) should be 2") + } + if MaxFloat64(-1, -2) != -1 { + t.Error("MaxFloat64(-1, -2) should be -1") + } + + // Test MinFloat64 + if MinFloat64(1, 2) != 1 { + t.Error("MinFloat64(1, 2) should be 1") + } + if MinFloat64(-1, -2) != -2 { + t.Error("MinFloat64(-1, -2) should be -2") + } + + // Test ClampFloat64 + if ClampFloat64(5, 1, 3) != 3 { + t.Error("ClampFloat64(5, 1, 3) should be 3") + } + if ClampFloat64(0, 1, 3) != 1 { + t.Error("ClampFloat64(0, 1, 3) should be 1") + } + if ClampFloat64(2, 1, 3) != 2 { + t.Error("ClampFloat64(2, 1, 3) should be 2") + } + + // Test AbsFloat64 + if AbsFloat64(-5) != 5 { + t.Error("AbsFloat64(-5) should be 5") + } + if AbsFloat64(5) != 5 { + t.Error("AbsFloat64(5) should be 5") + } + + // Test SignFloat64 + if SignFloat64(-5) != -1 { + t.Error("SignFloat64(-5) should be -1") + } + if SignFloat64(5) != 1 { + t.Error("SignFloat64(5) should be 1") + } + if SignFloat64(0.0) != 0 { + t.Error("SignFloat64(0.0) should be 0") + } + +} diff --git a/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno b/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno index 7ef0619188f..cd02db98683 100644 --- a/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno +++ b/examples/gno.land/p/n2p5/mgroup/mgroup_test.gno @@ -297,13 +297,13 @@ func TestManagedGroup(t *testing.T) { if len(owners) != 3 { t.Errorf("expected 2, got %v", len(owners)) } - if owners[0] != u2.String() { + if owners[0] != u1.String() { t.Errorf("expected %v, got %v", u2, owners[0]) } if owners[1] != u3.String() { t.Errorf("expected %v, got %v", u3, owners[1]) } - if owners[2] != u1.String() { + if owners[2] != u2.String() { t.Errorf("expected %v, got %v", u3, owners[1]) } }) @@ -317,13 +317,13 @@ func TestManagedGroup(t *testing.T) { if len(members) != 3 { t.Errorf("expected 2, got %v", len(members)) } - if members[0] != u2.String() { + if members[0] != u1.String() { t.Errorf("expected %v, got %v", u2, members[0]) } if members[1] != u3.String() { t.Errorf("expected %v, got %v", u3, members[1]) } - if members[2] != u1.String() { + if members[2] != u2.String() { t.Errorf("expected %v, got %v", u3, members[1]) } }) diff --git a/examples/gno.land/r/demo/bar20/bar20.gno b/examples/gno.land/r/demo/bar20/bar20.gno index 25636fcda78..52f1baa7408 100644 --- a/examples/gno.land/r/demo/bar20/bar20.gno +++ b/examples/gno.land/r/demo/bar20/bar20.gno @@ -18,8 +18,7 @@ var ( ) func init() { - getter := func() *grc20.Token { return Token } - grc20reg.Register(getter, "") + grc20reg.Register(Token.Getter(), "") } func Faucet() string { diff --git a/examples/gno.land/r/demo/btree_dao/btree_dao.gno b/examples/gno.land/r/demo/btree_dao/btree_dao.gno new file mode 100644 index 00000000000..c90742eb29b --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/btree_dao.gno @@ -0,0 +1,209 @@ +package btree_dao + +import ( + "errors" + "std" + "strings" + "time" + + "gno.land/p/demo/btree" + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" +) + +// RegistrationDetails holds the details of a user's registration in the BTree DAO. +// It stores the user's address, registration time, their B-Tree if they planted one, +// and their NFT ID. +type RegistrationDetails struct { + Address std.Address + RegTime time.Time + UserBTree *btree.BTree + NFTID string +} + +// Less implements the btree.Record interface for RegistrationDetails. +// It compares two RegistrationDetails based on their registration time. +// Returns true if the current registration time is before the other registration time. +func (rd *RegistrationDetails) Less(than btree.Record) bool { + other := than.(*RegistrationDetails) + return rd.RegTime.Before(other.RegTime) +} + +var ( + dao = grc721.NewBasicNFT("BTree DAO", "BTDAO") + tokenID = 0 + members = btree.New() +) + +// PlantTree allows a user to plant their B-Tree in the DAO forest. +// It mints an NFT to the user and registers their tree in the DAO. +// Returns an error if the tree is already planted, empty, or if NFT minting fails. +func PlantTree(userBTree *btree.BTree) error { + return plantImpl(userBTree, "") +} + +// PlantSeed allows a user to register as a seed in the DAO with a message. +// It mints an NFT to the user and registers them as a seed member. +// Returns an error if the message is empty or if NFT minting fails. +func PlantSeed(message string) error { + return plantImpl(nil, message) +} + +// plantImpl is the internal implementation that handles both tree planting and seed registration. +// For tree planting (userBTree != nil), it verifies the tree isn't already planted and isn't empty. +// For seed planting (userBTree == nil), it verifies the seed message isn't empty. +// In both cases, it mints an NFT to the user and adds their registration details to the members tree. +// Returns an error if any validation fails or if NFT minting fails. +func plantImpl(userBTree *btree.BTree, seedMessage string) error { + // Get the caller's address + userAddress := std.GetOrigCaller() + + var nftID string + var regDetails *RegistrationDetails + + if userBTree != nil { + // Handle tree planting + var treeExists bool + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == userBTree { + treeExists = true + return false + } + return true + }) + if treeExists { + return errors.New("tree is already planted in the forest") + } + + if userBTree.Len() == 0 { + return errors.New("cannot plant an empty tree") + } + + nftID = ufmt.Sprintf("%d", tokenID) + regDetails = &RegistrationDetails{ + Address: userAddress, + RegTime: time.Now(), + UserBTree: userBTree, + NFTID: nftID, + } + } else { + // Handle seed planting + if seedMessage == "" { + return errors.New("seed message cannot be empty") + } + nftID = "seed_" + ufmt.Sprintf("%d", tokenID) + regDetails = &RegistrationDetails{ + Address: userAddress, + RegTime: time.Now(), + UserBTree: nil, + NFTID: nftID, + } + } + + // Mint an NFT to the user + err := dao.Mint(userAddress, grc721.TokenID(nftID)) + if err != nil { + return err + } + + members.Insert(regDetails) + tokenID++ + return nil +} + +// Render generates a Markdown representation of the DAO members. +// It displays: +// - Total number of NFTs minted +// - Total number of members +// - Size of the biggest planted tree +// - The first 3 members (OGs) +// - The latest 10 members +// Each member entry includes their address and owned NFTs (🌳 for trees, 🌱 for seeds). +// The path parameter is currently unused. +// Returns a formatted Markdown string. +func Render(path string) string { + var latestMembers []string + var ogMembers []string + + // Get total size and first member + totalSize := members.Len() + biggestTree := 0 + if maxMember := members.Max(); maxMember != nil { + if userBTree := maxMember.(*RegistrationDetails).UserBTree; userBTree != nil { + biggestTree = userBTree.Len() + } + } + + // Collect the latest 10 members + members.Descend(func(record btree.Record) bool { + if len(latestMembers) < 10 { + regDetails := record.(*RegistrationDetails) + addr := regDetails.Address + nftList := "" + balance, err := dao.BalanceOf(addr) + if err == nil && balance > 0 { + nftList = " (NFTs: " + for i := uint64(0); i < balance; i++ { + if i > 0 { + nftList += ", " + } + if regDetails.UserBTree == nil { + nftList += "🌱#" + regDetails.NFTID + } else { + nftList += "🌳#" + regDetails.NFTID + } + } + nftList += ")" + } + latestMembers = append(latestMembers, string(addr)+nftList) + return true + } + return false + }) + + // Collect the first 3 members (OGs) + members.Ascend(func(record btree.Record) bool { + if len(ogMembers) < 3 { + regDetails := record.(*RegistrationDetails) + addr := regDetails.Address + nftList := "" + balance, err := dao.BalanceOf(addr) + if err == nil && balance > 0 { + nftList = " (NFTs: " + for i := uint64(0); i < balance; i++ { + if i > 0 { + nftList += ", " + } + if regDetails.UserBTree == nil { + nftList += "🌱#" + regDetails.NFTID + } else { + nftList += "🌳#" + regDetails.NFTID + } + } + nftList += ")" + } + ogMembers = append(ogMembers, string(addr)+nftList) + return true + } + return false + }) + + var sb strings.Builder + + sb.WriteString(md.H1("B-Tree DAO Members")) + sb.WriteString(md.H2("Total NFTs Minted")) + sb.WriteString(ufmt.Sprintf("Total NFTs minted: %d\n\n", dao.TokenCount())) + sb.WriteString(md.H2("Member Stats")) + sb.WriteString(ufmt.Sprintf("Total members: %d\n", totalSize)) + if biggestTree > 0 { + sb.WriteString(ufmt.Sprintf("Biggest tree size: %d\n", biggestTree)) + } + sb.WriteString(md.H2("OG Members")) + sb.WriteString(md.BulletList(ogMembers)) + sb.WriteString(md.H2("Latest Members")) + sb.WriteString(md.BulletList(latestMembers)) + + return sb.String() +} diff --git a/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno b/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno new file mode 100644 index 00000000000..0514f52f7b4 --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/btree_dao_test.gno @@ -0,0 +1,97 @@ +package btree_dao + +import ( + "std" + "strings" + "testing" + "time" + + "gno.land/p/demo/btree" + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func setupTest() { + std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y")) + members = btree.New() +} + +type TestElement struct { + value int +} + +func (te *TestElement) Less(than btree.Record) bool { + return te.value < than.(*TestElement).value +} + +func TestPlantTree(t *testing.T) { + setupTest() + + tree := btree.New() + elements := []int{30, 10, 50, 20, 40} + for _, val := range elements { + tree.Insert(&TestElement{value: val}) + } + + err := PlantTree(tree) + urequire.NoError(t, err) + + found := false + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == tree { + found = true + return false + } + return true + }) + uassert.True(t, found) + + err = PlantTree(tree) + uassert.Error(t, err) + + emptyTree := btree.New() + err = PlantTree(emptyTree) + uassert.Error(t, err) +} + +func TestPlantSeed(t *testing.T) { + setupTest() + + err := PlantSeed("Hello DAO!") + urequire.NoError(t, err) + + found := false + members.Ascend(func(record btree.Record) bool { + regDetails := record.(*RegistrationDetails) + if regDetails.UserBTree == nil { + found = true + uassert.NotEmpty(t, regDetails.NFTID) + uassert.True(t, strings.Contains(regDetails.NFTID, "seed_")) + return false + } + return true + }) + uassert.True(t, found) + + err = PlantSeed("") + uassert.Error(t, err) +} + +func TestRegistrationDetailsOrdering(t *testing.T) { + setupTest() + + rd1 := &RegistrationDetails{ + Address: std.Address("test1"), + RegTime: time.Now(), + NFTID: "0", + } + rd2 := &RegistrationDetails{ + Address: std.Address("test2"), + RegTime: time.Now().Add(time.Hour), + NFTID: "1", + } + + uassert.True(t, rd1.Less(rd2)) + uassert.False(t, rd2.Less(rd1)) +} diff --git a/examples/gno.land/r/demo/btree_dao/gno.mod b/examples/gno.land/r/demo/btree_dao/gno.mod new file mode 100644 index 00000000000..01b99acc300 --- /dev/null +++ b/examples/gno.land/r/demo/btree_dao/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/btree_dao diff --git a/examples/gno.land/r/demo/foo20/foo20.gno b/examples/gno.land/r/demo/foo20/foo20.gno index 5c7d7f12b99..6522fbdc90e 100644 --- a/examples/gno.land/r/demo/foo20/foo20.gno +++ b/examples/gno.land/r/demo/foo20/foo20.gno @@ -22,8 +22,7 @@ var ( func init() { privateLedger.Mint(Ownable.Owner(), 1_000_000*10_000) // @privateLedgeristrator (1M) - getter := func() *grc20.Token { return Token } - grc20reg.Register(getter, "") + grc20reg.Register(Token.Getter(), "") } func TotalSupply() uint64 { diff --git a/examples/gno.land/r/demo/grc20factory/grc20factory.gno b/examples/gno.land/r/demo/grc20factory/grc20factory.gno index 58874409d7f..aa91084ab32 100644 --- a/examples/gno.land/r/demo/grc20factory/grc20factory.gno +++ b/examples/gno.land/r/demo/grc20factory/grc20factory.gno @@ -43,8 +43,7 @@ func NewWithAdmin(name, symbol string, decimals uint, initialMint, faucet uint64 faucet: faucet, } instances.Set(symbol, &inst) - getter := func() *grc20.Token { return token } - grc20reg.Register(getter, symbol) + grc20reg.Register(token.Getter(), symbol) } func (inst instance) Token() *grc20.Token { diff --git a/examples/gno.land/r/demo/tests/test20/gno.mod b/examples/gno.land/r/demo/tests/test20/gno.mod new file mode 100644 index 00000000000..7a71668d2df --- /dev/null +++ b/examples/gno.land/r/demo/tests/test20/gno.mod @@ -0,0 +1 @@ +module gno.land/r/demo/tests/test20 diff --git a/examples/gno.land/r/demo/tests/test20/test20.gno b/examples/gno.land/r/demo/tests/test20/test20.gno new file mode 100644 index 00000000000..9c4df58d1c4 --- /dev/null +++ b/examples/gno.land/r/demo/tests/test20/test20.gno @@ -0,0 +1,20 @@ +// Package test20 implements a deliberately insecure ERC20 token for testing purposes. +// The Test20 token allows anyone to mint any amount of tokens to any address, making +// it unsuitable for production use. The primary goal of this package is to facilitate +// testing and experimentation without any security measures or restrictions. +// +// WARNING: This token is highly insecure and should not be used in any +// production environment. It is intended solely for testing and +// educational purposes. +package test20 + +import ( + "gno.land/p/demo/grc/grc20" + "gno.land/r/demo/grc20reg" +) + +var Token, PrivateLedger = grc20.NewToken("Test20", "TST", 4) + +func init() { + grc20reg.Register(Token.Getter(), "") +} diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index 8547a6e60e0..451afc7bf96 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -260,7 +260,7 @@ func GetUserByAddressOrName(input users.AddressOrName) *users.User { // Get a list of user names starting from the given prefix. Limit the // number of results to maxResults. (This can be used for a name search tool.) func ListUsersByPrefix(prefix string, maxResults int) []string { - return avlhelpers.ListByteStringKeysByPrefix(name2User, prefix, maxResults) + return avlhelpers.ListByteStringKeysByPrefix(&name2User, prefix, maxResults) } func Resolve(input users.AddressOrName) std.Address { diff --git a/examples/gno.land/r/demo/wugnot/wugnot.gno b/examples/gno.land/r/demo/wugnot/wugnot.gno index 09538b860ca..b72f5161e7d 100644 --- a/examples/gno.land/r/demo/wugnot/wugnot.gno +++ b/examples/gno.land/r/demo/wugnot/wugnot.gno @@ -19,8 +19,7 @@ const ( ) func init() { - getter := func() *grc20.Token { return Token } - grc20reg.Register(getter, "") + grc20reg.Register(Token.Getter(), "") } func Deposit() { diff --git a/examples/gno.land/r/nemanya/config/config.gno b/examples/gno.land/r/nemanya/config/config.gno new file mode 100644 index 00000000000..795e48c94c1 --- /dev/null +++ b/examples/gno.land/r/nemanya/config/config.gno @@ -0,0 +1,63 @@ +package config + +import ( + "errors" + "std" +) + +var ( + main std.Address + backup std.Address + + ErrInvalidAddr = errors.New("Invalid address") + ErrUnauthorized = errors.New("Unauthorized") +) + +func init() { + main = "g1x9qyf6f34v2g52k4q5smn5tctmj3hl2kj7l2ql" +} + +func Address() std.Address { + return main +} + +func Backup() std.Address { + return backup +} + +func SetAddress(a std.Address) error { + if !a.IsValid() { + return ErrInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + main = a + return nil +} + +func SetBackup(a std.Address) error { + if !a.IsValid() { + return ErrInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + backup = a + return nil +} + +func checkAuthorized() error { + caller := std.PrevRealm().Addr() + isAuthorized := caller == main || caller == backup + + if !isAuthorized { + return ErrUnauthorized + } + + return nil +} diff --git a/examples/gno.land/r/nemanya/config/gno.mod b/examples/gno.land/r/nemanya/config/gno.mod new file mode 100644 index 00000000000..4388b5bd525 --- /dev/null +++ b/examples/gno.land/r/nemanya/config/gno.mod @@ -0,0 +1 @@ +module gno.land/r/nemanya/config diff --git a/examples/gno.land/r/nemanya/home/gno.mod b/examples/gno.land/r/nemanya/home/gno.mod new file mode 100644 index 00000000000..d0220197489 --- /dev/null +++ b/examples/gno.land/r/nemanya/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/nemanya/home diff --git a/examples/gno.land/r/nemanya/home/home.gno b/examples/gno.land/r/nemanya/home/home.gno new file mode 100644 index 00000000000..08e24baecfd --- /dev/null +++ b/examples/gno.land/r/nemanya/home/home.gno @@ -0,0 +1,280 @@ +package home + +import ( + "std" + "strings" + + "gno.land/p/demo/ufmt" + "gno.land/r/nemanya/config" +) + +type SocialLink struct { + URL string + Text string +} + +type Sponsor struct { + Address std.Address + Amount std.Coins +} + +type Project struct { + Name string + Description string + URL string + ImageURL string + Sponsors map[std.Address]Sponsor +} + +var ( + textArt string + aboutMe string + sponsorInfo string + socialLinks map[string]SocialLink + gnoProjects map[string]Project + otherProjects map[string]Project + totalDonations std.Coins +) + +func init() { + textArt = renderTextArt() + aboutMe = "I am a student of IT at Faculty of Sciences in Novi Sad, Serbia. My background is mainly in web and low-level programming, but since Web3 Bootcamp at Petnica this year I've been actively learning about blockchain and adjacent technologies. I am excited about contributing to the gno.land ecosystem and learning from the community.\n\n" + sponsorInfo = "You can sponsor a project by sending GNOT to this address. Your sponsorship will be displayed on the project page. Thank you for supporting the development of gno.land!\n\n" + + socialLinks = map[string]SocialLink{ + "GitHub": {URL: "https://github.com/Nemanya8", Text: "Explore my repositories and open-source contributions."}, + "LinkedIn": {URL: "https://www.linkedin.com/in/nemanjamatic/", Text: "Connect with me professionally."}, + "Email Me": {URL: "mailto:matic.nemanya@gmail.com", Text: "Reach out for collaboration or inquiries."}, + } + + gnoProjects = make(map[string]Project) + otherProjects = make(map[string]Project) + + gnoProjects["Liberty Bridge"] = Project{ + Name: "Liberty Bridge", + Description: "Liberty Bridge was my first Web3 project, developed as part of the Web3 Bootcamp at Petnica. This project served as a centralized bridge between Ethereum and gno.land, enabling seamless asset transfers and fostering interoperability between the two ecosystems.\n\n The primary objective of Liberty Bridge was to address the challenges of connecting decentralized networks by implementing a user-friendly solution that simplified the process for users. The project incorporated mechanisms to securely transfer assets between the Ethereum and gno.land blockchains, ensuring efficiency and reliability while maintaining a centralized framework for governance and operations.\n\n Through this project, I gained hands-on knowledge of blockchain interoperability, Web3 protocols, and the intricacies of building solutions that bridge different blockchain ecosystems.\n\n", + URL: "https://gno.land", + ImageURL: "https://github.com/Milosevic02/LibertyBridge/raw/main/lb_banner.png", + Sponsors: make(map[std.Address]Sponsor), + } + + otherProjects["Incognito"] = Project{ + Name: "Incognito", + Description: "Incognito is a Web3 platform built for Ethereum-based chains, designed to connect advertisers with users in a privacy-first and mutually beneficial way. Its modular architecture makes it easily expandable to other blockchains. Developed during the ETH Sofia Hackathon, it was recognized as a winning project for its innovation and impact.\n\n The platform allows advertisers to send personalized ads while sharing a portion of the marketing budget with users. It uses machine learning to match users based on wallet activity, ensuring precise targeting. User emails are stored securely on-chain and never shared, prioritizing privacy and transparency.\n\n With all campaign data stored on-chain, Incognito ensures decentralization and accountability. By rewarding users and empowering advertisers, it sets a new standard for fair and transparent blockchain-based advertising.", + URL: "https://github.com/Milosevic02/Incognito-ETHSofia", + ImageURL: "", + Sponsors: make(map[std.Address]Sponsor), + } +} + +func Render(path string) string { + var sb strings.Builder + sb.WriteString("# Hi, I'm\n") + sb.WriteString(textArt) + sb.WriteString("---\n") + sb.WriteString("## About me\n") + sb.WriteString(aboutMe) + sb.WriteString(sponsorInfo) + sb.WriteString(ufmt.Sprintf("# Total Sponsor Donations: %s\n", totalDonations.String())) + sb.WriteString("---\n") + sb.WriteString(renderProjects(gnoProjects, "Gno Projects")) + sb.WriteString("---\n") + sb.WriteString(renderProjects(otherProjects, "Other Projects")) + sb.WriteString("---\n") + sb.WriteString(renderSocialLinks()) + + return sb.String() +} + +func renderTextArt() string { + var sb strings.Builder + sb.WriteString("```\n") + sb.WriteString(" ___ ___ ___ ___ ___ ___ ___ \n") + sb.WriteString(" /\\__\\ /\\ \\ /\\__\\ /\\ \\ /\\__\\ |\\__\\ /\\ \\ \n") + sb.WriteString(" /::| | /::\\ \\ /::| | /::\\ \\ /::| | |:| | /::\\ \\ \n") + sb.WriteString(" /:|:| | /:/\\:\\ \\ /:|:| | /:/\\:\\ \\ /:|:| | |:| | /:/\\:\\ \\ \n") + sb.WriteString(" /:/|:| |__ /::\\~\\:\\ \\ /:/|:|__|__ /::\\~\\:\\ \\ /:/|:| |__ |:|__|__ /::\\~\\:\\ \\ \n") + sb.WriteString(" /:/ |:| /\\__\\ /:/\\:\\ \\:\\__\\ /:/ |::::\\__\\ /:/\\:\\ \\:\\__\\ /:/ |:| /\\__\\ /::::\\__\\ /:/\\:\\ \\:\\__\\\n") + sb.WriteString(" \\/__|:|/:/ / \\:\\~\\:\\ \\/__/ \\/__/~~/:/ / \\/__\\:\\/:/ / \\/__|:|/:/ / /:/~~/~ \\/__\\:\\/:/ / \n") + sb.WriteString(" |:/:/ / \\:\\ \\:\\__\\ /:/ / \\::/ / |:/:/ / /:/ / \\::/ / \n") + sb.WriteString(" |::/ / \\:\\ \\/__/ /:/ / /:/ / |::/ / \\/__/ /:/ / \n") + sb.WriteString(" /:/ / \\:\\__\\ /:/ / /:/ / /:/ / /:/ / \n") + sb.WriteString(" \\/__/ \\/__/ \\/__/ \\/__/ \\/__/ \\/__/ \n") + sb.WriteString("\n```\n") + return sb.String() +} + +func renderSocialLinks() string { + var sb strings.Builder + sb.WriteString("## Links\n\n") + sb.WriteString("You can find me here:\n\n") + sb.WriteString(ufmt.Sprintf("- [GitHub](%s) - %s\n", socialLinks["GitHub"].URL, socialLinks["GitHub"].Text)) + sb.WriteString(ufmt.Sprintf("- [LinkedIn](%s) - %s\n", socialLinks["LinkedIn"].URL, socialLinks["LinkedIn"].Text)) + sb.WriteString(ufmt.Sprintf("- [Email Me](%s) - %s\n", socialLinks["Email Me"].URL, socialLinks["Email Me"].Text)) + sb.WriteString("\n") + return sb.String() +} + +func renderProjects(projectsMap map[string]Project, title string) string { + var sb strings.Builder + sb.WriteString(ufmt.Sprintf("## %s\n\n", title)) + for _, project := range projectsMap { + if project.ImageURL != "" { + sb.WriteString(ufmt.Sprintf("![%s](%s)\n\n", project.Name, project.ImageURL)) + } + sb.WriteString(ufmt.Sprintf("### [%s](%s)\n\n", project.Name, project.URL)) + sb.WriteString(project.Description + "\n\n") + + if len(project.Sponsors) > 0 { + sb.WriteString(ufmt.Sprintf("#### %s Sponsors\n", project.Name)) + for _, sponsor := range project.Sponsors { + sb.WriteString(ufmt.Sprintf("- %s: %s\n", sponsor.Address.String(), sponsor.Amount.String())) + } + sb.WriteString("\n") + } + } + return sb.String() +} + +func UpdateLink(name, newURL string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + if _, exists := socialLinks[name]; !exists { + panic("Link with the given name does not exist") + } + + socialLinks[name] = SocialLink{ + URL: newURL, + Text: socialLinks[name].Text, + } +} + +func UpdateAboutMe(text string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + aboutMe = text +} + +func AddGnoProject(name, description, url, imageURL string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + project := Project{ + Name: name, + Description: description, + URL: url, + ImageURL: imageURL, + Sponsors: make(map[std.Address]Sponsor), + } + gnoProjects[name] = project +} + +func DeleteGnoProject(projectName string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + if _, exists := gnoProjects[projectName]; !exists { + panic("Project not found") + } + + delete(gnoProjects, projectName) +} + +func AddOtherProject(name, description, url, imageURL string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + project := Project{ + Name: name, + Description: description, + URL: url, + ImageURL: imageURL, + Sponsors: make(map[std.Address]Sponsor), + } + otherProjects[name] = project +} + +func RemoveOtherProject(projectName string) { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + if _, exists := otherProjects[projectName]; !exists { + panic("Project not found") + } + + delete(otherProjects, projectName) +} + +func isAuthorized(addr std.Address) bool { + return addr == config.Address() || addr == config.Backup() +} + +func SponsorGnoProject(projectName string) { + address := std.GetOrigCaller() + amount := std.GetOrigSend() + + if amount.AmountOf("ugnot") == 0 { + panic("Donation must include GNOT") + } + + project, exists := gnoProjects[projectName] + if !exists { + panic("Gno project not found") + } + + project.Sponsors[address] = Sponsor{ + Address: address, + Amount: project.Sponsors[address].Amount.Add(amount), + } + + totalDonations = totalDonations.Add(amount) + + gnoProjects[projectName] = project +} + +func SponsorOtherProject(projectName string) { + address := std.GetOrigCaller() + amount := std.GetOrigSend() + + if amount.AmountOf("ugnot") == 0 { + panic("Donation must include GNOT") + } + + project, exists := otherProjects[projectName] + if !exists { + panic("Other project not found") + } + + project.Sponsors[address] = Sponsor{ + Address: address, + Amount: project.Sponsors[address].Amount.Add(amount), + } + + totalDonations = totalDonations.Add(amount) + + otherProjects[projectName] = project +} + +func Withdraw() string { + if !isAuthorized(std.PrevRealm().Addr()) { + panic(config.ErrUnauthorized) + } + + banker := std.GetBanker(std.BankerTypeRealmSend) + realmAddress := std.GetOrigPkgAddr() + coins := banker.GetCoins(realmAddress) + + if len(coins) == 0 { + return "No coins available to withdraw" + } + + banker.SendCoins(realmAddress, config.Address(), coins) + + return "Successfully withdrew all coins to config address" +} diff --git a/examples/gno.land/r/stefann/home/home.gno b/examples/gno.land/r/stefann/home/home.gno index 9586f377311..f54721ce37c 100644 --- a/examples/gno.land/r/stefann/home/home.gno +++ b/examples/gno.land/r/stefann/home/home.gno @@ -8,6 +8,8 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" + "gno.land/r/demo/users" + "gno.land/r/leon/hof" "gno.land/r/stefann/registry" ) @@ -23,7 +25,6 @@ type Sponsor struct { } type Profile struct { - pfp string aboutMe []string } @@ -49,15 +50,15 @@ var ( func init() { owner = ownable.NewWithAddress(registry.MainAddr()) + hof.Register() profile = Profile{ - pfp: "https://i.ibb.co/Bc5YNCx/DSC-0095a.jpg", aboutMe: []string{ - `### About Me`, - `Hey there! I’m Stefan, a student of Computer Science. I’m all about exploring and adventure — whether it’s diving into the latest tech or discovering a new city, I’m always up for the challenge!`, + `## About Me`, + `### Hey there! I’m Stefan, a student of Computer Science. I’m all about exploring and adventure — whether it’s diving into the latest tech or discovering a new city, I’m always up for the challenge!`, - `### Contributions`, - `I'm just getting started, but you can follow my journey through gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) 🔗`, + `## Contributions`, + `### I'm just getting started, but you can follow my journey through gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) 🔗`, }, } @@ -83,7 +84,7 @@ func init() { } sponsorship = Sponsorship{ - maxSponsors: 5, + maxSponsors: 3, sponsors: avl.NewTree(), DonationsCount: 0, sponsorsCount: 0, @@ -106,11 +107,6 @@ func UpdateJarLink(newLink string) { travel.jarLink = newLink } -func UpdatePFP(url string) { - owner.AssertCallerIsOwner() - profile.pfp = url -} - func UpdateAboutMe(aboutMeStr string) { owner.AssertCallerIsOwner() profile.aboutMe = strings.Split(aboutMeStr, "|") @@ -203,46 +199,27 @@ func Render(path string) string { } func renderAboutMe() string { - out := "
" - - out += "
\n\n" + out := "" - out += ufmt.Sprintf("
\n\n", travel.cities[travel.currentCityIndex%len(travel.cities)].URL) - - out += ufmt.Sprintf("my profile pic\n\n", profile.pfp) - - out += "
\n\n" + out += ufmt.Sprintf("![Current Location](%s)\n\n", travel.cities[travel.currentCityIndex%len(travel.cities)].URL) for _, rows := range profile.aboutMe { - out += "
\n\n" out += rows + "\n\n" - out += "
\n\n" } - out += "
\n\n" - return out } func renderTips() string { - out := `
` + "\n\n" + out := "# Help Me Travel The World\n\n" - out += `
` + "\n" + out += ufmt.Sprintf("## I am currently in %s, tip the jar to send me somewhere else!\n\n", travel.cities[travel.currentCityIndex].Name) + out += "### **Click** the jar, **tip** in GNOT coins, and **watch** my background change as I head to a new adventure!\n\n" - out += `

Help Me Travel The World

` + "\n\n" - - out += renderTipsJar() + "\n" - - out += ufmt.Sprintf(`I am currently in %s,
tip the jar to send me somewhere else!
`, travel.cities[travel.currentCityIndex].Name) - - out += `
Click the jar, tip in GNOT coins, and watch my background change as I head to a new adventure!

` + "\n\n" + out += renderTipsJar() + "\n\n" out += renderSponsors() - out += `
` + "\n\n" - - out += `
` + "\n" - return out } @@ -253,11 +230,27 @@ func formatAddress(address string) string { return address[:4] + "..." + address[len(address)-4:] } +func getDisplayName(addr std.Address) string { + if user := users.GetUserByAddress(addr); user != nil { + return user.Name + } + return formatAddress(addr.String()) +} + +func formatAmount(amount std.Coins) string { + ugnot := amount.AmountOf("ugnot") + if ugnot >= 1000000 { + gnot := float64(ugnot) / 1000000 + return ufmt.Sprintf("`%v`*GNOT*", gnot) + } + return ufmt.Sprintf("`%d`*ugnot*", ugnot) +} + func renderSponsors() string { - out := `

Sponsor Leaderboard

` + "\n" + out := "## Sponsor Leaderboard\n\n" if sponsorship.sponsorsCount == 0 { - return out + `

No sponsors yet. Be the first to tip the jar!

` + "\n" + return out + "No sponsors yet. Be the first to tip the jar!\n" } topSponsors := GetTopSponsors() @@ -266,38 +259,30 @@ func renderSponsors() string { numSponsors = sponsorship.maxSponsors } - out += `