From 37df80afb71ea85d853388b2d619376f309327f0 Mon Sep 17 00:00:00 2001
From: Patrick O'Grady
Date: Mon, 24 Jul 2023 08:15:15 -0700
Subject: [PATCH] MorpheusVM: The Choice is Yours (#258)
* first commit
* update workflows
* move to basevm
* remove all reference to lite
* move cmd
* update scripts run.sh
* go mod tidy
* replace lint
* restrict running of unit tests
* add badges
* fix genesis
* fix balance issue
* fix controller
* fix rpc
* cli compiles
* e2e compiles
* integration compiles
* lint passed
* fix milli
* remove unnecessary tests
* integration passing for base
* fix vmID
* e2e passing
* add stop.sh
* add basevm load test
* fix load test
* fix main README
* Separate Shared Components (#260)
* experimenting with CLI
* move more cli commands over
* port storage
* more progress
* fix prompt chain
* rewrite tokenvm action
* tokenvm chain
* make chain shared
* abstract watch
* rewrite key operations
* add support for prometheus
* introduce sendAndWait
* abstract submitDummy
* remove old submit dummy
* progress on cli spam
* generic spam implemented
* general spam
* remove unused errors
* close db earlier
* start cleanup of basevm
* cleaning up base
* basevm actions passing
* making progress with cleanup
* fix chain for basevm
* cleanup spam
* remove unnecessary errors
* fix lint in base and hypersk
* fix license spacing
* remove gosec from spam
* fix license header application
* add license to right files in token-cli
* upload logo for basevm
* Rename to `SimpleVM` (#266)
* rename to simple
* migrate from basevm to simplevm
* add basic README
* start demo work
* get fields into README
* fill in rest of README
* update avalanchego dep (#268)
* MorpheusVM: the last renaming of `basevm` (#270)
* update workflows
* move files
* update all references
* update addresses
* update README
---
.github/workflows/hypersdk-unit-tests.yml | 2 +
.github/workflows/morpheusvm-load-tests.yml | 40 +
.github/workflows/morpheusvm-release.yml | 69 ++
.../workflows/morpheusvm-static-analysis.yml | 35 +
.github/workflows/morpheusvm-sync-tests.yml | 39 +
.github/workflows/morpheusvm-unit-tests.yml | 58 +
.github/workflows/tokenvm-sync-tests.yml | 2 +-
.github/workflows/tokenvm-unit-tests.yml | 2 +
.gitignore | 1 +
.golangci.yml | 2 +-
README.md | 31 +-
chain/mock_action.go | 1 +
chain/mock_auth.go | 1 +
chain/mock_auth_factory.go | 1 +
chain/mock_rules.go | 1 +
cli/chain.go | 270 +++++
cli/cli.go | 23 +
cli/dependencies.go | 15 +
cli/errors.go | 18 +
cli/key.go | 125 ++
cli/prometheus.go | 95 ++
.../token-cli/cmd/utils.go => cli/prompt.go | 206 +---
cli/spam.go | 433 +++++++
.../cmd/token-cli/cmd => cli}/storage.go | 99 +-
cli/utils.go | 55 +
examples/README.md | 70 --
examples/morpheusvm/.golangci.yml | 116 ++
examples/morpheusvm/.goreleaser.yml | 66 ++
examples/morpheusvm/LICENSE | 58 +
examples/morpheusvm/README.md | 187 +++
examples/morpheusvm/actions/outputs.go | 6 +
examples/morpheusvm/actions/transfer.go | 81 ++
examples/morpheusvm/assets/hypersdk.png | Bin 0 -> 41378 bytes
examples/morpheusvm/assets/logo.jpeg | Bin 0 -> 1776865 bytes
examples/morpheusvm/auth/ed25519.go | 118 ++
examples/morpheusvm/auth/errors.go | 8 +
examples/morpheusvm/auth/helpers.go | 27 +
.../morpheusvm/cmd/morpheus-cli/cmd/action.go | 61 +
.../morpheusvm/cmd/morpheus-cli/cmd/chain.go | 73 ++
.../morpheusvm/cmd/morpheus-cli/cmd/errors.go | 11 +
.../cmd/morpheus-cli/cmd/genesis.go | 67 ++
.../cmd/morpheus-cli/cmd/handler.go | 108 ++
.../morpheusvm/cmd/morpheus-cli/cmd/key.go | 77 ++
.../cmd/morpheus-cli/cmd/prometheus.go | 63 +
.../cmd/morpheus-cli/cmd/resolutions.go | 69 ++
.../morpheusvm/cmd/morpheus-cli/cmd/root.go | 183 +++
.../morpheusvm/cmd/morpheus-cli/cmd/spam.go | 72 ++
examples/morpheusvm/cmd/morpheus-cli/main.go | 20 +
examples/morpheusvm/cmd/morpheusvm/main.go | 49 +
.../cmd/morpheusvm/version/version.go | 32 +
examples/morpheusvm/config/config.go | 126 ++
examples/morpheusvm/consts/consts.go | 37 +
examples/morpheusvm/controller/controller.go | 203 ++++
examples/morpheusvm/controller/metrics.go | 33 +
examples/morpheusvm/controller/resolutions.go | 41 +
examples/morpheusvm/factory.go | 19 +
examples/morpheusvm/genesis/errors.go | 11 +
examples/morpheusvm/genesis/genesis.go | 113 ++
examples/morpheusvm/genesis/rules.go | 75 ++
examples/morpheusvm/go.mod | 149 +++
examples/morpheusvm/go.sum | 1053 +++++++++++++++++
examples/morpheusvm/license.yml | 4 +
examples/morpheusvm/registry/registry.go | 33 +
examples/morpheusvm/rpc/consts.go | 6 +
examples/morpheusvm/rpc/dependencies.go | 20 +
examples/morpheusvm/rpc/errors.go | 8 +
examples/morpheusvm/rpc/jsonrpc_client.go | 156 +++
examples/morpheusvm/rpc/jsonrpc_server.go | 81 ++
examples/morpheusvm/scripts/build.release.sh | 29 +
examples/morpheusvm/scripts/build.sh | 30 +
examples/morpheusvm/scripts/fix.lint.sh | 23 +
examples/morpheusvm/scripts/run.sh | 242 ++++
examples/morpheusvm/scripts/stop.sh | 7 +
.../morpheusvm/scripts/tests.integration.sh | 39 +
examples/morpheusvm/scripts/tests.lint.sh | 81 ++
examples/morpheusvm/scripts/tests.load.sh | 34 +
examples/morpheusvm/scripts/tests.unit.sh | 18 +
examples/morpheusvm/storage/errors.go | 8 +
examples/morpheusvm/storage/state_manager.go | 22 +
examples/morpheusvm/storage/storage.go | 251 ++++
examples/morpheusvm/tests/e2e/e2e_test.go | 770 ++++++++++++
.../tests/integration/integration_test.go | 765 ++++++++++++
examples/morpheusvm/tests/load/load_test.go | 623 ++++++++++
examples/morpheusvm/utils/utils.go | 18 +
examples/morpheusvm/version/version.go | 12 +
examples/tokenvm/.golangci.yml | 2 +-
examples/tokenvm/LICENSE | 87 +-
examples/tokenvm/README.md | 4 +-
examples/tokenvm/cmd/token-cli/cmd/action.go | 398 ++-----
examples/tokenvm/cmd/token-cli/cmd/chain.go | 380 +-----
examples/tokenvm/cmd/token-cli/cmd/errors.go | 18 +-
examples/tokenvm/cmd/token-cli/cmd/handler.go | 143 +++
examples/tokenvm/cmd/token-cli/cmd/key.go | 143 +--
.../tokenvm/cmd/token-cli/cmd/prometheus.go | 153 +--
.../tokenvm/cmd/token-cli/cmd/resolutions.go | 170 +++
examples/tokenvm/cmd/token-cli/cmd/root.go | 28 +-
examples/tokenvm/cmd/token-cli/cmd/spam.go | 462 +-------
examples/tokenvm/config/config.go | 1 +
examples/tokenvm/go.mod | 11 +-
examples/tokenvm/go.sum | 9 +-
examples/tokenvm/license.yml | 3 +-
examples/tokenvm/scripts/fix.lint.sh | 4 +-
examples/tokenvm/scripts/run.sh | 3 +-
go.mod | 68 +-
go.sum | 236 +++-
gossiper/consts.go | 1 +
license.yml | 3 +-
vm/mock_controller.go | 1 +
108 files changed, 9058 insertions(+), 1656 deletions(-)
create mode 100644 .github/workflows/morpheusvm-load-tests.yml
create mode 100644 .github/workflows/morpheusvm-release.yml
create mode 100644 .github/workflows/morpheusvm-static-analysis.yml
create mode 100644 .github/workflows/morpheusvm-sync-tests.yml
create mode 100644 .github/workflows/morpheusvm-unit-tests.yml
create mode 100644 cli/chain.go
create mode 100644 cli/cli.go
create mode 100644 cli/dependencies.go
create mode 100644 cli/errors.go
create mode 100644 cli/key.go
create mode 100644 cli/prometheus.go
rename examples/tokenvm/cmd/token-cli/cmd/utils.go => cli/prompt.go (54%)
create mode 100644 cli/spam.go
rename {examples/tokenvm/cmd/token-cli/cmd => cli}/storage.go (55%)
create mode 100644 cli/utils.go
delete mode 100644 examples/README.md
create mode 100644 examples/morpheusvm/.golangci.yml
create mode 100644 examples/morpheusvm/.goreleaser.yml
create mode 100644 examples/morpheusvm/LICENSE
create mode 100644 examples/morpheusvm/README.md
create mode 100644 examples/morpheusvm/actions/outputs.go
create mode 100644 examples/morpheusvm/actions/transfer.go
create mode 100644 examples/morpheusvm/assets/hypersdk.png
create mode 100644 examples/morpheusvm/assets/logo.jpeg
create mode 100644 examples/morpheusvm/auth/ed25519.go
create mode 100644 examples/morpheusvm/auth/errors.go
create mode 100644 examples/morpheusvm/auth/helpers.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/cmd/action.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/cmd/chain.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/cmd/errors.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/cmd/genesis.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/cmd/handler.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/cmd/key.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/cmd/prometheus.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/cmd/resolutions.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/cmd/root.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/cmd/spam.go
create mode 100644 examples/morpheusvm/cmd/morpheus-cli/main.go
create mode 100644 examples/morpheusvm/cmd/morpheusvm/main.go
create mode 100644 examples/morpheusvm/cmd/morpheusvm/version/version.go
create mode 100644 examples/morpheusvm/config/config.go
create mode 100644 examples/morpheusvm/consts/consts.go
create mode 100644 examples/morpheusvm/controller/controller.go
create mode 100644 examples/morpheusvm/controller/metrics.go
create mode 100644 examples/morpheusvm/controller/resolutions.go
create mode 100644 examples/morpheusvm/factory.go
create mode 100644 examples/morpheusvm/genesis/errors.go
create mode 100644 examples/morpheusvm/genesis/genesis.go
create mode 100644 examples/morpheusvm/genesis/rules.go
create mode 100644 examples/morpheusvm/go.mod
create mode 100644 examples/morpheusvm/go.sum
create mode 100644 examples/morpheusvm/license.yml
create mode 100644 examples/morpheusvm/registry/registry.go
create mode 100644 examples/morpheusvm/rpc/consts.go
create mode 100644 examples/morpheusvm/rpc/dependencies.go
create mode 100644 examples/morpheusvm/rpc/errors.go
create mode 100644 examples/morpheusvm/rpc/jsonrpc_client.go
create mode 100644 examples/morpheusvm/rpc/jsonrpc_server.go
create mode 100755 examples/morpheusvm/scripts/build.release.sh
create mode 100755 examples/morpheusvm/scripts/build.sh
create mode 100755 examples/morpheusvm/scripts/fix.lint.sh
create mode 100755 examples/morpheusvm/scripts/run.sh
create mode 100755 examples/morpheusvm/scripts/stop.sh
create mode 100755 examples/morpheusvm/scripts/tests.integration.sh
create mode 100755 examples/morpheusvm/scripts/tests.lint.sh
create mode 100755 examples/morpheusvm/scripts/tests.load.sh
create mode 100755 examples/morpheusvm/scripts/tests.unit.sh
create mode 100644 examples/morpheusvm/storage/errors.go
create mode 100644 examples/morpheusvm/storage/state_manager.go
create mode 100644 examples/morpheusvm/storage/storage.go
create mode 100644 examples/morpheusvm/tests/e2e/e2e_test.go
create mode 100644 examples/morpheusvm/tests/integration/integration_test.go
create mode 100644 examples/morpheusvm/tests/load/load_test.go
create mode 100644 examples/morpheusvm/utils/utils.go
create mode 100644 examples/morpheusvm/version/version.go
create mode 100644 examples/tokenvm/cmd/token-cli/cmd/handler.go
create mode 100644 examples/tokenvm/cmd/token-cli/cmd/resolutions.go
diff --git a/.github/workflows/hypersdk-unit-tests.yml b/.github/workflows/hypersdk-unit-tests.yml
index b9c16bfd7b..5588829ffe 100644
--- a/.github/workflows/hypersdk-unit-tests.yml
+++ b/.github/workflows/hypersdk-unit-tests.yml
@@ -8,9 +8,11 @@ on:
branches:
- main
pull_request:
+ types: [labeled,synchronize,reopened]
jobs:
hypersdk-unit-tests:
+ if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'run unit') }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
diff --git a/.github/workflows/morpheusvm-load-tests.yml b/.github/workflows/morpheusvm-load-tests.yml
new file mode 100644
index 0000000000..6db410f162
--- /dev/null
+++ b/.github/workflows/morpheusvm-load-tests.yml
@@ -0,0 +1,40 @@
+# Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+# See the file LICENSE for licensing terms.
+
+name: MorpheusVM Load Tests
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [labeled,synchronize,reopened]
+
+jobs:
+ morpheusvm-load-tests:
+ if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'run load') }}
+ strategy:
+ matrix:
+ level: [v1, v2, v3] # v4 is not supported
+ runs-on:
+ labels: ubuntu-20.04-32
+ timeout-minutes: 10
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: "1.20"
+ check-latest: true
+ cache: true
+ cache-dependency-path: |
+ go.sum
+ examples/morpheusvm/go.sum
+ - name: Run load tests
+ working-directory: ./examples/morpheusvm
+ shell: bash
+ run: GOAMD64=${{ matrix.level }} scripts/tests.load.sh
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
diff --git a/.github/workflows/morpheusvm-release.yml b/.github/workflows/morpheusvm-release.yml
new file mode 100644
index 0000000000..30b66d0cc4
--- /dev/null
+++ b/.github/workflows/morpheusvm-release.yml
@@ -0,0 +1,69 @@
+# Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+# See the file LICENSE for licensing terms.
+
+name: MorpheusVM Release
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - "*"
+ pull_request:
+ types: [labeled,synchronize,reopened]
+
+jobs:
+ morpheusvm-release:
+ # We build with 20.04 to maintain max compatibility: https://github.com/golang/go/issues/57328
+ runs-on: ubuntu-20.04-32
+ if: ${{ github.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run release') }}
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v3
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: "1.20"
+ check-latest: true
+ cache: true
+ cache-dependency-path: |
+ go.sum
+ examples/morpheusvm/go.sum
+ - name: Set up arm64 cross compiler
+ run: |
+ sudo apt-get -y update
+ sudo apt-get -y install gcc-aarch64-linux-gnu
+ - name: Checkout osxcross
+ uses: actions/checkout@v2
+ with:
+ repository: tpoechtrager/osxcross
+ path: osxcross
+ - name: Build osxcross
+ run: |
+ sudo apt-get -y install clang llvm-dev libxml2-dev uuid-dev libssl-dev bash patch make tar xz-utils bzip2 gzip sed cpio libbz2-dev
+ cd osxcross
+ wget https://github.com/joseluisq/macosx-sdks/releases/download/12.3/$MACOS_SDK_FNAME -O tarballs/$MACOS_SDK_FNAME
+ echo $MACOS_SDK_CHECKSUM tarballs/$MACOS_SDK_FNAME | sha256sum -c -
+ UNATTENDED=1 ./build.sh
+ echo $PWD/target/bin >> $GITHUB_PATH
+ env:
+ MACOS_SDK_FNAME: MacOSX12.3.sdk.tar.xz
+ MACOS_SDK_CHECKSUM: 3abd261ceb483c44295a6623fdffe5d44fc4ac2c872526576ec5ab5ad0f6e26c
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v2
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release
+ workdir: ./examples/morpheusvm/
+ env:
+ # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Archive Builds
+ uses: actions/upload-artifact@v3
+ with:
+ name: dist
+ path: ./examples/morpheusvm/dist
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
diff --git a/.github/workflows/morpheusvm-static-analysis.yml b/.github/workflows/morpheusvm-static-analysis.yml
new file mode 100644
index 0000000000..0c42970719
--- /dev/null
+++ b/.github/workflows/morpheusvm-static-analysis.yml
@@ -0,0 +1,35 @@
+# Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+# See the file LICENSE for licensing terms.
+
+name: MorpheusVM Static Analysis
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+jobs:
+ morpheusvm-lint:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: "1.20"
+ check-latest: true
+ cache: true
+ cache-dependency-path: |
+ go.sum
+ examples/morpheusvm/go.sum
+ - name: Run static analysis tests
+ working-directory: ./examples/morpheusvm
+ shell: bash
+ run: scripts/tests.lint.sh
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
diff --git a/.github/workflows/morpheusvm-sync-tests.yml b/.github/workflows/morpheusvm-sync-tests.yml
new file mode 100644
index 0000000000..0164769149
--- /dev/null
+++ b/.github/workflows/morpheusvm-sync-tests.yml
@@ -0,0 +1,39 @@
+# Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+# See the file LICENSE for licensing terms.
+
+name: MorpheusVM Sync Tests
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [labeled,synchronize,reopened]
+
+jobs:
+ morpheusvm-sync-tests:
+ if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'run sync') }}
+ runs-on:
+ labels: ubuntu-20.04-32
+ timeout-minutes: 25
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: "1.20"
+ check-latest: true
+ cache: true
+ cache-dependency-path: |
+ go.sum
+ examples/morpheusvm/go.sum
+ - name: Run sync tests
+ working-directory: ./examples/morpheusvm
+ shell: bash
+ run: scripts/run.sh
+ env:
+ MODE: "full-test"
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
diff --git a/.github/workflows/morpheusvm-unit-tests.yml b/.github/workflows/morpheusvm-unit-tests.yml
new file mode 100644
index 0000000000..6f3931c142
--- /dev/null
+++ b/.github/workflows/morpheusvm-unit-tests.yml
@@ -0,0 +1,58 @@
+# Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+# See the file LICENSE for licensing terms.
+
+name: MorpheusVM Unit Tests
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [labeled,synchronize,reopened]
+
+jobs:
+ morpheusvm-unit-tests:
+ if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'run unit') }}
+ runs-on:
+ labels: ubuntu-20.04-32
+ timeout-minutes: 10
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: "1.20"
+ check-latest: true
+ cache: true
+ cache-dependency-path: |
+ go.sum
+ examples/morpheusvm/go.sum
+ - name: Run unit tests
+ working-directory: ./examples/morpheusvm
+ shell: bash
+ run: scripts/tests.unit.sh
+ - name: Run integration tests
+ working-directory: ./examples/morpheusvm
+ shell: bash
+ run: scripts/tests.integration.sh
+ - name: Archive code coverage results (text)
+ uses: actions/upload-artifact@v3
+ with:
+ name: code-coverage-out
+ path: ./examples/morpheusvm/integration.coverage.out
+ - name: Archive code coverage results (html)
+ uses: actions/upload-artifact@v3
+ with:
+ name: code-coverage-html
+ path: ./examples/morpheusvm/integration.coverage.html
+ - name: Run e2e tests
+ working-directory: ./examples/morpheusvm
+ shell: bash
+ run: scripts/run.sh
+ env:
+ MODE: "test"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
diff --git a/.github/workflows/tokenvm-sync-tests.yml b/.github/workflows/tokenvm-sync-tests.yml
index 03dfd472c3..013aae88f8 100644
--- a/.github/workflows/tokenvm-sync-tests.yml
+++ b/.github/workflows/tokenvm-sync-tests.yml
@@ -12,7 +12,7 @@ on:
jobs:
tokenvm-sync-tests:
- if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'run CI') }}
+ if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'run sync') }}
runs-on:
labels: ubuntu-20.04-32
timeout-minutes: 25
diff --git a/.github/workflows/tokenvm-unit-tests.yml b/.github/workflows/tokenvm-unit-tests.yml
index 6e23adc437..53aa637cb8 100644
--- a/.github/workflows/tokenvm-unit-tests.yml
+++ b/.github/workflows/tokenvm-unit-tests.yml
@@ -8,9 +8,11 @@ on:
branches:
- main
pull_request:
+ types: [labeled,synchronize,reopened]
jobs:
tokenvm-unit-tests:
+ if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'run unit') }}
runs-on:
labels: ubuntu-20.04-32
timeout-minutes: 10
diff --git a/.gitignore b/.gitignore
index fdf7bb8790..0d4a4649fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,6 +57,7 @@ dist/
*.pk
tmp-storage-testing
.token-cli*
+.morpheus-cli*
*.html
data/
osxcross/
diff --git a/.golangci.yml b/.golangci.yml
index 8cde165811..575c059579 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -46,7 +46,7 @@ linters:
- staticcheck
- bodyclose
- structcheck
- - lll
+ # - lll
# - gomnd
- goprintffuncname
- interfacer
diff --git a/README.md b/README.md
index 6cc3682618..6fedf90ba8 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,6 @@
-
@@ -252,10 +251,28 @@ these functions with avalanchego means existing avalanchego monitoring tools
work out of the box on your `hypervm`.
## Examples
-### Beginner: `tokenvm`
+We've created three `hypervm` examples, of increasing complexity, that demonstrate what you
+can build with the `hypersdk` (with more on the way).
+
+When you are ready to build your own `hypervm`, we recommend using the `morpheusvm` as a template!
+
+### Beginner: `morpheusvm`
+_[Who is Morpheus ("The Matrix")?](https://www.youtube.com/watch?v=zE7PKRjrid4)_
+
+The [`morpheusvm`](./examples/morpheusvm) provides the first glimpse into the world of the `hypersdk`.
+After learning how to implement native token transfers in a `hypervm` (one of the simplest Custom VMs
+you could make), you will have the choice to go deeper (red pill) or to turn back to the VMs that you
+already know (blue pill).
+
+_To ensure the `hypersdk` remains reliable as we optimize and evolve the codebase,
+we also run E2E tests in the `morpheusvm` on each PR to the `hypersdk` core modules._
+
+### Moderate: `tokenvm`
We created the [`tokenvm`](./examples/tokenvm) to showcase how to use the
`hypersdk` in an application most readers are already familiar with, token minting
-and token trading. The `tokenvm` lets anyone create any asset, mint more of
+and token trading.
+
+The `tokenvm` lets anyone create any asset, mint more of
their asset, modify the metadata of their asset (if they reveal some info), and
burn their asset. Additionally, there is an embedded on-chain exchange that
allows anyone to create orders and fill (partial) orders of anyone else. To
@@ -265,10 +282,12 @@ maintains by syncing blocks. If you are interested in the intersection of
exchanges and blockchains, it is definitely worth a read (the logic for filling
orders is < 100 lines of code!).
-To ensure the `hypersdk` remains reliable as we optimize and evolve the codebase,
-we also run E2E tests in the `tokenvm` on each PR to the `hypersdk` core modules.
+_To ensure the `hypersdk` remains reliable as we optimize and evolve the codebase,
+we also run E2E tests in the `tokenvm` on each PR to the `hypersdk` core modules._
+
+### Expert: `indexvm` [DEPRECATED]
+_The `indexvm` will be rewritten using the new WASM Progams module._
-### Expert: `indexvm`
The [`indexvm`](https://github.com/ava-labs/indexvm) is much more complex than
the `tokenvm` (more elaborate mechanisms and a new use case you may not be
familiar with). It was built during the design of the `hypersdk` to test out the
diff --git a/chain/mock_action.go b/chain/mock_action.go
index 6080d36507..5c20c70731 100644
--- a/chain/mock_action.go
+++ b/chain/mock_action.go
@@ -1,5 +1,6 @@
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
+
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ava-labs/hypersdk/chain (interfaces: Action)
diff --git a/chain/mock_auth.go b/chain/mock_auth.go
index 36d017dc84..8c57680d7a 100644
--- a/chain/mock_auth.go
+++ b/chain/mock_auth.go
@@ -1,5 +1,6 @@
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
+
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ava-labs/hypersdk/chain (interfaces: Auth)
diff --git a/chain/mock_auth_factory.go b/chain/mock_auth_factory.go
index 4420686840..28565028f4 100644
--- a/chain/mock_auth_factory.go
+++ b/chain/mock_auth_factory.go
@@ -1,5 +1,6 @@
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
+
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ava-labs/hypersdk/chain (interfaces: AuthFactory)
diff --git a/chain/mock_rules.go b/chain/mock_rules.go
index 595f238b18..94ae73ad5e 100644
--- a/chain/mock_rules.go
+++ b/chain/mock_rules.go
@@ -1,5 +1,6 @@
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
+
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ava-labs/hypersdk/chain (interfaces: Rules)
diff --git a/cli/chain.go b/cli/chain.go
new file mode 100644
index 0000000000..bb1323fa3a
--- /dev/null
+++ b/cli/chain.go
@@ -0,0 +1,270 @@
+// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package cli
+
+import (
+ "context"
+ "fmt"
+ "math"
+ "os"
+ "strings"
+ "time"
+
+ runner "github.com/ava-labs/avalanche-network-runner/client"
+ "github.com/ava-labs/avalanchego/ids"
+ "github.com/ava-labs/avalanchego/utils/logging"
+ "github.com/ava-labs/hypersdk/chain"
+ "github.com/ava-labs/hypersdk/consts"
+ "github.com/ava-labs/hypersdk/rpc"
+ "github.com/ava-labs/hypersdk/utils"
+ "github.com/ava-labs/hypersdk/window"
+ "gopkg.in/yaml.v2"
+)
+
+func (h *Handler) ImportChain() error {
+ chainID, err := h.PromptID("chainID")
+ if err != nil {
+ return err
+ }
+ uri, err := h.PromptString("uri", 0, consts.MaxInt)
+ if err != nil {
+ return err
+ }
+ if err := h.StoreChain(chainID, uri); err != nil {
+ return err
+ }
+ if err := h.StoreDefaultChain(chainID); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (h *Handler) ImportANR() error {
+ ctx := context.Background()
+
+ // Delete previous items
+ oldChains, err := h.DeleteChains()
+ if err != nil {
+ return err
+ }
+ if len(oldChains) > 0 {
+ utils.Outf("{{yellow}}deleted old chains:{{/}} %+v\n", oldChains)
+ }
+
+ // Load new items from ANR
+ anrCli, err := runner.New(runner.Config{
+ Endpoint: "0.0.0.0:12352",
+ DialTimeout: 10 * time.Second,
+ }, logging.NoLog{})
+ if err != nil {
+ return err
+ }
+ status, err := anrCli.Status(ctx)
+ if err != nil {
+ return err
+ }
+ subnets := map[ids.ID][]ids.ID{}
+ for chain, chainInfo := range status.ClusterInfo.CustomChains {
+ chainID, err := ids.FromString(chain)
+ if err != nil {
+ return err
+ }
+ subnetID, err := ids.FromString(chainInfo.SubnetId)
+ if err != nil {
+ return err
+ }
+ chainIDs, ok := subnets[subnetID]
+ if !ok {
+ chainIDs = []ids.ID{}
+ }
+ chainIDs = append(chainIDs, chainID)
+ subnets[subnetID] = chainIDs
+ }
+ var filledChainID ids.ID
+ for _, nodeInfo := range status.ClusterInfo.NodeInfos {
+ if len(nodeInfo.WhitelistedSubnets) == 0 {
+ continue
+ }
+ trackedSubnets := strings.Split(nodeInfo.WhitelistedSubnets, ",")
+ for _, subnet := range trackedSubnets {
+ subnetID, err := ids.FromString(subnet)
+ if err != nil {
+ return err
+ }
+ for _, chainID := range subnets[subnetID] {
+ uri := fmt.Sprintf("%s/ext/bc/%s", nodeInfo.Uri, chainID)
+ if err := h.StoreChain(chainID, uri); err != nil {
+ return err
+ }
+ utils.Outf(
+ "{{yellow}}stored chainID:{{/}} %s {{yellow}}uri:{{/}} %s\n",
+ chainID,
+ uri,
+ )
+ filledChainID = chainID
+ }
+ }
+ }
+ return h.StoreDefaultChain(filledChainID)
+}
+
+type AvalancheOpsConfig struct {
+ Resources struct {
+ CreatedNodes []struct {
+ HTTPEndpoint string `yaml:"httpEndpoint"`
+ } `yaml:"created_nodes"`
+ } `yaml:"resources"`
+}
+
+func (h *Handler) ImportOps(schainID string, opsPath string) error {
+ oldChains, err := h.DeleteChains()
+ if err != nil {
+ return err
+ }
+ if len(oldChains) > 0 {
+ utils.Outf("{{yellow}}deleted old chains:{{/}} %+v\n", oldChains)
+ }
+
+ // Load chainID
+ chainID, err := ids.FromString(schainID)
+ if err != nil {
+ return err
+ }
+
+ // Load yaml file
+ var opsConfig AvalancheOpsConfig
+ yamlFile, err := os.ReadFile(opsPath)
+ if err != nil {
+ return err
+ }
+ err = yaml.Unmarshal(yamlFile, &opsConfig)
+ if err != nil {
+ return err
+ }
+
+ // Add chains
+ for _, node := range opsConfig.Resources.CreatedNodes {
+ uri := fmt.Sprintf("%s/ext/bc/%s", node.HTTPEndpoint, chainID)
+ if err := h.StoreChain(chainID, uri); err != nil {
+ return err
+ }
+ utils.Outf(
+ "{{yellow}}stored chainID:{{/}} %s {{yellow}}uri:{{/}} %s\n",
+ chainID,
+ uri,
+ )
+ }
+ return h.StoreDefaultChain(chainID)
+}
+
+func (h *Handler) SetDefaultChain() error {
+ chainID, _, err := h.PromptChain("set default chain", nil)
+ if err != nil {
+ return err
+ }
+ return h.StoreDefaultChain(chainID)
+}
+
+func (h *Handler) PrintChainInfo() error {
+ _, uris, err := h.PromptChain("select chainID", nil)
+ if err != nil {
+ return err
+ }
+ cli := rpc.NewJSONRPCClient(uris[0])
+ networkID, subnetID, chainID, err := cli.Network(context.Background())
+ if err != nil {
+ return err
+ }
+ utils.Outf(
+ "{{cyan}}networkID:{{/}} %d {{cyan}}subnetID:{{/}} %s {{cyan}}chainID:{{/}} %s",
+ networkID,
+ subnetID,
+ chainID,
+ )
+ return nil
+}
+
+func (h *Handler) WatchChain(hideTxs bool, getParser func(string, uint32, ids.ID) (chain.Parser, error), handleTx func(*chain.Transaction, *chain.Result)) error {
+ ctx := context.Background()
+ chainID, uris, err := h.PromptChain("select chainID", nil)
+ if err != nil {
+ return err
+ }
+ if err := h.CloseDatabase(); err != nil {
+ return err
+ }
+ utils.Outf("{{yellow}}uri:{{/}} %s\n", uris[0])
+ rcli := rpc.NewJSONRPCClient(uris[0])
+ networkID, _, _, err := rcli.Network(context.TODO())
+ if err != nil {
+ return err
+ }
+ parser, err := getParser(uris[0], networkID, chainID)
+ if err != nil {
+ return err
+ }
+ scli, err := rpc.NewWebSocketClient(uris[0])
+ if err != nil {
+ return err
+ }
+ defer scli.Close()
+ if err := scli.RegisterBlocks(); err != nil {
+ return err
+ }
+ utils.Outf("{{green}}watching for new blocks on %s 👀{{/}}\n", chainID)
+ var (
+ start time.Time
+ lastBlock int64
+ lastBlockDetailed time.Time
+ tpsWindow = window.Window{}
+ )
+ for ctx.Err() == nil {
+ blk, results, err := scli.ListenBlock(ctx, parser)
+ if err != nil {
+ return err
+ }
+ now := time.Now()
+ if start.IsZero() {
+ start = now
+ }
+ if lastBlock != 0 {
+ since := now.Unix() - lastBlock
+ newWindow, err := window.Roll(tpsWindow, int(since))
+ if err != nil {
+ return err
+ }
+ tpsWindow = newWindow
+ window.Update(&tpsWindow, window.WindowSliceSize-consts.Uint64Len, uint64(len(blk.Txs)))
+ runningDuration := time.Since(start)
+ tpsDivisor := math.Min(window.WindowSize, runningDuration.Seconds())
+ utils.Outf(
+ "{{green}}height:{{/}}%d {{green}}txs:{{/}}%d {{green}}units:{{/}}%d {{green}}root:{{/}}%s {{green}}TPS:{{/}}%.2f {{green}}split:{{/}}%dms\n",
+ blk.Hght,
+ len(blk.Txs),
+ blk.UnitsConsumed,
+ blk.StateRoot,
+ float64(window.Sum(tpsWindow))/tpsDivisor,
+ time.Since(lastBlockDetailed).Milliseconds(),
+ )
+ } else {
+ utils.Outf(
+ "{{green}}height:{{/}}%d {{green}}txs:{{/}}%d {{green}}units:{{/}}%d {{green}}root:{{/}}%s\n",
+ blk.Hght,
+ len(blk.Txs),
+ blk.UnitsConsumed,
+ blk.StateRoot,
+ )
+ window.Update(&tpsWindow, window.WindowSliceSize-consts.Uint64Len, uint64(len(blk.Txs)))
+ }
+ lastBlock = now.Unix()
+ lastBlockDetailed = now
+ if hideTxs {
+ continue
+ }
+ for i, tx := range blk.Txs {
+ handleTx(tx, results[i])
+ }
+ }
+ return nil
+}
diff --git a/cli/cli.go b/cli/cli.go
new file mode 100644
index 0000000000..b8df7b2a6d
--- /dev/null
+++ b/cli/cli.go
@@ -0,0 +1,23 @@
+// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package cli
+
+import (
+ "github.com/ava-labs/avalanchego/database"
+ "github.com/ava-labs/hypersdk/pebble"
+)
+
+type Handler struct {
+ c Controller
+
+ db database.Database
+}
+
+func New(c Controller) (*Handler, error) {
+ db, err := pebble.New(c.DatabasePath(), pebble.NewDefaultConfig())
+ if err != nil {
+ return nil, err
+ }
+ return &Handler{c, db}, nil
+}
diff --git a/cli/dependencies.go b/cli/dependencies.go
new file mode 100644
index 0000000000..7b6a72ac50
--- /dev/null
+++ b/cli/dependencies.go
@@ -0,0 +1,15 @@
+// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package cli
+
+import (
+ "github.com/ava-labs/hypersdk/crypto"
+)
+
+type Controller interface {
+ DatabasePath() string
+ Symbol() string
+ Address(crypto.PublicKey) string
+ ParseAddress(string) (crypto.PublicKey, error)
+}
diff --git a/cli/errors.go b/cli/errors.go
new file mode 100644
index 0000000000..cecfefdaf2
--- /dev/null
+++ b/cli/errors.go
@@ -0,0 +1,18 @@
+// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package cli
+
+import "errors"
+
+var (
+ ErrInputEmpty = errors.New("input is empty")
+ ErrInputTooLarge = errors.New("input is too large")
+ ErrInvalidChoice = errors.New("invalid choice")
+ ErrIndexOutOfRange = errors.New("index out-of-range")
+ ErrInsufficientBalance = errors.New("insufficient balance")
+ ErrDuplicate = errors.New("duplicate")
+ ErrNoChains = errors.New("no available chains")
+ ErrNoKeys = errors.New("no available keys")
+ ErrTxFailed = errors.New("tx failed")
+)
diff --git a/cli/key.go b/cli/key.go
new file mode 100644
index 0000000000..b12e1cb453
--- /dev/null
+++ b/cli/key.go
@@ -0,0 +1,125 @@
+// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package cli
+
+import (
+ "context"
+
+ "github.com/ava-labs/avalanchego/ids"
+ "github.com/ava-labs/hypersdk/crypto"
+ "github.com/ava-labs/hypersdk/rpc"
+ "github.com/ava-labs/hypersdk/utils"
+)
+
+func (h *Handler) GenerateKey() error {
+ // TODO: encrypt key
+ priv, err := crypto.GeneratePrivateKey()
+ if err != nil {
+ return err
+ }
+ if err := h.StoreKey(priv); err != nil {
+ return err
+ }
+ publicKey := priv.PublicKey()
+ if err := h.StoreDefaultKey(publicKey); err != nil {
+ return err
+ }
+ utils.Outf(
+ "{{green}}created address:{{/}} %s",
+ h.c.Address(publicKey),
+ )
+ return nil
+}
+
+func (h *Handler) ImportKey(keyPath string) error {
+ priv, err := crypto.LoadKey(keyPath)
+ if err != nil {
+ return err
+ }
+ if err := h.StoreKey(priv); err != nil {
+ return err
+ }
+ publicKey := priv.PublicKey()
+ if err := h.StoreDefaultKey(publicKey); err != nil {
+ return err
+ }
+ utils.Outf(
+ "{{green}}imported address:{{/}} %s",
+ h.c.Address(publicKey),
+ )
+ return nil
+}
+
+func (h *Handler) SetKey(lookupBalance func(int, string, string, uint32, ids.ID) error) error {
+ keys, err := h.GetKeys()
+ if err != nil {
+ return err
+ }
+ if len(keys) == 0 {
+ utils.Outf("{{red}}no stored keys{{/}}\n")
+ return nil
+ }
+ chainID, uris, err := h.GetDefaultChain()
+ if err != nil {
+ return err
+ }
+ if len(uris) == 0 {
+ utils.Outf("{{red}}no available chains{{/}}\n")
+ return nil
+ }
+ rcli := rpc.NewJSONRPCClient(uris[0])
+ networkID, _, _, err := rcli.Network(context.TODO())
+ if err != nil {
+ return err
+ }
+ utils.Outf("{{cyan}}stored keys:{{/}} %d\n", len(keys))
+ for i := 0; i < len(keys); i++ {
+ if err := lookupBalance(i, h.c.Address(keys[i].PublicKey()), uris[0], networkID, chainID); err != nil {
+ return err
+ }
+ }
+
+ // Select key
+ keyIndex, err := h.PromptChoice("set default key", len(keys))
+ if err != nil {
+ return err
+ }
+ key := keys[keyIndex]
+ return h.StoreDefaultKey(key.PublicKey())
+}
+
+func (h *Handler) Balance(checkAllChains bool, promptAsset bool, printBalance func(crypto.PublicKey, string, uint32, ids.ID, ids.ID) error) error {
+ priv, err := h.GetDefaultKey()
+ if err != nil {
+ return err
+ }
+ chainID, uris, err := h.GetDefaultChain()
+ if err != nil {
+ return err
+ }
+ var assetID ids.ID
+ if promptAsset {
+ assetID, err = h.PromptAsset("assetID", true)
+ if err != nil {
+ return err
+ }
+ }
+
+ max := len(uris)
+ if !checkAllChains {
+ max = 1
+ }
+ for _, uri := range uris[:max] {
+ utils.Outf("{{yellow}}uri:{{/}} %s\n", uri)
+ rcli := rpc.NewJSONRPCClient(uris[0])
+ networkID, _, _, err := rcli.Network(context.TODO())
+ if err != nil {
+ return err
+ }
+ if err := printBalance(priv.PublicKey(), uri, networkID, chainID, assetID); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/cli/prometheus.go b/cli/prometheus.go
new file mode 100644
index 0000000000..209ba45df6
--- /dev/null
+++ b/cli/prometheus.go
@@ -0,0 +1,95 @@
+// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package cli
+
+import (
+ "fmt"
+ "net/url"
+ "os"
+
+ "github.com/ava-labs/avalanchego/ids"
+ "github.com/ava-labs/hypersdk/utils"
+ "gopkg.in/yaml.v2"
+)
+
+const fsModeWrite = 0o600
+
+type PrometheusStaticConfig struct {
+ Targets []string `yaml:"targets"`
+}
+
+type PrometheusScrapeConfig struct {
+ JobName string `yaml:"job_name"`
+ StaticConfigs []*PrometheusStaticConfig `yaml:"static_configs"`
+ MetricsPath string `yaml:"metrics_path"`
+}
+
+type PrometheusConfig struct {
+ Global struct {
+ ScrapeInterval string `yaml:"scrape_interval"`
+ EvaluationInterval string `yaml:"evaluation_interval"`
+ } `yaml:"global"`
+ ScrapeConfigs []*PrometheusScrapeConfig `yaml:"scrape_configs"`
+}
+
+func (h *Handler) GeneratePrometheus(prometheusFile string, prometheusData string, getPanels func(ids.ID) []string) error {
+ chainID, uris, err := h.PromptChain("select chainID", nil)
+ if err != nil {
+ return err
+ }
+ endpoints := make([]string, len(uris))
+ for i, uri := range uris {
+ host, err := utils.GetHost(uri)
+ if err != nil {
+ return err
+ }
+ port, err := utils.GetPort(uri)
+ if err != nil {
+ return err
+ }
+ endpoints[i] = fmt.Sprintf("%s:%s", host, port)
+ }
+
+ // Create Prometheus YAML
+ var prometheusConfig PrometheusConfig
+ prometheusConfig.Global.ScrapeInterval = "15s"
+ prometheusConfig.Global.EvaluationInterval = "15s"
+ prometheusConfig.ScrapeConfigs = []*PrometheusScrapeConfig{
+ {
+ JobName: "prometheus",
+ StaticConfigs: []*PrometheusStaticConfig{
+ {
+ Targets: endpoints,
+ },
+ },
+ MetricsPath: "/ext/metrics",
+ },
+ }
+ yamlData, err := yaml.Marshal(&prometheusConfig)
+ if err != nil {
+ return err
+ }
+ if err := os.WriteFile(prometheusFile, yamlData, fsModeWrite); err != nil {
+ return err
+ }
+
+ // Generated dashboard link
+ //
+ // We must manually encode the params because prometheus skips any panels
+ // that are not numerically sorted and `url.params` only sorts
+ // lexicographically.
+ dashboard := "http://localhost:9090/graph"
+ for i, panel := range getPanels(chainID) {
+ appendChar := "&"
+ if i == 0 {
+ appendChar = "?"
+ }
+ dashboard = fmt.Sprintf("%s%sg%d.expr=%s&g%d.tab=0", dashboard, appendChar, i, url.QueryEscape(panel), i)
+ }
+ utils.Outf("{{orange}}pre-built dashboard:{{/}} %s\n", dashboard)
+
+ // Emit command to run prometheus
+ utils.Outf("{{green}}prometheus cmd:{{/}} /tmp/prometheus --config.file=%s --storage.tsdb.path=%s\n", prometheusFile, prometheusData)
+ return nil
+}
diff --git a/examples/tokenvm/cmd/token-cli/cmd/utils.go b/cli/prompt.go
similarity index 54%
rename from examples/tokenvm/cmd/token-cli/cmd/utils.go
rename to cli/prompt.go
index 2a46d2d23c..43306f9a80 100644
--- a/examples/tokenvm/cmd/token-cli/cmd/utils.go
+++ b/cli/prompt.go
@@ -1,35 +1,28 @@
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
-package cmd
+package cli
import (
- "context"
"fmt"
"strconv"
"strings"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils/set"
- hconsts "github.com/ava-labs/hypersdk/consts"
"github.com/ava-labs/hypersdk/crypto"
- "github.com/ava-labs/hypersdk/examples/tokenvm/auth"
- "github.com/ava-labs/hypersdk/examples/tokenvm/consts"
- trpc "github.com/ava-labs/hypersdk/examples/tokenvm/rpc"
- "github.com/ava-labs/hypersdk/examples/tokenvm/utils"
- "github.com/ava-labs/hypersdk/rpc"
- hutils "github.com/ava-labs/hypersdk/utils"
+ "github.com/ava-labs/hypersdk/utils"
"github.com/manifoldco/promptui"
)
-func promptAddress(label string) (crypto.PublicKey, error) {
+func (h *Handler) PromptAddress(label string) (crypto.PublicKey, error) {
promptText := promptui.Prompt{
Label: label,
Validate: func(input string) error {
if len(input) == 0 {
return ErrInputEmpty
}
- _, err := utils.ParseAddress(input)
+ _, err := h.c.ParseAddress(input)
return err
},
}
@@ -38,16 +31,19 @@ func promptAddress(label string) (crypto.PublicKey, error) {
return crypto.EmptyPublicKey, err
}
recipient = strings.TrimSpace(recipient)
- return utils.ParseAddress(recipient)
+ return h.c.ParseAddress(recipient)
}
-func promptString(label string) (string, error) {
+func (*Handler) PromptString(label string, min int, max int) (string, error) {
promptText := promptui.Prompt{
Label: label,
Validate: func(input string) error {
- if len(input) == 0 {
+ if len(input) < min {
return ErrInputEmpty
}
+ if len(input) > max {
+ return ErrInputTooLarge
+ }
return nil
},
}
@@ -58,8 +54,9 @@ func promptString(label string) (string, error) {
return strings.TrimSpace(text), err
}
-func promptAsset(label string, allowNative bool) (ids.ID, error) {
- text := fmt.Sprintf("%s (use TKN for native token)", label)
+func (h *Handler) PromptAsset(label string, allowNative bool) (ids.ID, error) {
+ symbol := h.c.Symbol()
+ text := fmt.Sprintf("%s (use %s for native token)", label, symbol)
if !allowNative {
text = label
}
@@ -69,7 +66,7 @@ func promptAsset(label string, allowNative bool) (ids.ID, error) {
if len(input) == 0 {
return ErrInputEmpty
}
- if allowNative && len(input) == 3 && input == consts.Symbol {
+ if allowNative && input == symbol {
return nil
}
_, err := ids.FromString(input)
@@ -82,7 +79,7 @@ func promptAsset(label string, allowNative bool) (ids.ID, error) {
}
asset = strings.TrimSpace(asset)
var assetID ids.ID
- if asset != consts.Symbol {
+ if asset != symbol {
assetID, err = ids.FromString(asset)
if err != nil {
return ids.Empty, err
@@ -94,7 +91,7 @@ func promptAsset(label string, allowNative bool) (ids.ID, error) {
return assetID, nil
}
-func promptAmount(
+func (*Handler) PromptAmount(
label string,
assetID ids.ID,
balance uint64,
@@ -109,7 +106,7 @@ func promptAmount(
var amount uint64
var err error
if assetID == ids.Empty {
- amount, err = hutils.ParseBalance(input)
+ amount, err = utils.ParseBalance(input)
} else {
amount, err = strconv.ParseUint(input, 10, 64)
}
@@ -132,14 +129,14 @@ func promptAmount(
rawAmount = strings.TrimSpace(rawAmount)
var amount uint64
if assetID == ids.Empty {
- amount, err = hutils.ParseBalance(rawAmount)
+ amount, err = utils.ParseBalance(rawAmount)
} else {
amount, err = strconv.ParseUint(rawAmount, 10, 64)
}
return amount, err
}
-func promptInt(
+func (*Handler) PromptInt(
label string,
) (int, error) {
promptText := promptui.Prompt{
@@ -166,7 +163,7 @@ func promptInt(
return strconv.Atoi(rawAmount)
}
-func promptChoice(label string, max int) (int, error) {
+func (*Handler) PromptChoice(label string, max int) (int, error) {
promptText := promptui.Prompt{
Label: label,
Validate: func(input string) error {
@@ -190,7 +187,7 @@ func promptChoice(label string, max int) (int, error) {
return strconv.Atoi(rawIndex)
}
-func promptTime(label string) (int64, error) {
+func (*Handler) PromptTime(label string) (int64, error) {
promptText := promptui.Prompt{
Label: label,
Validate: func(input string) error {
@@ -208,7 +205,7 @@ func promptTime(label string) (int64, error) {
return strconv.ParseInt(rawTime, 10, 64)
}
-func promptContinue() (bool, error) {
+func (*Handler) PromptContinue() (bool, error) {
promptText := promptui.Prompt{
Label: "continue (y/n)",
Validate: func(input string) error {
@@ -228,13 +225,13 @@ func promptContinue() (bool, error) {
}
cont := strings.ToLower(rawContinue)
if cont == "n" {
- hutils.Outf("{{red}}exiting...{{/}}\n")
+ utils.Outf("{{red}}exiting...{{/}}\n")
return false, nil
}
return true, nil
}
-func promptBool(label string) (bool, error) {
+func (*Handler) PromptBool(label string) (bool, error) {
promptText := promptui.Prompt{
Label: fmt.Sprintf("%s (y/n)", label),
Validate: func(input string) error {
@@ -259,7 +256,7 @@ func promptBool(label string) (bool, error) {
return true, nil
}
-func promptID(label string) (ids.ID, error) {
+func (*Handler) PromptID(label string) (ids.ID, error) {
promptText := promptui.Prompt{
Label: label,
Validate: func(input string) error {
@@ -282,8 +279,8 @@ func promptID(label string) (ids.ID, error) {
return id, nil
}
-func promptChain(label string, excluded set.Set[ids.ID]) (ids.ID, []string, error) {
- chains, err := GetChains()
+func (h *Handler) PromptChain(label string, excluded set.Set[ids.ID]) (ids.ID, []string, error) {
+ chains, err := h.GetChains()
if err != nil {
return ids.Empty, nil, err
}
@@ -301,14 +298,14 @@ func promptChain(label string, excluded set.Set[ids.ID]) (ids.ID, []string, erro
}
// Select chain
- hutils.Outf(
+ utils.Outf(
"{{cyan}}available chains:{{/}} %d {{cyan}}excluded:{{/}} %+v\n",
len(filteredChains),
excludedChains,
)
keys := make([]ids.ID, 0, len(filteredChains))
for _, chainID := range filteredChains {
- hutils.Outf(
+ utils.Outf(
"%d) {{cyan}}chainID:{{/}} %s\n",
len(keys),
chainID,
@@ -316,7 +313,7 @@ func promptChain(label string, excluded set.Set[ids.ID]) (ids.ID, []string, erro
keys = append(keys, chainID)
}
- chainIndex, err := promptChoice(label, len(keys))
+ chainIndex, err := h.PromptChoice(label, len(keys))
if err != nil {
return ids.Empty, nil, err
}
@@ -324,156 +321,25 @@ func promptChain(label string, excluded set.Set[ids.ID]) (ids.ID, []string, erro
return chainID, chains[chainID], nil
}
-func valueString(assetID ids.ID, value uint64) string {
+func (*Handler) ValueString(assetID ids.ID, value uint64) string {
if assetID == ids.Empty {
- return hutils.FormatBalance(value)
+ return utils.FormatBalance(value)
}
// Custom assets are denoted in raw units
return strconv.FormatUint(value, 10)
}
-func assetString(assetID ids.ID) string {
+func (h *Handler) AssetString(assetID ids.ID) string {
if assetID == ids.Empty {
- return consts.Symbol
+ return h.c.Symbol()
}
return assetID.String()
}
-func printStatus(txID ids.ID, success bool) {
+func (*Handler) PrintStatus(txID ids.ID, success bool) {
status := "⚠️"
if success {
status = "✅"
}
- hutils.Outf("%s {{yellow}}txID:{{/}} %s\n", status, txID)
-}
-
-func getAssetInfo(
- ctx context.Context,
- cli *trpc.JSONRPCClient,
- publicKey crypto.PublicKey,
- assetID ids.ID,
- checkBalance bool,
-) (uint64, ids.ID, error) {
- var sourceChainID ids.ID
- if assetID != ids.Empty {
- exists, metadata, supply, _, warp, err := cli.Asset(ctx, assetID)
- if err != nil {
- return 0, ids.Empty, err
- }
- if !exists {
- hutils.Outf("{{red}}%s does not exist{{/}}\n", assetID)
- hutils.Outf("{{red}}exiting...{{/}}\n")
- return 0, ids.Empty, nil
- }
- if warp {
- sourceChainID = ids.ID(metadata[hconsts.IDLen:])
- sourceAssetID := ids.ID(metadata[:hconsts.IDLen])
- hutils.Outf(
- "{{yellow}}sourceChainID:{{/}} %s {{yellow}}sourceAssetID:{{/}} %s {{yellow}}supply:{{/}} %d\n",
- sourceChainID,
- sourceAssetID,
- supply,
- )
- } else {
- hutils.Outf(
- "{{yellow}}metadata:{{/}} %s {{yellow}}supply:{{/}} %d {{yellow}}warp:{{/}} %t\n",
- string(metadata),
- supply,
- warp,
- )
- }
- }
- if !checkBalance {
- return 0, sourceChainID, nil
- }
- addr := utils.Address(publicKey)
- balance, err := cli.Balance(ctx, addr, assetID)
- if err != nil {
- return 0, ids.Empty, err
- }
- if balance == 0 {
- hutils.Outf("{{red}}balance:{{/}} 0 %s\n", assetID)
- hutils.Outf("{{red}}please send funds to %s{{/}}\n", addr)
- hutils.Outf("{{red}}exiting...{{/}}\n")
- return 0, sourceChainID, nil
- }
- hutils.Outf(
- "{{yellow}}balance:{{/}} %s %s\n",
- valueString(assetID, balance),
- assetString(assetID),
- )
- return balance, sourceChainID, nil
-}
-
-//nolint:unparam
-func defaultActor() (
- uint32, ids.ID, crypto.PrivateKey, *auth.ED25519Factory,
- *rpc.JSONRPCClient, *trpc.JSONRPCClient, error,
-) {
- priv, err := GetDefaultKey()
- if err != nil {
- return 0, ids.Empty, crypto.EmptyPrivateKey, nil, nil, nil, err
- }
- chainID, uris, err := GetDefaultChain()
- if err != nil {
- return 0, ids.Empty, crypto.EmptyPrivateKey, nil, nil, nil, err
- }
- cli := rpc.NewJSONRPCClient(uris[0])
- networkID, _, _, err := cli.Network(context.TODO())
- if err != nil {
- return 0, ids.Empty, crypto.EmptyPrivateKey, nil, nil, nil, err
- }
- // For [defaultActor], we always send requests to the first returned URI.
- return networkID, chainID, priv, auth.NewED25519Factory(
- priv,
- ), cli,
- trpc.NewJSONRPCClient(
- uris[0],
- networkID,
- chainID,
- ), nil
-}
-
-func GetDefaultKey() (crypto.PrivateKey, error) {
- v, err := GetDefault(defaultKeyKey)
- if err != nil {
- return crypto.EmptyPrivateKey, err
- }
- if len(v) == 0 {
- return crypto.EmptyPrivateKey, ErrNoKeys
- }
- publicKey := crypto.PublicKey(v)
- priv, err := GetKey(publicKey)
- if err != nil {
- return crypto.EmptyPrivateKey, err
- }
- hutils.Outf("{{yellow}}address:{{/}} %s\n", utils.Address(publicKey))
- return priv, nil
-}
-
-func GetDefaultChain() (ids.ID, []string, error) {
- v, err := GetDefault(defaultChainKey)
- if err != nil {
- return ids.Empty, nil, err
- }
- if len(v) == 0 {
- return ids.Empty, nil, ErrNoChains
- }
- chainID := ids.ID(v)
- uris, err := GetChain(chainID)
- if err != nil {
- return ids.Empty, nil, err
- }
- hutils.Outf("{{yellow}}chainID:{{/}} %s\n", chainID)
- return chainID, uris, nil
-}
-
-func CloseDatabase() error {
- if db == nil {
- return nil
- }
- if err := db.Close(); err != nil {
- return fmt.Errorf("unable to close database: %w", err)
- }
- return nil
+ utils.Outf("%s {{yellow}}txID:{{/}} %s\n", status, txID)
}
diff --git a/cli/spam.go b/cli/spam.go
new file mode 100644
index 0000000000..85056a2fd4
--- /dev/null
+++ b/cli/spam.go
@@ -0,0 +1,433 @@
+// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+//nolint:gosec
+package cli
+
+import (
+ "context"
+ "math"
+ "math/rand"
+ "os"
+ "os/signal"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "syscall"
+ "time"
+
+ "github.com/ava-labs/avalanchego/ids"
+ "github.com/ava-labs/hypersdk/chain"
+ "github.com/ava-labs/hypersdk/consts"
+ "github.com/ava-labs/hypersdk/crypto"
+ "github.com/ava-labs/hypersdk/rpc"
+ "github.com/ava-labs/hypersdk/utils"
+ "github.com/neilotoole/errgroup"
+)
+
+const (
+ feePerTx = 1000
+ defaultRange = 32
+)
+
+func (h *Handler) Spam(
+ maxTxBacklog int, randomRecipient bool,
+ createClient func(string, uint32, ids.ID), // must save on caller side
+ getFactory func(crypto.PrivateKey) chain.AuthFactory,
+ lookupBalance func(int, string) (uint64, error),
+ getParser func(context.Context, ids.ID) (chain.Parser, error),
+ getTransfer func(crypto.PublicKey, uint64) chain.Action,
+ submitDummy func(*rpc.JSONRPCClient, crypto.PrivateKey) func(context.Context, uint64) error,
+) error {
+ ctx := context.Background()
+
+ // Select chain
+ chainID, uris, err := h.PromptChain("select chainID", nil)
+ if err != nil {
+ return err
+ }
+
+ // Select root key
+ keys, err := h.GetKeys()
+ if err != nil {
+ return err
+ }
+ if len(keys) == 0 {
+ return ErrNoKeys
+ }
+ utils.Outf("{{cyan}}stored keys:{{/}} %d\n", len(keys))
+ cli := rpc.NewJSONRPCClient(uris[0])
+ networkID, _, _, err := cli.Network(ctx)
+ if err != nil {
+ return err
+ }
+ balances := make([]uint64, len(keys))
+ createClient(uris[0], networkID, chainID)
+ for i := 0; i < len(keys); i++ {
+ address := h.c.Address(keys[i].PublicKey())
+ balance, err := lookupBalance(i, address)
+ if err != nil {
+ return err
+ }
+ balances[i] = balance
+ }
+ keyIndex, err := h.PromptChoice("select root key", len(keys))
+ if err != nil {
+ return err
+ }
+ key := keys[keyIndex]
+ balance := balances[keyIndex]
+ factory := getFactory(key)
+
+ // No longer using db, so we close
+ if err := h.CloseDatabase(); err != nil {
+ return err
+ }
+
+ // Distribute funds
+ numAccounts, err := h.PromptInt("number of accounts")
+ if err != nil {
+ return err
+ }
+ numTxsPerAccount, err := h.PromptInt("number of transactions per account per second")
+ if err != nil {
+ return err
+ }
+ witholding := uint64(feePerTx * numAccounts)
+ distAmount := (balance - witholding) / uint64(numAccounts)
+ utils.Outf(
+ "{{yellow}}distributing funds to each account:{{/}} %s %s\n",
+ h.ValueString(ids.Empty, distAmount),
+ h.AssetString(ids.Empty),
+ )
+ accounts := make([]crypto.PrivateKey, numAccounts)
+ dcli, err := rpc.NewWebSocketClient(uris[0])
+ if err != nil {
+ return err
+ }
+ funds := map[crypto.PublicKey]uint64{}
+ parser, err := getParser(ctx, chainID)
+ if err != nil {
+ return err
+ }
+ var fundsL sync.Mutex
+ for i := 0; i < numAccounts; i++ {
+ // Create account
+ pk, err := crypto.GeneratePrivateKey()
+ if err != nil {
+ return err
+ }
+ accounts[i] = pk
+
+ // Send funds
+ _, tx, _, err := cli.GenerateTransaction(ctx, parser, nil, getTransfer(pk.PublicKey(), distAmount), factory)
+ if err != nil {
+ return err
+ }
+ if err := dcli.RegisterTx(tx); err != nil {
+ return err
+ }
+ funds[pk.PublicKey()] = distAmount
+
+ // Ensure Snowman++ is activated
+ if i < 10 {
+ time.Sleep(500 * time.Millisecond)
+ }
+ }
+ for i := 0; i < numAccounts; i++ {
+ _, dErr, result, err := dcli.ListenTx(ctx)
+ if err != nil {
+ return err
+ }
+ if dErr != nil {
+ return dErr
+ }
+ if !result.Success {
+ // Should never happen
+ return ErrTxFailed
+ }
+ }
+ utils.Outf("{{yellow}}distributed funds to %d accounts{{/}}\n", numAccounts)
+
+ // Kickoff txs
+ clients := make([]*txIssuer, len(uris))
+ for i := 0; i < len(uris); i++ {
+ cli := rpc.NewJSONRPCClient(uris[i])
+ dcli, err := rpc.NewWebSocketClient(uris[i])
+ if err != nil {
+ return err
+ }
+ clients[i] = &txIssuer{c: cli, d: dcli}
+ }
+ signals := make(chan os.Signal, 2)
+ signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
+ var (
+ transferFee uint64
+ wg sync.WaitGroup
+
+ l sync.Mutex
+ confirmedTxs uint64
+ totalTxs uint64
+ )
+
+ // confirm txs (track failure rate)
+ cctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+ var inflight atomic.Int64
+ var sent atomic.Int64
+ var exiting sync.Once
+ for i := 0; i < len(clients); i++ {
+ issuer := clients[i]
+ wg.Add(1)
+ go func() {
+ for {
+ _, dErr, result, err := issuer.d.ListenTx(context.TODO())
+ if err != nil {
+ return
+ }
+ inflight.Add(-1)
+ issuer.l.Lock()
+ issuer.outstandingTxs--
+ issuer.l.Unlock()
+ l.Lock()
+ if result != nil {
+ if result.Success {
+ confirmedTxs++
+ } else {
+ utils.Outf("{{orange}}on-chain tx failure:{{/}} %s %t\n", string(result.Output), result.Success)
+ }
+ } else {
+ // We can't error match here because we receive it over the wire.
+ if !strings.Contains(dErr.Error(), rpc.ErrExpired.Error()) {
+ utils.Outf("{{orange}}pre-execute tx failure:{{/}} %v\n", dErr)
+ }
+ }
+ totalTxs++
+ l.Unlock()
+ }
+ }()
+ go func() {
+ <-cctx.Done()
+ for {
+ issuer.l.Lock()
+ outstanding := issuer.outstandingTxs
+ issuer.l.Unlock()
+ if outstanding == 0 {
+ _ = issuer.d.Close()
+ wg.Done()
+ return
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+ }()
+ }
+
+ // log stats
+ t := time.NewTicker(1 * time.Second) // ensure no duplicates created
+ defer t.Stop()
+ var psent int64
+ go func() {
+ for {
+ select {
+ case <-t.C:
+ current := sent.Load()
+ l.Lock()
+ if totalTxs > 0 {
+ utils.Outf(
+ "{{yellow}}txs seen:{{/}} %d {{yellow}}success rate:{{/}} %.2f%% {{yellow}}inflight:{{/}} %d {{yellow}}issued/s:{{/}} %d\n", //nolint:lll
+ totalTxs,
+ float64(confirmedTxs)/float64(totalTxs)*100,
+ inflight.Load(),
+ current-psent,
+ )
+ }
+ l.Unlock()
+ psent = current
+ case <-cctx.Done():
+ return
+ }
+ }
+ }()
+
+ // broadcast txs
+ unitPrice, err := clients[0].c.SuggestedRawFee(ctx)
+ if err != nil {
+ return err
+ }
+ g, gctx := errgroup.WithContext(ctx)
+ for ri := 0; ri < numAccounts; ri++ {
+ i := ri
+ g.Go(func() error {
+ t := time.NewTimer(0) // ensure no duplicates created
+ defer t.Stop()
+
+ issuer := getRandomIssuer(clients)
+ factory := getFactory(accounts[i])
+ fundsL.Lock()
+ balance := funds[accounts[i].PublicKey()]
+ fundsL.Unlock()
+ defer func() {
+ fundsL.Lock()
+ funds[accounts[i].PublicKey()] = balance
+ fundsL.Unlock()
+ }()
+ for {
+ select {
+ case <-t.C:
+ // Ensure we aren't too backlogged
+ if inflight.Load() > int64(maxTxBacklog) {
+ t.Reset(1 * time.Second)
+ continue
+ }
+
+ // Send transaction
+ start := time.Now()
+ selected := map[crypto.PublicKey]int{}
+ for k := 0; k < numTxsPerAccount; k++ {
+ recipient, err := getNextRecipient(randomRecipient, i, accounts)
+ if err != nil {
+ return err
+ }
+ v := selected[recipient] + 1
+ selected[recipient] = v
+ _, tx, fees, err := issuer.c.GenerateTransactionManual(parser, nil, getTransfer(recipient, uint64(v)), factory, unitPrice)
+ if err != nil {
+ utils.Outf("{{orange}}failed to generate:{{/}} %v\n", err)
+ continue
+ }
+ transferFee = fees
+ if err := issuer.d.RegisterTx(tx); err != nil {
+ continue
+ }
+ balance -= (fees + uint64(v))
+ issuer.l.Lock()
+ issuer.outstandingTxs++
+ issuer.l.Unlock()
+ inflight.Add(1)
+ sent.Add(1)
+ }
+
+ // Determine how long to sleep
+ dur := time.Since(start)
+ sleep := math.Max(float64(consts.MillisecondsPerSecond-dur.Milliseconds()), 0)
+ t.Reset(time.Duration(sleep) * time.Millisecond)
+ case <-gctx.Done():
+ return gctx.Err()
+ case <-cctx.Done():
+ return nil
+ case <-signals:
+ exiting.Do(func() {
+ utils.Outf("{{yellow}}exiting broadcast loop{{/}}\n")
+ cancel()
+ })
+ return nil
+ }
+ }
+ })
+ }
+ if err := g.Wait(); err != nil {
+ return err
+ }
+
+ // Wait for all issuers to finish
+ utils.Outf("{{yellow}}waiting for issuers to return{{/}}\n")
+ dctx, cancel := context.WithCancel(ctx)
+ go func() {
+ // Send a dummy transaction if shutdown is taking too long (listeners are
+ // expired on accept if dropped)
+ t := time.NewTicker(15 * time.Second)
+ defer t.Stop()
+ for {
+ select {
+ case <-t.C:
+ utils.Outf("{{yellow}}remaining:{{/}} %d\n", inflight.Load())
+ _ = h.SubmitDummy(dctx, cli, submitDummy(cli, key))
+ case <-dctx.Done():
+ return
+ }
+ }
+ }()
+ wg.Wait()
+ cancel()
+
+ // Return funds
+ utils.Outf("{{yellow}}returning funds to %s{{/}}\n", h.c.Address(key.PublicKey()))
+ var (
+ returnedBalance uint64
+ returnsSent int
+ )
+ for i := 0; i < numAccounts; i++ {
+ balance := funds[accounts[i].PublicKey()]
+ if transferFee > balance {
+ continue
+ }
+ returnsSent++
+ // Send funds
+ returnAmt := balance - transferFee
+ _, tx, _, err := cli.GenerateTransaction(ctx, parser, nil, getTransfer(key.PublicKey(), returnAmt), getFactory(accounts[i]))
+ if err != nil {
+ return err
+ }
+ if err := dcli.RegisterTx(tx); err != nil {
+ return err
+ }
+ returnedBalance += returnAmt
+
+ // Ensure Snowman++ is activated
+ if i < 10 {
+ time.Sleep(500 * time.Millisecond)
+ }
+ }
+ for i := 0; i < returnsSent; i++ {
+ _, dErr, result, err := dcli.ListenTx(ctx)
+ if err != nil {
+ return err
+ }
+ if dErr != nil {
+ return dErr
+ }
+ if !result.Success {
+ // Should never happen
+ return ErrTxFailed
+ }
+ }
+ utils.Outf(
+ "{{yellow}}returned funds:{{/}} %s %s\n",
+ h.ValueString(ids.Empty, returnedBalance),
+ h.AssetString(ids.Empty),
+ )
+ return nil
+}
+
+type txIssuer struct {
+ c *rpc.JSONRPCClient
+ d *rpc.WebSocketClient
+
+ l sync.Mutex
+ outstandingTxs int
+}
+
+func getNextRecipient(randomRecipient bool, self int, keys []crypto.PrivateKey) (crypto.PublicKey, error) {
+ if randomRecipient {
+ priv, err := crypto.GeneratePrivateKey()
+ if err != nil {
+ return crypto.EmptyPublicKey, err
+ }
+ return priv.PublicKey(), nil
+ }
+
+ // Select item from array
+ index := rand.Int() % len(keys)
+ if index == self {
+ index++
+ if index == len(keys) {
+ index = 0
+ }
+ }
+ return keys[index].PublicKey(), nil
+}
+
+func getRandomIssuer(issuers []*txIssuer) *txIssuer {
+ index := rand.Int() % len(issuers)
+ return issuers[index]
+}
diff --git a/examples/tokenvm/cmd/token-cli/cmd/storage.go b/cli/storage.go
similarity index 55%
rename from examples/tokenvm/cmd/token-cli/cmd/storage.go
rename to cli/storage.go
index 6d4cf2e6c2..510ea72420 100644
--- a/examples/tokenvm/cmd/token-cli/cmd/storage.go
+++ b/cli/storage.go
@@ -1,10 +1,11 @@
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
-package cmd
+package cli
import (
"errors"
+ "fmt"
"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/avalanchego/ids"
@@ -22,18 +23,18 @@ const (
defaultChainKey = "chain"
)
-func StoreDefault(key string, value []byte) error {
+func (h *Handler) StoreDefault(key string, value []byte) error {
k := make([]byte, 1+len(key))
k[0] = defaultPrefix
copy(k[1:], []byte(key))
- return db.Put(k, value)
+ return h.db.Put(k, value)
}
-func GetDefault(key string) ([]byte, error) {
+func (h *Handler) GetDefault(key string) ([]byte, error) {
k := make([]byte, 1+len(key))
k[0] = defaultPrefix
copy(k[1:], []byte(key))
- v, err := db.Get(k)
+ v, err := h.db.Get(k)
if errors.Is(err, database.ErrNotFound) {
return nil, nil
}
@@ -43,26 +44,47 @@ func GetDefault(key string) ([]byte, error) {
return v, nil
}
-func StoreKey(privateKey crypto.PrivateKey) error {
+func (h *Handler) StoreDefaultChain(chainID ids.ID) error {
+ return h.StoreDefault(defaultChainKey, chainID[:])
+}
+
+func (h *Handler) GetDefaultChain() (ids.ID, []string, error) {
+ v, err := h.GetDefault(defaultChainKey)
+ if err != nil {
+ return ids.Empty, nil, err
+ }
+ if len(v) == 0 {
+ return ids.Empty, nil, ErrNoChains
+ }
+ chainID := ids.ID(v)
+ uris, err := h.GetChain(chainID)
+ if err != nil {
+ return ids.Empty, nil, err
+ }
+ utils.Outf("{{yellow}}chainID:{{/}} %s\n", chainID)
+ return chainID, uris, nil
+}
+
+func (h *Handler) StoreKey(privateKey crypto.PrivateKey) error {
publicKey := privateKey.PublicKey()
k := make([]byte, 1+crypto.PublicKeyLen)
k[0] = keyPrefix
copy(k[1:], publicKey[:])
- has, err := db.Has(k)
+ has, err := h.db.Has(k)
if err != nil {
return err
}
if has {
return ErrDuplicate
}
- return db.Put(k, privateKey[:])
+ return h.db.Put(k, privateKey[:])
}
-func GetKey(publicKey crypto.PublicKey) (crypto.PrivateKey, error) {
+func (h *Handler) GetKey(publicKey crypto.PublicKey) (crypto.PrivateKey, error) {
k := make([]byte, 1+crypto.PublicKeyLen)
k[0] = keyPrefix
copy(k[1:], publicKey[:])
- v, err := db.Get(k)
+ v, err := h.db.Get(k)
if errors.Is(err, database.ErrNotFound) {
return crypto.EmptyPrivateKey, nil
}
@@ -72,8 +94,8 @@ func GetKey(publicKey crypto.PublicKey) (crypto.PrivateKey, error) {
return crypto.PrivateKey(v), nil
}
-func GetKeys() ([]crypto.PrivateKey, error) {
- iter := db.NewIteratorWithPrefix([]byte{keyPrefix})
+func (h *Handler) GetKeys() ([]crypto.PrivateKey, error) {
+ iter := h.db.NewIteratorWithPrefix([]byte{keyPrefix})
defer iter.Release()
privateKeys := []crypto.PrivateKey{}
@@ -85,30 +107,51 @@ func GetKeys() ([]crypto.PrivateKey, error) {
return privateKeys, iter.Error()
}
-func StoreChain(chainID ids.ID, rpc string) error {
+func (h *Handler) StoreDefaultKey(pk crypto.PublicKey) error {
+ return h.StoreDefault(defaultKeyKey, pk[:])
+}
+
+func (h *Handler) GetDefaultKey() (crypto.PrivateKey, error) {
+ v, err := h.GetDefault(defaultKeyKey)
+ if err != nil {
+ return crypto.EmptyPrivateKey, err
+ }
+ if len(v) == 0 {
+ return crypto.EmptyPrivateKey, ErrNoKeys
+ }
+ publicKey := crypto.PublicKey(v)
+ priv, err := h.GetKey(publicKey)
+ if err != nil {
+ return crypto.EmptyPrivateKey, err
+ }
+ utils.Outf("{{yellow}}address:{{/}} %s\n", h.c.Address(publicKey))
+ return priv, nil
+}
+
+func (h *Handler) StoreChain(chainID ids.ID, rpc string) error {
k := make([]byte, 1+consts.IDLen*2)
k[0] = chainPrefix
copy(k[1:], chainID[:])
brpc := []byte(rpc)
rpcID := utils.ToID(brpc)
copy(k[1+consts.IDLen:], rpcID[:])
- has, err := db.Has(k)
+ has, err := h.db.Has(k)
if err != nil {
return err
}
if has {
return ErrDuplicate
}
- return db.Put(k, brpc)
+ return h.db.Put(k, brpc)
}
-func GetChain(chainID ids.ID) ([]string, error) {
+func (h *Handler) GetChain(chainID ids.ID) ([]string, error) {
k := make([]byte, 1+consts.IDLen)
k[0] = chainPrefix
copy(k[1:], chainID[:])
rpcs := []string{}
- iter := db.NewIteratorWithPrefix(k)
+ iter := h.db.NewIteratorWithPrefix(k)
defer iter.Release()
for iter.Next() {
// It is safe to use these bytes directly because the database copies the
@@ -118,8 +161,8 @@ func GetChain(chainID ids.ID) ([]string, error) {
return rpcs, iter.Error()
}
-func GetChains() (map[ids.ID][]string, error) {
- iter := db.NewIteratorWithPrefix([]byte{chainPrefix})
+func (h *Handler) GetChains() (map[ids.ID][]string, error) {
+ iter := h.db.NewIteratorWithPrefix([]byte{chainPrefix})
defer iter.Release()
chains := map[ids.ID][]string{}
@@ -138,8 +181,8 @@ func GetChains() (map[ids.ID][]string, error) {
return chains, iter.Error()
}
-func DeleteChains() ([]ids.ID, error) {
- chains, err := GetChains()
+func (h *Handler) DeleteChains() ([]ids.ID, error) {
+ chains, err := h.GetChains()
if err != nil {
return nil, err
}
@@ -152,7 +195,7 @@ func DeleteChains() ([]ids.ID, error) {
brpc := []byte(rpc)
rpcID := utils.ToID(brpc)
copy(k[1+consts.IDLen:], rpcID[:])
- if err := db.Delete(k); err != nil {
+ if err := h.db.Delete(k); err != nil {
return nil, err
}
}
@@ -160,3 +203,15 @@ func DeleteChains() ([]ids.ID, error) {
}
return chainIDs, nil
}
+
+func (h *Handler) CloseDatabase() error {
+ if h.db == nil {
+ return nil
+ }
+ if err := h.db.Close(); err != nil {
+ return fmt.Errorf("unable to close database: %w", err)
+ }
+ // Allow DB to be closed multiple times
+ h.db = nil
+ return nil
+}
diff --git a/cli/utils.go b/cli/utils.go
new file mode 100644
index 0000000000..3869d38f14
--- /dev/null
+++ b/cli/utils.go
@@ -0,0 +1,55 @@
+// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package cli
+
+import (
+ "context"
+ "time"
+
+ "github.com/ava-labs/hypersdk/consts"
+ "github.com/ava-labs/hypersdk/rpc"
+ "github.com/ava-labs/hypersdk/utils"
+)
+
+const (
+ dummyBlockAgeThreshold = 25 * consts.MillisecondsPerSecond
+ dummyHeightThreshold = 3
+)
+
+func (*Handler) SubmitDummy(
+ ctx context.Context,
+ cli *rpc.JSONRPCClient,
+ sendAndWait func(context.Context, uint64) error,
+) error {
+ var (
+ logEmitted bool
+ txsSent uint64
+ )
+ for ctx.Err() == nil {
+ _, h, t, err := cli.Accepted(ctx)
+ if err != nil {
+ return err
+ }
+ underHeight := h < dummyHeightThreshold
+ if underHeight || time.Now().UnixMilli()-t > dummyBlockAgeThreshold {
+ if underHeight && !logEmitted {
+ utils.Outf(
+ "{{yellow}}waiting for snowman++ activation (needed for AWM)...{{/}}\n",
+ )
+ logEmitted = true
+ }
+ if err := sendAndWait(ctx, txsSent+1); err != nil {
+ return err
+ }
+ txsSent++
+ time.Sleep(750 * time.Millisecond)
+ continue
+ }
+ if logEmitted {
+ utils.Outf("{{yellow}}snowman++ activated{{/}}\n")
+ }
+ return nil
+ }
+ return ctx.Err()
+}
diff --git a/examples/README.md b/examples/README.md
deleted file mode 100644
index 10d439cf46..0000000000
--- a/examples/README.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# examples
-This folder is dedicated to all "example" `hypervms` that we build to showcase
-the capability of the `hypersdk`. If you are interested in writing your own
-`hypervm`, this is usally the right place to start.
-
-## Beginner: `tokenvm`
-We created the [`tokenvm`](./tokenvm) to showcase how to use the
-`hypersdk` in an application most readers are already familiar with, token minting
-and token trading. The `tokenvm` lets anyone create any asset, mint more of
-their asset, modify the metadata of their asset (if they reveal some info), and
-burn their asset. Additionally, there is an embedded on-chain exchange that
-allows anyone to create orders and fill (partial) orders of anyone else. To
-make this example easy to play with, the `tokenvm` also bundles a powerful CLI
-tool and serves RPC requests for trades out of an in-memory order book it
-maintains by syncing blocks. If you are interested in the intersection of
-exchanges and blockchains, it is definitely worth a read (the logic for filling
-orders is < 100 lines of code!).
-
-To ensure the `hypersdk` stays reliable as we optimize and evolve the codebase,
-we also run E2E tests in the `tokenvm` on each PR to the `hypersdk` core modules.
-
-## Expert: `indexvm`
-The [`indexvm`](https://github.com/ava-labs/indexvm) is much more complex than
-the `tokenvm` (more elaborate mechanisms and a new use case you may not be
-familiar with). It was built during the design of the `hypersdk` to test out the
-limits of the abstractions for building complex on-chain mechanisms. We recommend
-taking a look at this `hypervm` once you already have familiarity with the `hypersdk` to gain an
-even deeper understanding of how you can build a complex runtime on top of the `hypersdk`.
-
-The `indexvm` is dedicated to increasing the usefulness of the world's
-content-addressable data (like IPFS) by enabling anyone to "index it" by
-providing useful annotations (i.e. ratings, abuse reports, etc.) on it.
-Think up/down vote on any static file on the decentralized web.
-
-The transparent data feed generated by interactions on the `indexvm` can
-then be used by any third-party (or yourself) to build an AI/recommender
-system to curate things people might find interesting, based on their
-previous interactions/annotations.
-
-Less technical plz? Think TikTok/StumbleUpon over arbitrary IPFS data (like NFTs) but
-all your previous likes (across all services you've ever used) can be used to
-generate the next content recommendation for you.
-
-The fastest way to expedite the transition to a decentralized web is to make it
-more fun and more useful than the existing web. The `indexvm` hopes to play
-a small part in this movement by making it easier for anyone to generate
-world-class recommendations for anyone on the internet, even if you've never
-interacted with them before.
-
-## Future Work
-### storagevm
-It would be great to create a `hypervm` dedicated to the storage of arbitrary
-data blobs (maybe up to 64KB) that could be used by different on-chain
-applications, like NFTs, to store large data (which may utilize many data blobs on-chain).
-
-This `hypervm` should put the raw data blobs in each block but only store the
-hash in state (so that anyone syncing the network does not need to state sync
-everything in it). Nodes can then best-effort/selectively store the content
-that matters to them. We can additionally add some sort of DHT that could be
-used to locate chunks across the network (if they are still held by anyone).
-
-If we got ambitious, we could implement some sort of mechanism to perform
-random checks on the participants to ensure they hold certain pieces of data
-at a certain time (usually be requiring a node to hash some request payload
-with the raw file content). Maybe this could be "file submitter driven" and
-used to slash some host stake if they don't respond fast enough by re-inserting
-content on-chain.
-
-It would also be a good idea to add a "censor" key that could be used to remove
-content or ban accounts that post malicious information.
diff --git a/examples/morpheusvm/.golangci.yml b/examples/morpheusvm/.golangci.yml
new file mode 100644
index 0000000000..575c059579
--- /dev/null
+++ b/examples/morpheusvm/.golangci.yml
@@ -0,0 +1,116 @@
+# Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+# See the file LICENSE for licensing terms.
+
+# https://golangci-lint.run/usage/configuration/
+run:
+ timeout: 10m
+ # skip auto-generated files.
+ skip-files:
+ - ".*\\.pb\\.go$"
+ - ".*mock.*"
+
+issues:
+ # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
+ max-same-issues: 0
+
+linters:
+ # please, do not use `enable-all`: it's deprecated and will be removed soon.
+ # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
+ disable-all: true
+ enable:
+ - asciicheck
+ - depguard
+ - errcheck
+ - errorlint
+ - exportloopref
+ - goconst
+ - gocritic
+ - gofmt
+ - gofumpt
+ - goimports
+ - revive
+ - gosec
+ - gosimple
+ - govet
+ - ineffassign
+ - misspell
+ - nakedret
+ - nolintlint
+ - prealloc
+ - stylecheck
+ - unconvert
+ - unparam
+ - unused
+ - unconvert
+ - whitespace
+ - staticcheck
+ - bodyclose
+ - structcheck
+ # - lll
+ # - gomnd
+ - goprintffuncname
+ - interfacer
+ - typecheck
+ # - goerr113
+ - noctx
+
+linters-settings:
+ errorlint:
+ # Check for plain type assertions and type switches.
+ asserts: false
+ # Check for plain error comparisons.
+ comparison: false
+ revive:
+ rules:
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr
+ - name: bool-literal-in-expr
+ disabled: false
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#early-return
+ - name: early-return
+ disabled: false
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
+ - name: empty-lines
+ disabled: false
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag
+ - name: struct-tag
+ disabled: false
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming
+ - name: unexported-naming
+ disabled: false
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error
+ - name: unhandled-error
+ disabled: false
+ arguments:
+ - "fmt.Fprint"
+ - "fmt.Fprintf"
+ - "fmt.Print"
+ - "fmt.Printf"
+ - "fmt.Println"
+ - "rand.Read"
+ - "sb.WriteString"
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter
+ - name: unused-parameter
+ disabled: false
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
+ - name: unused-receiver
+ disabled: false
+ # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break
+ - name: useless-break
+ disabled: false
+ staticcheck:
+ go: "1.20"
+ # https://staticcheck.io/docs/options#checks
+ checks:
+ - "all"
+ - "-SA6002" # argument should be pointer-like to avoid allocation, for sync.Pool
+ - "-SA1019" # deprecated packages e.g., golang.org/x/crypto/ripemd160
+ # https://golangci-lint.run/usage/linters#gosec
+ gosec:
+ excludes:
+ - G107 # https://securego.io/docs/rules/g107.html
+ depguard:
+ list-type: blacklist
+ packages-with-error-message:
+ - io/ioutil: 'io/ioutil is deprecated. Use package io or os instead.'
+ - github.com/stretchr/testify/assert: 'github.com/stretchr/testify/require should be used instead.'
+ include-go-root: true
diff --git a/examples/morpheusvm/.goreleaser.yml b/examples/morpheusvm/.goreleaser.yml
new file mode 100644
index 0000000000..5915515fba
--- /dev/null
+++ b/examples/morpheusvm/.goreleaser.yml
@@ -0,0 +1,66 @@
+# Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+# See the file LICENSE for licensing terms.
+
+# ref. https://goreleaser.com/customization/build/
+builds:
+ - id: morpheus-cli
+ main: ./cmd/morpheus-cli
+ binary: morpheus-cli
+ flags:
+ - -v
+ goos:
+ - linux
+ - darwin
+ goarch:
+ - amd64
+ - arm64
+ env:
+ - CGO_ENABLED=1
+ - CGO_CFLAGS=-O -D__BLST_PORTABLE__ # Set the CGO flags to use the portable version of BLST
+ overrides:
+ - goos: linux
+ goarch: arm64
+ env:
+ - CC=aarch64-linux-gnu-gcc
+ - goos: darwin
+ goarch: arm64
+ env:
+ - CC=oa64-clang
+ - goos: darwin
+ goarch: amd64
+ goamd64: v1
+ env:
+ - CC=o64-clang
+ - id: morpheusvm
+ main: ./cmd/morpheusvm
+ binary: morpheusvm
+ flags:
+ - -v
+ goos:
+ - linux
+ - darwin
+ goarch:
+ - amd64
+ - arm64
+ env:
+ - CGO_ENABLED=1
+ - CGO_CFLAGS=-O -D__BLST_PORTABLE__ # Set the CGO flags to use the portable version of BLST
+ overrides:
+ - goos: linux
+ goarch: arm64
+ env:
+ - CC=aarch64-linux-gnu-gcc
+ - goos: darwin
+ goarch: arm64
+ env:
+ - CC=oa64-clang
+ - goos: darwin
+ goarch: amd64
+ goamd64: v1
+ env:
+ - CC=o64-clang
+
+release:
+ github:
+ owner: ava-labs
+ name: hypersdk
diff --git a/examples/morpheusvm/LICENSE b/examples/morpheusvm/LICENSE
new file mode 100644
index 0000000000..1ef0d904fa
--- /dev/null
+++ b/examples/morpheusvm/LICENSE
@@ -0,0 +1,58 @@
+Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+
+Ecosystem License
+
+Subject to the terms herein, Ava Labs, Inc. (**“Ava Labs”**) hereby grants you a
+limited, royalty-free, worldwide, non-sublicensable, non-transferable,
+non-exclusive license to use, copy, modify, create derivative works based on,
+and redistribute the Software, in source code, binary, or any other form, including any
+modifications or derivative works of the Software (collectively,**“Licensed Software”**),
+in each case subject to this Ecosystem License (**“License”**).
+
+This License applies to all copies, modifications, derivative works, and any
+other form or usage of the Licensed Software. You will include and display this
+License, without modification, with all uses of the Licensed Software, regardless
+of form.
+
+You will use the Licensed Software solely in connection with the Avalanche
+Public Blockchain platform and associated blockchains, comprised exclusively of
+the Avalanche X-Chain, C-Chain, P-Chain and any subnets linked to the
+P-Chain (“Avalanche Authorized Platform”). This License does not permit use of
+the Licensed Software in connection with any forks of the Avalanche Authorized
+Platform or in any manner not operationally connected to the Avalanche
+Authorized Platform. Ava Labs may publicly announce changes or additions to the
+Avalanche Authorized Platform, which may expand or modify usage of the Licensed
+Software. Upon such announcement, the Avalanche Authorized Platform will be
+deemed to be the then-current iteration of such platform.
+
+You hereby acknowledge and agree to the terms set forth at
+www.avalabs.org/important-notice.
+
+If you use the Licensed Software in violation of this License, this License will
+automatically terminate and Ava Labs reserves all rights to seek any remedy for
+such violation.
+
+Except for uses explicitly permitted in this License, Ava Labs retains all
+rights in the Licensed Software, including without limitation the ability to
+modify it.
+
+Except as required or explicitly permitted by this License, you will not use any
+Ava Labs names, logos, or trademarks without Ava Labs’ prior written consent.
+
+You may use this License for software other than the “Licensed Software”
+specified above, as long as the only change to this License is the definition of
+the term “Licensed Software.”
+
+The Licensed Software may reference third party components. You acknowledge and
+agree that these third party components may be governed by a separate license or
+terms and that you will comply with them.
+
+**TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE LICENSED SOFTWARE IS PROVIDED ON
+AN “AS IS” BASIS, AND AVA LABS EXPRESSLY DISCLAIMS AND EXCLUDES ALL
+REPRESENTATIONS, WARRANTIES AND OTHER TERMS AND CONDITIONS, WHETHER EXPRESS OR
+IMPLIED, INCLUDING WITHOUT LIMITATION BY OPERATION OF LAW OR BY CUSTOM, STATUTE
+OR OTHERWISE, AND INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTY, TERM, OR
+CONDITION OF NON-INFRINGEMENT, MERCHANTABILITY, TITLE, OR FITNESS FOR PARTICULAR
+PURPOSE. YOU USE THE LICENSED SOFTWARE AT YOUR OWN RISK. AVA LABS EXPRESSLY
+DISCLAIMS ALL LIABILITY (INCLUDING FOR ALL DIRECT, CONSEQUENTIAL OR OTHER
+DAMAGES OR LOSSES) RELATED TO ANY USE OF THE LICENSED SOFTWARE.**
diff --git a/examples/morpheusvm/README.md b/examples/morpheusvm/README.md
new file mode 100644
index 0000000000..e8e26f4cd7
--- /dev/null
+++ b/examples/morpheusvm/README.md
@@ -0,0 +1,187 @@
+
+
+
+
+ The Choice is Yours
+
+
+
+
+
+
+
+
+---
+
+_[Who is Morpheus ("The Matrix")?](https://www.youtube.com/watch?v=zE7PKRjrid4)_
+
+The [`morpheusvm`](./examples/morpheusvm) provides the first glimpse into the world of the `hypersdk`.
+After learning how to implement native token transfers in a `hypervm` (one of the simplest Custom VMs
+you could make), you will have the choice to go deeper (red pill) or to turn back to the VMs that you
+already know (blue pill).
+
+When you are ready to build your own `hypervm`, we recommend using the `morpheusvm` as a template!
+
+## Status
+`morpheusvm` is considered **ALPHA** software and is not safe to use in
+production. The framework is under active development and may change
+significantly over the coming months as its modules are optimized and
+audited.
+
+## Demo
+### Launch Subnet
+The first step to running this demo is to launch your own `morpheusvm` Subnet. You
+can do so by running the following command from this location (may take a few
+minutes):
+```bash
+./scripts/run.sh;
+```
+
+When the Subnet is running, you'll see the following logs emitted:
+```
+cluster is ready!
+avalanche-network-runner is running in the background...
+
+use the following command to terminate:
+
+./scripts/stop.sh;
+```
+
+_By default, this allocates all funds on the network to `morpheus1rvzhmceq997zntgvravfagsks6w0ryud3rylh4cdvayry0dl97nsp30ucp`. The private
+key for this address is `0x323b1d8f4eed5f0da9da93071b034f2dce9d2d22692c172f3cb252a64ddfafd01b057de320297c29ad0c1f589ea216869cf1938d88c9fbd70d6748323dbf2fa7`.
+For convenience, this key has is also stored at `demo.pk`._
+
+### Build `morpheus-cli`
+To make it easy to interact with the `morpheusvm`, we implemented the `morpheus-cli`.
+Next, you'll need to build this tool. You can use the following command:
+```bash
+./scripts/build.sh
+```
+
+_This command will put the compiled CLI in `./build/morpheus-cli`._
+
+### Configure `morpheus-cli`
+Next, you'll need to add the chains you created and the default key to the
+`morpheus-cli`. You can use the following commands from this location to do so:
+```bash
+./build/morpheus-cli key import demo.pk
+```
+
+If the key is added corretcly, you'll see the following log:
+```
+database: .morpheus-cli
+imported address: morpheus1rvzhmceq997zntgvravfagsks6w0ryud3rylh4cdvayry0dl97nsp30ucp
+```
+
+Next, you'll need to store the URLs of the nodes running on your Subnet:
+```bash
+./build/morpheus-cli chain import-anr
+```
+
+If `morpheus-cli` is able to connect to ANR, it will emit the following logs:
+```
+database: .morpheus-cli
+stored chainID: 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk uri: http://127.0.0.1:45778/ext/bc/2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+stored chainID: 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk uri: http://127.0.0.1:58191/ext/bc/2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+stored chainID: 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk uri: http://127.0.0.1:16561/ext/bc/2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+stored chainID: 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk uri: http://127.0.0.1:14628/ext/bc/2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+stored chainID: 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk uri: http://127.0.0.1:44160/ext/bc/2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+```
+
+_`./build/morpheus-cli chain import-anr` connects to the Avalanche Network Runner server running in
+the background and pulls the URIs of all nodes tracking each chain you
+created._
+
+
+### Check Balance
+To confirm you've done everything correctly up to this point, run the
+following command to get the current balance of the key you added:
+```bash
+./build/morpheus-cli key balance
+```
+
+If successful, the balance response should look like this:
+```
+database: .morpheus-cli
+address: morpheus1rvzhmceq997zntgvravfagsks6w0ryud3rylh4cdvayry0dl97nsp30ucp
+chainID: 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+uri: http://127.0.0.1:45778/ext/bc/2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+balance: 1000.000000000 RED
+```
+
+### Generate Another Address
+Now that we have a balance to send, we need to generate another address to send to. Because
+we use bech32 addresses, we can't just put a random string of characters as the reciepient
+(won't pass checksum test that protects users from sending to off-by-one addresses).
+```bash
+./build/morpheus-cli key generate
+```
+
+If successful, the `morpheus-cli` will emit the new address:
+```
+database: .morpheus-cli
+created address: morpheus1s3ukd2gnhxl96xa5spzg69w7qd2x4ypve0j5vm0qflvlqr4na5zsezaf2f
+```
+
+By default, the `morpheus-cli` sets newly generated addresses to be the default. We run
+the following command to set it back to `demo.pk`:
+```bash
+./build/morpheus-cli key set
+```
+
+You should see something like this:
+```
+database: .morpheus-cli
+chainID: 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+stored keys: 2
+0) address: morpheus1rvzhmceq997zntgvravfagsks6w0ryud3rylh4cdvayry0dl97nsp30ucp balance: 1000.000000000 RED
+1) address: morpheus1s3ukd2gnhxl96xa5spzg69w7qd2x4ypve0j5vm0qflvlqr4na5zsezaf2f balance: 0.000000000 RED
+set default key: 0
+```
+
+### Send Tokens
+Lastly, we trigger the transfer:
+```bash
+./build/morpheus-cli action transfer
+```
+
+The `morpheus-cli` will emit the following logs when the transfer is successful:
+```
+database: .morpheus-cli
+address: morpheus1rvzhmceq997zntgvravfagsks6w0ryud3rylh4cdvayry0dl97nsp30ucp
+chainID: 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+balance: 1000.000000000 RED
+recipient: morpheus1s3ukd2gnhxl96xa5spzg69w7qd2x4ypve0j5vm0qflvlqr4na5zsezaf2f
+✔ amount: 10
+continue (y/n): y
+✅ txID: sceRdaoqu2AAyLdHCdQkENZaXngGjRoc8nFdGyG8D9pCbTjbk
+```
+
+### Bonus: Watch Activity in Real-Time
+To provide a better sense of what is actually happening on-chain, the
+`morpheus-cli` comes bundled with a simple explorer that logs all blocks/txs that
+occur on-chain. You can run this utility by running the following command from
+this location:
+```bash
+./build/morpheus-cli chain watch
+```
+
+If you run it correctly, you'll see the following input (will run until the
+network shuts down or you exit):
+```
+database: .morpheus-cli
+available chains: 1 excluded: []
+0) chainID: 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+select chainID: 0
+uri: http://127.0.0.1:45778/ext/bc/2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk
+watching for new blocks on 2mQy8Q9Af9dtZvVM8pKsh2rB3cT3QNLjghpet5Mm5db4N7Hwgk 👀
+height:1 txs:1 units:440 root:WspVPrHNAwBcJRJPVwt7TW6WT4E74dN8DuD3WXueQTMt5FDdi
+✅ sceRdaoqu2AAyLdHCdQkENZaXngGjRoc8nFdGyG8D9pCbTjbk actor: morpheus1rvzhmceq997zntgvravfagsks6w0ryud3rylh4cdvayry0dl97nsp30ucp units: 440 summary (*actions.Transfer): [10.000000000 RED -> morpheus1s3ukd2gnhxl96xa5spzg69w7qd2x4ypve0j5vm0qflvlqr4na5zsezaf2f]
+```
+
+
+
+
+
+
+
diff --git a/examples/morpheusvm/actions/outputs.go b/examples/morpheusvm/actions/outputs.go
new file mode 100644
index 0000000000..231f36c44c
--- /dev/null
+++ b/examples/morpheusvm/actions/outputs.go
@@ -0,0 +1,6 @@
+// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package actions
+
+var OutputValueZero = []byte("value is zero")
diff --git a/examples/morpheusvm/actions/transfer.go b/examples/morpheusvm/actions/transfer.go
new file mode 100644
index 0000000000..beb27dab3f
--- /dev/null
+++ b/examples/morpheusvm/actions/transfer.go
@@ -0,0 +1,81 @@
+// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package actions
+
+import (
+ "context"
+
+ "github.com/ava-labs/avalanchego/ids"
+ "github.com/ava-labs/avalanchego/vms/platformvm/warp"
+ "github.com/ava-labs/hypersdk/chain"
+ "github.com/ava-labs/hypersdk/codec"
+ "github.com/ava-labs/hypersdk/consts"
+ "github.com/ava-labs/hypersdk/crypto"
+ "github.com/ava-labs/hypersdk/examples/morpheusvm/auth"
+ "github.com/ava-labs/hypersdk/examples/morpheusvm/storage"
+ "github.com/ava-labs/hypersdk/utils"
+)
+
+var _ chain.Action = (*Transfer)(nil)
+
+type Transfer struct {
+ // To is the recipient of the [Value].
+ To crypto.PublicKey `json:"to"`
+
+ // Amount are transferred to [To].
+ Value uint64 `json:"value"`
+}
+
+func (t *Transfer) StateKeys(rauth chain.Auth, _ ids.ID) [][]byte {
+ return [][]byte{
+ storage.PrefixBalanceKey(auth.GetActor(rauth)),
+ storage.PrefixBalanceKey(t.To),
+ }
+}
+
+func (t *Transfer) Execute(
+ ctx context.Context,
+ r chain.Rules,
+ db chain.Database,
+ _ int64,
+ rauth chain.Auth,
+ _ ids.ID,
+ _ bool,
+) (*chain.Result, error) {
+ actor := auth.GetActor(rauth)
+ unitsUsed := t.MaxUnits(r) // max units == units
+ if t.Value == 0 {
+ return &chain.Result{Success: false, Units: unitsUsed, Output: OutputValueZero}, nil
+ }
+ if err := storage.SubBalance(ctx, db, actor, t.Value); err != nil {
+ return &chain.Result{Success: false, Units: unitsUsed, Output: utils.ErrBytes(err)}, nil
+ }
+ if err := storage.AddBalance(ctx, db, t.To, t.Value); err != nil {
+ return &chain.Result{Success: false, Units: unitsUsed, Output: utils.ErrBytes(err)}, nil
+ }
+ return &chain.Result{Success: true, Units: unitsUsed}, nil
+}
+
+func (*Transfer) MaxUnits(chain.Rules) uint64 {
+ // We use size as the price of this transaction but we could just as easily
+ // use any other calculation.
+ return crypto.PublicKeyLen + consts.Uint64Len
+}
+
+func (t *Transfer) Marshal(p *codec.Packer) {
+ p.PackPublicKey(t.To)
+ p.PackUint64(t.Value)
+}
+
+func UnmarshalTransfer(p *codec.Packer, _ *warp.Message) (chain.Action, error) {
+ var transfer Transfer
+ p.UnpackPublicKey(false, &transfer.To) // can transfer to blackhole
+ transfer.Value = p.UnpackUint64(true)
+ return &transfer, p.Err()
+}
+
+func (*Transfer) ValidRange(chain.Rules) (int64, int64) {
+ // Returning -1, -1 means that the action is always valid.
+ return -1, -1
+}
diff --git a/examples/morpheusvm/assets/hypersdk.png b/examples/morpheusvm/assets/hypersdk.png
new file mode 100644
index 0000000000000000000000000000000000000000..68bc31700c6fc5ad32c30d549274cd6bb2a716f3
GIT binary patch
literal 41378
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≺K){%-eM3zCvzQvrnhBv*l
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