├── .github └── workflows │ ├── build-test-release.yml │ └── static-analysis.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .insecure.ewoq.key ├── LICENSE ├── LICENSE.header ├── README.md ├── client ├── client.go ├── info.go ├── keystore.go └── p.go ├── cmd ├── add.go ├── add_subnet_validator.go ├── add_validator.go ├── common.go ├── create.go ├── create_blockchain.go ├── create_key.go ├── create_subnet.go ├── create_vmid.go ├── errors.go ├── remove.go ├── remove_subnet_validator.go ├── root.go ├── status.go ├── status_blockchain.go └── wizard.go ├── go.mod ├── go.sum ├── img ├── add-subnet-validator-local-1.png ├── add-subnet-validator-local-2.png ├── add-validator-local-1.png ├── add-validator-local-2.png ├── create-blockchain-local-1.png ├── create-blockchain-local-2.png ├── create-subnet-local-1.png ├── create-subnet-local-2.png ├── wizard-1.png └── wizard-2.png ├── internal ├── README ├── avax │ └── avax.go ├── key │ ├── hard_key.go │ ├── key.go │ ├── key_test.go │ └── soft_key.go ├── platformvm │ ├── checker.go │ └── checker_test.go └── poll │ ├── poll.go │ └── poll_test.go ├── main.go ├── pkg ├── color │ └── color.go └── logutil │ └── zap.go ├── scripts ├── build.release.sh ├── build.sh ├── tests.e2e.sh ├── tests.lint.sh ├── tests.unit.sh └── updatedep.sh └── tests └── e2e └── e2e_test.go /.github/workflows/build-test-release.yml: -------------------------------------------------------------------------------- 1 | name: Build + test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "*" 9 | pull_request: 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | build_test_release: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Git checkout 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | path: subnet-cli 23 | - name: Set up Go 24 | uses: actions/setup-go@v2 25 | with: 26 | go-version: 1.18 27 | - name: Run unit tests 28 | shell: bash 29 | run: | 30 | cd subnet-cli 31 | scripts/tests.unit.sh 32 | - name: Run e2e tests 33 | shell: bash 34 | run: | 35 | cd subnet-cli 36 | scripts/tests.e2e.sh v1.9.0 37 | - name: Set up arm64 cross compiler 38 | run: sudo apt-get -y install gcc-aarch64-linux-gnu 39 | - name: Checkout osxcross 40 | uses: actions/checkout@v2 41 | with: 42 | repository: tpoechtrager/osxcross 43 | path: osxcross 44 | - name: Build osxcross 45 | run: | 46 | 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 47 | cd osxcross 48 | wget https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.3.sdk.tar.xz -O tarballs/MacOSX11.3.sdk.tar.xz 49 | echo cd4f08a75577145b8f05245a2975f7c81401d75e9535dcffbb879ee1deefcbf4 tarballs/MacOSX11.3.sdk.tar.xz | sha256sum -c - 50 | UNATTENDED=1 ./build.sh 51 | echo $PWD/target/bin >> $GITHUB_PATH 52 | - name: Run GoReleaser 53 | uses: goreleaser/goreleaser-action@v2 54 | with: 55 | distribution: goreleaser 56 | version: latest 57 | args: release --rm-dist 58 | workdir: ./subnet-cli/ 59 | env: 60 | # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | run_static_analysis: 11 | name: Static analysis 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | - name: Run static analysis tests 21 | shell: bash 22 | run: scripts/tests.lint.sh 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | subnet-cli 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | # goreleaser 19 | dist/ 20 | 21 | # keys 22 | *.key 23 | *.pk 24 | 25 | # vim 26 | *.swp 27 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # https://golangci-lint.run/usage/configuration/ 2 | run: 3 | timeout: 10m 4 | # skip auto-generated files. 5 | skip-files: 6 | - ".*\\.pb\\.go$" 7 | 8 | issues: 9 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 10 | max-same-issues: 0 11 | 12 | linters: 13 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 14 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 15 | disable-all: true 16 | enable: 17 | - asciicheck 18 | - deadcode 19 | - depguard 20 | - errcheck 21 | - exportloopref 22 | - goconst 23 | - gocritic 24 | - gofmt 25 | - gofumpt 26 | - goimports 27 | - revive 28 | - gosec 29 | - gosimple 30 | - govet 31 | - ineffassign 32 | - misspell 33 | - nakedret 34 | - nolintlint 35 | - prealloc 36 | - stylecheck 37 | - unconvert 38 | - unused 39 | - varcheck 40 | - unconvert 41 | - whitespace 42 | - staticcheck 43 | # - structcheck 44 | # - lll 45 | # - gomnd 46 | # - goprintffuncname 47 | # - interfacer 48 | # - typecheck 49 | # - goerr113 50 | # - noctx 51 | 52 | linters-settings: 53 | staticcheck: 54 | go: "1.18" 55 | # https://staticcheck.io/docs/options#checks 56 | checks: 57 | - "all" 58 | - "-SA6002" # argument should be pointer-like to avoid allocation, for sync.Pool 59 | - "-SA1019" # deprecated packages e.g., golang.org/x/crypto/ripemd160 60 | # https://golangci-lint.run/usage/linters#gosec 61 | gosec: 62 | excludes: 63 | - G107 # https://securego.io/docs/rules/g107.html 64 | depguard: 65 | list-type: blacklist 66 | packages-with-error-message: 67 | - io/ioutil: 'io/ioutil is deprecated. Use package io or os instead.' 68 | include-go-root: true 69 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # ref. https://goreleaser.com/customization/build/ 2 | builds: 3 | - id: subnet-cli 4 | main: . 5 | binary: subnet-cli 6 | flags: 7 | - -v 8 | goos: 9 | - linux 10 | - darwin 11 | goarch: 12 | - amd64 13 | - arm64 14 | env: 15 | - CGO_ENABLED=1 16 | - CGO_CFLAGS=-O -D__BLST_PORTABLE__ # Set the CGO flags to use the portable version of BLST 17 | overrides: 18 | - goos: linux 19 | goarch: arm64 20 | env: 21 | - CC=aarch64-linux-gnu-gcc 22 | - goos: darwin 23 | goarch: arm64 24 | env: 25 | - CC=oa64-clang 26 | - goos: darwin 27 | goarch: amd64 28 | goamd64: v1 29 | env: 30 | - CC=o64-clang 31 | 32 | release: 33 | github: 34 | owner: ava-labs 35 | name: subnet-cli 36 | -------------------------------------------------------------------------------- /.insecure.ewoq.key: -------------------------------------------------------------------------------- 1 | PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Ava Labs, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /LICENSE.header: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | See the file LICENSE for licensing terms. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # subnet-cli 2 | 3 | A command-line interface to manage [Avalanche Subnets](https://docs.avax.network/subnets/). 4 | 5 | ## Install 6 | 7 | ### Source 8 | 9 | ```bash 10 | git clone https://github.com/ava-labs/subnet-cli.git; 11 | cd subnet-cli; 12 | go install -v .; 13 | ``` 14 | 15 | Once you have installed `subnet-cli`, check the help page to confirm it is 16 | working as expected (_make sure your $GOBIN is in your $PATH_): 17 | 18 | ### Pre-Built Binaries 19 | 20 | ```bash 21 | VERSION=0.0.1 # Populate latest here 22 | 23 | GOARCH=$(go env GOARCH) 24 | GOOS=$(go env GOOS) 25 | DOWNLOAD_PATH=/tmp/subnet-cli.tar.gz 26 | DOWNLOAD_URL=https://github.com/ava-labs/subnet-cli/releases/download/v${VERSION}/subnet-cli_${VERSION}_linux_${GOARCH}.tar.gz 27 | if [[ ${GOOS} == "darwin" ]]; then 28 | DOWNLOAD_URL=https://github.com/ava-labs/subnet-cli/releases/download/v${VERSION}/subnet-cli_${VERSION}_darwin_${GOARCH}.tar.gz 29 | fi 30 | 31 | rm -f ${DOWNLOAD_PATH} 32 | rm -f /tmp/subnet-cli 33 | 34 | echo "downloading subnet-cli ${VERSION} at ${DOWNLOAD_URL}" 35 | curl -L ${DOWNLOAD_URL} -o ${DOWNLOAD_PATH} 36 | 37 | echo "extracting downloaded subnet-cli" 38 | tar xzvf ${DOWNLOAD_PATH} -C /tmp 39 | 40 | /tmp/subnet-cli -h 41 | 42 | # OR 43 | # mv /tmp/subnet-cli /usr/bin/subnet-cli 44 | # subnet-cli -h 45 | ``` 46 | 47 | ## Usage 48 | 49 | ```bash 50 | subnet-cli CLI 51 | 52 | Usage: 53 | subnet-cli [command] 54 | 55 | Available Commands: 56 | add Sub-commands for creating resources 57 | completion Generate the autocompletion script for the specified shell 58 | create Sub-commands for creating resources 59 | help Help about any command 60 | status status commands 61 | wizard A magical command for creating an entire subnet 62 | 63 | Flags: 64 | --enable-prompt 'true' to enable prompt mode (default true) 65 | -h, --help help for subnet-cli 66 | --log-level string log level (default "info") 67 | --poll-interval duration interval to poll tx/blockchain status (default 1s) 68 | --request-timeout duration request timeout (default 2m0s) 69 | 70 | Use "subnet-cli [command] --help" for more information about a command. 71 | ``` 72 | 73 | #### Ledger Support 74 | 75 | To use your [Ledger](https://www.ledger.com) with `subnet-cli`, just add the 76 | `-l`/`--ledger` flag to any command below. 77 | 78 | For example, to create 4 node network on Fuji with Ledger, you would run: 79 | 80 | ```bash 81 | subnet-cli wizard \ 82 | --ledger \ 83 | --node-ids=NodeID-741aqvs6R4iuHDyd1qT1NrFTmsgu78dc4,NodeID-K7Y79oAmBntAcdkyY1CLxCim8QuqcZbBp,NodeID-C3EY6u4v7DDi6YEbYf1wmXdvkEFXYuXNW,NodeID-AiLGeqQfh9gZY3Y8wLMD15tuJtsJHq5Qi \ 84 | --vm-genesis-path=fake-genesis.json \ 85 | --vm-id=tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH \ 86 | --chain-name=test 87 | ``` 88 | 89 | _Make sure you've downloaded the latest version of the 90 | [Avalanche Ledger App](https://docs.avax.network/learn/setup-your-ledger-nano-s-with-avalanche)!_ 91 | 92 | ### `subnet-cli create VMID` 93 | 94 | This command is used to generate a valid VMID based on some string to uniquely 95 | identify a VM. This should stay the same for all versions of the VM, so it 96 | should be based on a word rather than the hash of some code. 97 | 98 | ```bash 99 | subnet-cli create VMID [--hash] 100 | ``` 101 | 102 | ### `subnet-cli create key` 103 | 104 | ```bash 105 | subnet-cli create key 106 | ``` 107 | 108 | `subnet-cli` will assume you have funds on this key (or `--private-key-path`) on the P-Chain for the 109 | rest of this walkthrough. 110 | 111 | The easiest way to do this (**for testing only**) is: 112 | 113 | 1) Import your private key (`.subnet-cli.pk`) into the [web wallet](https://wallet.avax.network) 114 | 2) Request funds from the [faucet](https://faucet.avax-test.network) 115 | 3) Move the test funds (sent on either the X or C-Chain) to the P-Chain [(Tutorial)](https://docs.avax.network/build/tutorials/platform/transfer-avax-between-x-chain-and-p-chain/) 116 | 117 | After following these 3 steps, your test key should now have a balance on the 118 | P-Chain. 119 | 120 | ### `subnet-cli wizard` 121 | 122 | `wizard` is a magical command that: 123 | 124 | * Adds all NodeIDs as validators on the primary network (skipping any that 125 | already exist) 126 | * Creates a subnet 127 | * Adds all NodeIDs as validators on the subnet 128 | * Creates a new blockchain 129 | 130 | To create a 4 node subnet: 131 | 132 | ```bash 133 | subnet-cli wizard \ 134 | --node-ids=NodeID-741aqvs6R4iuHDyd1qT1NrFTmsgu78dc4,NodeID-K7Y79oAmBntAcdkyY1CLxCim8QuqcZbBp,NodeID-C3EY6u4v7DDi6YEbYf1wmXdvkEFXYuXNW,NodeID-AiLGeqQfh9gZY3Y8wLMD15tuJtsJHq5Qi \ 135 | --vm-genesis-path=fake-genesis.json \ 136 | --vm-id=tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH \ 137 | --chain-name=test 138 | ``` 139 | 140 | ![wizard-1](./img/wizard-1.png) 141 | ![wizard-2](./img/wizard-2.png) 142 | 143 | ### `subnet-cli create subnet` 144 | 145 | ```bash 146 | subnet-cli create subnet 147 | ``` 148 | 149 | To create a subnet in the local network: 150 | 151 | ```bash 152 | subnet-cli create subnet \ 153 | --private-key-path=.insecure.ewoq.key \ 154 | --public-uri=http://localhost:57786 155 | ``` 156 | 157 | ![create-subnet-local-1](./img/create-subnet-local-1.png) 158 | ![create-subnet-local-2](./img/create-subnet-local-2.png) 159 | 160 | ### `subnet-cli add validator` 161 | 162 | ```bash 163 | subnet-cli add validator \ 164 | --node-ids="[YOUR-NODE-ID]" \ 165 | --stake-amount=[STAKE-AMOUNT-IN-NANO-AVAX] \ 166 | --validate-reward-fee-percent=2 167 | ``` 168 | 169 | To add a validator to the local network: 170 | 171 | ```bash 172 | subnet-cli add validator \ 173 | --private-key-path=.insecure.ewoq.key \ 174 | --public-uri=http://localhost:57786 \ 175 | --node-ids="NodeID-4B4rc5vdD1758JSBYL1xyvE5NHGzz6xzH" \ 176 | --stake-amount=2000000000000 \ 177 | --validate-reward-fee-percent=3 178 | ``` 179 | 180 | ![add-validator-local-1](./img/add-validator-local-1.png) 181 | ![add-validator-local-2](./img/add-validator-local-2.png) 182 | 183 | ### `subnet-cli add subnet-validator` 184 | 185 | ```bash 186 | subnet-cli add subnet-validator \ 187 | --node-ids="[YOUR-NODE-ID]" \ 188 | --subnet-id="[YOUR-SUBNET-ID]" 189 | ``` 190 | 191 | To add a subnet validator to the local network: 192 | 193 | ```bash 194 | subnet-cli add subnet-validator \ 195 | --private-key-path=.insecure.ewoq.key \ 196 | --public-uri=http://localhost:57786 \ 197 | --node-ids="NodeID-4B4rc5vdD1758JSBYL1xyvE5NHGzz6xzH" \ 198 | --subnet-id="24tZhrm8j8GCJRE9PomW8FaeqbgGS4UAQjJnqqn8pq5NwYSYV1" 199 | ``` 200 | 201 | ![add-subnet-validator-local-1](./img/add-subnet-validator-local-1.png) 202 | ![add-subnet-validator-local-2](./img/add-subnet-validator-local-2.png) 203 | 204 | ### `subnet-cli create blockchain` 205 | 206 | ```bash 207 | subnet-cli create blockchain \ 208 | --subnet-id="[YOUR-SUBNET-ID]" \ 209 | --chain-name="[YOUR-CHAIN-NAME]" \ 210 | --vm-id="[YOUR-VM-ID]" \ 211 | --vm-genesis-path="[YOUR-VM-GENESIS-PATH]" 212 | ``` 213 | 214 | To create a blockchain with the local cluster: 215 | 216 | ```bash 217 | subnet-cli create blockchain \ 218 | --private-key-path=.insecure.ewoq.key \ 219 | --public-uri=http://localhost:57786 \ 220 | --subnet-id="24tZhrm8j8GCJRE9PomW8FaeqbgGS4UAQjJnqqn8pq5NwYSYV1" \ 221 | --chain-name=spacesvm \ 222 | --vm-id=tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH \ 223 | --vm-genesis-path=/tmp/spacesvm.genesis 224 | ``` 225 | 226 | ![create-blockchain-local-1](./img/create-blockchain-local-1.png) 227 | ![create-blockchain-local-2](./img/create-blockchain-local-2.png) 228 | 229 | ### `subnet-cli status blockchain` 230 | 231 | To check the status of the blockchain `2o5THyMs4kVfC42yAiSt2SrjWNkxCLYZef1kewkqYPEiBPjKtn` from a **private URI**: 232 | 233 | ```bash 234 | subnet-cli status blockchain \ 235 | --private-uri=http://localhost:57786 \ 236 | --blockchain-id="X5FJH9b8YGLhakW8GY2vdrKSZxLSN4SeB3tc1kJbKqnwoNQ5L" \ 237 | --check-bootstrapped 238 | ``` 239 | 240 | See [`scripts/tests.e2e.sh`](scripts/tests.e2e.sh) and [`tests/e2e/e2e_test.go`](tests/e2e/e2e_test.go) for example tests. 241 | 242 | ## Running with local network 243 | 244 | See [`network-runner`](https://github.com/ava-labs/avalanche-network-runner). 245 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // Package client implements client. 5 | // TODO: TO BE MIGRATED TO UPSTREAM AVALANCHEGO. 6 | package client 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | "net/url" 12 | "time" 13 | 14 | "github.com/ava-labs/avalanchego/ids" 15 | avago_constants "github.com/ava-labs/avalanchego/utils/constants" 16 | "github.com/ava-labs/avalanchego/vms/avm" 17 | "github.com/ava-labs/avalanchego/vms/platformvm" 18 | internal_platformvm "github.com/ava-labs/subnet-cli/internal/platformvm" 19 | "github.com/ava-labs/subnet-cli/internal/poll" 20 | "go.uber.org/zap" 21 | ) 22 | 23 | var ( 24 | ErrEmptyID = errors.New("empty ID") 25 | ErrEmptyURI = errors.New("empty URI") 26 | ErrInvalidInterval = errors.New("invalid interval") 27 | ) 28 | 29 | type Config struct { 30 | URI string 31 | u *url.URL 32 | PollInterval time.Duration 33 | } 34 | 35 | var _ Client = &client{} 36 | 37 | type Client interface { 38 | NetworkID() uint32 39 | Config() Config 40 | Info() Info 41 | KeyStore() KeyStore 42 | P() P 43 | } 44 | 45 | type client struct { 46 | cfg Config 47 | 48 | // fetched automatic 49 | networkName string 50 | networkID uint32 51 | assetID ids.ID 52 | xChainID ids.ID 53 | pChainID ids.ID 54 | 55 | i *info 56 | k *keyStore 57 | p *p 58 | } 59 | 60 | func New(cfg Config) (Client, error) { 61 | if cfg.URI == "" { 62 | return nil, ErrEmptyURI 63 | } 64 | if cfg.PollInterval == time.Duration(0) { 65 | return nil, ErrInvalidInterval 66 | } 67 | 68 | u, err := url.Parse(cfg.URI) 69 | if err != nil { 70 | return nil, err 71 | } 72 | cfg.u = u 73 | 74 | cli := &client{ 75 | cfg: cfg, 76 | pChainID: avago_constants.PlatformChainID, 77 | i: newInfo(cfg), 78 | k: newKeyStore(cfg), 79 | } 80 | 81 | zap.L().Info("fetching X-Chain id") 82 | xChainID, err := cli.i.Client().GetBlockchainID(context.TODO(), "X") 83 | if err != nil { 84 | return nil, err 85 | } 86 | cli.xChainID = xChainID 87 | zap.L().Info("fetched X-Chain id", zap.String("id", cli.xChainID.String())) 88 | 89 | uriX := u.Scheme + "://" + u.Host 90 | xChainName := cli.xChainID.String() 91 | if u.Port() == "" { 92 | // ref. https://docs.avax.network/build/avalanchego-apis/x-chain 93 | // e.g., https://api.avax-test.network 94 | xChainName = "X" 95 | } 96 | zap.L().Info("fetching AVAX asset id", 97 | zap.String("uri", uriX), 98 | ) 99 | xc := avm.NewClient(uriX, xChainName) 100 | avaxDesc, err := xc.GetAssetDescription(context.TODO(), "AVAX") 101 | if err != nil { 102 | return nil, err 103 | } 104 | cli.assetID = avaxDesc.AssetID 105 | zap.L().Info("fetched AVAX asset id", zap.String("id", cli.assetID.String())) 106 | 107 | zap.L().Info("fetching network information") 108 | cli.networkName, err = cli.i.Client().GetNetworkName(context.TODO()) 109 | if err != nil { 110 | return nil, err 111 | } 112 | cli.networkID, err = avago_constants.NetworkID(cli.networkName) 113 | if err != nil { 114 | return nil, err 115 | } 116 | zap.L().Info("fetched network information", 117 | zap.Uint32("networkId", cli.networkID), 118 | zap.String("networkName", cli.networkName), 119 | ) 120 | 121 | // "NewClient" already appends "/ext/P" 122 | // e.g., https://api.avax-test.network 123 | // ref. https://docs.avax.network/build/avalanchego-apis/p-chain 124 | uriP := u.Scheme + "://" + u.Host 125 | pc := platformvm.NewClient(uriP) 126 | cli.p = &p{ 127 | cfg: cfg, 128 | 129 | networkName: cli.networkName, 130 | networkID: cli.networkID, 131 | assetID: cli.assetID, 132 | pChainID: cli.pChainID, 133 | 134 | cli: pc, 135 | info: cli.i.Client(), 136 | checker: internal_platformvm.NewChecker( 137 | poll.New(cfg.PollInterval), 138 | pc, 139 | ), 140 | } 141 | return cli, nil 142 | } 143 | 144 | func (cc *client) NetworkID() uint32 { return cc.networkID } 145 | func (cc *client) Config() Config { return cc.cfg } 146 | 147 | func (cc *client) Info() Info { return cc.i } 148 | func (cc *client) KeyStore() KeyStore { return cc.k } 149 | 150 | func (cc *client) P() P { return cc.p } 151 | -------------------------------------------------------------------------------- /client/info.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package client 5 | 6 | import ( 7 | api_info "github.com/ava-labs/avalanchego/api/info" 8 | ) 9 | 10 | type Info interface { 11 | Client() api_info.Client 12 | } 13 | 14 | type info struct { 15 | cli api_info.Client 16 | cfg Config 17 | } 18 | 19 | func newInfo(cfg Config) *info { 20 | // "NewClient" already appends "/ext/info" 21 | // e.g., https://api.avax-test.network 22 | // ref. https://docs.avax.network/build/avalanchego-apis/info 23 | uri := cfg.u.Scheme + "://" + cfg.u.Host 24 | cli := api_info.NewClient(uri) 25 | return &info{ 26 | cli: cli, 27 | cfg: cfg, 28 | } 29 | } 30 | 31 | func (i *info) Client() api_info.Client { return i.cli } 32 | -------------------------------------------------------------------------------- /client/keystore.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package client 5 | 6 | import ( 7 | api_keystore "github.com/ava-labs/avalanchego/api/keystore" 8 | ) 9 | 10 | type KeyStore interface { 11 | Client() api_keystore.Client 12 | } 13 | 14 | type keyStore struct { 15 | cli api_keystore.Client 16 | cfg Config 17 | } 18 | 19 | func newKeyStore(cfg Config) *keyStore { 20 | // "NewClient" already appends "/ext/keystore" 21 | // e.g., https://api.avax-test.network 22 | // ref. https://docs.avax.network/build/avalanchego-apis/keystore 23 | uri := cfg.u.Scheme + "://" + cfg.u.Host 24 | cli := api_keystore.NewClient(uri) 25 | return &keyStore{ 26 | cli: cli, 27 | cfg: cfg, 28 | } 29 | } 30 | 31 | func (k *keyStore) Client() api_keystore.Client { return k.cli } 32 | -------------------------------------------------------------------------------- /client/p.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package client 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "time" 11 | 12 | api_info "github.com/ava-labs/avalanchego/api/info" 13 | "github.com/ava-labs/avalanchego/ids" 14 | "github.com/ava-labs/avalanchego/snow" 15 | "github.com/ava-labs/avalanchego/utils/constants" 16 | "github.com/ava-labs/avalanchego/utils/math" 17 | "github.com/ava-labs/avalanchego/utils/units" 18 | "github.com/ava-labs/avalanchego/vms/components/avax" 19 | "github.com/ava-labs/avalanchego/vms/components/verify" 20 | "github.com/ava-labs/avalanchego/vms/platformvm" 21 | "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" 22 | pstatus "github.com/ava-labs/avalanchego/vms/platformvm/status" 23 | "github.com/ava-labs/avalanchego/vms/platformvm/txs" 24 | "github.com/ava-labs/avalanchego/vms/platformvm/validator" 25 | "github.com/ava-labs/avalanchego/vms/secp256k1fx" 26 | internal_avax "github.com/ava-labs/subnet-cli/internal/avax" 27 | "github.com/ava-labs/subnet-cli/internal/key" 28 | internal_platformvm "github.com/ava-labs/subnet-cli/internal/platformvm" 29 | "go.uber.org/zap" 30 | ) 31 | 32 | var ( 33 | ErrInsufficientBalanceForGasFee = errors.New("insufficient balance for gas") 34 | ErrInsufficientBalanceForStakeAmount = errors.New("insufficient balance for stake amount") 35 | ErrUnexpectedSubnetID = errors.New("unexpected subnet ID") 36 | 37 | ErrEmptyValidator = errors.New("empty validator set") 38 | ErrAlreadyValidator = errors.New("already validator") 39 | ErrAlreadySubnetValidator = errors.New("already subnet validator") 40 | ErrNotValidatingPrimaryNetwork = errors.New("validator not validating the primary network") 41 | ErrInvalidSubnetValidatePeriod = errors.New("invalid subnet validate period") 42 | ErrInvalidValidatorData = errors.New("invalid validator data") 43 | ErrValidatorNotFound = errors.New("validator not found") 44 | 45 | // ref. "vms.platformvm". 46 | ErrWrongTxType = errors.New("wrong transaction type") 47 | ErrUnknownOwners = errors.New("unknown owners") 48 | ErrCantSign = errors.New("can't sign") 49 | ) 50 | 51 | type P interface { 52 | Client() platformvm.Client 53 | Checker() internal_platformvm.Checker 54 | Balance(ctx context.Context, key key.Key) (uint64, error) 55 | CreateSubnet( 56 | ctx context.Context, 57 | key key.Key, 58 | opts ...OpOption, 59 | ) (subnetID ids.ID, took time.Duration, err error) 60 | AddValidator( 61 | ctx context.Context, 62 | k key.Key, 63 | nodeID ids.NodeID, 64 | start time.Time, 65 | end time.Time, 66 | opts ...OpOption, 67 | ) (took time.Duration, err error) 68 | AddSubnetValidator( 69 | ctx context.Context, 70 | k key.Key, 71 | subnetID ids.ID, 72 | nodeID ids.NodeID, 73 | start time.Time, 74 | end time.Time, 75 | weight uint64, 76 | opts ...OpOption, 77 | ) (took time.Duration, err error) 78 | RemoveSubnetValidator( 79 | ctx context.Context, 80 | k key.Key, 81 | subnetID ids.ID, 82 | nodeID ids.NodeID, 83 | opts ...OpOption, 84 | ) (took time.Duration, err error) 85 | CreateBlockchain( 86 | ctx context.Context, 87 | key key.Key, 88 | subnetID ids.ID, 89 | chainName string, 90 | vmID ids.ID, 91 | vmGenesis []byte, 92 | opts ...OpOption, 93 | ) (blkChainID ids.ID, took time.Duration, err error) 94 | GetValidator( 95 | ctx context.Context, 96 | rsubnetID ids.ID, 97 | nodeID ids.NodeID, 98 | ) (start time.Time, end time.Time, err error) 99 | } 100 | 101 | type p struct { 102 | cfg Config 103 | networkName string 104 | networkID uint32 105 | assetID ids.ID 106 | pChainID ids.ID 107 | 108 | cli platformvm.Client 109 | info api_info.Client 110 | checker internal_platformvm.Checker 111 | } 112 | 113 | func (pc *p) Client() platformvm.Client { return pc.cli } 114 | func (pc *p) Checker() internal_platformvm.Checker { return pc.checker } 115 | 116 | func (pc *p) Balance(ctx context.Context, key key.Key) (uint64, error) { 117 | pb, err := pc.cli.GetBalance(ctx, key.Addresses()) 118 | if err != nil { 119 | return 0, err 120 | } 121 | return uint64(pb.Balance), nil 122 | } 123 | 124 | // ref. "platformvm.VM.newCreateSubnetTx". 125 | func (pc *p) CreateSubnet( 126 | ctx context.Context, 127 | k key.Key, 128 | opts ...OpOption, 129 | ) (subnetID ids.ID, took time.Duration, err error) { 130 | ret := &Op{} 131 | ret.applyOpts(opts) 132 | 133 | fi, err := pc.info.GetTxFee(ctx) 134 | if err != nil { 135 | return ids.Empty, 0, err 136 | } 137 | createSubnetTxFee := uint64(fi.CreateSubnetTxFee) 138 | 139 | zap.L().Info("creating subnet", 140 | zap.Bool("dryMode", ret.dryMode), 141 | zap.String("assetId", pc.assetID.String()), 142 | zap.Uint64("createSubnetTxFee", createSubnetTxFee), 143 | ) 144 | ins, returnedOuts, _, signers, err := pc.stake(ctx, k, createSubnetTxFee) 145 | if err != nil { 146 | return ids.Empty, 0, err 147 | } 148 | 149 | utx := &txs.CreateSubnetTx{ 150 | BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ 151 | NetworkID: pc.networkID, 152 | BlockchainID: pc.pChainID, 153 | Ins: ins, 154 | Outs: returnedOuts, 155 | }}, 156 | Owner: &secp256k1fx.OutputOwners{ 157 | // [threshold] of [ownerAddrs] needed to manage this subnet 158 | Threshold: 1, 159 | 160 | // address to send change to, if there is any, 161 | // control addresses for the new subnet 162 | Addrs: []ids.ShortID{k.Addresses()[0]}, 163 | }, 164 | } 165 | pTx := &txs.Tx{ 166 | Unsigned: utx, 167 | } 168 | if err := k.Sign(pTx, signers); err != nil { 169 | return ids.Empty, 0, err 170 | } 171 | if err := utx.SyntacticVerify(&snow.Context{ 172 | NetworkID: pc.networkID, 173 | ChainID: pc.pChainID, 174 | }); err != nil { 175 | return ids.Empty, 0, err 176 | } 177 | 178 | // subnet tx ID is the subnet ID based on ins/outs 179 | subnetID = pTx.ID() 180 | if ret.dryMode { 181 | return subnetID, 0, nil 182 | } 183 | 184 | txID, err := pc.cli.IssueTx(ctx, pTx.Bytes()) 185 | if err != nil { 186 | return subnetID, 0, fmt.Errorf("failed to issue tx: %w", err) 187 | } 188 | if txID != subnetID { 189 | return subnetID, 0, ErrUnexpectedSubnetID 190 | } 191 | 192 | took, err = pc.checker.PollSubnet(ctx, txID) 193 | return txID, took, err 194 | } 195 | 196 | func (pc *p) GetValidator(ctx context.Context, rsubnetID ids.ID, nodeID ids.NodeID) (start time.Time, end time.Time, err error) { 197 | // If no [rsubnetID] is provided, just use the PrimaryNetworkID value. 198 | subnetID := constants.PrimaryNetworkID 199 | if rsubnetID != ids.Empty { 200 | subnetID = rsubnetID 201 | } 202 | 203 | // Find validator data associated with [nodeID] 204 | vs, err := pc.Client().GetCurrentValidators(ctx, subnetID, []ids.NodeID{nodeID}) 205 | if err != nil { 206 | return time.Time{}, time.Time{}, err 207 | } 208 | 209 | // If the validator is not found, it will return a string record indicating 210 | // that it was "unable to get mainnet validator record". 211 | if len(vs) < 1 { 212 | return time.Time{}, time.Time{}, ErrValidatorNotFound 213 | } 214 | for _, v := range vs { 215 | if v.NodeID == nodeID { 216 | return time.Unix(int64(v.StartTime), 0), time.Unix(int64(v.EndTime), 0), nil 217 | } 218 | } 219 | // This should never happen if the length of [vs] > 1, however, 220 | // we defend against it in case. 221 | return time.Time{}, time.Time{}, ErrValidatorNotFound 222 | } 223 | 224 | // ref. "platformvm.VM.newAddSubnetValidatorTx". 225 | func (pc *p) AddSubnetValidator( 226 | ctx context.Context, 227 | k key.Key, 228 | subnetID ids.ID, 229 | nodeID ids.NodeID, 230 | start time.Time, 231 | end time.Time, 232 | weight uint64, 233 | opts ...OpOption, 234 | ) (took time.Duration, err error) { 235 | ret := &Op{} 236 | ret.applyOpts(opts) 237 | 238 | if subnetID == ids.Empty { 239 | // same as "ErrNamedSubnetCantBePrimary" 240 | // in case "subnetID == constants.PrimaryNetworkID" 241 | return 0, ErrEmptyID 242 | } 243 | if nodeID == ids.EmptyNodeID { 244 | return 0, ErrEmptyID 245 | } 246 | 247 | _, _, err = pc.GetValidator(ctx, subnetID, nodeID) 248 | if !errors.Is(err, ErrValidatorNotFound) { 249 | return 0, ErrAlreadySubnetValidator 250 | } 251 | 252 | validateStart, validateEnd, err := pc.GetValidator(ctx, ids.ID{}, nodeID) 253 | if errors.Is(err, ErrValidatorNotFound) { 254 | return 0, ErrNotValidatingPrimaryNetwork 255 | } else if err != nil { 256 | return 0, fmt.Errorf("%w: unable to get primary network validator record", err) 257 | } 258 | // make sure the range is within staker validation start/end on the primary network 259 | // TODO: official wallet client should define the error value for such case 260 | // currently just returns "staking too short" 261 | if start.Before(validateStart) { 262 | return 0, fmt.Errorf("%w (validate start %v expected >%v)", ErrInvalidSubnetValidatePeriod, start, validateStart) 263 | } 264 | if end.After(validateEnd) { 265 | return 0, fmt.Errorf("%w (validate end %v expected <%v)", ErrInvalidSubnetValidatePeriod, end, validateEnd) 266 | } 267 | 268 | fi, err := pc.info.GetTxFee(ctx) 269 | if err != nil { 270 | return 0, err 271 | } 272 | txFee := uint64(fi.TxFee) 273 | 274 | zap.L().Info("adding subnet validator", 275 | zap.String("subnetId", subnetID.String()), 276 | zap.Uint64("txFee", txFee), 277 | zap.Time("start", start), 278 | zap.Time("end", end), 279 | zap.Uint64("weight", weight), 280 | ) 281 | ins, returnedOuts, _, signers, err := pc.stake(ctx, k, txFee) 282 | if err != nil { 283 | return 0, err 284 | } 285 | subnetAuth, subnetSigners, err := pc.authorize(ctx, k, subnetID) 286 | if err != nil { 287 | return 0, err 288 | } 289 | signers = append(signers, subnetSigners) 290 | 291 | utx := &txs.AddSubnetValidatorTx{ 292 | BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ 293 | NetworkID: pc.networkID, 294 | BlockchainID: pc.pChainID, 295 | Ins: ins, 296 | Outs: returnedOuts, 297 | }}, 298 | Validator: validator.SubnetValidator{ 299 | Validator: validator.Validator{ 300 | NodeID: nodeID, 301 | Start: uint64(start.Unix()), 302 | End: uint64(end.Unix()), 303 | Wght: weight, 304 | }, 305 | Subnet: subnetID, 306 | }, 307 | SubnetAuth: subnetAuth, 308 | } 309 | pTx := &txs.Tx{ 310 | Unsigned: utx, 311 | } 312 | if err := k.Sign(pTx, signers); err != nil { 313 | return 0, err 314 | } 315 | if err := utx.SyntacticVerify(&snow.Context{ 316 | NetworkID: pc.networkID, 317 | ChainID: pc.pChainID, 318 | }); err != nil { 319 | return 0, err 320 | } 321 | txID, err := pc.cli.IssueTx(ctx, pTx.Bytes()) 322 | if err != nil { 323 | return 0, fmt.Errorf("failed to issue tx: %w", err) 324 | } 325 | 326 | return pc.checker.PollTx(ctx, txID, pstatus.Committed) 327 | } 328 | 329 | // ref. "platformvm.VM.newRemoveSubnetValidatorTx". 330 | func (pc *p) RemoveSubnetValidator( 331 | ctx context.Context, 332 | k key.Key, 333 | subnetID ids.ID, 334 | nodeID ids.NodeID, 335 | opts ...OpOption, 336 | ) (took time.Duration, err error) { 337 | ret := &Op{} 338 | ret.applyOpts(opts) 339 | 340 | if subnetID == ids.Empty { 341 | // same as "ErrNamedSubnetCantBePrimary" 342 | // in case "subnetID == constants.PrimaryNetworkID" 343 | return 0, ErrEmptyID 344 | } 345 | if nodeID == ids.EmptyNodeID { 346 | return 0, ErrEmptyID 347 | } 348 | 349 | _, validateEnd, err := pc.GetValidator(ctx, subnetID, nodeID) 350 | if errors.Is(err, ErrValidatorNotFound) { 351 | return 0, ErrValidatorNotFound 352 | } else if err != nil { 353 | return 0, fmt.Errorf("%w: unable to get subnet validator record", err) 354 | } 355 | // make sure the range is within staker validation start/end on the subnet 356 | now := time.Now() 357 | // We don't check [validateStart] because we can remove pending validators. 358 | if now.After(validateEnd) { 359 | return 0, fmt.Errorf("%w (validate end %v expected <%v)", ErrInvalidSubnetValidatePeriod, now, validateEnd) 360 | } 361 | 362 | fi, err := pc.info.GetTxFee(ctx) 363 | if err != nil { 364 | return 0, err 365 | } 366 | txFee := uint64(fi.TxFee) 367 | 368 | zap.L().Info("removing subnet validator", 369 | zap.String("subnetId", subnetID.String()), 370 | zap.Uint64("txFee", txFee), 371 | ) 372 | ins, returnedOuts, _, signers, err := pc.stake(ctx, k, txFee) 373 | if err != nil { 374 | return 0, err 375 | } 376 | subnetAuth, subnetSigners, err := pc.authorize(ctx, k, subnetID) 377 | if err != nil { 378 | return 0, err 379 | } 380 | signers = append(signers, subnetSigners) 381 | 382 | utx := &txs.RemoveSubnetValidatorTx{ 383 | BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ 384 | NetworkID: pc.networkID, 385 | BlockchainID: pc.pChainID, 386 | Ins: ins, 387 | Outs: returnedOuts, 388 | }}, 389 | NodeID: nodeID, 390 | Subnet: subnetID, 391 | SubnetAuth: subnetAuth, 392 | } 393 | pTx := &txs.Tx{ 394 | Unsigned: utx, 395 | } 396 | if err := k.Sign(pTx, signers); err != nil { 397 | return 0, err 398 | } 399 | if err := utx.SyntacticVerify(&snow.Context{ 400 | NetworkID: pc.networkID, 401 | ChainID: pc.pChainID, 402 | }); err != nil { 403 | return 0, err 404 | } 405 | txID, err := pc.cli.IssueTx(ctx, pTx.Bytes()) 406 | if err != nil { 407 | return 0, fmt.Errorf("failed to issue tx: %w", err) 408 | } 409 | 410 | return pc.checker.PollTx(ctx, txID, pstatus.Committed) 411 | } 412 | 413 | // ref. "platformvm.VM.newAddValidatorTx". 414 | func (pc *p) AddValidator( 415 | ctx context.Context, 416 | k key.Key, 417 | nodeID ids.NodeID, 418 | start time.Time, 419 | end time.Time, 420 | opts ...OpOption, 421 | ) (took time.Duration, err error) { 422 | ret := &Op{} 423 | ret.applyOpts(opts) 424 | 425 | if nodeID == ids.EmptyNodeID { 426 | return 0, ErrEmptyID 427 | } 428 | 429 | _, _, err = pc.GetValidator(ctx, ids.ID{}, nodeID) 430 | if err == nil { 431 | return 0, ErrAlreadyValidator 432 | } else if !errors.Is(err, ErrValidatorNotFound) { 433 | return 0, err 434 | } 435 | 436 | // ref. https://docs.avax.network/learn/platform-overview/staking/#staking-parameters-on-avalanche 437 | // ref. https://docs.avax.network/learn/platform-overview/staking/#validating-in-fuji 438 | if ret.stakeAmt == 0 { 439 | switch pc.networkName { 440 | case constants.MainnetName: 441 | ret.stakeAmt = 2000 * units.Avax 442 | case constants.LocalName, 443 | constants.FujiName: 444 | ret.stakeAmt = 1 * units.Avax 445 | } 446 | zap.L().Info("stake amount not set, default to network setting", 447 | zap.String("networkName", pc.networkName), 448 | zap.Uint64("stakeAmount", ret.stakeAmt), 449 | ) 450 | } 451 | if ret.rewardAddr == ids.ShortEmpty { 452 | ret.rewardAddr = k.Addresses()[0] 453 | zap.L().Warn("reward address not set, default to self", 454 | zap.String("rewardAddress", ret.rewardAddr.String()), 455 | ) 456 | } 457 | if ret.changeAddr == ids.ShortEmpty { 458 | ret.changeAddr = k.Addresses()[0] 459 | zap.L().Warn("change address not set", 460 | zap.String("changeAddress", ret.changeAddr.String()), 461 | ) 462 | } 463 | 464 | zap.L().Info("adding validator", 465 | zap.Time("start", start), 466 | zap.Time("end", end), 467 | zap.Uint64("stakeAmount", ret.stakeAmt), 468 | zap.String("rewardAddress", ret.rewardAddr.String()), 469 | zap.String("changeAddress", ret.changeAddr.String()), 470 | ) 471 | 472 | // ref. https://docs.avax.network/learn/platform-overview/transaction-fees/#fee-schedule 473 | addStakerTxFee := uint64(0) 474 | 475 | ins, returnedOuts, stakedOuts, signers, err := pc.stake( 476 | ctx, 477 | k, 478 | addStakerTxFee, 479 | WithStakeAmount(ret.stakeAmt), 480 | WithRewardAddress(ret.rewardAddr), 481 | WithRewardShares(ret.rewardShares), 482 | WithChangeAddress(ret.changeAddr), 483 | ) 484 | if err != nil { 485 | return 0, err 486 | } 487 | 488 | utx := &txs.AddValidatorTx{ 489 | BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ 490 | NetworkID: pc.networkID, 491 | BlockchainID: pc.pChainID, 492 | Ins: ins, 493 | Outs: returnedOuts, 494 | }}, 495 | Validator: validator.Validator{ 496 | NodeID: nodeID, 497 | Start: uint64(start.Unix()), 498 | End: uint64(end.Unix()), 499 | Wght: ret.stakeAmt, 500 | }, 501 | StakeOuts: stakedOuts, 502 | RewardsOwner: &secp256k1fx.OutputOwners{ 503 | Locktime: 0, 504 | Threshold: 1, 505 | Addrs: []ids.ShortID{ret.rewardAddr}, 506 | }, 507 | DelegationShares: ret.rewardShares, 508 | } 509 | pTx := &txs.Tx{ 510 | Unsigned: utx, 511 | } 512 | if err := k.Sign(pTx, signers); err != nil { 513 | return 0, err 514 | } 515 | if err := utx.SyntacticVerify(&snow.Context{ 516 | NetworkID: pc.networkID, 517 | ChainID: pc.pChainID, 518 | AVAXAssetID: pc.assetID, 519 | }); err != nil { 520 | return 0, err 521 | } 522 | txID, err := pc.cli.IssueTx(ctx, pTx.Bytes()) 523 | if err != nil { 524 | return 0, fmt.Errorf("failed to issue tx: %w", err) 525 | } 526 | 527 | return pc.checker.PollTx(ctx, txID, pstatus.Committed) 528 | } 529 | 530 | // ref. "platformvm.VM.newCreateChainTx". 531 | func (pc *p) CreateBlockchain( 532 | ctx context.Context, 533 | k key.Key, 534 | subnetID ids.ID, 535 | chainName string, 536 | vmID ids.ID, 537 | vmGenesis []byte, 538 | opts ...OpOption, 539 | ) (blkChainID ids.ID, took time.Duration, err error) { 540 | ret := &Op{} 541 | ret.applyOpts(opts) 542 | 543 | if subnetID == ids.Empty { 544 | return ids.Empty, 0, ErrEmptyID 545 | } 546 | if vmID == ids.Empty { 547 | return ids.Empty, 0, ErrEmptyID 548 | } 549 | 550 | fi, err := pc.info.GetTxFee(ctx) 551 | if err != nil { 552 | return ids.Empty, 0, err 553 | } 554 | createBlkChainTxFee := uint64(fi.CreateBlockchainTxFee) 555 | 556 | now := time.Now() 557 | zap.L().Info("creating blockchain", 558 | zap.String("subnetId", subnetID.String()), 559 | zap.String("chainName", chainName), 560 | zap.String("vmId", vmID.String()), 561 | zap.Uint64("createBlockchainTxFee", createBlkChainTxFee), 562 | ) 563 | ins, returnedOuts, _, signers, err := pc.stake(ctx, k, createBlkChainTxFee) 564 | if err != nil { 565 | return ids.Empty, 0, err 566 | } 567 | subnetAuth, subnetSigners, err := pc.authorize(ctx, k, subnetID) 568 | if err != nil { 569 | return ids.Empty, 0, err 570 | } 571 | signers = append(signers, subnetSigners) 572 | 573 | utx := &txs.CreateChainTx{ 574 | BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ 575 | NetworkID: pc.networkID, 576 | BlockchainID: pc.pChainID, 577 | Ins: ins, 578 | Outs: returnedOuts, 579 | }}, 580 | SubnetID: subnetID, 581 | ChainName: chainName, 582 | VMID: vmID, 583 | FxIDs: nil, 584 | GenesisData: vmGenesis, 585 | SubnetAuth: subnetAuth, 586 | } 587 | pTx := &txs.Tx{ 588 | Unsigned: utx, 589 | } 590 | if err := k.Sign(pTx, signers); err != nil { 591 | return ids.Empty, 0, err 592 | } 593 | if err := utx.SyntacticVerify(&snow.Context{ 594 | NetworkID: pc.networkID, 595 | ChainID: pc.pChainID, 596 | }); err != nil { 597 | return ids.Empty, 0, err 598 | } 599 | blkChainID, err = pc.cli.IssueTx(ctx, pTx.Bytes()) 600 | if err != nil { 601 | return ids.Empty, 0, fmt.Errorf("failed to issue tx: %w", err) 602 | } 603 | 604 | took = time.Since(now) 605 | if ret.poll { 606 | var bTook time.Duration 607 | bTook, err = pc.checker.PollBlockchain( 608 | ctx, 609 | internal_platformvm.WithSubnetID(subnetID), 610 | internal_platformvm.WithBlockchainID(blkChainID), 611 | internal_platformvm.WithBlockchainStatus(pstatus.Validating), 612 | internal_platformvm.WithCheckBlockchainBootstrapped(pc.info), 613 | ) 614 | took += bTook 615 | } 616 | return blkChainID, took, err 617 | } 618 | 619 | type Op struct { 620 | stakeAmt uint64 621 | rewardShares uint32 622 | rewardAddr ids.ShortID 623 | changeAddr ids.ShortID 624 | 625 | dryMode bool 626 | poll bool 627 | } 628 | 629 | type OpOption func(*Op) 630 | 631 | func (op *Op) applyOpts(opts []OpOption) { 632 | for _, opt := range opts { 633 | opt(op) 634 | } 635 | } 636 | 637 | func WithStakeAmount(v uint64) OpOption { 638 | return func(op *Op) { 639 | op.stakeAmt = v 640 | } 641 | } 642 | 643 | func WithRewardShares(v uint32) OpOption { 644 | return func(op *Op) { 645 | op.rewardShares = v 646 | } 647 | } 648 | 649 | func WithRewardAddress(v ids.ShortID) OpOption { 650 | return func(op *Op) { 651 | op.rewardAddr = v 652 | } 653 | } 654 | 655 | func WithChangeAddress(v ids.ShortID) OpOption { 656 | return func(op *Op) { 657 | op.changeAddr = v 658 | } 659 | } 660 | 661 | func WithDryMode(b bool) OpOption { 662 | return func(op *Op) { 663 | op.dryMode = b 664 | } 665 | } 666 | 667 | func WithPoll(b bool) OpOption { 668 | return func(op *Op) { 669 | op.poll = b 670 | } 671 | } 672 | 673 | // ref. "platformvm.VM.stake". 674 | func (pc *p) stake(ctx context.Context, k key.Key, fee uint64, opts ...OpOption) ( 675 | ins []*avax.TransferableInput, 676 | returnedOuts []*avax.TransferableOutput, 677 | stakedOuts []*avax.TransferableOutput, 678 | signers [][]ids.ShortID, 679 | err error, 680 | ) { 681 | ret := &Op{} 682 | ret.applyOpts(opts) 683 | if ret.rewardAddr == ids.ShortEmpty { 684 | ret.rewardAddr = k.Addresses()[0] 685 | } 686 | if ret.changeAddr == ids.ShortEmpty { 687 | ret.changeAddr = k.Addresses()[0] 688 | } 689 | 690 | ubs, _, _, err := pc.cli.GetAtomicUTXOs(ctx, k.Addresses(), "", 100, ids.ShortEmpty, ids.Empty) 691 | if err != nil { 692 | return nil, nil, nil, nil, err 693 | } 694 | 695 | now := uint64(time.Now().Unix()) 696 | 697 | ins = make([]*avax.TransferableInput, 0) 698 | returnedOuts = make([]*avax.TransferableOutput, 0) 699 | stakedOuts = make([]*avax.TransferableOutput, 0) 700 | 701 | utxos := make([]*avax.UTXO, len(ubs)) 702 | for i, ub := range ubs { 703 | utxos[i], err = internal_avax.ParseUTXO(ub, txs.Codec) 704 | if err != nil { 705 | return nil, nil, nil, nil, err 706 | } 707 | } 708 | 709 | // amount of AVAX that has been staked 710 | amountStaked := uint64(0) 711 | for _, utxo := range utxos { 712 | // have staked more AVAX then we need to 713 | // no need to consume more AVAX 714 | if amountStaked >= ret.stakeAmt { 715 | break 716 | } 717 | // assume "AssetID" is set to "AVAX" asset ID 718 | if utxo.AssetID() != pc.assetID { 719 | continue 720 | } 721 | 722 | out, ok := utxo.Out.(*stakeable.LockOut) 723 | if !ok { 724 | // This output isn't locked, so it will be handled during the next 725 | // iteration of the UTXO set 726 | continue 727 | } 728 | if out.Locktime <= now { 729 | // This output is no longer locked, so it will be handled during the 730 | // next iteration of the UTXO set 731 | continue 732 | } 733 | 734 | inner, ok := out.TransferableOut.(*secp256k1fx.TransferOutput) 735 | if !ok { 736 | // We only know how to clone secp256k1 outputs for now 737 | continue 738 | } 739 | 740 | _, inputs, inputSigners := k.Spends([]*avax.UTXO{utxo}, key.WithTime(now)) 741 | if len(inputs) == 0 { 742 | // cannot spend this UTXO, skip to try next one 743 | continue 744 | } 745 | in := inputs[0] 746 | 747 | // The remaining value is initially the full value of the input 748 | remainingValue := in.In.Amount() 749 | 750 | // Stake any value that should be staked 751 | amountToStake := math.Min64( 752 | ret.stakeAmt-amountStaked, // Amount we still need to stake 753 | remainingValue, // Amount available to stake 754 | ) 755 | amountStaked += amountToStake 756 | remainingValue -= amountToStake 757 | 758 | // Add the output to the staked outputs 759 | stakedOuts = append(stakedOuts, &avax.TransferableOutput{ 760 | Asset: avax.Asset{ID: pc.assetID}, 761 | Out: &stakeable.LockOut{ 762 | Locktime: out.Locktime, 763 | TransferableOut: &secp256k1fx.TransferOutput{ 764 | Amt: amountToStake, 765 | OutputOwners: inner.OutputOwners, 766 | }, 767 | }, 768 | }) 769 | 770 | if remainingValue > 0 { 771 | // input had extra value, so some of it must be returned 772 | returnedOuts = append(returnedOuts, &avax.TransferableOutput{ 773 | Asset: avax.Asset{ID: pc.assetID}, 774 | Out: &secp256k1fx.TransferOutput{ 775 | Amt: remainingValue, 776 | OutputOwners: secp256k1fx.OutputOwners{ 777 | Locktime: 0, 778 | Threshold: 1, 779 | 780 | // address to send change to, if there is any 781 | Addrs: []ids.ShortID{ret.changeAddr}, 782 | }, 783 | }, 784 | }) 785 | } 786 | 787 | // add the input to the consumed inputs 788 | ins = append(ins, in) 789 | signers = append(signers, inputSigners...) 790 | } 791 | 792 | // amount of AVAX that has been burned 793 | amountBurned := uint64(0) 794 | for _, utxo := range utxos { 795 | // have staked more AVAX then we need to 796 | // have burned more AVAX then we need to 797 | // no need to consume more AVAX 798 | if amountStaked >= ret.stakeAmt && amountBurned >= fee { 799 | break 800 | } 801 | // assume "AssetID" is set to "AVAX" asset ID 802 | if utxo.AssetID() != pc.assetID { 803 | continue 804 | } 805 | 806 | out := utxo.Out 807 | inner, ok := out.(*stakeable.LockOut) 808 | if ok { 809 | if inner.Locktime > now { 810 | // output currently locked, can't be burned 811 | // skip for next UTXO 812 | continue 813 | } 814 | utxo.Out = inner.TransferableOut 815 | } 816 | _, inputs, inputSigners := k.Spends([]*avax.UTXO{utxo}, key.WithTime(now)) 817 | if len(inputs) == 0 { 818 | // cannot spend this UTXO, skip to try next one 819 | continue 820 | } 821 | in := inputs[0] 822 | 823 | // initially the full value of the input 824 | remainingValue := in.In.Amount() 825 | 826 | // burn any value that should be burned 827 | amountToBurn := math.Min64( 828 | fee-amountBurned, // amount we still need to burn 829 | remainingValue, // amount available to burn 830 | ) 831 | amountBurned += amountToBurn 832 | remainingValue -= amountToBurn 833 | 834 | // stake any value that should be staked 835 | amountToStake := math.Min64( 836 | ret.stakeAmt-amountStaked, // Amount we still need to stake 837 | remainingValue, // Amount available to stake 838 | ) 839 | amountStaked += amountToStake 840 | remainingValue -= amountToStake 841 | 842 | if amountToStake > 0 { 843 | // Some of this input was put for staking 844 | stakedOuts = append(stakedOuts, &avax.TransferableOutput{ 845 | Asset: avax.Asset{ID: pc.assetID}, 846 | Out: &secp256k1fx.TransferOutput{ 847 | Amt: amountToStake, 848 | OutputOwners: secp256k1fx.OutputOwners{ 849 | Locktime: 0, 850 | Threshold: 1, 851 | Addrs: []ids.ShortID{ret.changeAddr}, 852 | }, 853 | }, 854 | }) 855 | } 856 | 857 | if remainingValue > 0 { 858 | // input had extra value, so some of it must be returned 859 | returnedOuts = append(returnedOuts, &avax.TransferableOutput{ 860 | Asset: avax.Asset{ID: pc.assetID}, 861 | Out: &secp256k1fx.TransferOutput{ 862 | Amt: remainingValue, 863 | OutputOwners: secp256k1fx.OutputOwners{ 864 | Locktime: 0, 865 | Threshold: 1, 866 | 867 | // address to send change to, if there is any 868 | Addrs: []ids.ShortID{ret.changeAddr}, 869 | }, 870 | }, 871 | }) 872 | } 873 | 874 | // add the input to the consumed inputs 875 | ins = append(ins, in) 876 | signers = append(signers, inputSigners...) 877 | } 878 | 879 | if amountStaked > 0 && amountStaked < ret.stakeAmt { 880 | return nil, nil, nil, nil, ErrInsufficientBalanceForStakeAmount 881 | } 882 | if amountBurned > 0 && amountBurned < fee { 883 | return nil, nil, nil, nil, ErrInsufficientBalanceForGasFee 884 | } 885 | 886 | key.SortTransferableInputsWithSigners(ins, signers) // sort inputs 887 | avax.SortTransferableOutputs(returnedOuts, txs.Codec) // sort outputs 888 | avax.SortTransferableOutputs(stakedOuts, txs.Codec) // sort outputs 889 | 890 | return ins, returnedOuts, stakedOuts, signers, nil 891 | } 892 | 893 | // ref. "platformvm.VM.authorize". 894 | func (pc *p) authorize(ctx context.Context, k key.Key, subnetID ids.ID) ( 895 | auth verify.Verifiable, // input that names owners 896 | signers []ids.ShortID, 897 | err error, 898 | ) { 899 | tb, err := pc.cli.GetTx(ctx, subnetID) 900 | if err != nil { 901 | return nil, nil, err 902 | } 903 | 904 | tx := new(txs.Tx) 905 | if _, err = txs.Codec.Unmarshal(tb, tx); err != nil { 906 | return nil, nil, err 907 | } 908 | 909 | subnetTx, ok := tx.Unsigned.(*txs.CreateSubnetTx) 910 | if !ok { 911 | return nil, nil, ErrWrongTxType 912 | } 913 | 914 | owner, ok := subnetTx.Owner.(*secp256k1fx.OutputOwners) 915 | if !ok { 916 | return nil, nil, ErrUnknownOwners 917 | } 918 | now := uint64(time.Now().Unix()) 919 | indices, signers, ok := k.Match(owner, now) 920 | if !ok { 921 | return nil, nil, ErrCantSign 922 | } 923 | return &secp256k1fx.Input{SigIndices: indices}, signers, nil 924 | } 925 | -------------------------------------------------------------------------------- /cmd/add.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "time" 8 | 9 | "github.com/ava-labs/avalanchego/ids" 10 | "github.com/dustin/go-humanize" 11 | "github.com/onsi/ginkgo/v2/formatter" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // AddCommand implements "subnet-cli add" command. 16 | func AddCommand() *cobra.Command { 17 | cmd := &cobra.Command{ 18 | Use: "add", 19 | Short: "Sub-commands for creating resources", 20 | } 21 | cmd.AddCommand( 22 | newAddValidatorCommand(), 23 | newAddSubnetValidatorCommand(), 24 | ) 25 | cmd.PersistentFlags().StringVar(&publicURI, "public-uri", "https://api.avax-test.network", "URI for avalanche network endpoints") 26 | cmd.PersistentFlags().StringVar(&privKeyPath, "private-key-path", ".subnet-cli.pk", "private key file path") 27 | cmd.PersistentFlags().BoolVarP(&useLedger, "ledger", "l", false, "use ledger to sign transactions") 28 | return cmd 29 | } 30 | 31 | func CreateAddTable(i *Info) string { 32 | buf, tb := BaseTableSetup(i) 33 | tb.Append([]string{formatter.F("{{orange}}NODE IDs{{/}}"), formatter.F("{{light-gray}}{{bold}}%v{{/}}", i.nodeIDs)}) 34 | if i.subnetID != ids.Empty { 35 | tb.Append([]string{formatter.F("{{blue}}SUBNET ID{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.subnetID)}) 36 | } 37 | if !i.validateStart.IsZero() { 38 | tb.Append([]string{formatter.F("{{magenta}}VALIDATE START{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.validateStart.Format(time.RFC3339))}) 39 | } 40 | if !i.validateEnd.IsZero() { 41 | tb.Append([]string{formatter.F("{{magenta}}VALIDATE END{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.validateEnd.Format(time.RFC3339))}) 42 | } 43 | if i.validateWeight > 0 { 44 | tb.Append([]string{formatter.F("{{magenta}}VALIDATE WEIGHT{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", humanize.Comma(int64(i.validateWeight)))}) 45 | } 46 | if i.validateRewardFeePercent > 0 { 47 | validateRewardFeePercent := humanize.FormatFloat("#,###.###", float64(i.validateRewardFeePercent)) 48 | tb.Append([]string{formatter.F("{{magenta}}VALIDATE REWARD FEE{{/}}"), formatter.F("{{light-gray}}{{bold}}{{underline}}%s{{/}} %%", validateRewardFeePercent)}) 49 | } 50 | if i.rewardAddr != ids.ShortEmpty { 51 | tb.Append([]string{formatter.F("{{cyan}}{{bold}}REWARD ADDRESS{{/}}"), formatter.F("{{light-gray}}%s{{/}}", i.rewardAddr)}) 52 | } 53 | if i.changeAddr != ids.ShortEmpty { 54 | tb.Append([]string{formatter.F("{{cyan}}{{bold}}CHANGE ADDRESS{{/}}"), formatter.F("{{light-gray}}%s{{/}}", i.changeAddr)}) 55 | } 56 | tb.Render() 57 | return buf.String() 58 | } 59 | -------------------------------------------------------------------------------- /cmd/add_subnet_validator.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "os" 11 | "time" 12 | 13 | "github.com/ava-labs/avalanchego/ids" 14 | "github.com/ava-labs/subnet-cli/pkg/color" 15 | "github.com/manifoldco/promptui" 16 | "github.com/onsi/ginkgo/v2/formatter" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | const ( 21 | defaultValidateWeight = 1000 22 | ) 23 | 24 | func newAddSubnetValidatorCommand() *cobra.Command { 25 | cmd := &cobra.Command{ 26 | Use: "subnet-validator", 27 | Short: "Adds a subnet to the validator", 28 | Long: ` 29 | Adds a subnet to the validator. 30 | 31 | $ subnet-cli add subnet-validator \ 32 | --private-key-path=.insecure.ewoq.key \ 33 | --public-uri=http://localhost:52250 \ 34 | --subnet-id="24tZhrm8j8GCJRE9PomW8FaeqbgGS4UAQjJnqqn8pq5NwYSYV1" \ 35 | --node-ids="NodeID-4B4rc5vdD1758JSBYL1xyvE5NHGzz6xzH" \ 36 | --validate-weight=1000 37 | 38 | `, 39 | RunE: createSubnetValidatorFunc, 40 | } 41 | 42 | cmd.PersistentFlags().StringVar(&subnetIDs, "subnet-id", "", "subnet ID (must be formatted in ids.ID)") 43 | cmd.PersistentFlags().StringSliceVar(&nodeIDs, "node-ids", nil, "a list of node IDs (must be formatted in ids.ID)") 44 | cmd.PersistentFlags().Uint64Var(&validateWeight, "validate-weight", defaultValidateWeight, "validate weight") 45 | 46 | return cmd 47 | } 48 | 49 | var errZeroValidateWeight = errors.New("zero validate weight") 50 | 51 | func createSubnetValidatorFunc(cmd *cobra.Command, args []string) error { 52 | cli, info, err := InitClient(publicURI, true) 53 | if err != nil { 54 | return err 55 | } 56 | info.subnetID, err = ids.FromString(subnetIDs) 57 | if err != nil { 58 | return err 59 | } 60 | info.txFee = uint64(info.feeData.TxFee) 61 | if err := ParseNodeIDs(cli, info, true); err != nil { 62 | return err 63 | } 64 | if len(info.nodeIDs) == 0 { 65 | color.Outf("{{magenta}}no subnet validators to add{{/}}\n") 66 | return nil 67 | } 68 | 69 | info.validateWeight = validateWeight 70 | info.validateRewardFeePercent = 0 71 | if info.validateWeight == 0 { 72 | return errZeroValidateWeight 73 | } 74 | 75 | info.rewardAddr = ids.ShortEmpty 76 | info.changeAddr = ids.ShortEmpty 77 | 78 | info.txFee *= uint64(len(info.nodeIDs)) 79 | info.requiredBalance = info.txFee 80 | if err := info.CheckBalance(); err != nil { 81 | return err 82 | } 83 | msg := CreateAddTable(info) 84 | if enablePrompt { 85 | msg = formatter.F("\n{{blue}}{{bold}}Ready to add subnet validator, should we continue?{{/}}\n") + msg 86 | } 87 | fmt.Fprint(formatter.ColorableStdOut, msg) 88 | 89 | if enablePrompt { 90 | prompt := promptui.Select{ 91 | Label: "\n", 92 | Stdout: os.Stdout, 93 | Items: []string{ 94 | formatter.F("{{green}}Yes, let's create! {{bold}}{{underline}}I agree to pay the fee{{/}}{{green}}!{{/}}"), 95 | formatter.F("{{red}}No, stop it!{{/}}"), 96 | }, 97 | } 98 | idx, _, err := prompt.Run() 99 | if err != nil { 100 | return nil //nolint:nilerr 101 | } 102 | if idx == 1 { 103 | return nil 104 | } 105 | } 106 | 107 | println() 108 | println() 109 | println() 110 | for _, nodeID := range info.nodeIDs { 111 | // valInfo is not populated because [ParseNodeIDs] called on info.subnetID 112 | // 113 | // TODO: cleanup 114 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 115 | _, end, err := cli.P().GetValidator(ctx, ids.Empty, nodeID) 116 | cancel() 117 | if err != nil { 118 | return err 119 | } 120 | info.validateStart = time.Now().Add(30 * time.Second) 121 | info.validateEnd = end 122 | ctx, cancel = context.WithTimeout(context.Background(), requestTimeout) 123 | took, err := cli.P().AddSubnetValidator( 124 | ctx, 125 | info.key, 126 | info.subnetID, 127 | nodeID, 128 | info.validateStart, 129 | info.validateEnd, 130 | validateWeight, 131 | ) 132 | cancel() 133 | if err != nil { 134 | return err 135 | } 136 | color.Outf("{{magenta}}added %s to subnet %s validator set{{/}} {{light-gray}}(took %v){{/}}\n\n", nodeID, info.subnetID, took) 137 | } 138 | WaitValidator(cli, info.nodeIDs, info) 139 | info.requiredBalance = 0 140 | info.stakeAmount = 0 141 | info.txFee = 0 142 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 143 | info.balance, err = cli.P().Balance(ctx, info.key) 144 | cancel() 145 | if err != nil { 146 | return err 147 | } 148 | fmt.Fprint(formatter.ColorableStdOut, CreateAddTable(info)) 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /cmd/add_validator.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "os" 11 | "time" 12 | 13 | "github.com/ava-labs/avalanchego/ids" 14 | "github.com/ava-labs/avalanchego/utils/formatting/address" 15 | "github.com/ava-labs/avalanchego/utils/units" 16 | "github.com/ava-labs/subnet-cli/client" 17 | "github.com/ava-labs/subnet-cli/pkg/color" 18 | "github.com/manifoldco/promptui" 19 | "github.com/onsi/ginkgo/v2/formatter" 20 | "github.com/spf13/cobra" 21 | ) 22 | 23 | const ( 24 | defaultStakeAmount = 1 * units.Avax 25 | defaultValFeePercent = 2 26 | defaultStagger = 2 * time.Hour 27 | defaultValDuration = 300 * 24 * time.Hour 28 | ) 29 | 30 | func newAddValidatorCommand() *cobra.Command { 31 | cmd := &cobra.Command{ 32 | Use: "validator", 33 | Short: "Adds a node as a validator", 34 | Long: ` 35 | Adds a node as a validator. 36 | 37 | $ subnet-cli add validator \ 38 | --private-key-path=.insecure.ewoq.key \ 39 | --public-uri=http://localhost:52250 \ 40 | --node-ids="NodeID-4B4rc5vdD1758JSBYL1xyvE5NHGzz6xzH" \ 41 | --stake-amount=2000000000000 \ 42 | --validate-reward-fee-percent=2 43 | 44 | `, 45 | RunE: createValidatorFunc, 46 | } 47 | 48 | cmd.PersistentFlags().StringSliceVar(&nodeIDs, "node-ids", nil, "a list of node IDs (must be formatted in ids.ID)") 49 | cmd.PersistentFlags().Uint64Var(&stakeAmount, "stake-amount", defaultStakeAmount, "stake amount denominated in nano AVAX (minimum amount that a validator must stake is 2,000 AVAX)") 50 | 51 | end := time.Now().Add(defaultValDuration) 52 | cmd.PersistentFlags().StringVar(&validateEnds, "validate-end", end.Format(time.RFC3339), "validate start timestamp in RFC3339 format") 53 | cmd.PersistentFlags().Uint32Var(&validateRewardFeePercent, "validate-reward-fee-percent", defaultValFeePercent, "percentage of fee that the validator will take rewards from its delegators") 54 | cmd.PersistentFlags().StringVar(&rewardAddrs, "reward-address", "", "node address to send rewards to (default to key owner)") 55 | cmd.PersistentFlags().StringVar(&changeAddrs, "change-address", "", "node address to send changes to (default to key owner)") 56 | 57 | return cmd 58 | } 59 | 60 | var errInvalidValidateRewardFeePercent = errors.New("invalid validate reward fee percent") 61 | 62 | func createValidatorFunc(cmd *cobra.Command, args []string) error { 63 | cli, info, err := InitClient(publicURI, true) 64 | if err != nil { 65 | return err 66 | } 67 | info.stakeAmount = stakeAmount 68 | 69 | info.subnetID = ids.Empty 70 | if err := ParseNodeIDs(cli, info, true); err != nil { 71 | return err 72 | } 73 | if len(info.nodeIDs) == 0 { 74 | color.Outf("{{magenta}}no primary network validators to add{{/}}\n") 75 | return nil 76 | } 77 | info.validateEnd, err = time.Parse(time.RFC3339, validateEnds) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | info.validateWeight = 0 83 | info.validateRewardFeePercent = validateRewardFeePercent 84 | if info.validateRewardFeePercent < 2 { 85 | return errInvalidValidateRewardFeePercent 86 | } 87 | 88 | if rewardAddrs != "" { 89 | info.rewardAddr, err = address.ParseToID(rewardAddrs) 90 | if err != nil { 91 | return err 92 | } 93 | } else { 94 | info.rewardAddr = info.key.Addresses()[0] 95 | } 96 | if changeAddrs != "" { 97 | info.changeAddr, err = address.ParseToID(changeAddrs) 98 | if err != nil { 99 | return err 100 | } 101 | } else { 102 | info.changeAddr = info.key.Addresses()[0] 103 | } 104 | info.requiredBalance = info.stakeAmount * uint64(len(info.nodeIDs)) 105 | if err := info.CheckBalance(); err != nil { 106 | return err 107 | } 108 | msg := CreateAddTable(info) 109 | if enablePrompt { 110 | msg = formatter.F("\n{{blue}}{{bold}}Ready to add validator, should we continue?{{/}}\n") + msg 111 | } 112 | fmt.Fprint(formatter.ColorableStdOut, msg) 113 | 114 | if enablePrompt { 115 | prompt := promptui.Select{ 116 | Label: "\n", 117 | Stdout: os.Stdout, 118 | Items: []string{ 119 | formatter.F("{{green}}Yes, let's create! {{bold}}{{underline}}I agree to pay the fee{{/}}{{green}}!{{/}}"), 120 | formatter.F("{{red}}No, stop it!{{/}}"), 121 | }, 122 | } 123 | idx, _, err := prompt.Run() 124 | if err != nil { 125 | return nil //nolint:nilerr 126 | } 127 | if idx == 1 { 128 | return nil 129 | } 130 | } 131 | 132 | println() 133 | println() 134 | println() 135 | for i, nodeID := range info.nodeIDs { 136 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 137 | info.validateStart = time.Now().Add(30 * time.Second) 138 | took, err := cli.P().AddValidator( 139 | ctx, 140 | info.key, 141 | nodeID, 142 | info.validateStart, 143 | info.validateEnd, 144 | client.WithStakeAmount(info.stakeAmount), 145 | client.WithRewardShares(info.validateRewardFeePercent*10000), 146 | client.WithRewardAddress(info.rewardAddr), 147 | client.WithChangeAddress(info.changeAddr), 148 | ) 149 | cancel() 150 | if err != nil { 151 | return err 152 | } 153 | color.Outf("{{magenta}}added %s to primary network validator set{{/}} {{light-gray}}(took %v){{/}}\n\n", nodeID, took) 154 | if i < len(info.nodeIDs)-1 { 155 | info.validateEnd = info.validateEnd.Add(defaultStagger) 156 | } 157 | } 158 | WaitValidator(cli, info.nodeIDs, info) 159 | info.requiredBalance = 0 160 | info.stakeAmount = 0 161 | info.txFee = 0 162 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 163 | info.balance, err = cli.P().Balance(ctx, info.key) 164 | cancel() 165 | if err != nil { 166 | return err 167 | } 168 | fmt.Fprint(formatter.ColorableStdOut, CreateAddTable(info)) 169 | return nil 170 | } 171 | -------------------------------------------------------------------------------- /cmd/common.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "errors" 10 | "fmt" 11 | "time" 12 | 13 | "github.com/ava-labs/avalanchego/api/info" 14 | "github.com/ava-labs/avalanchego/ids" 15 | "github.com/ava-labs/avalanchego/utils/units" 16 | "github.com/dustin/go-humanize" 17 | "github.com/olekukonko/tablewriter" 18 | "github.com/onsi/ginkgo/v2/formatter" 19 | "go.uber.org/zap" 20 | 21 | "github.com/ava-labs/subnet-cli/client" 22 | "github.com/ava-labs/subnet-cli/internal/key" 23 | "github.com/ava-labs/subnet-cli/pkg/color" 24 | "github.com/ava-labs/subnet-cli/pkg/logutil" 25 | ) 26 | 27 | const ( 28 | Version = "0.0.4" 29 | ) 30 | 31 | type ValInfo struct { 32 | start time.Time 33 | end time.Time 34 | } 35 | 36 | type Info struct { 37 | uri string 38 | 39 | feeData *info.GetTxFeeResponse 40 | balance uint64 41 | 42 | txFee uint64 43 | stakeAmount uint64 44 | totalStakeAmount uint64 45 | requiredBalance uint64 46 | 47 | key key.Key 48 | 49 | networkName string 50 | 51 | subnetIDType string 52 | subnetID ids.ID 53 | 54 | nodeIDs []ids.NodeID 55 | allNodeIDs []ids.NodeID 56 | valInfos map[ids.NodeID]*ValInfo 57 | 58 | blockchainID ids.ID 59 | chainName string 60 | vmID ids.ID 61 | vmGenesisPath string 62 | 63 | validateStart time.Time 64 | validateEnd time.Time 65 | validateWeight uint64 66 | validateRewardFeePercent uint32 67 | 68 | rewardAddr ids.ShortID 69 | changeAddr ids.ShortID 70 | } 71 | 72 | func InitClient(uri string, loadKey bool) (client.Client, *Info, error) { 73 | cli, err := client.New(client.Config{ 74 | URI: uri, 75 | PollInterval: pollInterval, 76 | }) 77 | if err != nil { 78 | return nil, nil, err 79 | } 80 | txFee, err := cli.Info().Client().GetTxFee(context.TODO()) 81 | if err != nil { 82 | return nil, nil, err 83 | } 84 | networkName, err := cli.Info().Client().GetNetworkName(context.TODO()) 85 | if err != nil { 86 | return nil, nil, err 87 | } 88 | info := &Info{ 89 | uri: uri, 90 | feeData: txFee, 91 | networkName: networkName, 92 | valInfos: map[ids.NodeID]*ValInfo{}, 93 | } 94 | if !loadKey { 95 | return cli, info, nil 96 | } 97 | 98 | if !useLedger { 99 | info.key, err = key.LoadSoft(cli.NetworkID(), privKeyPath) 100 | if err != nil { 101 | return nil, nil, err 102 | } 103 | } else { 104 | info.key, err = key.NewHard(cli.NetworkID()) 105 | if err != nil { 106 | return nil, nil, err 107 | } 108 | } 109 | 110 | info.balance, err = cli.P().Balance(context.TODO(), info.key) 111 | if err != nil { 112 | return nil, nil, err 113 | } 114 | return cli, info, nil 115 | } 116 | 117 | func CreateLogger() error { 118 | lcfg := logutil.GetDefaultZapLoggerConfig() 119 | lcfg.Level = zap.NewAtomicLevelAt(logutil.ConvertToZapLevel(logLevel)) 120 | logger, err := lcfg.Build() 121 | if err != nil { 122 | return err 123 | } 124 | _ = zap.ReplaceGlobals(logger) 125 | return nil 126 | } 127 | 128 | func (i *Info) CheckBalance() error { 129 | if i.balance < i.requiredBalance { 130 | color.Outf("{{red}}insufficient funds to perform operation. get more at https://faucet.avax-test.network{{/}}\n") 131 | return fmt.Errorf("%w: on %s (expected=%d, have=%d)", ErrInsufficientFunds, i.key.P(), i.requiredBalance, i.balance) 132 | } 133 | return nil 134 | } 135 | 136 | func BaseTableSetup(i *Info) (*bytes.Buffer, *tablewriter.Table) { 137 | // P-Chain balance is denominated by units.Avax or 10^9 nano-Avax 138 | curPChainDenominatedP := float64(i.balance) / float64(units.Avax) 139 | curPChainDenominatedBalanceP := humanize.FormatFloat("#,###.#######", curPChainDenominatedP) 140 | 141 | buf := bytes.NewBuffer(nil) 142 | tb := tablewriter.NewWriter(buf) 143 | 144 | tb.SetAutoWrapText(false) 145 | tb.SetColWidth(1500) 146 | tb.SetCenterSeparator("*") 147 | 148 | tb.SetRowLine(true) 149 | tb.SetAlignment(tablewriter.ALIGN_LEFT) 150 | 151 | tb.Append([]string{formatter.F("{{cyan}}{{bold}}PRIMARY P-CHAIN ADDRESS{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.key.P()[0])}) 152 | tb.Append([]string{formatter.F("{{coral}}{{bold}}TOTAL P-CHAIN BALANCE{{/}} "), formatter.F("{{light-gray}}{{bold}}{{underline}}%s{{/}} $AVAX", curPChainDenominatedBalanceP)}) 153 | if i.txFee > 0 { 154 | txFee := float64(i.txFee) / float64(units.Avax) 155 | txFees := humanize.FormatFloat("#,###.###", txFee) 156 | tb.Append([]string{formatter.F("{{red}}{{bold}}TX FEE{{/}}"), formatter.F("{{light-gray}}{{bold}}{{underline}}%s{{/}} $AVAX", txFees)}) 157 | } 158 | if i.stakeAmount > 0 { 159 | stakeAmount := float64(i.stakeAmount) / float64(units.Avax) 160 | stakeAmounts := humanize.FormatFloat("#,###.###", stakeAmount) 161 | tb.Append([]string{formatter.F("{{red}}{{bold}}EACH STAKE AMOUNT{{/}}"), formatter.F("{{light-gray}}{{bold}}{{underline}}%s{{/}} $AVAX", stakeAmounts)}) 162 | } 163 | if i.totalStakeAmount > 0 { 164 | totalStakeAmount := float64(i.totalStakeAmount) / float64(units.Avax) 165 | totalStakeAmounts := humanize.FormatFloat("#,###.###", totalStakeAmount) 166 | tb.Append([]string{formatter.F("{{red}}{{bold}}TOTAL STAKE AMOUNT{{/}}"), formatter.F("{{light-gray}}{{bold}}{{underline}}%s{{/}} $AVAX", totalStakeAmounts)}) 167 | } 168 | if i.requiredBalance > 0 { 169 | requiredBalance := float64(i.requiredBalance) / float64(units.Avax) 170 | requiredBalances := humanize.FormatFloat("#,###.###", requiredBalance) 171 | tb.Append([]string{formatter.F("{{red}}{{bold}}REQUIRED BALANCE{{/}}"), formatter.F("{{light-gray}}{{bold}}{{underline}}%s{{/}} $AVAX", requiredBalances)}) 172 | } 173 | 174 | tb.Append([]string{formatter.F("{{orange}}URI{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.uri)}) 175 | tb.Append([]string{formatter.F("{{orange}}NETWORK NAME{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.networkName)}) 176 | return buf, tb 177 | } 178 | 179 | func ParseNodeIDs(cli client.Client, i *Info, add bool) error { 180 | // TODO: make this parsing logic more explicit (+ store per subnetID, not 181 | // just whatever was called last) 182 | i.nodeIDs = []ids.NodeID{} 183 | i.allNodeIDs = make([]ids.NodeID, len(nodeIDs)) 184 | for idx, rnodeID := range nodeIDs { 185 | nodeID, err := ids.NodeIDFromString(rnodeID) 186 | if err != nil { 187 | return err 188 | } 189 | i.allNodeIDs[idx] = nodeID 190 | 191 | start, end, err := cli.P().GetValidator(context.Background(), i.subnetID, nodeID) 192 | i.valInfos[nodeID] = &ValInfo{start, end} 193 | switch { 194 | case add && errors.Is(err, client.ErrValidatorNotFound): 195 | i.nodeIDs = append(i.nodeIDs, nodeID) 196 | case !add && err == nil: 197 | i.nodeIDs = append(i.nodeIDs, nodeID) 198 | case !add && errors.Is(err, client.ErrValidatorNotFound): 199 | color.Outf("\n{{yellow}}%s is not yet a validator on %s{{/}}\n", nodeID, i.subnetID) 200 | case err != nil: 201 | return err 202 | case add: 203 | color.Outf("\n{{yellow}}%s is already a validator on %s{{/}}\n", nodeID, i.subnetID) 204 | } 205 | } 206 | return nil 207 | } 208 | 209 | func WaitValidator(cli client.Client, nodeIDs []ids.NodeID, i *Info) { 210 | for _, nodeID := range nodeIDs { 211 | color.Outf("{{yellow}}waiting for validator %s to start validating %s...(could take a few minutes){{/}}\n", nodeID, i.subnetID) 212 | for { 213 | start, end, err := cli.P().GetValidator(context.Background(), i.subnetID, nodeID) 214 | if err == nil { 215 | if i.subnetID == ids.Empty { 216 | i.valInfos[nodeID] = &ValInfo{start, end} 217 | } 218 | break 219 | } 220 | time.Sleep(10 * time.Second) 221 | } 222 | } 223 | } 224 | 225 | func WaitValidatorRemoval(cli client.Client, nodeIDs []ids.NodeID, i *Info) { 226 | for _, nodeID := range nodeIDs { 227 | color.Outf("{{yellow}}waiting for validator %s to stop validating %s...(could take a few minutes){{/}}\n", nodeID, i.subnetID) 228 | for { 229 | _, _, err := cli.P().GetValidator(context.Background(), i.subnetID, nodeID) 230 | if errors.Is(err, client.ErrValidatorNotFound) { 231 | break 232 | } 233 | time.Sleep(10 * time.Second) 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /cmd/create.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "github.com/ava-labs/avalanchego/ids" 8 | "github.com/onsi/ginkgo/v2/formatter" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // CreateCommand implements "subnet-cli create" command. 13 | func CreateCommand() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "create", 16 | Short: "Sub-commands for creating resources", 17 | } 18 | cmd.AddCommand( 19 | newCreateKeyCommand(), 20 | newCreateSubnetCommand(), 21 | newCreateBlockchainCommand(), 22 | newCreateVMIDCommand(), 23 | ) 24 | cmd.PersistentFlags().StringVar(&publicURI, "public-uri", "https://api.avax-test.network", "URI for avalanche network endpoints") 25 | cmd.PersistentFlags().StringVar(&privKeyPath, "private-key-path", ".subnet-cli.pk", "private key file path") 26 | cmd.PersistentFlags().BoolVarP(&useLedger, "ledger", "l", false, "use ledger to sign transactions") 27 | return cmd 28 | } 29 | 30 | func MakeCreateTable(i *Info) string { 31 | buf, tb := BaseTableSetup(i) 32 | if i.subnetID != ids.Empty { 33 | tb.Append([]string{formatter.F("{{blue}}%s{{/}}", i.subnetIDType), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.subnetID)}) 34 | } 35 | if i.blockchainID != ids.Empty { 36 | tb.Append([]string{formatter.F("{{blue}}CREATED BLOCKCHAIN ID{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.blockchainID)}) 37 | } 38 | if i.chainName != "" { 39 | tb.Append([]string{formatter.F("{{dark-green}}CHAIN NAME{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.chainName)}) 40 | tb.Append([]string{formatter.F("{{dark-green}}VM ID{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.vmID)}) 41 | tb.Append([]string{formatter.F("{{dark-green}}VM GENESIS PATH{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.vmGenesisPath)}) 42 | } 43 | tb.Render() 44 | return buf.String() 45 | } 46 | -------------------------------------------------------------------------------- /cmd/create_blockchain.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | 11 | "github.com/ava-labs/avalanchego/ids" 12 | "github.com/ava-labs/subnet-cli/pkg/color" 13 | "github.com/manifoldco/promptui" 14 | "github.com/onsi/ginkgo/v2/formatter" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | func newCreateBlockchainCommand() *cobra.Command { 19 | cmd := &cobra.Command{ 20 | Use: "blockchain [options]", 21 | Short: "Creates a blockchain", 22 | Long: ` 23 | Creates a blockchain. 24 | 25 | $ subnet-cli create blockchain \ 26 | --private-key-path=.insecure.ewoq.key \ 27 | --subnet-id="24tZhrm8j8GCJRE9PomW8FaeqbgGS4UAQjJnqqn8pq5NwYSYV1" \ 28 | --chain-name=my-custom-chain \ 29 | --vm-id=tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH \ 30 | --vm-genesis-path=.my-custom-vm.genesis 31 | 32 | `, 33 | RunE: createBlockchainFunc, 34 | } 35 | 36 | cmd.PersistentFlags().StringVar(&subnetIDs, "subnet-id", "", "subnet ID (must be formatted in ids.ID)") 37 | cmd.PersistentFlags().StringVar(&chainName, "chain-name", "", "chain name") 38 | cmd.PersistentFlags().StringVar(&vmIDs, "vm-id", "", "VM ID (must be formatted in ids.ID)") 39 | cmd.PersistentFlags().StringVar(&vmGenesisPath, "vm-genesis-path", "", "VM genesis file path") 40 | 41 | return cmd 42 | } 43 | 44 | func createBlockchainFunc(cmd *cobra.Command, args []string) error { 45 | cli, info, err := InitClient(publicURI, true) 46 | if err != nil { 47 | return err 48 | } 49 | info.subnetIDType = "SUBNET ID" 50 | info.subnetID, err = ids.FromString(subnetIDs) 51 | if err != nil { 52 | return err 53 | } 54 | info.vmID, err = ids.FromString(vmIDs) 55 | if err != nil { 56 | return err 57 | } 58 | vmGenesisBytes, err := os.ReadFile(vmGenesisPath) 59 | if err != nil { 60 | return err 61 | } 62 | info.txFee = uint64(info.feeData.CreateBlockchainTxFee) 63 | info.requiredBalance = info.txFee 64 | if err := info.CheckBalance(); err != nil { 65 | return err 66 | } 67 | info.chainName = chainName 68 | info.vmGenesisPath = vmGenesisPath 69 | 70 | msg := MakeCreateTable(info) 71 | if enablePrompt { 72 | msg = formatter.F("\n{{blue}}{{bold}}Ready to create blockchain resources, should we continue?{{/}}\n") + msg 73 | } 74 | fmt.Fprint(formatter.ColorableStdOut, msg) 75 | 76 | if enablePrompt { 77 | prompt := promptui.Select{ 78 | Label: "\n", 79 | Stdout: os.Stdout, 80 | Items: []string{ 81 | formatter.F("{{green}}Yes, let's create! {{bold}}{{underline}}I agree to pay the fee{{/}}{{green}}!{{/}}"), 82 | formatter.F("{{red}}No, stop it!{{/}}"), 83 | }, 84 | } 85 | idx, _, err := prompt.Run() 86 | if err != nil { 87 | return nil //nolint:nilerr 88 | } 89 | if idx == 1 { 90 | return nil 91 | } 92 | } 93 | println() 94 | println() 95 | println() 96 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 97 | blockchainID, took, err := cli.P().CreateBlockchain( 98 | ctx, 99 | info.key, 100 | info.subnetID, 101 | info.chainName, 102 | info.vmID, 103 | vmGenesisBytes, 104 | ) 105 | cancel() 106 | if err != nil { 107 | return err 108 | } 109 | info.blockchainID = blockchainID 110 | color.Outf("{{magenta}}created blockchain{{/}} %q {{light-gray}}(took %v){{/}}\n\n", info.blockchainID, took) 111 | 112 | info.requiredBalance = 0 113 | info.stakeAmount = 0 114 | info.txFee = 0 115 | ctx, cancel = context.WithTimeout(context.Background(), requestTimeout) 116 | info.balance, err = cli.P().Balance(ctx, info.key) 117 | cancel() 118 | if err != nil { 119 | return err 120 | } 121 | fmt.Fprint(formatter.ColorableStdOut, MakeCreateTable(info)) 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /cmd/create_key.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "os" 8 | 9 | "github.com/ava-labs/subnet-cli/internal/key" 10 | "github.com/ava-labs/subnet-cli/pkg/color" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func newCreateKeyCommand() *cobra.Command { 15 | cmd := &cobra.Command{ 16 | Use: "key [options]", 17 | Short: "Generates a private key", 18 | Long: ` 19 | Generates a private key. 20 | 21 | $ subnet-cli create key --private-key-path=.insecure.test.key 22 | 23 | `, 24 | RunE: createKeyFunc, 25 | } 26 | return cmd 27 | } 28 | 29 | func createKeyFunc(cmd *cobra.Command, args []string) error { 30 | if _, err := os.Stat(privKeyPath); err == nil { 31 | color.Outf("{{red}}key already found at %q{{/}}\n", privKeyPath) 32 | return os.ErrExist 33 | } 34 | k, err := key.NewSoft(0) 35 | if err != nil { 36 | return err 37 | } 38 | if err := k.Save(privKeyPath); err != nil { 39 | return err 40 | } 41 | color.Outf("{{green}}created a new key %q{{/}}\n", privKeyPath) 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /cmd/create_subnet.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | 11 | "github.com/ava-labs/subnet-cli/client" 12 | "github.com/ava-labs/subnet-cli/pkg/color" 13 | "github.com/manifoldco/promptui" 14 | "github.com/onsi/ginkgo/v2/formatter" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | func newCreateSubnetCommand() *cobra.Command { 19 | cmd := &cobra.Command{ 20 | Use: "subnet", 21 | Short: "Creates a subnet", 22 | Long: ` 23 | Creates a subnet based on the configuration. 24 | 25 | $ subnet-cli create subnet \ 26 | --private-key-path=.insecure.ewoq.key \ 27 | --public-uri=http://localhost:52250 28 | 29 | `, 30 | RunE: createSubnetFunc, 31 | } 32 | 33 | return cmd 34 | } 35 | 36 | func createSubnetFunc(cmd *cobra.Command, args []string) error { 37 | cli, info, err := InitClient(publicURI, true) 38 | if err != nil { 39 | return err 40 | } 41 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 42 | sid, _, err := cli.P().CreateSubnet(ctx, info.key, client.WithDryMode(true)) 43 | cancel() 44 | if err != nil { 45 | return err 46 | } 47 | info.txFee = uint64(info.feeData.CreateSubnetTxFee) 48 | info.subnetIDType = "EXPECTED SUBNET ID" 49 | info.subnetID = sid 50 | if err := info.CheckBalance(); err != nil { 51 | return err 52 | } 53 | 54 | msg := MakeCreateTable(info) 55 | if enablePrompt { 56 | msg = formatter.F("\n{{blue}}{{bold}}Ready to create subnet resources, should we continue?{{/}}\n") + msg 57 | } 58 | fmt.Fprint(formatter.ColorableStdOut, msg) 59 | 60 | if enablePrompt { 61 | prompt := promptui.Select{ 62 | Label: "\n", 63 | Stdout: os.Stdout, 64 | Items: []string{ 65 | formatter.F("{{red}}No, stop it!{{/}}"), 66 | formatter.F("{{green}}Yes, let's create! {{bold}}{{underline}}I agree to pay the fee{{/}}{{green}}!{{/}}"), 67 | }, 68 | } 69 | idx, _, err := prompt.Run() 70 | if err != nil { 71 | panic(err) 72 | } 73 | if idx == 0 { 74 | return nil 75 | } 76 | } 77 | 78 | println() 79 | println() 80 | println() 81 | ctx, cancel = context.WithTimeout(context.Background(), requestTimeout) 82 | subnetID, took, err := cli.P().CreateSubnet(ctx, info.key) 83 | cancel() 84 | if err != nil { 85 | return err 86 | } 87 | info.subnetIDType = "CREATED SUBNET ID" 88 | info.subnetID = subnetID 89 | 90 | color.Outf("{{magenta}}created subnet{{/}} %q {{light-gray}}(took %v){{/}}\n", info.subnetID, took) 91 | color.Outf("({{orange}}subnet must be whitelisted beforehand via{{/}} {{cyan}}{{bold}}--whitelisted-subnets{{/}} {{orange}}flag!{{/}})\n\n") 92 | 93 | info.requiredBalance = 0 94 | info.stakeAmount = 0 95 | info.txFee = 0 96 | ctx, cancel = context.WithTimeout(context.Background(), requestTimeout) 97 | info.balance, err = cli.P().Balance(ctx, info.key) 98 | cancel() 99 | if err != nil { 100 | return err 101 | } 102 | fmt.Fprint(formatter.ColorableStdOut, MakeCreateTable(info)) 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /cmd/create_vmid.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/ava-labs/avalanchego/ids" 10 | "github.com/ava-labs/avalanchego/utils/hashing" 11 | "github.com/ava-labs/subnet-cli/pkg/color" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | const ( 16 | IDLen = 32 17 | ) 18 | 19 | var h bool 20 | 21 | func newCreateVMIDCommand() *cobra.Command { 22 | cmd := &cobra.Command{ 23 | Use: "VMID [options] ", 24 | Short: "Creates a new encoded VMID from a string", 25 | RunE: createVMIDFunc, 26 | } 27 | 28 | cmd.PersistentFlags().BoolVar(&h, "hash", false, "whether or not to hash the identifier argument") 29 | 30 | return cmd 31 | } 32 | 33 | func createVMIDFunc(cmd *cobra.Command, args []string) error { 34 | if len(args) != 1 { 35 | return fmt.Errorf("expected 1 argument but got %d", len(args)) 36 | } 37 | 38 | identifier := []byte(args[0]) //nolint:ifshort 39 | var b []byte 40 | if h { 41 | b = hashing.ComputeHash256(identifier) 42 | } else { 43 | if len(identifier) > IDLen { 44 | return fmt.Errorf("non-hashed name must be <= 32 bytes, found %d", len(identifier)) 45 | } 46 | b = make([]byte, IDLen) 47 | copy(b, identifier) 48 | } 49 | 50 | id, err := ids.ToID(b) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | color.Outf("{{green}}created a new VMID %s from %s{{/}}\n", id.String(), args[0]) 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /cmd/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "errors" 8 | ) 9 | 10 | var ErrInsufficientFunds = errors.New("insufficient funds") 11 | -------------------------------------------------------------------------------- /cmd/remove.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "github.com/ava-labs/avalanchego/ids" 8 | "github.com/onsi/ginkgo/v2/formatter" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // RemoveCommand implements "subnet-cli remove" command. 13 | func RemoveCommand() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "remove", 16 | Short: "Sub-commands for removing resources", 17 | } 18 | cmd.AddCommand( 19 | newRemoveSubnetValidatorCommand(), 20 | ) 21 | cmd.PersistentFlags().StringVar(&publicURI, "public-uri", "https://api.avax-test.network", "URI for avalanche network endpoints") 22 | cmd.PersistentFlags().StringVar(&privKeyPath, "private-key-path", ".subnet-cli.pk", "private key file path") 23 | cmd.PersistentFlags().BoolVarP(&useLedger, "ledger", "l", false, "use ledger to sign transactions") 24 | return cmd 25 | } 26 | 27 | func CreateRemoveValidator(i *Info) string { 28 | buf, tb := BaseTableSetup(i) 29 | tb.Append([]string{formatter.F("{{orange}}NODE IDs{{/}}"), formatter.F("{{light-gray}}{{bold}}%v{{/}}", i.nodeIDs)}) 30 | if i.subnetID != ids.Empty { 31 | tb.Append([]string{formatter.F("{{blue}}SUBNET ID{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.subnetID)}) 32 | } 33 | if i.rewardAddr != ids.ShortEmpty { 34 | tb.Append([]string{formatter.F("{{cyan}}{{bold}}REWARD ADDRESS{{/}}"), formatter.F("{{light-gray}}%s{{/}}", i.rewardAddr)}) 35 | } 36 | if i.changeAddr != ids.ShortEmpty { 37 | tb.Append([]string{formatter.F("{{cyan}}{{bold}}CHANGE ADDRESS{{/}}"), formatter.F("{{light-gray}}%s{{/}}", i.changeAddr)}) 38 | } 39 | tb.Render() 40 | return buf.String() 41 | } 42 | -------------------------------------------------------------------------------- /cmd/remove_subnet_validator.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | 11 | "github.com/ava-labs/avalanchego/ids" 12 | "github.com/ava-labs/subnet-cli/pkg/color" 13 | "github.com/manifoldco/promptui" 14 | "github.com/onsi/ginkgo/v2/formatter" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | func newRemoveSubnetValidatorCommand() *cobra.Command { 19 | cmd := &cobra.Command{ 20 | Use: "subnet-validator", 21 | Short: "Removes the validator from a subnet", 22 | Long: ` 23 | Removes a subnet validator. 24 | 25 | $ subnet-cli remove subnet-validator \ 26 | --private-key-path=.insecure.ewoq.key \ 27 | --public-uri=http://localhost:52250 \ 28 | --subnet-id="24tZhrm8j8GCJRE9PomW8FaeqbgGS4UAQjJnqqn8pq5NwYSYV1" \ 29 | --node-ids="NodeID-4B4rc5vdD1758JSBYL1xyvE5NHGzz6xzH" 30 | `, 31 | RunE: removeSubnetValidatorFunc, 32 | } 33 | 34 | cmd.PersistentFlags().StringVar(&subnetIDs, "subnet-id", "", "subnet ID (must be formatted in ids.ID)") 35 | cmd.PersistentFlags().StringSliceVar(&nodeIDs, "node-ids", nil, "a list of node IDs (must be formatted in ids.ID)") 36 | 37 | return cmd 38 | } 39 | 40 | func removeSubnetValidatorFunc(cmd *cobra.Command, args []string) error { 41 | cli, info, err := InitClient(publicURI, true) 42 | if err != nil { 43 | return err 44 | } 45 | info.subnetID, err = ids.FromString(subnetIDs) 46 | if err != nil { 47 | return err 48 | } 49 | info.txFee = uint64(info.feeData.TxFee) 50 | if err := ParseNodeIDs(cli, info, false); err != nil { 51 | return err 52 | } 53 | if len(info.nodeIDs) == 0 { 54 | color.Outf("{{magenta}}no subnet validators to add{{/}}\n") 55 | return nil 56 | } 57 | info.txFee *= uint64(len(info.nodeIDs)) 58 | info.requiredBalance = info.txFee 59 | if err := info.CheckBalance(); err != nil { 60 | return err 61 | } 62 | msg := CreateRemoveValidator(info) 63 | if enablePrompt { 64 | msg = formatter.F("\n{{blue}}{{bold}}Ready to remove subnet validator, should we continue?{{/}}\n") + msg 65 | } 66 | fmt.Fprint(formatter.ColorableStdOut, msg) 67 | 68 | if enablePrompt { 69 | prompt := promptui.Select{ 70 | Label: "\n", 71 | Stdout: os.Stdout, 72 | Items: []string{ 73 | formatter.F("{{green}}Yes, let's remove! {{bold}}{{underline}}I agree to pay the fee{{/}}{{green}}!{{/}}"), 74 | formatter.F("{{red}}No, stop it!{{/}}"), 75 | }, 76 | } 77 | idx, _, err := prompt.Run() 78 | if err != nil { 79 | return nil //nolint:nilerr 80 | } 81 | if idx == 1 { 82 | return nil 83 | } 84 | } 85 | 86 | println() 87 | println() 88 | println() 89 | for _, nodeID := range info.nodeIDs { 90 | // valInfo is not populated because [ParseNodeIDs] called on info.subnetID 91 | // 92 | // TODO: cleanup 93 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 94 | took, err := cli.P().RemoveSubnetValidator( 95 | ctx, 96 | info.key, 97 | info.subnetID, 98 | nodeID, 99 | ) 100 | cancel() 101 | if err != nil { 102 | return err 103 | } 104 | color.Outf("{{magenta}}removed %s from subnet %s validator set{{/}} {{light-gray}}(took %v){{/}}\n\n", nodeID, info.subnetID, took) 105 | } 106 | WaitValidatorRemoval(cli, info.nodeIDs, info) 107 | info.requiredBalance = 0 108 | info.stakeAmount = 0 109 | info.txFee = 0 110 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 111 | info.balance, err = cli.P().Balance(ctx, info.key) 112 | cancel() 113 | if err != nil { 114 | return err 115 | } 116 | fmt.Fprint(formatter.ColorableStdOut, CreateAddTable(info)) 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "time" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/ava-labs/subnet-cli/pkg/logutil" 12 | ) 13 | 14 | var rootCmd = &cobra.Command{ 15 | Use: "subnet-cli", 16 | Short: "subnet-cli CLI", 17 | Version: Version, 18 | SuggestFor: []string{"subnet-cli", "subnetcli", "subnetctl"}, 19 | } 20 | 21 | var ( 22 | enablePrompt bool 23 | logLevel string 24 | 25 | privKeyPath string 26 | useLedger bool 27 | 28 | privateURI string 29 | publicURI string 30 | 31 | pollInterval time.Duration 32 | requestTimeout time.Duration 33 | 34 | subnetIDs string 35 | nodeIDs []string 36 | stakeAmount uint64 37 | 38 | validateEnds string 39 | validateWeight uint64 40 | validateRewardFeePercent uint32 41 | 42 | rewardAddrs string 43 | changeAddrs string 44 | 45 | chainName string 46 | vmIDs string 47 | vmGenesisPath string 48 | 49 | blockchainID string 50 | checkBootstrapped bool 51 | ) 52 | 53 | func init() { 54 | cobra.EnablePrefixMatching = true 55 | 56 | rootCmd.AddCommand( 57 | CreateCommand(), 58 | AddCommand(), 59 | RemoveCommand(), 60 | StatusCommand(), 61 | WizardCommand(), 62 | ) 63 | 64 | rootCmd.PersistentFlags().BoolVar(&enablePrompt, "enable-prompt", true, "'true' to enable prompt mode") 65 | rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", logutil.DefaultLogLevel, "log level") 66 | rootCmd.PersistentFlags().DurationVar(&pollInterval, "poll-interval", time.Second, "interval to poll tx/blockchain status") 67 | rootCmd.PersistentFlags().DurationVar(&requestTimeout, "request-timeout", 2*time.Minute, "request timeout") 68 | } 69 | 70 | func Execute() error { 71 | if err := CreateLogger(); err != nil { 72 | return err 73 | } 74 | return rootCmd.Execute() 75 | } 76 | -------------------------------------------------------------------------------- /cmd/status.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // NewCommand implements "subnet-cli status" command. 11 | func StatusCommand() *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "status", 14 | Short: "status commands", 15 | } 16 | cmd.AddCommand( 17 | newStatusBlockchainCommand(), 18 | ) 19 | cmd.PersistentFlags().StringVar(&privateURI, "private-uri", "", "URI for avalanche network endpoints") 20 | return cmd 21 | } 22 | -------------------------------------------------------------------------------- /cmd/status_blockchain.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // Package implements "status" sub-commands. 5 | package cmd 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/ava-labs/avalanchego/ids" 11 | pstatus "github.com/ava-labs/avalanchego/vms/platformvm/status" 12 | internal_platformvm "github.com/ava-labs/subnet-cli/internal/platformvm" 13 | "github.com/ava-labs/subnet-cli/pkg/color" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | func newStatusBlockchainCommand() *cobra.Command { 18 | cmd := &cobra.Command{ 19 | Use: "blockchain [BLOCKCHAIN ID]", 20 | Short: "blockchain commands", 21 | Long: ` 22 | Checks the status of the blockchain. 23 | 24 | $ subnet-cli status blockchain \ 25 | --blockchain-id=[BLOCKCHAIN ID] \ 26 | --private-uri=http://localhost:49738 \ 27 | --check-bootstrapped 28 | 29 | `, 30 | RunE: createStatusFunc, 31 | } 32 | 33 | cmd.PersistentFlags().StringVar(&blockchainID, "blockchain-id", "", "blockchain to check the status of") 34 | cmd.PersistentFlags().BoolVar(&checkBootstrapped, "check-bootstrapped", false, "'true' to wait until the blockchain is bootstrapped") 35 | return cmd 36 | } 37 | 38 | func createStatusFunc(cmd *cobra.Command, args []string) error { 39 | cli, _, err := InitClient(privateURI, false) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | blkChainID, err := ids.FromString(blockchainID) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | opts := []internal_platformvm.OpOption{ 50 | internal_platformvm.WithBlockchainID(blkChainID), 51 | internal_platformvm.WithBlockchainStatus(pstatus.Validating), 52 | } 53 | if checkBootstrapped { 54 | opts = append(opts, internal_platformvm.WithCheckBlockchainBootstrapped(cli.Info().Client())) 55 | } 56 | 57 | color.Outf("\n{{blue}}Checking blockchain...{{/}}\n") 58 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 59 | _, err = cli.P().Checker().PollBlockchain(ctx, opts...) 60 | cancel() 61 | return err 62 | } 63 | -------------------------------------------------------------------------------- /cmd/wizard.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package cmd 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "os" 11 | "time" 12 | 13 | "github.com/ava-labs/avalanchego/ids" 14 | "github.com/ava-labs/avalanchego/utils/units" 15 | "github.com/dustin/go-humanize" 16 | "github.com/manifoldco/promptui" 17 | "github.com/onsi/ginkgo/v2/formatter" 18 | "github.com/spf13/cobra" 19 | 20 | "github.com/ava-labs/subnet-cli/client" 21 | "github.com/ava-labs/subnet-cli/pkg/color" 22 | ) 23 | 24 | // WizardCommand implements "subnet-cli wizard" command. 25 | func WizardCommand() *cobra.Command { 26 | cmd := &cobra.Command{ 27 | Use: "wizard", 28 | Short: "A magical command for creating an entire subnet", 29 | RunE: wizardFunc, 30 | } 31 | 32 | // "create subnet" 33 | cmd.PersistentFlags().StringVar(&publicURI, "public-uri", "https://api.avax-test.network", "URI for avalanche network endpoints") 34 | cmd.PersistentFlags().StringVar(&privKeyPath, "private-key-path", ".subnet-cli.pk", "private key file path") 35 | cmd.PersistentFlags().BoolVarP(&useLedger, "ledger", "l", false, "use ledger to sign transactions") 36 | 37 | // "add validator" 38 | cmd.PersistentFlags().StringSliceVar(&nodeIDs, "node-ids", nil, "a list of node IDs (must be formatted in ids.ID)") 39 | end := time.Now().Add(defaultValDuration) 40 | cmd.PersistentFlags().StringVar(&validateEnds, "validate-end", end.Format(time.RFC3339), "validate start timestamp in RFC3339 format") 41 | 42 | // "create blockchain" 43 | cmd.PersistentFlags().StringVar(&chainName, "chain-name", "", "chain name") 44 | cmd.PersistentFlags().StringVar(&vmIDs, "vm-id", "", "VM ID (must be formatted in ids.ID)") 45 | cmd.PersistentFlags().StringVar(&vmGenesisPath, "vm-genesis-path", "", "VM genesis file path") 46 | 47 | return cmd 48 | } 49 | 50 | func wizardFunc(cmd *cobra.Command, args []string) error { 51 | cli, info, err := InitClient(publicURI, true) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | if len(nodeIDs) == 0 { 57 | return errors.New("no NodeIDs provided") 58 | } 59 | 60 | // Parse Args 61 | info.subnetID = ids.Empty 62 | if err := ParseNodeIDs(cli, info, true); err != nil { 63 | return err 64 | } 65 | info.stakeAmount = stakeAmount 66 | info.validateEnd, err = time.Parse(time.RFC3339, validateEnds) 67 | if err != nil { 68 | return err 69 | } 70 | info.validateWeight = defaultValidateWeight 71 | info.validateRewardFeePercent = defaultValFeePercent 72 | info.rewardAddr = info.key.Addresses()[0] 73 | info.changeAddr = info.key.Addresses()[0] 74 | info.vmID, err = ids.FromString(vmIDs) 75 | if err != nil { 76 | return err 77 | } 78 | vmGenesisBytes, err := os.ReadFile(vmGenesisPath) 79 | if err != nil { 80 | return err 81 | } 82 | info.chainName = chainName 83 | info.vmGenesisPath = vmGenesisPath 84 | 85 | // Compute dry run cost/actions for approval 86 | info.totalStakeAmount = uint64(len(info.nodeIDs)) * info.stakeAmount 87 | info.txFee = uint64(info.feeData.CreateSubnetTxFee) + uint64(info.feeData.TxFee)*uint64(len(info.allNodeIDs)) + uint64(info.feeData.CreateBlockchainTxFee) 88 | info.requiredBalance = info.stakeAmount + info.txFee 89 | if err := info.CheckBalance(); err != nil { 90 | return err 91 | } 92 | 93 | msg := CreateSpellPreTable(info) 94 | if enablePrompt { 95 | msg = formatter.F("\n{{blue}}{{bold}}Ready to run wizard, should we continue?{{/}}\n") + msg 96 | } 97 | fmt.Fprint(formatter.ColorableStdOut, msg) 98 | 99 | prompt := promptui.Select{ 100 | Label: "\n", 101 | Stdout: os.Stdout, 102 | Items: []string{ 103 | formatter.F("{{green}}Yes, let's create! {{bold}}{{underline}}I agree to pay the fee{{/}}{{green}}!{{/}}"), 104 | formatter.F("{{red}}No, stop it!{{/}}"), 105 | }, 106 | } 107 | idx, _, err := prompt.Run() 108 | if err != nil { 109 | return nil //nolint:nilerr 110 | } 111 | if idx == 1 { 112 | return nil 113 | } 114 | println() 115 | println() 116 | 117 | // Ensure all nodes are validators on the primary network 118 | for i, nodeID := range info.nodeIDs { 119 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 120 | info.validateStart = time.Now().Add(30 * time.Second) 121 | took, err := cli.P().AddValidator( 122 | ctx, 123 | info.key, 124 | nodeID, 125 | info.validateStart, 126 | info.validateEnd, 127 | client.WithStakeAmount(info.stakeAmount), 128 | client.WithRewardShares(info.validateRewardFeePercent*10000), 129 | client.WithRewardAddress(info.rewardAddr), 130 | client.WithChangeAddress(info.changeAddr), 131 | ) 132 | cancel() 133 | if err != nil { 134 | return err 135 | } 136 | color.Outf("{{magenta}}added %s to primary network validator set{{/}} {{light-gray}}(took %v){{/}}\n\n", nodeID, took) 137 | if i < len(info.nodeIDs)-1 { 138 | info.validateEnd = info.validateEnd.Add(defaultStagger) 139 | } 140 | } 141 | if len(info.nodeIDs) > 0 { 142 | WaitValidator(cli, info.nodeIDs, info) 143 | println() 144 | println() 145 | } 146 | 147 | // Create subnet 148 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 149 | subnetID, took, err := cli.P().CreateSubnet(ctx, info.key) 150 | cancel() 151 | if err != nil { 152 | return err 153 | } 154 | info.subnetID = subnetID 155 | color.Outf("{{magenta}}created subnet{{/}} %q {{light-gray}}(took %v){{/}}\n", info.subnetID, took) 156 | 157 | // Pause for operator to whitelist subnet on all validators (and to remind 158 | // that a binary by the name of [vmIDs] must be in the plugins dir) 159 | color.Outf("\n\n\n{{cyan}}Now, time for some config changes on your node(s).\nSet --whitelisted-subnets=%s and move the compiled VM %s to /plugins/%s.\nWhen you're finished, restart your node.{{/}}\n", info.subnetID, info.vmID, info.vmID) 160 | prompt = promptui.Select{ 161 | Label: "\n", 162 | Stdout: os.Stdout, 163 | Items: []string{ 164 | formatter.F("{{green}}Yes, let's continue!{{bold}}{{underline}} I've updated --whitelisted-subnets, built my VM, and restarted my node(s)!{{/}}"), 165 | formatter.F("{{red}}No, stop it!{{/}}"), 166 | }, 167 | } 168 | idx, _, err = prompt.Run() 169 | if err != nil { 170 | return nil //nolint:nilerr 171 | } 172 | if idx == 1 { 173 | return nil 174 | } 175 | println() 176 | println() 177 | 178 | // Add validators to subnet 179 | for _, nodeID := range info.allNodeIDs { // do all nodes, not parsed 180 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 181 | valInfo := info.valInfos[nodeID] 182 | start := time.Now().Add(30 * time.Second) 183 | took, err := cli.P().AddSubnetValidator( 184 | ctx, 185 | info.key, 186 | info.subnetID, 187 | nodeID, 188 | start, 189 | valInfo.end, 190 | validateWeight, 191 | ) 192 | cancel() 193 | if err != nil { 194 | return err 195 | } 196 | color.Outf("{{magenta}}added %s to subnet %s validator set{{/}} {{light-gray}}(took %v){{/}}\n\n", nodeID, info.subnetID, took) 197 | } 198 | 199 | // Because [info.subnetID] was set to the new subnetID, [WaitValidator] will 200 | // lookup status for subnetID 201 | WaitValidator(cli, info.allNodeIDs, info) 202 | println() 203 | println() 204 | 205 | // Add blockchain to subnet 206 | ctx, cancel = context.WithTimeout(context.Background(), requestTimeout) 207 | blockchainID, took, err := cli.P().CreateBlockchain( 208 | ctx, 209 | info.key, 210 | info.subnetID, 211 | info.chainName, 212 | info.vmID, 213 | vmGenesisBytes, 214 | ) 215 | cancel() 216 | if err != nil { 217 | return err 218 | } 219 | info.blockchainID = blockchainID 220 | color.Outf("{{magenta}}created blockchain{{/}} %q {{light-gray}}(took %v){{/}}\n\n", info.blockchainID, took) 221 | 222 | // Print out summary of actions (subnetID, chainID, validator periods) 223 | info.requiredBalance = 0 224 | info.stakeAmount = 0 225 | info.totalStakeAmount = 0 226 | info.txFee = 0 227 | ctx, cancel = context.WithTimeout(context.Background(), requestTimeout) 228 | info.balance, err = cli.P().Balance(ctx, info.key) 229 | cancel() 230 | if err != nil { 231 | return err 232 | } 233 | fmt.Fprint(formatter.ColorableStdOut, CreateSpellPostTable(info)) 234 | return nil 235 | } 236 | 237 | func CreateSpellPreTable(i *Info) string { 238 | buf, tb := BaseTableSetup(i) 239 | if len(i.nodeIDs) > 0 { 240 | tb.Append([]string{formatter.F("{{magenta}}NEW PRIMARY NETWORK VALIDATORS{{/}}"), formatter.F("{{light-gray}}{{bold}}%v{{/}}", i.nodeIDs)}) 241 | tb.Append([]string{formatter.F("{{magenta}}VALIDATE END{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.validateEnd.Format(time.RFC3339))}) 242 | stakeAmount := float64(i.stakeAmount) / float64(units.Avax) 243 | stakeAmounts := humanize.FormatFloat("#,###.###", stakeAmount) 244 | tb.Append([]string{formatter.F("{{magenta}}STAKE AMOUNT{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}} $AVAX", stakeAmounts)}) 245 | validateRewardFeePercent := humanize.FormatFloat("#,###.###", float64(i.validateRewardFeePercent)) 246 | tb.Append([]string{formatter.F("{{magenta}}VALIDATE REWARD FEE{{/}}"), formatter.F("{{light-gray}}{{bold}}{{underline}}%s{{/}} %%", validateRewardFeePercent)}) 247 | tb.Append([]string{formatter.F("{{cyan}}{{bold}}REWARD ADDRESS{{/}}"), formatter.F("{{light-gray}}%s{{/}}", i.rewardAddr)}) 248 | tb.Append([]string{formatter.F("{{cyan}}{{bold}}CHANGE ADDRESS{{/}}"), formatter.F("{{light-gray}}%s{{/}}", i.changeAddr)}) 249 | } 250 | 251 | tb.Append([]string{formatter.F("{{orange}}NEW SUBNET VALIDATORS{{/}}"), formatter.F("{{light-gray}}{{bold}}%v{{/}}", i.allNodeIDs)}) 252 | tb.Append([]string{formatter.F("{{magenta}}SUBNET VALIDATION WEIGHT{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", humanize.Comma(int64(i.validateWeight)))}) 253 | 254 | tb.Append([]string{formatter.F("{{dark-green}}CHAIN NAME{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.chainName)}) 255 | tb.Append([]string{formatter.F("{{dark-green}}VM ID{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.vmID)}) 256 | tb.Append([]string{formatter.F("{{dark-green}}VM GENESIS PATH{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.vmGenesisPath)}) 257 | tb.Render() 258 | return buf.String() 259 | } 260 | 261 | func CreateSpellPostTable(i *Info) string { 262 | buf, tb := BaseTableSetup(i) 263 | if len(i.nodeIDs) > 0 { 264 | tb.Append([]string{formatter.F("{{magenta}}PRIMARY NETWORK VALIDATORS{{/}}"), formatter.F("{{light-gray}}{{bold}}%v{{/}}", i.nodeIDs)}) 265 | } 266 | 267 | tb.Append([]string{formatter.F("{{orange}}SUBNET VALIDATORS{{/}}"), formatter.F("{{light-gray}}{{bold}}%v{{/}}", i.allNodeIDs)}) 268 | tb.Append([]string{formatter.F("{{blue}}SUBNET ID{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.subnetID)}) 269 | tb.Append([]string{formatter.F("{{blue}}BLOCKCHAIN ID{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.blockchainID)}) 270 | 271 | tb.Append([]string{formatter.F("{{dark-green}}CHAIN NAME{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.chainName)}) 272 | tb.Append([]string{formatter.F("{{dark-green}}VM ID{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.vmID)}) 273 | tb.Append([]string{formatter.F("{{dark-green}}VM GENESIS PATH{{/}}"), formatter.F("{{light-gray}}{{bold}}%s{{/}}", i.vmGenesisPath)}) 274 | tb.Render() 275 | return buf.String() 276 | } 277 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ava-labs/subnet-cli 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/ava-labs/avalanche-ledger-go v0.0.9 7 | github.com/ava-labs/avalanche-network-runner v1.2.4-0.20221013165946-228f1f3a6d9e 8 | github.com/ava-labs/avalanchego v1.9.0 9 | github.com/dustin/go-humanize v1.0.0 10 | github.com/gyuho/avax-tester v0.0.4 11 | github.com/manifoldco/promptui v0.9.0 12 | github.com/olekukonko/tablewriter v0.0.5 13 | github.com/onsi/ginkgo/v2 v2.3.1 14 | github.com/onsi/gomega v1.22.0 15 | github.com/spf13/cobra v1.5.0 16 | go.uber.org/zap v1.23.0 17 | ) 18 | 19 | require ( 20 | github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect 21 | github.com/Microsoft/go-winio v0.5.2 // indirect 22 | github.com/NYTimes/gziphandler v1.1.1 // indirect 23 | github.com/VictoriaMetrics/fastcache v1.10.0 // indirect 24 | github.com/aead/siphash v1.0.1 // indirect 25 | github.com/ava-labs/coreth v0.11.0-rc.4 // indirect 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/btcsuite/btcd v0.23.1 // indirect 28 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect 29 | github.com/btcsuite/btcd/btcutil v1.1.1 // indirect 30 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect 31 | github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect 32 | github.com/btcsuite/btcutil v1.0.2 // indirect 33 | github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect 34 | github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect 35 | github.com/btcsuite/winsvc v1.0.0 // indirect 36 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 37 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 38 | github.com/davecgh/go-spew v1.1.1 // indirect 39 | github.com/deckarep/golang-set v1.8.0 // indirect 40 | github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect 41 | github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0-20200627015759-01fd2de07837 // indirect 42 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 43 | github.com/decred/dcrd/lru v1.1.1 // indirect 44 | github.com/ethereum/go-ethereum v1.10.25 // indirect 45 | github.com/fatih/color v1.13.0 // indirect 46 | github.com/felixge/httpsnoop v1.0.1 // indirect 47 | github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 // indirect 48 | github.com/fsnotify/fsnotify v1.5.4 // indirect 49 | github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect 50 | github.com/go-ole/go-ole v1.2.6 // indirect 51 | github.com/go-stack/stack v1.8.0 // indirect 52 | github.com/golang-jwt/jwt v3.2.1+incompatible // indirect 53 | github.com/golang/mock v1.6.0 // indirect 54 | github.com/golang/protobuf v1.5.2 // indirect 55 | github.com/golang/snappy v0.0.4 // indirect 56 | github.com/google/btree v1.1.2 // indirect 57 | github.com/google/go-cmp v0.5.8 // indirect 58 | github.com/google/uuid v1.2.0 // indirect 59 | github.com/gorilla/handlers v1.5.1 // indirect 60 | github.com/gorilla/mux v1.8.0 // indirect 61 | github.com/gorilla/rpc v1.2.0 // indirect 62 | github.com/gorilla/websocket v1.4.2 // indirect 63 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 64 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect 65 | github.com/hashicorp/go-bexpr v0.1.10 // indirect 66 | github.com/hashicorp/go-hclog v1.2.2 // indirect 67 | github.com/hashicorp/go-plugin v1.4.4 // indirect 68 | github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect 69 | github.com/hashicorp/hcl v1.0.0 // indirect 70 | github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce // indirect 71 | github.com/holiman/bloomfilter/v2 v2.0.3 // indirect 72 | github.com/holiman/uint256 v1.2.0 // indirect 73 | github.com/huin/goupnp v1.0.3 // indirect 74 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 75 | github.com/jackpal/gateway v1.0.6 // indirect 76 | github.com/jackpal/go-nat-pmp v1.0.2 // indirect 77 | github.com/jessevdk/go-flags v1.5.0 // indirect 78 | github.com/jrick/logrotate v1.0.0 // indirect 79 | github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect 80 | github.com/kkdai/bstream v1.0.0 // indirect 81 | github.com/linxGnu/grocksdb v1.6.34 // indirect 82 | github.com/magiconair/properties v1.8.6 // indirect 83 | github.com/mattn/go-colorable v0.1.12 // indirect 84 | github.com/mattn/go-isatty v0.0.14 // indirect 85 | github.com/mattn/go-runewidth v0.0.9 // indirect 86 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 87 | github.com/mitchellh/go-homedir v1.1.0 // indirect 88 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 89 | github.com/mitchellh/mapstructure v1.5.0 // indirect 90 | github.com/mitchellh/pointerstructure v1.2.0 // indirect 91 | github.com/mr-tron/base58 v1.2.0 // indirect 92 | github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect 93 | github.com/oklog/run v1.1.0 // indirect 94 | github.com/otiai10/copy v1.7.0 // indirect 95 | github.com/pelletier/go-toml v1.9.5 // indirect 96 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 97 | github.com/pkg/errors v0.9.1 // indirect 98 | github.com/pmezard/go-difflib v1.0.0 // indirect 99 | github.com/prometheus/client_golang v1.13.0 // indirect 100 | github.com/prometheus/client_model v0.2.0 // indirect 101 | github.com/prometheus/common v0.37.0 // indirect 102 | github.com/prometheus/procfs v0.8.0 // indirect 103 | github.com/rjeczalik/notify v0.9.2 // indirect 104 | github.com/rs/cors v1.7.0 // indirect 105 | github.com/shirou/gopsutil v3.21.11+incompatible // indirect 106 | github.com/spaolacci/murmur3 v1.1.0 // indirect 107 | github.com/spf13/afero v1.8.2 // indirect 108 | github.com/spf13/cast v1.5.0 // indirect 109 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 110 | github.com/spf13/pflag v1.0.5 // indirect 111 | github.com/spf13/viper v1.12.0 // indirect 112 | github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969 // indirect 113 | github.com/stretchr/objx v0.4.0 // indirect 114 | github.com/stretchr/testify v1.8.0 // indirect 115 | github.com/subosito/gotenv v1.3.0 // indirect 116 | github.com/supranational/blst v0.3.11-0.20220920110316-f72618070295 // indirect 117 | github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect 118 | github.com/tklauser/go-sysconf v0.3.5 // indirect 119 | github.com/tklauser/numcpus v0.2.2 // indirect 120 | github.com/tyler-smith/go-bip39 v1.0.2 // indirect 121 | github.com/yusufpapurcu/wmi v1.2.2 // indirect 122 | github.com/zondax/hid v0.9.0 // indirect 123 | github.com/zondax/ledger-go v0.12.3-0.20221005223406-dbd460b7296d // indirect 124 | go.uber.org/atomic v1.9.0 // indirect 125 | go.uber.org/multierr v1.8.0 // indirect 126 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect 127 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect 128 | golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect 129 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect 130 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 131 | golang.org/x/text v0.3.7 // indirect 132 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 133 | gonum.org/v1/gonum v0.11.0 // indirect 134 | google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect 135 | google.golang.org/grpc v1.50.0-dev // indirect 136 | google.golang.org/protobuf v1.28.1 // indirect 137 | gopkg.in/ini.v1 v1.66.4 // indirect 138 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 139 | gopkg.in/urfave/cli.v1 v1.20.0 // indirect 140 | gopkg.in/yaml.v2 v2.4.0 // indirect 141 | gopkg.in/yaml.v3 v3.0.1 // indirect 142 | ) 143 | -------------------------------------------------------------------------------- /img/add-subnet-validator-local-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/subnet-cli/c7819ca6349a3818269bca4227970c422a9c687d/img/add-subnet-validator-local-1.png -------------------------------------------------------------------------------- /img/add-subnet-validator-local-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/subnet-cli/c7819ca6349a3818269bca4227970c422a9c687d/img/add-subnet-validator-local-2.png -------------------------------------------------------------------------------- /img/add-validator-local-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/subnet-cli/c7819ca6349a3818269bca4227970c422a9c687d/img/add-validator-local-1.png -------------------------------------------------------------------------------- /img/add-validator-local-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/subnet-cli/c7819ca6349a3818269bca4227970c422a9c687d/img/add-validator-local-2.png -------------------------------------------------------------------------------- /img/create-blockchain-local-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/subnet-cli/c7819ca6349a3818269bca4227970c422a9c687d/img/create-blockchain-local-1.png -------------------------------------------------------------------------------- /img/create-blockchain-local-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/subnet-cli/c7819ca6349a3818269bca4227970c422a9c687d/img/create-blockchain-local-2.png -------------------------------------------------------------------------------- /img/create-subnet-local-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/subnet-cli/c7819ca6349a3818269bca4227970c422a9c687d/img/create-subnet-local-1.png -------------------------------------------------------------------------------- /img/create-subnet-local-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/subnet-cli/c7819ca6349a3818269bca4227970c422a9c687d/img/create-subnet-local-2.png -------------------------------------------------------------------------------- /img/wizard-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/subnet-cli/c7819ca6349a3818269bca4227970c422a9c687d/img/wizard-1.png -------------------------------------------------------------------------------- /img/wizard-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ava-labs/subnet-cli/c7819ca6349a3818269bca4227970c422a9c687d/img/wizard-2.png -------------------------------------------------------------------------------- /internal/README: -------------------------------------------------------------------------------- 1 | 2 | All helper functions/methods to be moved back to avalanchego. 3 | -------------------------------------------------------------------------------- /internal/avax/avax.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package avax 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/ava-labs/avalanchego/codec" 10 | "github.com/ava-labs/avalanchego/vms/components/avax" 11 | ) 12 | 13 | func ParseUTXO(ub []byte, cd codec.Manager) (*avax.UTXO, error) { 14 | utxo := new(avax.UTXO) 15 | if _, err := cd.Unmarshal(ub, utxo); err != nil { 16 | return nil, fmt.Errorf("failed to unmarshal utxo bytes: %w", err) 17 | } 18 | return utxo, nil 19 | } 20 | -------------------------------------------------------------------------------- /internal/key/hard_key.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package key 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/ava-labs/subnet-cli/pkg/color" 12 | 13 | ledger "github.com/ava-labs/avalanche-ledger-go" 14 | "github.com/ava-labs/avalanchego/ids" 15 | "github.com/ava-labs/avalanchego/utils/crypto" 16 | "github.com/ava-labs/avalanchego/utils/formatting/address" 17 | "github.com/ava-labs/avalanchego/utils/hashing" 18 | "github.com/ava-labs/avalanchego/vms/components/avax" 19 | "github.com/ava-labs/avalanchego/vms/components/verify" 20 | "github.com/ava-labs/avalanchego/vms/platformvm/txs" 21 | "github.com/ava-labs/avalanchego/vms/secp256k1fx" 22 | "github.com/manifoldco/promptui" 23 | "github.com/onsi/ginkgo/v2/formatter" 24 | "go.uber.org/zap" 25 | ) 26 | 27 | const ( 28 | numAddresses = 1024 29 | ) 30 | 31 | var _ Key = &HardKey{} 32 | 33 | type HardKey struct { 34 | l ledger.Ledger 35 | 36 | pAddrs []string 37 | shortAddrs []ids.ShortID 38 | shortAddrMap map[ids.ShortID]uint32 39 | } 40 | 41 | func parseLedgerErr(err error, fallback string) { 42 | errString := err.Error() 43 | switch { 44 | case strings.Contains(errString, "LedgerHID device") && strings.Contains(errString, "not found"): 45 | color.Outf("{{red}}ledger is not connected{{/}}\n") 46 | case strings.Contains(errString, "6b0c"): 47 | color.Outf("{{red}}ledger is not unlocked{{/}}\n") 48 | case strings.Contains(errString, "APDU_CODE_CONDITIONS_NOT_SATISFIED"): 49 | color.Outf("{{red}}ledger rejected signing{{/}}\n") 50 | default: 51 | color.Outf("{{red}}%s: %v{{/}}\n", fallback, err) 52 | } 53 | } 54 | 55 | // retriableLedgerAction wraps all Ledger calls to allow the user to try and 56 | // recover instead of exiting (in case their Ledger locks). 57 | func retriableLegerAction(f func() error, fallback string) error { 58 | for { 59 | rerr := f() 60 | if rerr == nil { 61 | return nil 62 | } 63 | parseLedgerErr(rerr, fallback) 64 | 65 | color.Outf("\n{{cyan}}ledger action failed...what now?{{/}}\n") 66 | prompt := promptui.Select{ 67 | Label: "\n", 68 | Stdout: os.Stdout, 69 | Items: []string{ 70 | formatter.F("{{green}}retry{{/}}"), 71 | formatter.F("{{red}}exit{{/}}"), 72 | }, 73 | } 74 | idx, _, err := prompt.Run() 75 | if err != nil || idx == 1 { 76 | return rerr 77 | } 78 | } 79 | } 80 | 81 | func NewHard(networkID uint32) (*HardKey, error) { 82 | k := &HardKey{} 83 | color.Outf("{{yellow}}connecting to ledger...{{/}}\n") 84 | if err := retriableLegerAction(func() error { 85 | l, err := ledger.New() 86 | if err != nil { 87 | return err 88 | } 89 | k.l = l 90 | return nil 91 | }, "failed to connect to ledger"); err != nil { 92 | return nil, err 93 | } 94 | 95 | color.Outf("{{yellow}}deriving address from ledger...{{/}}\n") 96 | hrp := getHRP(networkID) 97 | if err := retriableLegerAction(func() error { 98 | addrs, err := k.l.Addresses(numAddresses) 99 | if err != nil { 100 | return err 101 | } 102 | laddrs := len(addrs) 103 | k.pAddrs = make([]string, laddrs) 104 | k.shortAddrs = make([]ids.ShortID, laddrs) 105 | k.shortAddrMap = map[ids.ShortID]uint32{} 106 | for i, addr := range addrs { 107 | k.pAddrs[i], err = address.Format("P", hrp, addr[:]) 108 | if err != nil { 109 | return err 110 | } 111 | k.shortAddrs[i] = addr 112 | k.shortAddrMap[addr] = uint32(i) 113 | } 114 | return nil 115 | }, "failed to get extended public key"); err != nil { 116 | return nil, err 117 | } 118 | 119 | color.Outf("{{yellow}}derived primary address from ledger: %s{{/}}\n", k.pAddrs[0]) 120 | return k, nil 121 | } 122 | 123 | func (h *HardKey) Disconnect() error { 124 | return h.l.Disconnect() 125 | } 126 | 127 | func (h *HardKey) P() []string { return h.pAddrs } 128 | 129 | func (h *HardKey) Addresses() []ids.ShortID { 130 | return h.shortAddrs 131 | } 132 | 133 | func (h *HardKey) Spends(outputs []*avax.UTXO, opts ...OpOption) ( 134 | totalBalanceToSpend uint64, 135 | inputs []*avax.TransferableInput, 136 | signers [][]ids.ShortID, 137 | ) { 138 | ret := &Op{} 139 | ret.applyOpts(opts) 140 | 141 | for _, out := range outputs { 142 | input, txsigners, err := h.spend(out, ret.time) 143 | if err != nil { 144 | zap.L().Warn("cannot spend with current key", zap.Error(err)) 145 | continue 146 | } 147 | totalBalanceToSpend += input.Amount() 148 | inputs = append(inputs, &avax.TransferableInput{ 149 | UTXOID: out.UTXOID, 150 | Asset: out.Asset, 151 | In: input, 152 | }) 153 | signers = append(signers, txsigners) 154 | if ret.targetAmount > 0 && 155 | totalBalanceToSpend > ret.targetAmount+ret.feeDeduct { 156 | break 157 | } 158 | } 159 | SortTransferableInputsWithSigners(inputs, signers) 160 | return totalBalanceToSpend, inputs, signers 161 | } 162 | 163 | func (h *HardKey) spend(output *avax.UTXO, time uint64) ( 164 | input avax.TransferableIn, 165 | signers []ids.ShortID, 166 | err error, 167 | ) { 168 | // "time" is used to check whether the key owner 169 | // is still within the lock time (thus can't spend). 170 | inputf, signers, err := h.lspend(output.Out, time) 171 | if err != nil { 172 | return nil, nil, err 173 | } 174 | var ok bool 175 | input, ok = inputf.(avax.TransferableIn) 176 | if !ok { 177 | return nil, nil, ErrInvalidType 178 | } 179 | return input, signers, nil 180 | } 181 | 182 | func (h *HardKey) lspend(out verify.Verifiable, time uint64) (verify.Verifiable, []ids.ShortID, error) { 183 | switch out := out.(type) { 184 | case *secp256k1fx.MintOutput: 185 | if sigIndices, signers, able := h.Match(&out.OutputOwners, time); able { 186 | return &secp256k1fx.Input{ 187 | SigIndices: sigIndices, 188 | }, signers, nil 189 | } 190 | return nil, nil, ErrCantSpend 191 | case *secp256k1fx.TransferOutput: 192 | if sigIndices, signers, able := h.Match(&out.OutputOwners, time); able { 193 | return &secp256k1fx.TransferInput{ 194 | Amt: out.Amt, 195 | Input: secp256k1fx.Input{ 196 | SigIndices: sigIndices, 197 | }, 198 | }, signers, nil 199 | } 200 | return nil, nil, ErrCantSpend 201 | } 202 | return nil, nil, fmt.Errorf("can't spend UTXO because it is unexpected type %T", out) 203 | } 204 | 205 | func (h *HardKey) Match(owners *secp256k1fx.OutputOwners, time uint64) ([]uint32, []ids.ShortID, bool) { 206 | if time < owners.Locktime { 207 | return nil, nil, false 208 | } 209 | sigs := make([]uint32, 0, owners.Threshold) 210 | signers := make([]ids.ShortID, 0, owners.Threshold) 211 | for i := uint32(0); i < uint32(len(owners.Addrs)) && uint32(len(sigs)) < owners.Threshold; i++ { 212 | if _, ok := h.shortAddrMap[owners.Addrs[i]]; ok { 213 | sigs = append(sigs, i) 214 | signers = append(signers, owners.Addrs[i]) 215 | } 216 | } 217 | return sigs, signers, uint32(len(sigs)) == owners.Threshold 218 | } 219 | 220 | // Sign transaction with the Ledger private key 221 | // 222 | // This is a slightly modified version of *platformvm.Tx.Sign(). 223 | func (h *HardKey) Sign(pTx *txs.Tx, signers [][]ids.ShortID) error { 224 | unsignedBytes, err := txs.Codec.Marshal(txs.Version, &pTx.Unsigned) 225 | if err != nil { 226 | return fmt.Errorf("couldn't marshal UnsignedTx: %w", err) 227 | } 228 | hash := hashing.ComputeHash256(unsignedBytes) 229 | 230 | // Generate signature 231 | uniqueSigners := map[uint32]struct{}{} 232 | for _, inputSigners := range signers { 233 | for _, signer := range inputSigners { 234 | if v, ok := h.shortAddrMap[signer]; ok { 235 | uniqueSigners[v] = struct{}{} 236 | } else { 237 | // Should never happen 238 | return ErrCantSpend 239 | } 240 | } 241 | } 242 | indices := make([]uint32, 0, len(uniqueSigners)) 243 | for idx := range uniqueSigners { 244 | indices = append(indices, idx) 245 | } 246 | 247 | var sigs [][]byte 248 | if err := retriableLegerAction(func() error { 249 | sigs, err = h.l.SignHash(hash, indices) 250 | if err != nil { 251 | return err 252 | } 253 | return nil 254 | }, "failed to sign hash"); err != nil { 255 | return fmt.Errorf("problem generating signatures: %w", err) 256 | } 257 | sigMap := map[ids.ShortID][]byte{} 258 | for i, idx := range indices { 259 | sigMap[h.shortAddrs[idx]] = sigs[i] 260 | } 261 | 262 | // Add credentials to transaction 263 | for _, inputSigners := range signers { 264 | cred := &secp256k1fx.Credential{ 265 | Sigs: make([][crypto.SECP256K1RSigLen]byte, len(inputSigners)), 266 | } 267 | for i, signer := range inputSigners { 268 | copy(cred.Sigs[i][:], sigMap[signer]) 269 | } 270 | pTx.Creds = append(pTx.Creds, cred) 271 | } 272 | 273 | // Create signed tx bytes 274 | signedBytes, err := txs.Codec.Marshal(txs.Version, pTx) 275 | if err != nil { 276 | return fmt.Errorf("couldn't marshal ProposalTx: %w", err) 277 | } 278 | pTx.Initialize(unsignedBytes, signedBytes) 279 | return nil 280 | } 281 | -------------------------------------------------------------------------------- /internal/key/key.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // Package key implements key manager and helper functions. 5 | package key 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "sort" 11 | 12 | "github.com/ava-labs/avalanchego/ids" 13 | "github.com/ava-labs/avalanchego/utils/constants" 14 | "github.com/ava-labs/avalanchego/vms/components/avax" 15 | "github.com/ava-labs/avalanchego/vms/platformvm/txs" 16 | "github.com/ava-labs/avalanchego/vms/secp256k1fx" 17 | ) 18 | 19 | var ( 20 | ErrInvalidType = errors.New("invalid type") 21 | ErrCantSpend = errors.New("can't spend") 22 | ) 23 | 24 | // Key defines methods for key manager interface. 25 | type Key interface { 26 | // P returns all formatted P-Chain addresses. 27 | P() []string 28 | // Addresses returns the all raw ids.ShortID address. 29 | Addresses() []ids.ShortID 30 | // Match attempts to match a list of addresses up to the provided threshold. 31 | Match(owners *secp256k1fx.OutputOwners, time uint64) ([]uint32, []ids.ShortID, bool) 32 | // Spend attempts to spend all specified UTXOs (outputs) 33 | // and returns the new UTXO inputs. 34 | // 35 | // If target amount is specified, it only uses the 36 | // outputs until the total spending is below the target 37 | // amount. 38 | Spends(outputs []*avax.UTXO, opts ...OpOption) ( 39 | totalBalanceToSpend uint64, 40 | inputs []*avax.TransferableInput, 41 | signers [][]ids.ShortID, 42 | ) 43 | // Sign generates [numSigs] signatures and attaches them to [pTx]. 44 | Sign(pTx *txs.Tx, signers [][]ids.ShortID) error 45 | } 46 | 47 | type Op struct { 48 | time uint64 49 | targetAmount uint64 50 | feeDeduct uint64 51 | } 52 | 53 | type OpOption func(*Op) 54 | 55 | func (op *Op) applyOpts(opts []OpOption) { 56 | for _, opt := range opts { 57 | opt(op) 58 | } 59 | } 60 | 61 | func WithTime(t uint64) OpOption { 62 | return func(op *Op) { 63 | op.time = t 64 | } 65 | } 66 | 67 | func WithTargetAmount(ta uint64) OpOption { 68 | return func(op *Op) { 69 | op.targetAmount = ta 70 | } 71 | } 72 | 73 | // To deduct transfer fee from total spend (output). 74 | // e.g., "units.MilliAvax" for X/P-Chain transfer. 75 | func WithFeeDeduct(fee uint64) OpOption { 76 | return func(op *Op) { 77 | op.feeDeduct = fee 78 | } 79 | } 80 | 81 | func getHRP(networkID uint32) string { 82 | switch networkID { 83 | case constants.LocalID: 84 | return constants.LocalHRP 85 | case constants.FujiID: 86 | return constants.FujiHRP 87 | case constants.MainnetID: 88 | return constants.MainnetHRP 89 | default: 90 | return constants.FallbackHRP 91 | } 92 | } 93 | 94 | type innerSortTransferableInputsWithSigners struct { 95 | ins []*avax.TransferableInput 96 | signers [][]ids.ShortID 97 | } 98 | 99 | func (ins *innerSortTransferableInputsWithSigners) Less(i, j int) bool { 100 | iID, iIndex := ins.ins[i].InputSource() 101 | jID, jIndex := ins.ins[j].InputSource() 102 | 103 | switch bytes.Compare(iID[:], jID[:]) { 104 | case -1: 105 | return true 106 | case 0: 107 | return iIndex < jIndex 108 | default: 109 | return false 110 | } 111 | } 112 | func (ins *innerSortTransferableInputsWithSigners) Len() int { return len(ins.ins) } 113 | func (ins *innerSortTransferableInputsWithSigners) Swap(i, j int) { 114 | ins.ins[j], ins.ins[i] = ins.ins[i], ins.ins[j] 115 | ins.signers[j], ins.signers[i] = ins.signers[i], ins.signers[j] 116 | } 117 | 118 | // SortTransferableInputsWithSigners sorts the inputs and signers based on the 119 | // input's utxo ID. 120 | // 121 | // This is based off of (generics?): https://github.com/ava-labs/avalanchego/blob/224c9fd23d41839201dd0275ac864a845de6e93e/vms/components/avax/transferables.go#L202 122 | func SortTransferableInputsWithSigners(ins []*avax.TransferableInput, signers [][]ids.ShortID) { 123 | sort.Sort(&innerSortTransferableInputsWithSigners{ins: ins, signers: signers}) 124 | } 125 | -------------------------------------------------------------------------------- /internal/key/key_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package key 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "path/filepath" 10 | "testing" 11 | 12 | "github.com/ava-labs/avalanchego/utils/cb58" 13 | "github.com/ava-labs/avalanchego/utils/crypto" 14 | ) 15 | 16 | const ( 17 | ewoqPChainAddr = "P-custom18jma8ppw3nhx5r4ap8clazz0dps7rv5u9xde7p" 18 | fallbackNetworkID = 999999 // unaffiliated networkID should trigger HRP Fallback 19 | ) 20 | 21 | func TestNewKeyEwoq(t *testing.T) { 22 | t.Parallel() 23 | 24 | m, err := NewSoft( 25 | fallbackNetworkID, 26 | WithPrivateKeyEncoded(EwoqPrivateKey), 27 | ) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | if m.P()[0] != ewoqPChainAddr { 33 | t.Fatalf("unexpected P-Chain address %q, expected %q", m.P(), ewoqPChainAddr) 34 | } 35 | 36 | keyPath := filepath.Join(t.TempDir(), "key.pk") 37 | if err := m.Save(keyPath); err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | m2, err := LoadSoft(fallbackNetworkID, keyPath) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | if !bytes.Equal(m.Raw(), m2.Raw()) { 47 | t.Fatalf("loaded key unexpected %v, expected %v", m2.Raw(), m.Raw()) 48 | } 49 | } 50 | 51 | func TestNewKey(t *testing.T) { 52 | t.Parallel() 53 | 54 | skBytes, err := cb58.Decode(rawEwoqPk) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | factory := &crypto.FactorySECP256K1R{} 59 | rpk, err := factory.ToPrivateKey(skBytes) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | ewoqPk, _ := rpk.(*crypto.PrivateKeySECP256K1R) 64 | 65 | rpk2, err := factory.NewPrivateKey() 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | privKey2, _ := rpk2.(*crypto.PrivateKeySECP256K1R) 70 | 71 | tt := []struct { 72 | name string 73 | opts []SOpOption 74 | expErr error 75 | }{ 76 | { 77 | name: "test", 78 | opts: nil, 79 | expErr: nil, 80 | }, 81 | { 82 | name: "ewop with WithPrivateKey", 83 | opts: []SOpOption{ 84 | WithPrivateKey(ewoqPk), 85 | }, 86 | expErr: nil, 87 | }, 88 | { 89 | name: "ewop with WithPrivateKeyEncoded", 90 | opts: []SOpOption{ 91 | WithPrivateKeyEncoded(EwoqPrivateKey), 92 | }, 93 | expErr: nil, 94 | }, 95 | { 96 | name: "ewop with WithPrivateKey/WithPrivateKeyEncoded", 97 | opts: []SOpOption{ 98 | WithPrivateKey(ewoqPk), 99 | WithPrivateKeyEncoded(EwoqPrivateKey), 100 | }, 101 | expErr: nil, 102 | }, 103 | { 104 | name: "ewop with invalid WithPrivateKey", 105 | opts: []SOpOption{ 106 | WithPrivateKey(privKey2), 107 | WithPrivateKeyEncoded(EwoqPrivateKey), 108 | }, 109 | expErr: ErrInvalidPrivateKey, 110 | }, 111 | } 112 | for i, tv := range tt { 113 | _, err := NewSoft(fallbackNetworkID, tv.opts...) 114 | if !errors.Is(err, tv.expErr) { 115 | t.Fatalf("#%d(%s): unexpected error %v, expected %v", i, tv.name, err, tv.expErr) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /internal/key/soft_key.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package key 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "encoding/hex" 10 | "errors" 11 | "io" 12 | "os" 13 | "strings" 14 | 15 | "github.com/ava-labs/avalanchego/ids" 16 | "github.com/ava-labs/avalanchego/utils/cb58" 17 | "github.com/ava-labs/avalanchego/utils/crypto" 18 | "github.com/ava-labs/avalanchego/utils/formatting/address" 19 | "github.com/ava-labs/avalanchego/vms/components/avax" 20 | "github.com/ava-labs/avalanchego/vms/platformvm/txs" 21 | "github.com/ava-labs/avalanchego/vms/secp256k1fx" 22 | "go.uber.org/zap" 23 | ) 24 | 25 | var ( 26 | ErrInvalidPrivateKey = errors.New("invalid private key") 27 | ErrInvalidPrivateKeyLen = errors.New("invalid private key length (expect 64 bytes in hex)") 28 | ErrInvalidPrivateKeyEnding = errors.New("invalid private key ending") 29 | ErrInvalidPrivateKeyEncoding = errors.New("invalid private key encoding") 30 | ) 31 | 32 | var _ Key = &SoftKey{} 33 | 34 | type SoftKey struct { 35 | privKey *crypto.PrivateKeySECP256K1R 36 | privKeyRaw []byte 37 | privKeyEncoded string 38 | 39 | pAddr string 40 | 41 | keyChain *secp256k1fx.Keychain 42 | } 43 | 44 | const ( 45 | privKeyEncPfx = "PrivateKey-" 46 | privKeySize = 64 47 | 48 | rawEwoqPk = "ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN" 49 | EwoqPrivateKey = "PrivateKey-" + rawEwoqPk 50 | ) 51 | 52 | var keyFactory = new(crypto.FactorySECP256K1R) 53 | 54 | type SOp struct { 55 | privKey *crypto.PrivateKeySECP256K1R 56 | privKeyEncoded string 57 | } 58 | 59 | type SOpOption func(*SOp) 60 | 61 | func (sop *SOp) applyOpts(opts []SOpOption) { 62 | for _, opt := range opts { 63 | opt(sop) 64 | } 65 | } 66 | 67 | // To create a new key SoftKey with a pre-loaded private key. 68 | func WithPrivateKey(privKey *crypto.PrivateKeySECP256K1R) SOpOption { 69 | return func(sop *SOp) { 70 | sop.privKey = privKey 71 | } 72 | } 73 | 74 | // To create a new key SoftKey with a pre-defined private key. 75 | func WithPrivateKeyEncoded(privKey string) SOpOption { 76 | return func(sop *SOp) { 77 | sop.privKeyEncoded = privKey 78 | } 79 | } 80 | 81 | func NewSoft(networkID uint32, opts ...SOpOption) (*SoftKey, error) { 82 | ret := &SOp{} 83 | ret.applyOpts(opts) 84 | 85 | // set via "WithPrivateKeyEncoded" 86 | if len(ret.privKeyEncoded) > 0 { 87 | privKey, err := decodePrivateKey(ret.privKeyEncoded) 88 | if err != nil { 89 | return nil, err 90 | } 91 | // to not overwrite 92 | if ret.privKey != nil && 93 | !bytes.Equal(ret.privKey.Bytes(), privKey.Bytes()) { 94 | return nil, ErrInvalidPrivateKey 95 | } 96 | ret.privKey = privKey 97 | } 98 | 99 | // generate a new one 100 | if ret.privKey == nil { 101 | rpk, err := keyFactory.NewPrivateKey() 102 | if err != nil { 103 | return nil, err 104 | } 105 | var ok bool 106 | ret.privKey, ok = rpk.(*crypto.PrivateKeySECP256K1R) 107 | if !ok { 108 | return nil, ErrInvalidType 109 | } 110 | } 111 | 112 | privKey := ret.privKey 113 | privKeyEncoded, err := encodePrivateKey(ret.privKey) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | // double-check encoding is consistent 119 | if ret.privKeyEncoded != "" && 120 | ret.privKeyEncoded != privKeyEncoded { 121 | return nil, ErrInvalidPrivateKeyEncoding 122 | } 123 | 124 | keyChain := secp256k1fx.NewKeychain() 125 | keyChain.Add(privKey) 126 | 127 | m := &SoftKey{ 128 | privKey: privKey, 129 | privKeyRaw: privKey.Bytes(), 130 | privKeyEncoded: privKeyEncoded, 131 | 132 | keyChain: keyChain, 133 | } 134 | 135 | // Parse HRP to create valid address 136 | hrp := getHRP(networkID) 137 | m.pAddr, err = address.Format("P", hrp, m.privKey.PublicKey().Address().Bytes()) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return m, nil 143 | } 144 | 145 | // LoadSoft loads the private key from disk and creates the corresponding SoftKey. 146 | func LoadSoft(networkID uint32, keyPath string) (*SoftKey, error) { 147 | kb, err := os.ReadFile(keyPath) 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | // in case, it's already encoded 153 | k, err := NewSoft(networkID, WithPrivateKeyEncoded(string(kb))) 154 | if err == nil { 155 | return k, nil 156 | } 157 | 158 | r := bufio.NewReader(bytes.NewBuffer(kb)) 159 | buf := make([]byte, privKeySize) 160 | n, err := readASCII(buf, r) 161 | if err != nil { 162 | return nil, err 163 | } 164 | if n != len(buf) { 165 | return nil, ErrInvalidPrivateKeyLen 166 | } 167 | if err := checkKeyFileEnd(r); err != nil { 168 | return nil, err 169 | } 170 | 171 | skBytes, err := hex.DecodeString(string(buf)) 172 | if err != nil { 173 | return nil, err 174 | } 175 | rpk, err := keyFactory.ToPrivateKey(skBytes) 176 | if err != nil { 177 | return nil, err 178 | } 179 | privKey, ok := rpk.(*crypto.PrivateKeySECP256K1R) 180 | if !ok { 181 | return nil, ErrInvalidType 182 | } 183 | 184 | return NewSoft(networkID, WithPrivateKey(privKey)) 185 | } 186 | 187 | // readASCII reads into 'buf', stopping when the buffer is full or 188 | // when a non-printable control character is encountered. 189 | func readASCII(buf []byte, r io.ByteReader) (n int, err error) { 190 | for ; n < len(buf); n++ { 191 | buf[n], err = r.ReadByte() 192 | switch { 193 | case errors.Is(err, io.EOF) || buf[n] < '!': 194 | return n, nil 195 | case err != nil: 196 | return n, err 197 | } 198 | } 199 | return n, nil 200 | } 201 | 202 | const fileEndLimit = 1 203 | 204 | // checkKeyFileEnd skips over additional newlines at the end of a key file. 205 | func checkKeyFileEnd(r io.ByteReader) error { 206 | for idx := 0; ; idx++ { 207 | b, err := r.ReadByte() 208 | switch { 209 | case errors.Is(err, io.EOF): 210 | return nil 211 | case err != nil: 212 | return err 213 | case b != '\n' && b != '\r': 214 | return ErrInvalidPrivateKeyEnding 215 | case idx > fileEndLimit: 216 | return ErrInvalidPrivateKeyLen 217 | } 218 | } 219 | } 220 | 221 | func encodePrivateKey(pk *crypto.PrivateKeySECP256K1R) (string, error) { 222 | privKeyRaw := pk.Bytes() 223 | enc, err := cb58.Encode(privKeyRaw) 224 | if err != nil { 225 | return "", err 226 | } 227 | return privKeyEncPfx + enc, nil 228 | } 229 | 230 | func decodePrivateKey(enc string) (*crypto.PrivateKeySECP256K1R, error) { 231 | rawPk := strings.Replace(enc, privKeyEncPfx, "", 1) 232 | skBytes, err := cb58.Decode(rawPk) 233 | if err != nil { 234 | return nil, err 235 | } 236 | rpk, err := keyFactory.ToPrivateKey(skBytes) 237 | if err != nil { 238 | return nil, err 239 | } 240 | privKey, ok := rpk.(*crypto.PrivateKeySECP256K1R) 241 | if !ok { 242 | return nil, ErrInvalidType 243 | } 244 | return privKey, nil 245 | } 246 | 247 | // Returns the private key. 248 | func (m *SoftKey) Key() *crypto.PrivateKeySECP256K1R { 249 | return m.privKey 250 | } 251 | 252 | // Returns the private key in raw bytes. 253 | func (m *SoftKey) Raw() []byte { 254 | return m.privKeyRaw 255 | } 256 | 257 | // Returns the private key encoded in CB58 and "PrivateKey-" prefix. 258 | func (m *SoftKey) Encode() string { 259 | return m.privKeyEncoded 260 | } 261 | 262 | // Saves the private key to disk with hex encoding. 263 | func (m *SoftKey) Save(p string) error { 264 | k := hex.EncodeToString(m.privKeyRaw) 265 | return os.WriteFile(p, []byte(k), fsModeWrite) 266 | } 267 | 268 | func (m *SoftKey) P() []string { return []string{m.pAddr} } 269 | 270 | func (m *SoftKey) Spends(outputs []*avax.UTXO, opts ...OpOption) ( 271 | totalBalanceToSpend uint64, 272 | inputs []*avax.TransferableInput, 273 | signers [][]ids.ShortID, 274 | ) { 275 | ret := &Op{} 276 | ret.applyOpts(opts) 277 | 278 | for _, out := range outputs { 279 | input, psigners, err := m.spend(out, ret.time) 280 | if err != nil { 281 | zap.L().Warn("cannot spend with current key", zap.Error(err)) 282 | continue 283 | } 284 | totalBalanceToSpend += input.Amount() 285 | inputs = append(inputs, &avax.TransferableInput{ 286 | UTXOID: out.UTXOID, 287 | Asset: out.Asset, 288 | In: input, 289 | }) 290 | // Convert to ids.ShortID to adhere with interface 291 | pksigners := make([]ids.ShortID, len(psigners)) 292 | for i, psigner := range psigners { 293 | pksigners[i] = psigner.PublicKey().Address() 294 | } 295 | signers = append(signers, pksigners) 296 | if ret.targetAmount > 0 && 297 | totalBalanceToSpend > ret.targetAmount+ret.feeDeduct { 298 | break 299 | } 300 | } 301 | SortTransferableInputsWithSigners(inputs, signers) 302 | return totalBalanceToSpend, inputs, signers 303 | } 304 | 305 | func (m *SoftKey) spend(output *avax.UTXO, time uint64) ( 306 | input avax.TransferableIn, 307 | signers []*crypto.PrivateKeySECP256K1R, 308 | err error, 309 | ) { 310 | // "time" is used to check whether the key owner 311 | // is still within the lock time (thus can't spend). 312 | inputf, psigners, err := m.keyChain.Spend(output.Out, time) 313 | if err != nil { 314 | return nil, nil, err 315 | } 316 | var ok bool 317 | input, ok = inputf.(avax.TransferableIn) 318 | if !ok { 319 | return nil, nil, ErrInvalidType 320 | } 321 | return input, psigners, nil 322 | } 323 | 324 | const fsModeWrite = 0o600 325 | 326 | func (m *SoftKey) Addresses() []ids.ShortID { 327 | return []ids.ShortID{m.privKey.PublicKey().Address()} 328 | } 329 | 330 | func (m *SoftKey) Sign(pTx *txs.Tx, signers [][]ids.ShortID) error { 331 | privsigners := make([][]*crypto.PrivateKeySECP256K1R, len(signers)) 332 | for i, inputSigners := range signers { 333 | privsigners[i] = make([]*crypto.PrivateKeySECP256K1R, len(inputSigners)) 334 | for j, signer := range inputSigners { 335 | if signer != m.privKey.PublicKey().Address() { 336 | // Should never happen 337 | return ErrCantSpend 338 | } 339 | privsigners[i][j] = m.privKey 340 | } 341 | } 342 | 343 | return pTx.Sign(txs.Codec, privsigners) 344 | } 345 | 346 | func (m *SoftKey) Match(owners *secp256k1fx.OutputOwners, time uint64) ([]uint32, []ids.ShortID, bool) { 347 | indices, privs, ok := m.keyChain.Match(owners, time) 348 | pks := make([]ids.ShortID, len(privs)) 349 | for i, priv := range privs { 350 | pks[i] = priv.PublicKey().Address() 351 | } 352 | return indices, pks, ok 353 | } 354 | -------------------------------------------------------------------------------- /internal/platformvm/checker.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package platformvm 5 | 6 | // TO BE MOVED TO "github.com/ava-labs/avalanchego/vms/platformvm" 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | "time" 12 | 13 | "github.com/ava-labs/avalanchego/api/info" 14 | "github.com/ava-labs/avalanchego/ids" 15 | "github.com/ava-labs/avalanchego/vms/platformvm" 16 | pstatus "github.com/ava-labs/avalanchego/vms/platformvm/status" 17 | "github.com/ava-labs/subnet-cli/internal/poll" 18 | "go.uber.org/zap" 19 | ) 20 | 21 | var ( 22 | ErrInvalidCheckerOpOption = errors.New("invalid checker OpOption") 23 | ErrEmptyID = errors.New("empty ID") 24 | ErrAbortedDropped = errors.New("aborted/dropped") 25 | ) 26 | 27 | type Checker interface { 28 | PollTx(ctx context.Context, txID ids.ID, s pstatus.Status) (time.Duration, error) 29 | PollSubnet(ctx context.Context, subnetID ids.ID) (time.Duration, error) 30 | PollBlockchain(ctx context.Context, opts ...OpOption) (time.Duration, error) 31 | } 32 | 33 | var _ Checker = &checker{} 34 | 35 | type checker struct { 36 | poller poll.Poller 37 | cli platformvm.Client 38 | } 39 | 40 | func NewChecker(poller poll.Poller, cli platformvm.Client) Checker { 41 | return &checker{ 42 | poller: poller, 43 | cli: cli, 44 | } 45 | } 46 | 47 | func (c *checker) PollTx(ctx context.Context, txID ids.ID, s pstatus.Status) (time.Duration, error) { 48 | zap.L().Info("polling P-Chain tx", 49 | zap.String("txId", txID.String()), 50 | zap.String("expectedStatus", s.String()), 51 | ) 52 | return c.poller.Poll(ctx, func() (done bool, err error) { 53 | status, err := c.cli.GetTxStatus(ctx, txID) 54 | if err != nil { 55 | return false, err 56 | } 57 | zap.L().Debug("tx", 58 | zap.String("status", status.Status.String()), 59 | zap.String("reason", status.Reason), 60 | ) 61 | if s == pstatus.Committed && 62 | (status.Status == pstatus.Aborted || status.Status == pstatus.Dropped) { 63 | return true, ErrAbortedDropped 64 | } 65 | return status.Status == s, nil 66 | }) 67 | } 68 | 69 | func (c *checker) PollSubnet(ctx context.Context, subnetID ids.ID) (took time.Duration, err error) { 70 | if subnetID == ids.Empty { 71 | return took, ErrEmptyID 72 | } 73 | 74 | zap.L().Info("polling subnet", 75 | zap.String("subnetId", subnetID.String()), 76 | ) 77 | took, err = c.PollTx(ctx, subnetID, pstatus.Committed) 78 | if err != nil { 79 | return took, err 80 | } 81 | prev := took 82 | took, err = c.findSubnet(ctx, subnetID) 83 | took += prev 84 | return took, err 85 | } 86 | 87 | func (c *checker) findSubnet(ctx context.Context, subnetID ids.ID) (took time.Duration, err error) { 88 | zap.L().Info("finding subnets", 89 | zap.String("subnetId", subnetID.String()), 90 | ) 91 | took, err = c.poller.Poll(ctx, func() (done bool, err error) { 92 | ss, err := c.cli.GetSubnets(ctx, []ids.ID{subnetID}) 93 | if err != nil { 94 | return false, err 95 | } 96 | if len(ss) != 1 { 97 | return false, nil 98 | } 99 | return ss[0].ID == subnetID, nil 100 | }) 101 | return took, err 102 | } 103 | 104 | func (c *checker) PollBlockchain(ctx context.Context, opts ...OpOption) (took time.Duration, err error) { 105 | ret := &Op{} 106 | ret.applyOpts(opts) 107 | 108 | if ret.subnetID == ids.Empty && 109 | ret.blockchainID == ids.Empty { 110 | return took, ErrEmptyID 111 | } 112 | 113 | if ret.blockchainID == ids.Empty { 114 | ret.blockchainID, took, err = c.findBlockchain(ctx, ret.subnetID) 115 | if err != nil { 116 | return took, err 117 | } 118 | } 119 | if ret.blockchainID == ids.Empty { 120 | return took, ErrEmptyID 121 | } 122 | 123 | if ret.checkBlockchainBootstrapped && ret.info == nil { 124 | return took, ErrInvalidCheckerOpOption 125 | } 126 | 127 | zap.L().Info("polling blockchain", 128 | zap.String("blockchainId", ret.blockchainID.String()), 129 | zap.String("expectedBlockchainStatus", ret.blockchainStatus.String()), 130 | ) 131 | 132 | prev := took 133 | took, err = c.PollTx(ctx, ret.blockchainID, pstatus.Committed) 134 | took += prev 135 | if err != nil { 136 | return took, err 137 | } 138 | 139 | statusPolled := false 140 | prev = took 141 | took, err = c.poller.Poll(ctx, func() (done bool, err error) { 142 | if !statusPolled { 143 | status, err := c.cli.GetBlockchainStatus(ctx, ret.blockchainID.String()) 144 | if err != nil { 145 | return false, err 146 | } 147 | if status != ret.blockchainStatus { 148 | zap.L().Info("waiting for blockchain status", 149 | zap.String("current", status.String()), 150 | ) 151 | return false, nil 152 | } 153 | statusPolled = true 154 | if !ret.checkBlockchainBootstrapped { 155 | return true, nil 156 | } 157 | } 158 | 159 | bootstrapped, err := ret.info.IsBootstrapped(ctx, ret.blockchainID.String()) 160 | if err != nil { 161 | return false, err 162 | } 163 | if !bootstrapped { 164 | zap.L().Debug("blockchain not bootstrapped yet; retrying") 165 | return false, nil 166 | } 167 | return true, nil 168 | }) 169 | took += prev 170 | return took, err 171 | } 172 | 173 | func (c *checker) findBlockchain(ctx context.Context, subnetID ids.ID) (bchID ids.ID, took time.Duration, err error) { 174 | zap.L().Info("finding blockchains", 175 | zap.String("subnetId", subnetID.String()), 176 | ) 177 | took, err = c.poller.Poll(ctx, func() (done bool, err error) { 178 | bcs, err := c.cli.GetBlockchains(ctx) 179 | if err != nil { 180 | return false, err 181 | } 182 | bchID = ids.Empty 183 | for _, blockchain := range bcs { 184 | if blockchain.SubnetID == subnetID { 185 | bchID = blockchain.ID 186 | break 187 | } 188 | } 189 | return bchID == ids.Empty, nil 190 | }) 191 | return bchID, took, err 192 | } 193 | 194 | type Op struct { 195 | subnetID ids.ID 196 | blockchainID ids.ID 197 | 198 | blockchainStatus pstatus.BlockchainStatus 199 | 200 | info info.Client 201 | checkBlockchainBootstrapped bool 202 | } 203 | 204 | type OpOption func(*Op) 205 | 206 | func (op *Op) applyOpts(opts []OpOption) { 207 | for _, opt := range opts { 208 | opt(op) 209 | } 210 | } 211 | 212 | func WithSubnetID(subnetID ids.ID) OpOption { 213 | return func(op *Op) { 214 | op.subnetID = subnetID 215 | } 216 | } 217 | 218 | func WithBlockchainID(bch ids.ID) OpOption { 219 | return func(op *Op) { 220 | op.blockchainID = bch 221 | } 222 | } 223 | 224 | func WithBlockchainStatus(s pstatus.BlockchainStatus) OpOption { 225 | return func(op *Op) { 226 | op.blockchainStatus = s 227 | } 228 | } 229 | 230 | // TODO: avalanchego "GetBlockchainStatusReply" should have "Bootstrapped". 231 | // e.g., "service.vm.Chains.IsBootstrapped" in "GetBlockchainStatus". 232 | func WithCheckBlockchainBootstrapped(info info.Client) OpOption { 233 | return func(op *Op) { 234 | op.info = info 235 | op.checkBlockchainBootstrapped = true 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /internal/platformvm/checker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package platformvm 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "testing" 10 | ) 11 | 12 | func TestChecker(t *testing.T) { 13 | t.Parallel() 14 | 15 | ck := NewChecker(nil, nil) 16 | _, err := ck.PollBlockchain(context.Background()) 17 | if !errors.Is(err, ErrEmptyID) { 18 | t.Fatalf("unexpected error %v, expected %v", err, ErrEmptyID) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/poll/poll.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // Package poll defines polling mechanisms. 5 | package poll 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "time" 11 | 12 | "go.uber.org/zap" 13 | ) 14 | 15 | var ErrAborted = errors.New("aborted") 16 | 17 | type Poller interface { 18 | // Polls until "check" function returns "done=true". 19 | // If "check" returns a non-empty error, it logs and 20 | // continues the polling until context is canceled. 21 | // It returns the duration that it took to complete the check. 22 | Poll( 23 | ctx context.Context, 24 | check func() (done bool, err error), 25 | ) (time.Duration, error) 26 | } 27 | 28 | var _ Poller = &poller{} 29 | 30 | type poller struct { 31 | interval time.Duration 32 | } 33 | 34 | func New(interval time.Duration) Poller { 35 | return &poller{ 36 | interval: interval, 37 | } 38 | } 39 | 40 | func (pl *poller) Poll(ctx context.Context, check func() (done bool, err error)) (took time.Duration, err error) { 41 | start := time.Now() 42 | zap.L().Info("start polling", zap.String("internal", pl.interval.String())) 43 | 44 | // poll first with no wait 45 | tc := time.NewTicker(1) 46 | defer tc.Stop() 47 | 48 | for ctx.Err() == nil { 49 | select { 50 | case <-ctx.Done(): 51 | return time.Since(start), ctx.Err() 52 | case <-tc.C: 53 | tc.Reset(pl.interval) 54 | } 55 | 56 | done, err := check() 57 | if err != nil { 58 | zap.L().Warn("poll check failed", zap.Error(err)) 59 | continue 60 | } 61 | if !done { 62 | continue 63 | } 64 | 65 | took := time.Since(start) 66 | zap.L().Info("poll confirmed", zap.String("took", took.String())) 67 | return took, nil 68 | } 69 | 70 | return time.Since(start), ctx.Err() 71 | } 72 | -------------------------------------------------------------------------------- /internal/poll/poll_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package poll 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestPoll(t *testing.T) { 14 | t.Parallel() 15 | 16 | pl := New(time.Minute) 17 | 18 | rootCtx, cancel := context.WithCancel(context.Background()) 19 | cancel() 20 | if _, err := pl.Poll(rootCtx, nil); !errors.Is(err, context.Canceled) { 21 | t.Fatalf("unexpected Poll error %v", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/ava-labs/subnet-cli/cmd" 11 | ) 12 | 13 | func main() { 14 | if err := cmd.Execute(); err != nil { 15 | fmt.Fprintf(os.Stderr, "subnet-cli failed %v\n", err) 16 | os.Exit(1) 17 | } 18 | os.Exit(0) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/color/color.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package color 5 | 6 | import ( 7 | "fmt" 8 | 9 | formatter "github.com/onsi/ginkgo/v2/formatter" 10 | ) 11 | 12 | // Outputs to stdout. 13 | // 14 | // e.g., 15 | // Out("{{green}}{{bold}}hi there %q{{/}}", "aa") 16 | // Out("{{magenta}}{{bold}}hi therea{{/}} {{cyan}}{{underline}}b{{/}}") 17 | // 18 | // ref. 19 | // https://github.com/onsi/ginkgo/blob/v2.0.0/formatter/formatter.go#L52-L73 20 | // 21 | func Outf(format string, args ...interface{}) { 22 | s := formatter.F(format, args...) 23 | fmt.Fprint(formatter.ColorableStdOut, s) 24 | } 25 | 26 | // Outputs to stderr. 27 | func Errf(format string, args ...interface{}) { 28 | s := formatter.F(format, args...) 29 | fmt.Fprint(formatter.ColorableStdErr, s) 30 | } 31 | 32 | func Greenf(format string, args ...interface{}) { 33 | f := fmt.Sprintf("{{green}}%s{{/}}", format) 34 | Outf(f, args...) 35 | } 36 | 37 | func Redf(format string, args ...interface{}) { 38 | f := fmt.Sprintf("{{red}}%s{{/}}", format) 39 | Outf(f, args...) 40 | } 41 | 42 | func Bluef(format string, args ...interface{}) { 43 | f := fmt.Sprintf("{{blue}}%s{{/}}", format) 44 | Outf(f, args...) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/logutil/zap.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // Package logutil implements various log utilities. 5 | package logutil 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | 11 | "go.uber.org/zap" 12 | "go.uber.org/zap/zapcore" 13 | ) 14 | 15 | func init() { 16 | logger, err := GetDefaultZapLogger() 17 | if err != nil { 18 | log.Fatalf("Failed to initialize global logger, %v", err) 19 | } 20 | _ = zap.ReplaceGlobals(logger) 21 | } 22 | 23 | // GetDefaultZapLoggerConfig returns a new default zap logger configuration. 24 | func GetDefaultZapLoggerConfig() zap.Config { 25 | return zap.Config{ 26 | Level: zap.NewAtomicLevelAt(ConvertToZapLevel(DefaultLogLevel)), 27 | 28 | Development: false, 29 | Sampling: &zap.SamplingConfig{ 30 | Initial: 100, 31 | Thereafter: 100, 32 | }, 33 | 34 | Encoding: "console", 35 | 36 | // copied from "zap.NewProductionEncoderConfig" with some updates 37 | EncoderConfig: zapcore.EncoderConfig{ 38 | TimeKey: "ts", 39 | LevelKey: "level", 40 | NameKey: "logger", 41 | CallerKey: "caller", 42 | MessageKey: "msg", 43 | StacktraceKey: "stacktrace", 44 | LineEnding: zapcore.DefaultLineEnding, 45 | EncodeLevel: zapcore.LowercaseLevelEncoder, 46 | EncodeTime: zapcore.ISO8601TimeEncoder, 47 | EncodeDuration: zapcore.StringDurationEncoder, 48 | EncodeCaller: zapcore.ShortCallerEncoder, 49 | }, 50 | 51 | // Use "/dev/null" to discard all 52 | OutputPaths: []string{"stderr"}, 53 | ErrorOutputPaths: []string{"stderr"}, 54 | } 55 | } 56 | 57 | // GetDefaultZapLogger returns a new default logger. 58 | func GetDefaultZapLogger() (*zap.Logger, error) { 59 | lcfg := GetDefaultZapLoggerConfig() 60 | return lcfg.Build() 61 | } 62 | 63 | // DefaultLogLevel is the default log level. 64 | var DefaultLogLevel = "info" 65 | 66 | // ConvertToZapLevel converts log level string to zapcore.Level. 67 | func ConvertToZapLevel(lvl string) zapcore.Level { 68 | switch lvl { 69 | case "debug": 70 | return zap.DebugLevel 71 | case "info": 72 | return zap.InfoLevel 73 | case "warn": 74 | return zap.WarnLevel 75 | case "error": 76 | return zap.ErrorLevel 77 | case "dpanic": 78 | return zap.DPanicLevel 79 | case "panic": 80 | return zap.PanicLevel 81 | case "fatal": 82 | return zap.FatalLevel 83 | default: 84 | panic(fmt.Sprintf("unknown level %q", lvl)) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /scripts/build.release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | if ! [[ "$0" =~ scripts/build.release.sh ]]; then 8 | echo "must be run from repository root" 9 | exit 255 10 | fi 11 | 12 | # https://goreleaser.com/install/ 13 | go install -v github.com/goreleaser/goreleaser@latest 14 | 15 | # e.g., 16 | # git tag 1.0.0 17 | goreleaser release \ 18 | --config .goreleaser.yml \ 19 | --skip-announce \ 20 | --skip-publish 21 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run with ./scripts/build.sh 4 | 5 | if ! [[ "$0" =~ scripts/build.sh ]]; then 6 | echo "must be run from repository root" 7 | exit 1 8 | fi 9 | 10 | # Set the CGO flags to use the portable version of BLST 11 | # 12 | # We use "export" here instead of just setting a bash variable because we need 13 | # to pass this flag to all child processes spawned by the shell. 14 | export CGO_CFLAGS="-O -D__BLST_PORTABLE__" 15 | 16 | go build -v 17 | -------------------------------------------------------------------------------- /scripts/tests.e2e.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # e.g., 5 | # ./scripts/tests.e2e.sh 1.7.4 6 | if ! [[ "$0" =~ scripts/tests.e2e.sh ]]; then 7 | echo "must be run from repository root" 8 | exit 255 9 | fi 10 | 11 | VERSION=$1 12 | if [[ -z "${VERSION}" ]]; then 13 | echo "Missing version argument!" 14 | echo "Usage: ${0} [VERSION]" >> /dev/stderr 15 | exit 255 16 | fi 17 | 18 | ################################# 19 | # download avalanchego 20 | # https://github.com/ava-labs/avalanchego/releases 21 | GOARCH=$(go env GOARCH) 22 | GOOS=$(go env GOOS) 23 | BASEDIR=/tmp/subnet-cli-runner 24 | mkdir -p ${BASEDIR} 25 | AVAGO_DOWNLOAD_URL=https://github.com/ava-labs/avalanchego/releases/download/${VERSION}/avalanchego-linux-${GOARCH}-${VERSION}.tar.gz 26 | AVAGO_DOWNLOAD_PATH=${BASEDIR}/avalanchego-linux-${GOARCH}-${VERSION}.tar.gz 27 | if [[ ${GOOS} == "darwin" ]]; then 28 | AVAGO_DOWNLOAD_URL=https://github.com/ava-labs/avalanchego/releases/download/${VERSION}/avalanchego-macos-${VERSION}.zip 29 | AVAGO_DOWNLOAD_PATH=${BASEDIR}/avalanchego-macos-${VERSION}.zip 30 | fi 31 | 32 | 33 | AVAGO_FILEPATH=${BASEDIR}/avalanchego-${VERSION} 34 | if [[ ! -d ${AVAGO_FILEPATH} ]]; then 35 | if [[ ! -f ${AVAGO_DOWNLOAD_PATH} ]]; then 36 | echo "downloading avalanchego ${VERSION} at ${AVAGO_DOWNLOAD_URL} to ${AVAGO_DOWNLOAD_PATH}" 37 | curl -L ${AVAGO_DOWNLOAD_URL} -o ${AVAGO_DOWNLOAD_PATH} 38 | fi 39 | echo "extracting downloaded avalanchego to ${AVAGO_FILEPATH}" 40 | if [[ ${GOOS} == "linux" ]]; then 41 | mkdir -p ${AVAGO_FILEPATH} && tar xzvf ${AVAGO_DOWNLOAD_PATH} --directory ${AVAGO_FILEPATH} --strip-components 1 42 | elif [[ ${GOOS} == "darwin" ]]; then 43 | unzip ${AVAGO_DOWNLOAD_PATH} -d ${AVAGO_FILEPATH} 44 | mv ${AVAGO_FILEPATH}/build/* ${AVAGO_FILEPATH} 45 | rm -rf ${AVAGO_FILEPATH}/build/ 46 | fi 47 | find ${BASEDIR}/avalanchego-${VERSION} 48 | fi 49 | 50 | AVALANCHEGO_PATH=${AVAGO_FILEPATH}/avalanchego 51 | AVALANCHEGO_PLUGIN_DIR=${AVAGO_FILEPATH}/plugins 52 | 53 | ################################# 54 | # download avalanche-network-runner 55 | # https://github.com/ava-labs/avalanche-network-runner 56 | ANR_REPO_PATH=github.com/ava-labs/avalanche-network-runner 57 | ANR_VERSION=v1.2.3 58 | # version set 59 | go install -v ${ANR_REPO_PATH}@${ANR_VERSION} 60 | 61 | ################################# 62 | echo "building e2e.test" 63 | # to install the ginkgo binary (required for test build and run) 64 | go install -v github.com/onsi/ginkgo/v2/ginkgo@v2.0.0 65 | 66 | # Set the CGO flags to use the portable version of BLST 67 | # 68 | # We use "export" here instead of just setting a bash variable because we need 69 | # to pass this flag to all child processes spawned by the shell. 70 | export CGO_CFLAGS="-O -D__BLST_PORTABLE__" 71 | 72 | ACK_GINKGO_RC=true ginkgo build ./tests/e2e 73 | ./tests/e2e/e2e.test --help 74 | 75 | # run "avalanche-network-runner" server 76 | GOPATH=$(go env GOPATH) 77 | if [[ -z ${GOBIN+x} ]]; then 78 | # no gobin set 79 | BIN=${GOPATH}/bin/avalanche-network-runner 80 | else 81 | # gobin set 82 | BIN=${GOBIN}/avalanche-network-runner 83 | fi 84 | echo "launch avalanche-network-runner in the background" 85 | $BIN server \ 86 | --log-level debug \ 87 | --port=":8080" \ 88 | --grpc-gateway-port=":8081" & 89 | PID=${!} 90 | 91 | ################################# 92 | echo "running e2e tests against the local cluster" 93 | ./tests/e2e/e2e.test \ 94 | --ginkgo.v \ 95 | --log-level debug \ 96 | --grpc-endpoint="0.0.0.0:8080" \ 97 | --grpc-gateway-endpoint="0.0.0.0:8081" \ 98 | --avalanchego-path=${AVALANCHEGO_PATH} || (kill ${PID} && exit) 99 | 100 | echo "ALL SUCCESS!" 101 | kill ${PID} 102 | -------------------------------------------------------------------------------- /scripts/tests.lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -e 6 | 7 | if ! [[ "$0" =~ scripts/tests.lint.sh ]]; then 8 | echo "must be run from repository root" 9 | exit 255 10 | fi 11 | 12 | if [ "$#" -eq 0 ]; then 13 | # by default, check all source code 14 | # to test only "mempool" package 15 | # ./scripts/lint.sh ./mempool/... 16 | TARGET="./..." 17 | else 18 | TARGET="${1}" 19 | fi 20 | 21 | # by default, "./scripts/lint.sh" runs all lint tests 22 | # to run only "license_header" test 23 | # TESTS='license_header' ./scripts/lint.sh 24 | TESTS=${TESTS:-"golangci_lint license_header"} 25 | 26 | # https://github.com/golangci/golangci-lint/releases 27 | function test_golangci_lint { 28 | go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@v1.43.0 29 | golangci-lint run --config .golangci.yml 30 | } 31 | 32 | # find_go_files [package] 33 | # all go files except generated ones 34 | function find_go_files { 35 | local target="${1}" 36 | go fmt -n "${target}" | grep -Eo "([^ ]*)$" | grep -vE "(\\.pb\\.go|\\.pb\\.gw.go)" 37 | } 38 | 39 | # automatically checks license headers 40 | # to modify the file headers (if missing), remove "--check" flag 41 | # TESTS='license_header' ADDLICENSE_FLAGS="-v" ./scripts/lint.sh 42 | _addlicense_flags=${ADDLICENSE_FLAGS:-"--check -v"} 43 | function test_license_header { 44 | go install -v github.com/google/addlicense@latest 45 | local target="${1}" 46 | local files=() 47 | while IFS= read -r line; do files+=("$line"); done < <(find_go_files "${target}") 48 | 49 | # ignore 3rd party code 50 | addlicense \ 51 | -f ./LICENSE.header \ 52 | ${_addlicense_flags} \ 53 | "${files[@]}" 54 | } 55 | 56 | function run { 57 | local test="${1}" 58 | shift 1 59 | echo "START: '${test}' at $(date)" 60 | if "test_${test}" "$@" ; then 61 | echo "SUCCESS: '${test}' completed at $(date)" 62 | else 63 | echo "FAIL: '${test}' failed at $(date)" 64 | exit 255 65 | fi 66 | } 67 | 68 | echo "Running '$TESTS' at: $(date)" 69 | for test in $TESTS; do 70 | run "${test}" "${TARGET}" 71 | done 72 | 73 | echo "ALL SUCCESS!" 74 | -------------------------------------------------------------------------------- /scripts/tests.unit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if ! [[ "$0" =~ scripts/tests.unit.sh ]]; then 5 | echo "must be run from repository root" 6 | exit 255 7 | fi 8 | 9 | # Set the CGO flags to use the portable version of BLST 10 | # 11 | # We use "export" here instead of just setting a bash variable because we need 12 | # to pass this flag to all child processes spawned by the shell. 13 | export CGO_CFLAGS="-O -D__BLST_PORTABLE__" 14 | 15 | go test -v -race -timeout="3m" -coverprofile="coverage.out" -covermode="atomic" $(go list ./... | grep -v tests) 16 | -------------------------------------------------------------------------------- /scripts/updatedep.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | if ! [[ "$0" =~ scripts/updatedep.sh ]]; then 5 | echo "must be run from repository root" 6 | exit 255 7 | fi 8 | 9 | # TODO: automatically bump up dependencies 10 | go mod tidy -v 11 | -------------------------------------------------------------------------------- /tests/e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // e2e implements the e2e tests. 5 | package e2e_test 6 | 7 | import ( 8 | "context" 9 | "flag" 10 | "testing" 11 | "time" 12 | 13 | runner_client "github.com/ava-labs/avalanche-network-runner/client" 14 | "github.com/ava-labs/avalanchego/ids" 15 | "github.com/ava-labs/avalanchego/utils/logging" 16 | "github.com/ava-labs/avalanchego/utils/units" 17 | "github.com/ava-labs/subnet-cli/client" 18 | "github.com/ava-labs/subnet-cli/internal/key" 19 | "github.com/ava-labs/subnet-cli/pkg/color" 20 | "github.com/ava-labs/subnet-cli/pkg/logutil" 21 | ginkgo "github.com/onsi/ginkgo/v2" 22 | "github.com/onsi/gomega" 23 | ) 24 | 25 | func TestE2e(t *testing.T) { 26 | gomega.RegisterFailHandler(ginkgo.Fail) 27 | ginkgo.RunSpecs(t, "subnet-cli e2e test suites") 28 | } 29 | 30 | var ( 31 | logLevel string 32 | gRPCEp string 33 | gRPCGatewayEp string 34 | execPath string 35 | ) 36 | 37 | func init() { 38 | flag.StringVar( 39 | &logLevel, 40 | "log-level", 41 | logutil.DefaultLogLevel, 42 | "log level", 43 | ) 44 | flag.StringVar( 45 | &gRPCEp, 46 | "grpc-endpoint", 47 | "0.0.0.0:8080", 48 | "gRPC server endpoint", 49 | ) 50 | flag.StringVar( 51 | &gRPCGatewayEp, 52 | "grpc-gateway-endpoint", 53 | "0.0.0.0:8081", 54 | "gRPC gateway endpoint", 55 | ) 56 | flag.StringVar( 57 | &execPath, 58 | "avalanchego-path", 59 | "", 60 | "avalanchego executable path", 61 | ) 62 | } 63 | 64 | var ( 65 | runnerClient runner_client.Client 66 | cli client.Client 67 | k key.Key 68 | ) 69 | 70 | var _ = ginkgo.BeforeSuite(func() { 71 | var err error 72 | runnerClient, err = runner_client.New(runner_client.Config{ 73 | Endpoint: gRPCEp, 74 | DialTimeout: 10 * time.Second, 75 | }, logging.NoLog{}) 76 | gomega.Ω(err).Should(gomega.BeNil()) 77 | 78 | // TODO: pass subnet whitelisting 79 | color.Outf("{{green}}starting:{{/}} %q\n", execPath) 80 | ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 81 | _, err = runnerClient.Start(ctx, execPath) 82 | cancel() 83 | gomega.Ω(err).Should(gomega.BeNil()) 84 | 85 | ctx, cancel = context.WithTimeout(context.Background(), 2*time.Minute) 86 | _, err = runnerClient.Health(ctx) 87 | cancel() 88 | gomega.Ω(err).Should(gomega.BeNil()) 89 | 90 | color.Outf("{{green}}getting URIs{{/}}\n") 91 | var uris []string 92 | ctx, cancel = context.WithTimeout(context.Background(), 2*time.Minute) 93 | uris, err = runnerClient.URIs(ctx) 94 | cancel() 95 | gomega.Ω(err).Should(gomega.BeNil()) 96 | 97 | color.Outf("{{green}}creating subnet-cli client{{/}}\n") 98 | cli, err = client.New(client.Config{ 99 | URI: uris[0], 100 | PollInterval: time.Second, 101 | }) 102 | gomega.Ω(err).Should(gomega.BeNil()) 103 | 104 | k, err = key.NewSoft(9999999, key.WithPrivateKeyEncoded(key.EwoqPrivateKey)) 105 | gomega.Ω(err).Should(gomega.BeNil()) 106 | }) 107 | 108 | var _ = ginkgo.AfterSuite(func() { 109 | color.Outf("{{red}}shutting down cluster{{/}}\n") 110 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) 111 | _, err := runnerClient.Stop(ctx) 112 | cancel() 113 | gomega.Ω(err).Should(gomega.BeNil()) 114 | 115 | color.Outf("{{red}}shutting down client{{/}}\n") 116 | err = runnerClient.Close() 117 | gomega.Ω(err).Should(gomega.BeNil()) 118 | }) 119 | 120 | var subnetID = ids.Empty 121 | 122 | var _ = ginkgo.Describe("[CreateSubnet/CreateBlockchain]", func() { 123 | ginkgo.It("can issue CreateSubnetTx", func() { 124 | balance, err := cli.P().Balance(context.Background(), k) 125 | gomega.Ω(err).Should(gomega.BeNil()) 126 | feeInfo, err := cli.Info().Client().GetTxFee(context.Background()) 127 | gomega.Ω(err).Should(gomega.BeNil()) 128 | subnetTxFee := uint64(feeInfo.CreateSubnetTxFee) 129 | expectedBalance := balance - subnetTxFee 130 | 131 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 132 | subnet1, _, err := cli.P().CreateSubnet(ctx, k, client.WithDryMode(true)) 133 | cancel() 134 | gomega.Ω(err).Should(gomega.BeNil()) 135 | 136 | ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) 137 | subnet2, _, err := cli.P().CreateSubnet(ctx, k, client.WithDryMode(false)) 138 | cancel() 139 | gomega.Ω(err).Should(gomega.BeNil()) 140 | 141 | ginkgo.By("returns an identical subnet ID with dry mode", func() { 142 | gomega.Ω(subnet1).Should(gomega.Equal(subnet2)) 143 | }) 144 | subnetID = subnet1 145 | 146 | ginkgo.By("returns a tx-fee deducted balance", func() { 147 | curBal, err := cli.P().Balance(context.Background(), k) 148 | gomega.Ω(err).Should(gomega.BeNil()) 149 | gomega.Ω(curBal).Should(gomega.Equal(expectedBalance)) 150 | }) 151 | }) 152 | 153 | ginkgo.It("can add subnet/validators", func() { 154 | nodeID, _, err := cli.Info().Client().GetNodeID(context.Background()) 155 | gomega.Ω(err).Should(gomega.BeNil()) 156 | 157 | ginkgo.By("fails when subnet ID is empty", func() { 158 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 159 | _, err = cli.P().AddSubnetValidator( 160 | ctx, 161 | k, 162 | ids.Empty, 163 | nodeID, 164 | time.Now(), 165 | time.Now(), 166 | 1000, 167 | ) 168 | cancel() 169 | gomega.Ω(err.Error()).Should(gomega.Equal(client.ErrEmptyID.Error())) 170 | }) 171 | 172 | ginkgo.By("fails when node ID is empty", func() { 173 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 174 | _, err = cli.P().AddSubnetValidator( 175 | ctx, 176 | k, 177 | subnetID, 178 | ids.EmptyNodeID, 179 | time.Now(), 180 | time.Now(), 181 | 1000, 182 | ) 183 | cancel() 184 | gomega.Ω(err.Error()).Should(gomega.Equal(client.ErrEmptyID.Error())) 185 | }) 186 | 187 | ginkgo.By("fails to add an invalid subnet as a validator, when nodeID isn't validating the primary network", func() { 188 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 189 | _, err = cli.P().AddSubnetValidator( 190 | ctx, 191 | k, 192 | subnetID, 193 | ids.GenerateTestNodeID(), 194 | time.Now().Add(30*time.Second), 195 | time.Now().Add(2*24*time.Hour), 196 | 1000, 197 | ) 198 | cancel() 199 | gomega.Ω(err.Error()).Should(gomega.Equal(client.ErrNotValidatingPrimaryNetwork.Error())) 200 | }) 201 | 202 | ginkgo.By("fails when validate start/end times are invalid", func() { 203 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 204 | _, err = cli.P().AddSubnetValidator( 205 | ctx, 206 | k, 207 | subnetID, 208 | nodeID, 209 | time.Now(), 210 | time.Now().Add(5*time.Second), 211 | 1000, 212 | ) 213 | cancel() 214 | // e.g., "failed to issue tx: couldn't issue tx: staking period is too short" 215 | gomega.Ω(err.Error()).Should(gomega.ContainSubstring("staking period is too short")) 216 | }) 217 | 218 | ginkgo.By("fails to add duplicate validator", func() { 219 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 220 | _, err = cli.P().AddValidator( 221 | ctx, 222 | k, 223 | nodeID, 224 | time.Now().Add(30*time.Second), 225 | time.Now().Add(5*24*time.Hour), 226 | client.WithStakeAmount(2*units.KiloAvax), 227 | // ref. "genesis/genesis_local.go". 228 | client.WithRewardShares(30000), // 3% 229 | ) 230 | cancel() 231 | gomega.Ω(err.Error()).Should(gomega.Equal(client.ErrAlreadyValidator.Error())) 232 | }) 233 | }) 234 | 235 | ginkgo.It("can issue CreateBlockchain", func() { 236 | ginkgo.By("fails when subnet ID is empty", func() { 237 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 238 | _, _, err := cli.P().CreateBlockchain( 239 | ctx, 240 | k, 241 | ids.Empty, 242 | "", 243 | ids.Empty, 244 | nil, 245 | ) 246 | cancel() 247 | gomega.Ω(err.Error()).Should(gomega.Equal(client.ErrEmptyID.Error())) 248 | }) 249 | 250 | ginkgo.By("fails when vm ID is empty", func() { 251 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 252 | _, _, err := cli.P().CreateBlockchain( 253 | ctx, 254 | k, 255 | subnetID, 256 | "", 257 | ids.Empty, 258 | nil, 259 | ) 260 | cancel() 261 | gomega.Ω(err.Error()).Should(gomega.Equal(client.ErrEmptyID.Error())) 262 | }) 263 | 264 | ginkgo.Skip("TODO: once we have a testable spaces VM in public") 265 | 266 | balance, err := cli.P().Balance(context.Background(), k) 267 | gomega.Ω(err).Should(gomega.BeNil()) 268 | feeInfo, err := cli.Info().Client().GetTxFee(context.Background()) 269 | gomega.Ω(err).Should(gomega.BeNil()) 270 | blkChainFee := uint64(feeInfo.CreateBlockchainTxFee) 271 | expectedBalance := balance - blkChainFee 272 | 273 | ginkgo.By("returns a tx-fee deducted balance", func() { 274 | curBal, err := cli.P().Balance(context.Background(), k) 275 | gomega.Ω(err).Should(gomega.BeNil()) 276 | gomega.Ω(curBal).Should(gomega.Equal(expectedBalance)) 277 | }) 278 | }) 279 | }) 280 | --------------------------------------------------------------------------------