HyperSDK Starter includes:
- Boilerplate VM based on MorpheusVM
- Universal frontend
- Metamask Snap wallet
- A quick start guide (this document)
- Docker (recent version)
- Optional: Golang v1.22.5+
- Optional: NodeJS v20+
- Optional: Metamask Flask. Disable normal Metamask, Core wallet, and other wallets. Do not use your real private key with Flask.
git clone https://github.com/ava-labs/hypersdk-starter-kit.git
Run: docker compose up -d --build devnet faucet frontend
. This may take 5 minutes to download dependencies.
For devcontainers or codespaces, forward ports 8765
for faucet, 9650
for the chain, and 5173
for the frontend.
When finished, stop everything with: docker compose down
This repo includes MorpheusVM, the simplest HyperSDK VM. It supports one action (Transfer) for moving funds and tracking balances.
Open http://localhost:5173 to see the frontend.
We recommend using a Snap (requires Metamask Flask) for the full experience, but a temporary wallet works too.
Actions can be executed on-chain (in a transaction) with results saved to a block, or off-chain (read-only). MorpheusVM has one action. Try executing it read-only. It shows expected balances of the sender and receiver. See the logic in actions/transfer.go
.
Now, write data to the chain. Click "Execute in transaction". All fields are pre-filled with default values.
After mining, the transaction appears in the right column. This column shows all non-empty blocks on the chain.
Logs are located inside the Docker container. To view them, you'll need to open a bash terminal inside the container and navigate to the folder with the current network:
docker exec -it devnet bash -c "cd /root/.tmpnet/networks/latest_morpheusvm-e2e-tests && bash"
This isn’t the best developer experience, and we’re working on improving it.
Think of actions in HyperSDK like functions in EVMs. They have inputs, outputs, and execution logic.
Let's add the Greeting
action. This action doesn’t change anything; it simply prints your balance and the current date. However, if it's executed in a transaction, the output will be recorded in a block on the chain.
Place the following code in actions/greeting.go
. The code includes some comments, but for more details, check out the docs folder in HyperSDK.
package actions
import (
"context"
"fmt"
"time"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/hypersdk-starter-kit/consts"
"github.com/ava-labs/hypersdk-starter-kit/storage"
"github.com/ava-labs/hypersdk/chain"
"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/state"
"github.com/ava-labs/hypersdk/utils"
)
// Please see chain.Action interface description for more information
var _ chain.Action = (*Greeting)(nil)
// Action struct. All "serialize" marked fields will be saved on chain
type Greeting struct {
Name string `serialize:"true" json:"name"`
}
// TypeID, has to be unique across all actions
func (*Greeting) GetTypeID() uint8 {
return consts.HiID
}
// All database keys that could be touched during execution.
// Will fail if a key is missing or has wrong permissions
func (g *Greeting) StateKeys(actor codec.Address, _ ids.ID) state.Keys {
return state.Keys{
string(storage.BalanceKey(actor)): state.Read,
}
}
// The "main" function of the action
func (g *Greeting) Execute(
ctx context.Context,
_ chain.Rules,
mu state.Mutable, // That's how we read and write to the database
timestamp int64, // Timestamp of the block or the time of simulation
actor codec.Address, // Whoever signed the transaction, or a placeholder address in case of read-only action
_ ids.ID, // actionID
) (codec.Typed, error) {
balance, err := storage.GetBalance(ctx, mu, actor)
if err != nil {
return nil, err
}
currentTime := time.Unix(timestamp/1000, 0).Format("January 2, 2006")
greeting := fmt.Sprintf(
"Hi, dear %s! Today, %s, your balance is %s %s.",
g.Name,
currentTime,
utils.FormatBalance(balance),
consts.Symbol,
)
return &GreetingResult{
Greeting: greeting,
}, nil
}
// How many compute units to charge for executing this action. Can be dynamic based on the action.
func (*Greeting) ComputeUnits(chain.Rules) uint64 {
return 1
}
// ValidRange is the timestamp range (in ms) that this [Action] is considered valid.
// -1 means no start/end
func (*Greeting) ValidRange(chain.Rules) (int64, int64) {
return -1, -1
}
// Result of execution of greeting action
type GreetingResult struct {
Greeting string `serialize:"true" json:"greeting"`
}
// Has to implement codec.Typed for on-chain serialization
var _ codec.Typed = (*GreetingResult)(nil)
// TypeID of the action result, could be the same as the action ID
func (g *GreetingResult) GetTypeID() uint8 {
return consts.HiID
}
Also, we would need to define the HiID
constant in the consts/types.go
:
package consts
const (
// Action TypeIDs
TransferID uint8 = 0
HiID uint8 = 1//Add this line
)
Now, you need to make both the VM and clients (via ABI) aware of this action.
To do this, register your action in vm/vm.go
after the line ActionParser.Register(&actions.Transfer{}, nil):
ActionParser.Register(&actions.Greeting{}, nil),
Then, register its output after the line OutputParser.Register(&actions.TransferResult{}, nil):
OutputParser.Register(&actions.GreetingResult{}, nil),
docker compose down -t 1; docker compose up -d --build devnet faucet frontend
HyperSDK uses ABI, an autogenerated description of all the actions in your VM. Thanks to this, the frontend already knows how to interact with your new action. Every action you add will be displayed on the frontend and supported by the wallet as soon as the node restarts.
Now, enter your name and see the result:
You can also send it as a transaction, but this doesn't make much sense since there’s nothing to write to the chain's state.
Congrats! You've just created your first action for HyperSDK.
This covers nearly half of what you need to build your own blockchain on HyperSDK. The remaining part is state management, which you can explore in storage/storage.go
. Dive in and enjoy your journey!
- If you started anything, bring everything down:
docker compose down
- Start only the devnet and faucet:
docker compose up -d --build devnet faucet
- Navigate to the web wallet:
cd web_wallet
- Install dependencies and start the dev server:
npm i && npm run dev
Make sure ports 8765
(faucet), 9650
(chain), and 5173
(frontend) are forwarded.
Learn more from npm:hypersdk-client and the web_wallet
folder in this repo.
- You can launch everything without Docker:
- Faucet:
go run ./cmd/faucet/
- Chain:
./scripts/run.sh
, and use./scripts/stop.sh
to stop - Frontend:
npm run dev
inweb_wallet
- Faucet:
- Be aware of potential port conflicts. If issues arise,
docker rm -f $(docker ps -a -q)
will help. - For VM development, you don’t need to know JavaScript—you can use an existing frontend, and all actions will be added automatically.
- If the frontend works with an ephemeral private key but doesn't work with the Snap, delete the Snap, refresh the page, and try again. The Snap might be outdated.
- Instead of using
./build/morpheus-cli
commands, please directly usego run ./cmd/morpheus-cli/
for the CLI. - Always ensure that you have the
hypersdk-client
npm version and the golanggithub.com/ava-labs/hypersdk
version from the same commit of the starter kit. HyperSDK evolves rapidly.
Install CLI with a version matching go.mod
:
go install github.com/ava-labs/hypersdk/cmd/hypersdk-cli@fb8b6bf17264
Set the endpoint to your local instance of the HyperSDK app:
hypersdk-cli endpoint set --endpoint http://localhost:9650/ext/bc/morpheusvm/
Import the faucet key:
hypersdk-cli key set --key ./demo.pk
Check the balance:
hypersdk-cli balance
Execute a read-only action:
hypersdk-cli read Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000a7396ce9,value=12,memo=0xdeadc0de
Execute a transaction action:
hypersdk-cli tx Transfer --data to=0x000000000000000000000000000000000000000000000000000000000000000000a7396ce9,value=12,memo=0x001234
Check the new balance:
hypersdk-cli balance --sender 0x000000000000000000000000000000000000000000000000000000000000000000a7396ce9
Read more at github.com/ava-labs/hypersdk/tree/main/cmd/hypersdk-cli