├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── new-feature.yml └── workflows │ └── pr-checks.yml ├── .gitignore ├── .gitpod.yml ├── .mocharc.json ├── .prettierrc ├── LICENSE ├── README.md ├── base-image ├── Dockerfile └── README.md ├── bin ├── run.cmd └── run.js ├── logo.png ├── package.json ├── src ├── commands │ ├── account │ │ ├── balance.ts │ │ ├── create.ts │ │ ├── default.ts │ │ ├── faucet.ts │ │ └── list.ts │ ├── clear │ │ └── index.ts │ ├── contract │ │ ├── compile.ts │ │ ├── deploy.ts │ │ ├── explain.ts │ │ ├── new.ts │ │ ├── query.ts │ │ ├── test.ts │ │ ├── tx.ts │ │ └── verify.ts │ ├── env │ │ ├── check.ts │ │ └── install.ts │ ├── generate │ │ ├── tests.ts │ │ └── types.ts │ ├── init │ │ └── index.ts │ ├── node │ │ ├── chopsticks │ │ │ ├── init.ts │ │ │ └── start.ts │ │ ├── install.ts │ │ ├── purge.ts │ │ ├── start.ts │ │ └── version.ts │ └── zombienet │ │ ├── init.ts │ │ └── start.ts ├── hooks │ └── command_not_found │ │ └── command_not_found.ts ├── index.ts ├── lib │ ├── account.ts │ ├── cargoContractInfo.ts │ ├── checks.ts │ ├── command-utils.ts │ ├── config-builder.ts │ ├── config.ts │ ├── consts.ts │ ├── contract.ts │ ├── contractCall.ts │ ├── crypto.ts │ ├── errors.ts │ ├── index.ts │ ├── logger.ts │ ├── nodeInfo.ts │ ├── prompts.ts │ ├── spinner.ts │ ├── substrate-api.ts │ ├── swankyCommand.ts │ ├── tasks.ts │ ├── templates.ts │ └── zombienetInfo.ts ├── templates │ ├── Cargo.toml │ ├── LICENSE │ ├── chopsticks │ │ └── dev.yml │ ├── contracts │ │ ├── blank │ │ │ ├── contract │ │ │ │ ├── Cargo.toml.hbs │ │ │ │ └── src │ │ │ │ │ └── lib.rs.hbs │ │ │ └── test │ │ │ │ └── index.test.ts.hbs │ │ ├── flipper │ │ │ ├── contract │ │ │ │ ├── Cargo.toml.hbs │ │ │ │ └── src │ │ │ │ │ └── lib.rs.hbs │ │ │ └── test │ │ │ │ └── index.test.ts.hbs │ │ └── psp22 │ │ │ ├── contract │ │ │ ├── Cargo.toml.hbs │ │ │ └── src │ │ │ │ └── lib.rs.hbs │ │ │ └── test │ │ │ └── index.test.ts.hbs │ ├── github │ │ └── workflows │ │ │ └── tests.yaml │ ├── gitignore │ ├── mocharc.json │ ├── package.json.hbs │ ├── test_helpers │ │ ├── Cargo.toml.hbs │ │ └── lib.rs.hbs │ ├── tsconfig.json │ └── zombienet │ │ ├── astar-collator.toml │ │ ├── polkadot-parachain.toml │ │ ├── polkadot.toml │ │ └── zombienet.config.toml ├── test │ ├── helpers │ │ └── init.js │ └── lib │ │ └── prompts.test.ts └── types │ └── index.ts ├── tsconfig.json └── yarn.lock /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swanky-env", 3 | "image": "ghcr.io/inkdevhub/swanky-cli/swanky-base:swanky4.0.0_v2.2.0", 4 | "features": { 5 | "ghcr.io/devcontainers/features/docker-in-docker:2.8.0": {} 6 | }, 7 | // Mount the workspace volume 8 | "mounts": ["source=${localWorkspaceFolder},target=/workspaces,type=bind,consistency=cached"], 9 | "workspaceFolder": "/workspaces", 10 | 11 | "containerEnv": { 12 | "CARGO_TARGET_DIR": "/tmp" 13 | }, 14 | 15 | // Configure tool-specific properties. 16 | "customizations": { 17 | "vscode": { 18 | "extensions": ["esbenp.prettier-vscode", "dtsvet.vscode-wasm", "redhat.vscode-yaml"] 19 | } 20 | }, 21 | 22 | "remoteUser": "root" 23 | } 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | src/templates 3 | /temp* 4 | tmp -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "plugins": ["@typescript-eslint", "prettier"], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-type-checked", 11 | "plugin:@typescript-eslint/stylistic-type-checked", 12 | "prettier" 13 | ], 14 | "rules": { 15 | "no-throw-literal": "off", 16 | "@typescript-eslint/ban-ts-comment": "off", 17 | "@typescript-eslint/no-explicit-any": "off", 18 | "@typescript-eslint/no-unsafe-call": "off", 19 | "@typescript-eslint/no-unsafe-member-access": "off", 20 | "@typescript-eslint/no-unsafe-assignment": "off", 21 | "@typescript-eslint/no-unsafe-return": "off", 22 | "@typescript-eslint/no-unsafe-argument": "off", 23 | "@typescript-eslint/no-misused-promises": "off", 24 | "@typescript-eslint/no-floating-promises": "off", 25 | "@typescript-eslint/require-await": "off" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve 3 | title: "[BUG]: " 4 | labels: ["bug"] 5 | projects: ["swanky-cli"] 6 | 7 | body: 8 | - type: textarea 9 | id: what-happened 10 | attributes: 11 | label: What happened? 12 | description: Tell us what happened. In particular, tell us how and why you are using this project, and describe the bug that you encountered. Please note that we are not able to support all conceivable use cases, but the more information you are able to provide the more equipped we will be to help. 13 | placeholder: Write your bug report here 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: steps-to-reproduce 18 | attributes: 19 | label: Steps to reproduce 20 | description: Replace the example steps below with actual steps to reproduce the bug you're reporting. 21 | value: | 22 | 1. Go to '...' 23 | 2. Click on '....' 24 | 3. Scroll down to '....' 25 | 4. See error 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: expected-behavior 30 | attributes: 31 | label: Expected behavior 32 | description: Tell us what should happen 33 | placeholder: Write your expected behavior here. 34 | validations: 35 | required: true 36 | - type: textarea 37 | id: actual-behavior 38 | attributes: 39 | label: Actual behavior 40 | description: Tell us what happens instead 41 | placeholder: Write your actual behavior here. 42 | validations: 43 | required: true 44 | - type: textarea 45 | id: environment 46 | attributes: 47 | label: Environment 48 | description: Describe the environment in which you encountered this bug. Use the list below as a starting point and add additional information if you think it's relevant. 49 | value: | 50 | - Operating System 51 | - Project version/tag: (run 'swanky version') 52 | - Rust version (run `rustup show`) 53 | - Node version (run `node --version`) 54 | validations: 55 | required: true 56 | - type: textarea 57 | id: logs 58 | attributes: 59 | label: Logs, Errors, Screenshots 60 | description: Please provide the text of any logs or errors that you experienced; if applicable, provide screenshots to help illustrate the problem. 61 | placeholder: | 62 | Paste your logs here 63 | - type: textarea 64 | id: additional-info 65 | attributes: 66 | label: Additional information 67 | description: Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: New feature/ticket for the project 3 | labels: ["enhancement"] 4 | projects: ["swanky-cli"] 5 | 6 | body: 7 | - type: textarea 8 | id: overview 9 | attributes: 10 | label: Overview (What and Why) 11 | description: Write description of what the feature should be, why 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | id: how 17 | attributes: 18 | label: How to do it? 19 | description: Describe what needs to be done. 20 | placeholder: | 21 | - Task 1 22 | - Task 2 23 | validations: 24 | required: true 25 | 26 | - type: textarea 27 | id: definition-of-done 28 | attributes: 29 | label: Definition of Done 30 | description: Write definition of done, how to verify 31 | placeholder: | 32 | - Unit test cases 33 | - Docs updated 34 | - PR approved and merged to master 35 | - etc. 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: open-issues-and-blockers 41 | attributes: 42 | label: Open Issues and Blockers 43 | description: Dependencies issue or PR or some other blocker. 44 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: PR Checks 2 | on: 3 | pull_request: 4 | types: [opened, reopened, synchronize, ready_for_review] 5 | workflow_dispatch: 6 | env: 7 | NODE_VER: 18.x 8 | jobs: 9 | test: 10 | if: github.event.pull_request.draft == false 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout the source code 14 | uses: actions/checkout@v3 15 | 16 | - name: Install & display rust toolchain 17 | run: | 18 | rustup show 19 | rustup component add rust-src 20 | 21 | - name: Check targets are installed correctly 22 | run: rustup target list --installed 23 | 24 | - name: Cache Crates 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/.cargo 28 | key: ${{ runner.os }}-rust-${{ hashFiles('rust-toolchain.toml') }} 29 | restore-keys: | 30 | ${{ runner.os }}-rust 31 | 32 | - name: Check if cargo-contract exists 33 | id: check-cargo-contract 34 | continue-on-error: true 35 | run: cargo contract --version 36 | 37 | - name: Install cargo contract 38 | if: ${{ steps.check-cargo-contract.outcome == 'failure' }} 39 | run: | 40 | cargo install cargo-dylint dylint-link 41 | cargo install --force --locked cargo-contract 42 | 43 | - name: Use Node.js 44 | uses: actions/setup-node@v3 45 | with: 46 | node-version: ${{ env.NODE_VER }} 47 | 48 | - name: Check test 49 | run: yarn && yarn test 50 | 51 | lint-check: 52 | if: github.event.pull_request.draft == false 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout the source code 56 | uses: actions/checkout@v3 57 | 58 | - name: Use Node.js 59 | uses: actions/setup-node@v3 60 | with: 61 | node-version: ${{ env.NODE_VER }} 62 | 63 | - name: Check lint 64 | run: yarn && yarn lint 65 | 66 | build-check: 67 | if: github.event.pull_request.draft == false 68 | runs-on: ubuntu-latest 69 | steps: 70 | - name: Checkout the source code 71 | uses: actions/checkout@v3 72 | 73 | - name: Use Node.js 74 | uses: actions/setup-node@v3 75 | with: 76 | node-version: ${{ env.NODE_VER }} 77 | 78 | - name: Check build 79 | run: yarn && yarn build 80 | 81 | format-check: 82 | if: github.event.pull_request.draft == false 83 | runs-on: ubuntu-latest 84 | steps: 85 | - name: Checkout the source code 86 | uses: actions/checkout@v3 87 | 88 | - name: Use Node.js 89 | uses: actions/setup-node@v3 90 | with: 91 | node-version: ${{ env.NODE_VER }} 92 | 93 | - name: Check formatting 94 | run: yarn && yarn format -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node,macos,visualstudiocode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,visualstudiocode 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .com.apple.timemachine.donotpresent 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | ### macOS Patch ### 35 | # iCloud generated files 36 | *.icloud 37 | 38 | ### Node ### 39 | # Logs 40 | logs 41 | *.log 42 | npm-debug.log* 43 | yarn-debug.log* 44 | yarn-error.log* 45 | lerna-debug.log* 46 | .pnpm-debug.log* 47 | 48 | # Diagnostic reports (https://nodejs.org/api/report.html) 49 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 50 | 51 | # Runtime data 52 | pids 53 | *.pid 54 | *.seed 55 | *.pid.lock 56 | 57 | # Directory for instrumented libs generated by jscoverage/JSCover 58 | lib-cov 59 | 60 | # Coverage directory used by tools like istanbul 61 | coverage 62 | *.lcov 63 | 64 | # nyc test coverage 65 | .nyc_output 66 | 67 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 68 | .grunt 69 | 70 | # Bower dependency directory (https://bower.io/) 71 | bower_components 72 | 73 | # node-waf configuration 74 | .lock-wscript 75 | 76 | # Compiled binary addons (https://nodejs.org/api/addons.html) 77 | build/Release 78 | 79 | # Dependency directories 80 | node_modules/ 81 | jspm_packages/ 82 | 83 | # Snowpack dependency directory (https://snowpack.dev/) 84 | web_modules/ 85 | 86 | # TypeScript cache 87 | *.tsbuildinfo 88 | 89 | # Optional npm cache directory 90 | .npm 91 | 92 | # Optional eslint cache 93 | .eslintcache 94 | 95 | # Optional stylelint cache 96 | .stylelintcache 97 | 98 | # Microbundle cache 99 | .rpt2_cache/ 100 | .rts2_cache_cjs/ 101 | .rts2_cache_es/ 102 | .rts2_cache_umd/ 103 | 104 | # Optional REPL history 105 | .node_repl_history 106 | 107 | # Output of 'npm pack' 108 | *.tgz 109 | 110 | # Yarn Integrity file 111 | .yarn-integrity 112 | 113 | # dotenv environment variable files 114 | .env 115 | .env.development.local 116 | .env.test.local 117 | .env.production.local 118 | .env.local 119 | 120 | # parcel-bundler cache (https://parceljs.org/) 121 | .cache 122 | .parcel-cache 123 | 124 | # Next.js build output 125 | .next 126 | out 127 | 128 | # Nuxt.js build / generate output 129 | .nuxt 130 | dist 131 | 132 | # Gatsby files 133 | .cache/ 134 | # Comment in the public line in if your project uses Gatsby and not Next.js 135 | # https://nextjs.org/blog/next-9-1#public-directory-support 136 | # public 137 | 138 | # vuepress build output 139 | .vuepress/dist 140 | 141 | # vuepress v2.x temp and cache directory 142 | .temp 143 | 144 | # Docusaurus cache and generated files 145 | .docusaurus 146 | 147 | # Serverless directories 148 | .serverless/ 149 | 150 | # FuseBox cache 151 | .fusebox/ 152 | 153 | # DynamoDB Local files 154 | .dynamodb/ 155 | 156 | # TernJS port file 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | .vscode-test 161 | 162 | # yarn v2 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.* 168 | 169 | ### Node Patch ### 170 | # Serverless Webpack directories 171 | .webpack/ 172 | 173 | # Optional stylelint cache 174 | 175 | # SvelteKit build / generate output 176 | .svelte-kit 177 | 178 | ### VisualStudioCode ### 179 | .vscode/* 180 | !.vscode/settings.json 181 | !.vscode/tasks.json 182 | !.vscode/launch.json 183 | !.vscode/extensions.json 184 | !.vscode/*.code-snippets 185 | 186 | # Local History for Visual Studio Code 187 | .history/ 188 | 189 | # Built Visual Studio Code Extensions 190 | *.vsix 191 | 192 | ### VisualStudioCode Patch ### 193 | # Ignore all local history of files 194 | .history 195 | .ionide 196 | 197 | # Support for Project snippet scope 198 | .vscode/*.code-snippets 199 | 200 | # Ignore code-workspaces 201 | *.code-workspace 202 | 203 | # End of https://www.toptal.com/developers/gitignore/api/node,macos,visualstudiocode 204 | 205 | *-debug.log 206 | *-error.log 207 | /.nyc_output 208 | /dist 209 | /lib 210 | /package-lock.json 211 | /tmp 212 | node_modules 213 | oclif.manifest.json 214 | 215 | test_project/**/* 216 | temp_project/**/* 217 | temp_proj/**/* 218 | tmp/**/* -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 6 | 7 | ports: 8 | - name: Swanky Node 9 | port: 9944 10 | 11 | vscode: 12 | extensions: 13 | - rust-lang.rust-analyzer 14 | 15 | tasks: 16 | - init: | 17 | # Add wasm target 18 | rustup target add wasm32-unknown-unknown 19 | 20 | # Add necessary components 21 | rustup component add rust-src 22 | 23 | # Install or update cargo packages 24 | cargo install --force --locked cargo-contract 25 | cargo install cargo-dylint dylint-link 26 | 27 | yarn install 28 | yarn build 29 | command: | 30 | echo "Swanky Dev Environment ready!" 31 | echo "Use Swanky directly by running \"./bin/run.js COMMAND\"" 32 | echo "For example:" 33 | echo "./bin/run.js init temp_project" 34 | echo "cd temp_project" 35 | echo "../bin/run.js contract compile flipper" 36 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["src/test/helpers/init.js, mocha-suppress-logs"], 3 | "loader": "ts-node/esm", 4 | "spec": "src/test/**/*.test.ts", 5 | "watch-extensions": ["ts"], 6 | "recursive": true, 7 | "reporter": "spec", 8 | "timeout": 60000, 9 | "forbid-only": true 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "printWidth": 100 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Salesforce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /base-image/Dockerfile: -------------------------------------------------------------------------------- 1 | # Start from the base image 2 | FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/base:ubuntu-22.04 3 | 4 | LABEL org.opencontainers.image.source=https://github.com/swankyhub/swanky-cli 5 | 6 | # Update the package lists 7 | RUN apt-get update 8 | 9 | # Install Node.js 10 | RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs 11 | 12 | # Install binaryen, pkg-config, and libssl-dev 13 | RUN apt-get install -y binaryen pkg-config libssl-dev 14 | 15 | # Download and install swanky-cli and verify the installation 16 | RUN curl -L https://github.com/inkdevhub/swanky-cli/releases/download/v4.0.0/swanky-v4.0.0-81446c0-linux-x64.tar.gz | tar xz -C /opt && \ 17 | ln -s /opt/swanky/bin/swanky /usr/local/bin/swanky 18 | 19 | # Install Rustup and Rust, additional components, packages, and verify the installations 20 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ 21 | /bin/bash -c "source $HOME/.cargo/env && \ 22 | rustup install 1.76 && \ 23 | rustup default 1.76 && \ 24 | rustup component add rust-src && \ 25 | rustup target add wasm32-unknown-unknown && \ 26 | cargo install cargo-dylint dylint-link && \ 27 | cargo install cargo-contract --version 4.0.2 && \ 28 | rustc --version" 29 | 30 | # Install Yarn 1.x 31 | RUN npm install -g yarn@1 32 | 33 | # Verify installations 34 | RUN node --version && \ 35 | wasm-opt --version && \ 36 | pkg-config --version && \ 37 | openssl version && \ 38 | swanky --version && \ 39 | yarn --version 40 | 41 | # Clean up the package lists to reduce image size 42 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* 43 | -------------------------------------------------------------------------------- /base-image/README.md: -------------------------------------------------------------------------------- 1 | ## Building and publishing the image 2 | 3 | ### Build the image: 4 | 5 | ``` 6 | docker build -t swanky-base . 7 | ``` 8 | 9 | ### Tag it with swanky version used, and it's own version: 10 | 11 | ``` 12 | docker tag [IMAGE_NAME] ghcr.io/astarnetwork/swanky-cli/swanky-base:swanky[X.X.X]_v[X.X.X] 13 | ``` 14 | 15 | Example: 16 | 17 | ``` 18 | docker tag swanky-base ghcr.io/astarnetwork/swanky-cli/swanky-base:swanky2.2.2_v1.1.4 19 | ``` 20 | 21 | ### Push to repo: 22 | 23 | Use the same tag created in the previous step 24 | 25 | ``` 26 | docker push ghcr.io/astarnetwork/swanky-cli/swanky-base:swanky2.2.3_v1.1.4 27 | ``` 28 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /bin/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = await import("@oclif/core"); 4 | await oclif.execute({ type: "esm", dir: import.meta.url }); 5 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inkdevhub/swanky-cli/a0a88258bc4e8593b6041c492dad329fb9d20cc0/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astar-network/swanky-cli", 3 | "version": "4.0.0", 4 | "description": "All in one WASM smart contract development toolset", 5 | "author": "Astar network", 6 | "license": "MIT", 7 | "repository": "https://github.com/inkdevhub/swanky-cli", 8 | "homepage": "https://github.com/inkdevhub/swanky-cli", 9 | "bugs": "https://github.com/inkdevhub/swanky-cli/issues", 10 | "type": "module", 11 | "bin": { 12 | "swanky": "./bin/run.js" 13 | }, 14 | "main": "dist/index.js", 15 | "files": [ 16 | "/bin", 17 | "/dist", 18 | "/npm-shrinkwrap.json", 19 | "/oclif.manifest.json" 20 | ], 21 | "dependencies": { 22 | "@iarna/toml": "^2.2.5", 23 | "@oclif/core": "3.24.0", 24 | "@oclif/plugin-help": "6.0.18", 25 | "@oclif/plugin-plugins": "4.3.5", 26 | "@oclif/plugin-version": "2.0.14", 27 | "@polkadot/api": "10.12.2", 28 | "@polkadot/api-augment": "10.12.2", 29 | "@polkadot/api-contract": "10.12.2", 30 | "@polkadot/keyring": "12.6.2", 31 | "@polkadot/types": "10.12.2", 32 | "@polkadot/types-codec": "10.12.2", 33 | "@polkadot/util": "12.6.2", 34 | "@polkadot/util-crypto": "12.6.2", 35 | "bn.js": "5.2.1", 36 | "chalk": "5.3.0", 37 | "change-case": "5.4.3", 38 | "decompress": "4.2.1", 39 | "enquirer": "^2.4.1", 40 | "execa": "8.0.1", 41 | "fs-extra": "11.2.0", 42 | "globby": "^14.0.1", 43 | "handlebars": "4.7.8", 44 | "inquirer": "9.2.15", 45 | "inquirer-fuzzy-path": "^2.3.0", 46 | "listr2": "8.0.2", 47 | "lodash-es": "^4.17.21", 48 | "mocha": "10.3.0", 49 | "mocha-suppress-logs": "0.5.1", 50 | "mochawesome": "7.1.3", 51 | "modern-errors": "^7.0.0", 52 | "modern-errors-bugs": "^5.0.0", 53 | "modern-errors-clean": "^6.0.0", 54 | "modern-errors-winston": "^5.0.0", 55 | "node-downloader-helper": "2.1.9", 56 | "ora": "8.0.1", 57 | "semver": "7.6.0", 58 | "toml": "^3.0.0", 59 | "ts-mocha": "^10.0.0", 60 | "winston": "^3.12.0" 61 | }, 62 | "devDependencies": { 63 | "@oclif/test": "3.2.5", 64 | "@types/bn.js": "^5.1.5", 65 | "@types/chai": "4", 66 | "@types/decompress": "4.2.7", 67 | "@types/fs-extra": "11.0.4", 68 | "@types/iarna__toml": "^2.0.5", 69 | "@types/inquirer": "9.0.7", 70 | "@types/inquirer-fuzzy-path": "^2.3.9", 71 | "@types/lodash-es": "^4.17.12", 72 | "@types/mocha": "10.0.6", 73 | "@types/node": "^20.11.26", 74 | "@types/semver": "7.5.8", 75 | "@typescript-eslint/eslint-plugin": "7.2.0", 76 | "@typescript-eslint/parser": "7.2.0", 77 | "chai": "5", 78 | "eslint": "8.57.0", 79 | "eslint-config-prettier": "9.1.0", 80 | "eslint-plugin-prettier": "5.1.3", 81 | "nodemon": "^3.1.0", 82 | "oclif": "4.5.4", 83 | "prettier": "3.2.5", 84 | "shx": "0.3.4", 85 | "ts-node": "10.9.2", 86 | "tslib": "2.6.2", 87 | "typescript": "5.4.2" 88 | }, 89 | "oclif": { 90 | "bin": "swanky", 91 | "dirname": "swanky", 92 | "commands": "./dist/commands", 93 | "topicSeparator": " ", 94 | "additionalHelpFlags": [ 95 | "-h" 96 | ], 97 | "additionalVersionFlags": [ 98 | "-v", 99 | "-V" 100 | ], 101 | "plugins": [ 102 | "@oclif/plugin-help", 103 | "@oclif/plugin-version", 104 | "@oclif/plugin-plugins" 105 | ], 106 | "hooks": { 107 | "command_not_found": [ 108 | "./dist/hooks/command_not_found/command_not_found" 109 | ] 110 | } 111 | }, 112 | "scripts": { 113 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,css,md}\"", 114 | "lint": "eslint . --ext .ts --quiet --config .eslintrc", 115 | "postpack": "shx rm -f oclif.manifest.json", 116 | "prepack": "yarn build && oclif manifest && oclif readme", 117 | "test": "mocha --config .mocharc.json", 118 | "version": "oclif readme && git add README.md", 119 | "tarball:macos": "oclif pack tarballs --targets=darwin-x64 --no-xz", 120 | "tarball:linux": "oclif pack tarballs --targets=linux-x64 --no-xz", 121 | "tarball:all": "oclif pack tarballs --targets=linux-x64,linux-arm64,darwin-x64,darwin-arm64 --no-xz", 122 | "build:clean": "shx rm -rf dist && rm -f tsconfig.tsbuildinfo", 123 | "build:ts": "tsc -b", 124 | "build:templates": "cp -R ./src/templates ./dist", 125 | "build": "yarn build:clean && yarn build:ts && yarn build:templates", 126 | "dev:ts": "tsc --watch", 127 | "dev:templates": "yarn build:templates && nodemon --watch ./src/templates --ext '*' --exec yarn build:templates", 128 | "dev": "yarn dev:ts & yarn dev:templates", 129 | "dev:run": "./bin/run.js" 130 | }, 131 | "engines": { 132 | "node": ">=18.0.0" 133 | }, 134 | "keywords": [ 135 | "oclif", 136 | "swanky", 137 | "cli", 138 | "astar", 139 | "shiden", 140 | "wasm" 141 | ], 142 | "types": "dist/index.d.ts", 143 | "gitHead": "cccb996036cf2b6fbbe4e1f02c31079ba99fc517", 144 | "publishConfig": { 145 | "access": "public" 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/commands/account/balance.ts: -------------------------------------------------------------------------------- 1 | import { Args } from "@oclif/core"; 2 | import { ApiPromise } from "@polkadot/api"; 3 | import type { AccountInfo, Balance as BalanceType } from "@polkadot/types/interfaces"; 4 | import { ChainApi, resolveNetworkUrl } from "../../lib/index.js"; 5 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 6 | import { InputError } from "../../lib/errors.js"; 7 | import { formatBalance } from "@polkadot/util"; 8 | 9 | export class Balance extends SwankyCommand { 10 | static description = "Balance of an account"; 11 | 12 | static args = { 13 | alias: Args.string({ 14 | name: "alias", 15 | description: "Alias of account to be used", 16 | }), 17 | }; 18 | async run(): Promise { 19 | const { args } = await this.parse(Balance); 20 | 21 | if (!args.alias) { 22 | throw new InputError( 23 | "Missing argument! Please provide an alias account to get the balance from. Example usage: `swanky account balance `" 24 | ); 25 | } 26 | 27 | const accountData = this.findAccountByAlias(args.alias); 28 | const networkUrl = resolveNetworkUrl(this.swankyConfig, ""); 29 | 30 | const api = (await this.spinner.runCommand(async () => { 31 | const api = await ChainApi.create(networkUrl); 32 | await api.start(); 33 | return api.apiInst; 34 | }, "Connecting to node")) as ApiPromise; 35 | 36 | const decimals = api.registry.chainDecimals[0]; 37 | formatBalance.setDefaults({ unit: "UNIT", decimals }); 38 | 39 | const { nonce, data: balance } = await api.query.system.account( 40 | accountData.address 41 | ); 42 | const { free, reserved, miscFrozen, feeFrozen } = balance; 43 | 44 | let frozen: BalanceType; 45 | if (feeFrozen.gt(miscFrozen)) { 46 | frozen = feeFrozen; 47 | } else { 48 | frozen = miscFrozen; 49 | } 50 | 51 | const transferrableBalance = free.sub(frozen); 52 | const totalBalance = free.add(reserved); 53 | 54 | console.log("Transferrable Balance:", formatBalance(transferrableBalance)); 55 | if (!transferrableBalance.eq(totalBalance)) { 56 | console.log("Total Balance:", formatBalance(totalBalance)); 57 | console.log("Raw Balances:", balance.toHuman()); 58 | } 59 | console.log("Account Nonce:", nonce.toHuman()); 60 | 61 | await api.disconnect(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/commands/account/create.ts: -------------------------------------------------------------------------------- 1 | import { Flags } from "@oclif/core"; 2 | import chalk from "chalk"; 3 | import { 4 | ChainAccount, 5 | encrypt, 6 | getSwankyConfig, 7 | isLocalConfigCheck, 8 | SwankyAccountCommand, 9 | } from "../../lib/index.js"; 10 | import { AccountData } from "../../types/index.js"; 11 | import inquirer from "inquirer"; 12 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 13 | import { FileError } from "../../lib/errors.js"; 14 | import { ConfigBuilder } from "../../lib/config-builder.js"; 15 | 16 | export class CreateAccount extends SwankyAccountCommand { 17 | static description = "Create a new dev account in config"; 18 | 19 | static flags = { 20 | global: Flags.boolean({ 21 | char: "g", 22 | description: "Create account globally stored in Swanky system config.", 23 | }), 24 | new: Flags.boolean({ 25 | char: "n", 26 | description: "Generate a brand new account.", 27 | }), 28 | dev: Flags.boolean({ 29 | char: "d", 30 | description: "Make this account a dev account for local network usage.", 31 | }), 32 | }; 33 | 34 | constructor(argv: string[], baseConfig: any) { 35 | super(argv, baseConfig); 36 | (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG = false; 37 | } 38 | 39 | async run(): Promise { 40 | const { flags } = await this.parse(CreateAccount); 41 | 42 | const isDev = 43 | flags.dev ?? 44 | ( 45 | await inquirer.prompt([ 46 | { type: "confirm", message: "Is this a DEV account? ", name: "isDev", default: false }, 47 | ]) 48 | ).isDev; 49 | 50 | if (isDev) { 51 | console.log( 52 | `${chalk.redBright( 53 | "DEV account mnemonic will be stored in plain text. DO NOT USE IN PROD!" 54 | )}` 55 | ); 56 | } 57 | 58 | let tmpMnemonic = ""; 59 | if (flags.new) { 60 | tmpMnemonic = ChainAccount.generate(); 61 | console.log( 62 | `${ 63 | isDev 64 | ? "" 65 | : chalk.yellowBright( 66 | "This is your mnemonic. Copy it to a secure place, as it will be encrypted and not accessible anymore." 67 | ) 68 | } 69 | ${"-".repeat(tmpMnemonic.length)} 70 | ${tmpMnemonic} 71 | ${"-".repeat(tmpMnemonic.length)}` 72 | ); 73 | } else { 74 | tmpMnemonic = ( 75 | await inquirer.prompt([{ type: "input", message: "Enter mnemonic: ", name: "mnemonic" }]) 76 | ).mnemonic; 77 | } 78 | 79 | const accountData: AccountData = { 80 | mnemonic: "", 81 | isDev, 82 | alias: (await inquirer.prompt([{ type: "input", message: "Enter alias: ", name: "alias" }])) 83 | .alias, 84 | address: new ChainAccount(tmpMnemonic).pair.address, 85 | }; 86 | 87 | if (!isDev) { 88 | const password = ( 89 | await inquirer.prompt([ 90 | { type: "password", message: "Enter encryption password: ", name: "password" }, 91 | ]) 92 | ).password; 93 | accountData.mnemonic = encrypt(tmpMnemonic, password); 94 | } else { 95 | accountData.mnemonic = tmpMnemonic; 96 | } 97 | 98 | const configType = flags.global ? "global" : isLocalConfigCheck() ? "local" : "global"; 99 | const config = configType === "global" ? getSwankyConfig("global") : getSwankyConfig("local"); 100 | 101 | const configBuilder = new ConfigBuilder(config).addAccount(accountData); 102 | 103 | if (config.defaultAccount === null) { 104 | configBuilder.setDefaultAccount(accountData.alias); 105 | } 106 | 107 | try { 108 | await this.storeConfig(configBuilder.build(), configType); 109 | } catch (cause) { 110 | throw new FileError(`Error storing created account in ${configType} config`, { 111 | cause, 112 | }); 113 | } 114 | 115 | this.log( 116 | `${chalk.greenBright("✔")} Account with alias ${chalk.yellowBright( 117 | accountData.alias 118 | )} stored to config` 119 | ); 120 | 121 | await this.performFaucetTransfer(accountData, true); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/commands/account/default.ts: -------------------------------------------------------------------------------- 1 | import { Args, Flags } from "@oclif/core"; 2 | import chalk from "chalk"; 3 | import { SwankySystemConfig } from "../../types/index.js"; 4 | import inquirer from "inquirer"; 5 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 6 | import { ConfigError, FileError } from "../../lib/errors.js"; 7 | import { getSwankyConfig, isLocalConfigCheck } from "../../lib/index.js"; 8 | import { ConfigBuilder } from "../../lib/config-builder.js"; 9 | 10 | export class DefaultAccount extends SwankyCommand { 11 | static description = "Set default account to use"; 12 | 13 | static flags = { 14 | global: Flags.boolean({ 15 | char: "g", 16 | description: "Set default account globally in Swanky system config.", 17 | }), 18 | }; 19 | 20 | static args = { 21 | accountAlias: Args.string({ 22 | name: "accountAlias", 23 | required: false, 24 | description: "Alias of account to be used as default", 25 | }), 26 | }; 27 | 28 | constructor(argv: string[], baseConfig: any) { 29 | super(argv, baseConfig); 30 | (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG = false; 31 | } 32 | 33 | async run(): Promise { 34 | const { args, flags } = await this.parse(DefaultAccount); 35 | 36 | const configType = flags.global ? "global" : isLocalConfigCheck() ? "local" : "global"; 37 | const config = configType === "global" ? getSwankyConfig("global") : getSwankyConfig("local"); 38 | 39 | const accountAlias = args.accountAlias ?? (await this.promptForAccountAlias(config)); 40 | this.ensureAccountExists(config, accountAlias); 41 | 42 | const newConfig = new ConfigBuilder(config).setDefaultAccount(accountAlias).build(); 43 | 44 | try { 45 | await this.storeConfig(newConfig, configType); 46 | } catch (cause) { 47 | throw new FileError(`Error storing default account in ${configType} config`, { 48 | cause, 49 | }); 50 | } 51 | 52 | this.log( 53 | `${chalk.greenBright("✔")} Account with alias ${chalk.yellowBright( 54 | accountAlias 55 | )} set as default in ${configType} config` 56 | ); 57 | } 58 | 59 | private async promptForAccountAlias(config: SwankySystemConfig): Promise { 60 | const choices = config.accounts.map((account) => ({ 61 | name: `${account.alias} (${account.address})`, 62 | value: account.alias, 63 | })); 64 | 65 | const answer = await inquirer.prompt([ 66 | { 67 | type: "list", 68 | name: "defaultAccount", 69 | message: "Select default account", 70 | choices: choices, 71 | }, 72 | ]); 73 | 74 | return answer.defaultAccount; 75 | } 76 | 77 | private ensureAccountExists(config: SwankySystemConfig, alias: string) { 78 | const isSomeAccount = config.accounts.some((account) => account.alias === alias); 79 | if (!isSomeAccount) 80 | throw new ConfigError(`Provided account alias ${chalk.yellowBright(alias)} not found`); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/commands/account/faucet.ts: -------------------------------------------------------------------------------- 1 | import { Args } from "@oclif/core"; 2 | import { SwankyAccountCommand } from "../../lib/index.js"; 3 | 4 | export class Faucet extends SwankyAccountCommand { 5 | static description = "Transfer some tokens from faucet to an account"; 6 | 7 | static args = { 8 | alias: Args.string({ 9 | name: "alias", 10 | required: true, 11 | description: "Alias of account to be used", 12 | }), 13 | }; 14 | 15 | async run(): Promise { 16 | const { args } = await this.parse(Faucet); 17 | 18 | const accountData = this.findAccountByAlias(args.alias); 19 | await this.performFaucetTransfer(accountData); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/account/list.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 3 | 4 | export class ListAccounts extends SwankyCommand { 5 | static description = "List dev accounts stored in config"; 6 | static aliases = [`account:ls`]; 7 | 8 | constructor(argv: string[], baseConfig: any) { 9 | super(argv, baseConfig); 10 | (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG = false; 11 | } 12 | 13 | async run(): Promise { 14 | const countOfDevAccounts = this.swankyConfig.accounts.filter((account) => account.isDev).length; 15 | 16 | if (countOfDevAccounts !== 0) { 17 | this.log(`${chalk.greenBright("✔")} Stored dev accounts:`); 18 | 19 | for (const account of this.swankyConfig.accounts) { 20 | if (account.isDev) { 21 | this.log(`\t${chalk.yellowBright("Alias: ")} ${account.alias} \ 22 | ${chalk.yellowBright("Address: ")} ${account.address} ${this.swankyConfig.defaultAccount === account.alias ? chalk.greenBright("<- Default") : ""}`); 23 | } 24 | } 25 | } 26 | 27 | const countOfProdAccounts = this.swankyConfig.accounts.length - countOfDevAccounts; 28 | 29 | if (countOfProdAccounts !== 0) { 30 | this.log(`${chalk.greenBright("✔")} Stored prod accounts:`); 31 | 32 | for (const account of this.swankyConfig.accounts) { 33 | if (!account.isDev) { 34 | this.log(`\t${chalk.yellowBright("Alias: ")} ${account.alias} \ 35 | ${chalk.yellowBright("Address: ")} ${account.address} ${this.swankyConfig.defaultAccount === account.alias ? chalk.greenBright("<- Default") : ""}`); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/clear/index.ts: -------------------------------------------------------------------------------- 1 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 2 | import { FileError } from "../../lib/errors.js"; 3 | import fs from "fs-extra"; 4 | import path from "node:path"; 5 | import { Args, Flags } from "@oclif/core"; 6 | import { ensureContractNameOrAllFlagIsSet } from "../../lib/checks.js"; 7 | 8 | interface Folder { 9 | name: string; 10 | contractName?: string; 11 | path: string; 12 | } 13 | 14 | export default class Clear extends SwankyCommand { 15 | static flags = { 16 | all: Flags.boolean({ 17 | char: "a", 18 | description: "Select all the project artifacts for delete", 19 | }), 20 | }; 21 | 22 | static args = { 23 | contractName: Args.string({ 24 | name: "contractName", 25 | required: false, 26 | description: "Name of the contract artifact to clear", 27 | }), 28 | }; 29 | 30 | async deleteFolder(path: string): Promise { 31 | try { 32 | await fs.remove(path); 33 | this.log(`Successfully deleted ${path}`); 34 | } catch (err: any) { 35 | if (err.code === "ENOENT") { 36 | this.log(`Folder ${path} does not exist, skipping.`); 37 | } else { 38 | throw new FileError(`Error deleting the folder ${path}.`, { cause: err }); 39 | } 40 | } 41 | } 42 | 43 | public async run(): Promise { 44 | const { flags, args } = await this.parse(Clear); 45 | 46 | ensureContractNameOrAllFlagIsSet(args, flags); 47 | 48 | const workDirectory = process.cwd(); 49 | const foldersToDelete: Folder[] = flags.all 50 | ? [ 51 | { name: "Artifacts", path: path.join(workDirectory, "./artifacts") }, 52 | { name: "Target", path: path.join(workDirectory, "./target") }, 53 | ] 54 | : args.contractName 55 | ? [ 56 | { 57 | name: "Artifacts", 58 | contractName: args.contractName, 59 | path: path.join(workDirectory, "./artifacts/", args.contractName), 60 | }, 61 | { name: "Target", path: path.join(workDirectory, "./target") }, 62 | { 63 | name: "TestArtifacts", 64 | contractName: args.contractName, 65 | path: path.join(workDirectory, "./tests/", args.contractName, "/artifacts"), 66 | }, 67 | ] 68 | : []; 69 | for (const folder of foldersToDelete) { 70 | await this.spinner.runCommand( 71 | async () => this.deleteFolder(folder.path), 72 | `Deleting the ${folder.name} folder ${folder.contractName ? `for ${folder.contractName} contract` : ""}`, 73 | `Successfully deleted the ${folder.name} folder ${folder.contractName ? `for ${folder.contractName} contract` : ""}\n at ${folder.path}` 74 | ); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/commands/contract/compile.ts: -------------------------------------------------------------------------------- 1 | import { Args, Flags } from "@oclif/core"; 2 | import { spawn } from "node:child_process"; 3 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 4 | import { 5 | extractCargoContractVersion, 6 | Spinner, 7 | storeArtifacts, 8 | getSwankyConfig, 9 | findContractRecord, 10 | } from "../../lib/index.js"; 11 | import { InputError, ProcessError } from "../../lib/errors.js"; 12 | import { BuildMode, SwankyConfig } from "../../index.js"; 13 | import { ConfigBuilder } from "../../lib/config-builder.js"; 14 | import { 15 | ensureContractNameOrAllFlagIsSet, 16 | ensureContractPathExists, 17 | ensureCargoContractVersionCompatibility, 18 | } from "../../lib/checks.js"; 19 | 20 | export class CompileContract extends SwankyCommand { 21 | static description = "Compile the smart contract(s) in your contracts directory"; 22 | 23 | static flags = { 24 | release: Flags.boolean({ 25 | default: false, 26 | char: "r", 27 | description: 28 | "A production contract should always be build in `release` mode for building optimized wasm", 29 | }), 30 | verifiable: Flags.boolean({ 31 | default: false, 32 | description: 33 | "A production contract should be build in `verifiable` mode to deploy on a public network. Ensure Docker Engine is up and running.", 34 | }), 35 | all: Flags.boolean({ 36 | default: false, 37 | char: "a", 38 | description: "Set all to true to compile all contracts", 39 | }), 40 | }; 41 | 42 | static args = { 43 | contractName: Args.string({ 44 | name: "contractName", 45 | required: false, 46 | default: "", 47 | description: "Name of the contract to compile", 48 | }), 49 | }; 50 | 51 | async run(): Promise { 52 | const { args, flags } = await this.parse(CompileContract); 53 | 54 | const localConfig = getSwankyConfig("local") as SwankyConfig; 55 | 56 | ensureContractNameOrAllFlagIsSet(args, flags); 57 | 58 | const contractNames = flags.all 59 | ? Object.keys(this.swankyConfig.contracts) 60 | : [args.contractName]; 61 | const spinner = new Spinner(); 62 | 63 | for (const contractName of contractNames) { 64 | this.logger.info(`Started compiling contract [${contractName}]`); 65 | 66 | const contractRecord = findContractRecord(this.swankyConfig, contractName); 67 | 68 | await ensureContractPathExists(contractName); 69 | 70 | let buildMode = BuildMode.Debug; 71 | const compilationResult = await spinner.runCommand( 72 | async () => { 73 | return new Promise((resolve, reject) => { 74 | const compileArgs = [ 75 | "contract", 76 | "build", 77 | "--manifest-path", 78 | `contracts/${contractName}/Cargo.toml`, 79 | ]; 80 | if (flags.release && !flags.verifiable) { 81 | buildMode = BuildMode.Release; 82 | compileArgs.push("--release"); 83 | } 84 | if (flags.verifiable) { 85 | buildMode = BuildMode.Verifiable; 86 | const cargoContractVersion = extractCargoContractVersion(); 87 | if (cargoContractVersion === null) 88 | throw new InputError( 89 | `Cargo contract tool is required for verifiable mode. Please ensure it is installed.` 90 | ); 91 | 92 | ensureCargoContractVersionCompatibility(cargoContractVersion, "4.0.0", [ 93 | "4.0.0-alpha", 94 | ]); 95 | compileArgs.push("--verifiable"); 96 | } 97 | const compile = spawn("cargo", compileArgs); 98 | this.logger.info(`Running compile command: [${JSON.stringify(compile.spawnargs)}]`); 99 | let outputBuffer = ""; 100 | let errorBuffer = ""; 101 | 102 | compile.stdout.on("data", (data) => { 103 | outputBuffer += data.toString(); 104 | spinner.ora.clear(); 105 | }); 106 | compile.stdout.pipe(process.stdout); 107 | 108 | compile.stderr.on("data", (data) => { 109 | errorBuffer += data; 110 | }); 111 | 112 | compile.on("exit", (code) => { 113 | if (code === 0) { 114 | const regex = /Your contract artifacts are ready\. You can find them in:\n(.*)/; 115 | const match = outputBuffer.match(regex); 116 | if (match) { 117 | this.logger.info(`Contract ${contractName} compilation done.`); 118 | resolve(match[1]); 119 | } 120 | } else { 121 | reject(new ProcessError(errorBuffer)); 122 | } 123 | }); 124 | }); 125 | }, 126 | `Compiling ${contractName} contract`, 127 | `${contractName} Contract compiled successfully` 128 | ); 129 | 130 | const artifactsPath = compilationResult as string; 131 | 132 | await spinner.runCommand(async () => { 133 | return storeArtifacts(artifactsPath, contractRecord.name, contractRecord.moduleName); 134 | }, "Moving artifacts"); 135 | 136 | await this.spinner.runCommand(async () => { 137 | const buildData = { 138 | timestamp: Date.now(), 139 | artifactsPath, 140 | buildMode, 141 | isVerified: false, 142 | }; 143 | const newLocalConfig = new ConfigBuilder(localConfig) 144 | .addContractBuild(args.contractName, buildData) 145 | .build(); 146 | await this.storeConfig(newLocalConfig, "local"); 147 | }, "Writing config"); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/commands/contract/deploy.ts: -------------------------------------------------------------------------------- 1 | import { Args, Flags } from "@oclif/core"; 2 | import { cryptoWaitReady } from "@polkadot/util-crypto/crypto"; 3 | import { 4 | AbiType, 5 | ChainAccount, 6 | ChainApi, 7 | decrypt, 8 | resolveNetworkUrl, 9 | ensureAccountIsSet, 10 | getSwankyConfig, 11 | findContractRecord, 12 | } from "../../lib/index.js"; 13 | import { BuildMode, Encrypted, SwankyConfig } from "../../types/index.js"; 14 | import inquirer from "inquirer"; 15 | import chalk from "chalk"; 16 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 17 | import { ApiError, ProcessError } from "../../lib/errors.js"; 18 | import { ConfigBuilder } from "../../lib/config-builder.js"; 19 | import { 20 | contractFromRecord, 21 | ensureArtifactsExist, 22 | ensureDevAccountNotInProduction, 23 | } from "../../lib/checks.js"; 24 | 25 | export class DeployContract extends SwankyCommand { 26 | static description = "Deploy contract to a running node"; 27 | 28 | static flags = { 29 | account: Flags.string({ 30 | description: "Account alias to deploy contract with", 31 | }), 32 | gas: Flags.integer({ 33 | char: "g", 34 | }), 35 | args: Flags.string({ 36 | char: "a", 37 | multiple: true, 38 | }), 39 | constructorName: Flags.string({ 40 | default: "new", 41 | char: "c", 42 | description: "Constructor function name of a contract to deploy", 43 | }), 44 | network: Flags.string({ 45 | char: "n", 46 | default: "local", 47 | description: "Network name to connect to", 48 | }), 49 | }; 50 | 51 | static args = { 52 | contractName: Args.string({ 53 | name: "contractName", 54 | required: true, 55 | description: "Name of the contract to deploy", 56 | }), 57 | }; 58 | 59 | async run(): Promise { 60 | const { args, flags } = await this.parse(DeployContract); 61 | 62 | const localConfig = getSwankyConfig("local") as SwankyConfig; 63 | 64 | const contractRecord = findContractRecord(localConfig, args.contractName); 65 | 66 | const contract = await contractFromRecord(contractRecord); 67 | 68 | await ensureArtifactsExist(contract); 69 | 70 | if (contract.buildMode === undefined) { 71 | throw new ProcessError( 72 | `Build mode is undefined for contract ${args.contractName}. Please ensure the contract is correctly compiled.` 73 | ); 74 | } else if (contract.buildMode !== BuildMode.Verifiable) { 75 | await inquirer 76 | .prompt([ 77 | { 78 | type: "confirm", 79 | message: `You are deploying a not verified contract in ${ 80 | contract.buildMode === BuildMode.Release ? "release" : "debug" 81 | } mode. Are you sure you want to continue?`, 82 | name: "confirm", 83 | }, 84 | ]) 85 | .then((answers) => { 86 | if (!answers.confirm) { 87 | this.log( 88 | `${chalk.redBright("✖")} Aborted deployment of ${chalk.yellowBright( 89 | args.contractName 90 | )}` 91 | ); 92 | process.exit(0); 93 | } 94 | }); 95 | } 96 | 97 | ensureAccountIsSet(flags.account, this.swankyConfig); 98 | 99 | const accountAlias = (flags.account ?? this.swankyConfig.defaultAccount)!; 100 | 101 | const accountData = this.findAccountByAlias(accountAlias); 102 | 103 | ensureDevAccountNotInProduction(accountData, flags.network); 104 | 105 | const mnemonic = accountData.isDev 106 | ? (accountData.mnemonic as string) 107 | : decrypt( 108 | accountData.mnemonic as Encrypted, 109 | ( 110 | await inquirer.prompt([ 111 | { 112 | type: "password", 113 | message: `Enter password for ${chalk.yellowBright(accountData.alias)}: `, 114 | name: "password", 115 | }, 116 | ]) 117 | ).password 118 | ); 119 | 120 | const account = (await this.spinner.runCommand(async () => { 121 | await cryptoWaitReady(); 122 | return new ChainAccount(mnemonic); 123 | }, "Initialising")) as ChainAccount; 124 | 125 | const { abi, wasm } = (await this.spinner.runCommand(async () => { 126 | const abi = await contract.getABI(); 127 | const wasm = await contract.getWasm(); 128 | return { abi, wasm }; 129 | }, "Getting WASM")) as { abi: AbiType; wasm: Buffer }; 130 | 131 | const networkUrl = resolveNetworkUrl(this.swankyConfig, flags.network ?? ""); 132 | 133 | const api = (await this.spinner.runCommand(async () => { 134 | const api = await ChainApi.create(networkUrl); 135 | await api.start(); 136 | return api; 137 | }, "Connecting to node")) as ChainApi; 138 | 139 | const contractAddress = (await this.spinner.runCommand(async () => { 140 | try { 141 | const contractAddress = await api.deploy( 142 | abi, 143 | wasm, 144 | flags.constructorName, 145 | account.pair, 146 | flags.args!, 147 | flags.gas 148 | ); 149 | return contractAddress; 150 | } catch (cause) { 151 | throw new ApiError("Error deploying", { cause }); 152 | } 153 | }, "Deploying")) as string; 154 | 155 | await this.spinner.runCommand(async () => { 156 | const deploymentData = { 157 | timestamp: Date.now(), 158 | address: contractAddress, 159 | networkUrl, 160 | deployerAlias: accountAlias, 161 | }; 162 | const newLocalConfig = new ConfigBuilder(localConfig) 163 | .addContractDeployment(args.contractName, deploymentData) 164 | .build(); 165 | await this.storeConfig(newLocalConfig, "local"); 166 | }, "Writing config"); 167 | 168 | this.log(`Contract deployed!`); 169 | this.log(`Contract address: ${contractAddress}`); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/commands/contract/explain.ts: -------------------------------------------------------------------------------- 1 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 2 | import { Args } from "@oclif/core"; 3 | import { findContractRecord } from "../../lib/index.js"; 4 | import { contractFromRecord, ensureArtifactsExist } from "../../lib/checks.js"; 5 | 6 | export class ExplainContract extends SwankyCommand { 7 | static description = "Explain contract messages based on the contracts' metadata"; 8 | 9 | static args = { 10 | contractName: Args.string({ 11 | name: "contractName", 12 | required: true, 13 | description: "Name of the contract", 14 | }), 15 | }; 16 | 17 | async run(): Promise { 18 | const { args } = await this.parse(ExplainContract); 19 | 20 | const contractRecord = findContractRecord(this.swankyConfig, args.contractName); 21 | 22 | const contract = await contractFromRecord(contractRecord); 23 | 24 | await ensureArtifactsExist(contract); 25 | 26 | await contract.printInfo(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/contract/new.ts: -------------------------------------------------------------------------------- 1 | import { Args, Flags } from "@oclif/core"; 2 | import path from "node:path"; 3 | import { ensureDir, pathExists, pathExistsSync } from "fs-extra/esm"; 4 | import { 5 | checkCliDependencies, 6 | copyContractTemplateFiles, 7 | processTemplates, 8 | getTemplates, 9 | prepareTestFiles, 10 | getSwankyConfig, 11 | configName, 12 | } from "../../lib/index.js"; 13 | import { email, name, pickTemplate } from "../../lib/prompts.js"; 14 | import { kebabCase, pascalCase, snakeCase } from "change-case"; 15 | import { execaCommandSync } from "execa"; 16 | import inquirer from "inquirer"; 17 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 18 | import { InputError } from "../../lib/errors.js"; 19 | import { ConfigBuilder } from "../../lib/config-builder.js"; 20 | 21 | export class NewContract extends SwankyCommand { 22 | static description = "Generate a new smart contract template inside a project"; 23 | 24 | static flags = { 25 | template: Flags.string({ 26 | options: getTemplates().contractTemplatesList, 27 | }), 28 | verbose: Flags.boolean({ char: "v" }), 29 | }; 30 | 31 | static args = { 32 | contractName: Args.string({ 33 | name: "contractName", 34 | required: true, 35 | description: "Name of the new contract", 36 | }), 37 | }; 38 | 39 | async run(): Promise { 40 | const projectPath = process.cwd(); 41 | const { args, flags } = await this.parse(NewContract); 42 | 43 | if (await pathExists(path.resolve(projectPath, "contracts", args.contractName))) { 44 | throw new InputError(`Contract folder '${args.contractName}' already exists`); 45 | } 46 | 47 | if (this.swankyConfig.contracts[args.contractName]) { 48 | throw new InputError( 49 | `Contract with a name '${args.contractName}' already exists in ${configName()}` 50 | ); 51 | } 52 | 53 | const templates = getTemplates(); 54 | 55 | const { contractTemplate } = flags.template 56 | ? { contractTemplate: flags.template } 57 | : await inquirer.prompt([pickTemplate(templates.contractTemplatesList)]); 58 | 59 | const questions = [ 60 | name( 61 | "author", 62 | () => execaCommandSync("git config --get user.name").stdout, 63 | "What is your name?" 64 | ), 65 | email(), 66 | ]; 67 | 68 | const answers = await inquirer.prompt(questions); 69 | 70 | await this.spinner.runCommand( 71 | () => checkCliDependencies(this.spinner), 72 | "Checking dependencies" 73 | ); 74 | 75 | await this.spinner.runCommand( 76 | () => 77 | copyContractTemplateFiles( 78 | path.resolve(templates.contractTemplatesPath, contractTemplate), 79 | args.contractName, 80 | projectPath 81 | ), 82 | "Copying contract template files" 83 | ); 84 | 85 | if (contractTemplate === "psp22") { 86 | const e2eTestHelpersPath = path.resolve(projectPath, "tests", "test_helpers"); 87 | if (!pathExistsSync(e2eTestHelpersPath)) { 88 | await this.spinner.runCommand( 89 | () => prepareTestFiles("e2e", path.resolve(templates.templatesPath), projectPath), 90 | "Copying e2e test helpers" 91 | ); 92 | } else { 93 | console.log("e2e test helpers already exist. No files were copied."); 94 | } 95 | } 96 | 97 | await this.spinner.runCommand( 98 | () => 99 | processTemplates(projectPath, { 100 | project_name: kebabCase(this.config.pjson.name), 101 | author_name: answers.authorName, 102 | author_email: answers.email, 103 | swanky_version: this.config.pjson.version, 104 | contract_name: args.contractName, 105 | contract_name_snake: snakeCase(args.contractName), 106 | contract_name_pascal: pascalCase(args.contractName), 107 | }), 108 | "Processing contract templates" 109 | ); 110 | 111 | await ensureDir(path.resolve(projectPath, "artifacts", args.contractName)); 112 | 113 | await this.spinner.runCommand(async () => { 114 | const newLocalConfig = new ConfigBuilder(getSwankyConfig("local")) 115 | .addContract(args.contractName) 116 | .build(); 117 | await this.storeConfig(newLocalConfig, "local"); 118 | }, "Writing config"); 119 | 120 | this.log("😎 New contract successfully generated! 😎"); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/commands/contract/query.ts: -------------------------------------------------------------------------------- 1 | import { ContractPromise } from "@polkadot/api-contract/promise"; 2 | import { ContractCall } from "../../lib/contractCall.js"; 3 | 4 | export class Query extends ContractCall { 5 | static description = "Call a query message on smart contract"; 6 | 7 | static args = { ...ContractCall.callArgs }; 8 | 9 | static flags = { ...ContractCall.callFlags }; 10 | 11 | public async run(): Promise { 12 | const { flags, args } = await this.parse(Query); 13 | 14 | const contract = new ContractPromise( 15 | this.api.apiInst, 16 | this.metadata, 17 | this.deploymentInfo.address 18 | ); 19 | 20 | const storageDepositLimit = null; 21 | 22 | const gasLimit: any = this.api.apiInst.registry.createType("WeightV2", { 23 | refTime: BigInt(10000000000), 24 | proofSize: BigInt(10000000000), 25 | }); 26 | const queryResult = await contract.query[args.messageName]( 27 | this.account.pair.address, 28 | { 29 | gasLimit, 30 | storageDepositLimit, 31 | }, 32 | ...flags.params 33 | ); 34 | 35 | await this.api.apiInst.disconnect(); 36 | console.log(`Query result: ${queryResult.output?.toString()}`); 37 | if (flags.verbose) console.log(queryResult.result.toHuman()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/contract/test.ts: -------------------------------------------------------------------------------- 1 | import "ts-mocha"; 2 | import { Flags, Args } from "@oclif/core"; 3 | import path from "node:path"; 4 | import { globby } from "globby"; 5 | import Mocha from "mocha"; 6 | import { emptyDir, pathExistsSync } from "fs-extra/esm"; 7 | import { Contract } from "../../lib/contract.js"; 8 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 9 | import { FileError, ProcessError, TestError } from "../../lib/errors.js"; 10 | import { spawn } from "node:child_process"; 11 | import { findContractRecord, Spinner } from "../../lib/index.js"; 12 | import { 13 | contractFromRecord, 14 | ensureArtifactsExist, 15 | ensureContractNameOrAllFlagIsSet, 16 | ensureTypedContractExists, 17 | } from "../../lib/checks.js"; 18 | 19 | declare global { 20 | var contractTypesPath: string; // eslint-disable-line no-var 21 | } 22 | 23 | export class TestContract extends SwankyCommand { 24 | static description = "Run tests for a given contact"; 25 | 26 | static flags = { 27 | all: Flags.boolean({ 28 | default: false, 29 | char: "a", 30 | description: "Run tests for all contracts", 31 | }), 32 | mocha: Flags.boolean({ 33 | default: false, 34 | description: "Run tests with mocha", 35 | }), 36 | }; 37 | 38 | static args = { 39 | contractName: Args.string({ 40 | name: "contractName", 41 | default: "", 42 | description: "Name of the contract to test", 43 | }), 44 | }; 45 | 46 | async run(): Promise { 47 | const { args, flags } = await this.parse(TestContract); 48 | 49 | ensureContractNameOrAllFlagIsSet(args, flags); 50 | 51 | const contractNames = flags.all 52 | ? Object.keys(this.swankyConfig.contracts) 53 | : [args.contractName]; 54 | 55 | const spinner = new Spinner(); 56 | 57 | for (const contractName of contractNames) { 58 | const contractRecord = findContractRecord(this.swankyConfig, contractName); 59 | 60 | const contract = await contractFromRecord(contractRecord); 61 | 62 | console.log(`Testing contract: ${contractName}`); 63 | 64 | if (flags.mocha) { 65 | await this.runMochaTests(contract); 66 | } else { 67 | await spinner.runCommand( 68 | async () => { 69 | return new Promise((resolve, reject) => { 70 | const compileArgs = [ 71 | "test", 72 | "--features", 73 | "e2e-tests", 74 | "--manifest-path", 75 | `contracts/${contractName}/Cargo.toml`, 76 | "--release", 77 | ]; 78 | 79 | const compile = spawn("cargo", compileArgs); 80 | this.logger.info(`Running e2e-tests command: [${JSON.stringify(compile.spawnargs)}]`); 81 | let outputBuffer = ""; 82 | let errorBuffer = ""; 83 | 84 | compile.stdout.on("data", (data) => { 85 | outputBuffer += data.toString(); 86 | spinner.ora.clear(); 87 | }); 88 | compile.stdout.pipe(process.stdout); 89 | 90 | compile.stderr.on("data", (data) => { 91 | errorBuffer += data; 92 | }); 93 | 94 | compile.on("exit", (code) => { 95 | if (code === 0) { 96 | const regex = /test result: (.*)/; 97 | const match = outputBuffer.match(regex); 98 | if (match) { 99 | this.logger.info(`Contract ${contractName} e2e-testing done.`); 100 | resolve(match[1]); 101 | } 102 | } else { 103 | reject(new ProcessError(errorBuffer)); 104 | } 105 | }); 106 | }); 107 | }, 108 | `Testing ${contractName} contract`, 109 | `${contractName} testing finished successfully` 110 | ); 111 | } 112 | } 113 | } 114 | 115 | async runMochaTests(contract: Contract): Promise { 116 | const testDir = path.resolve("tests", contract.name); 117 | if (!pathExistsSync(testDir)) { 118 | throw new FileError(`Test directory does not exist: ${testDir}`); 119 | } 120 | 121 | await ensureArtifactsExist(contract); 122 | 123 | await ensureTypedContractExists(contract); 124 | 125 | const reportDir = path.resolve(testDir, "testReports"); 126 | await emptyDir(reportDir); 127 | 128 | const mocha = new Mocha({ 129 | timeout: 200000, 130 | reporter: "mochawesome", 131 | reporterOptions: { 132 | reportDir, 133 | quiet: true, 134 | }, 135 | }); 136 | 137 | const testFiles = await globby(`${testDir}/*.test.ts`); 138 | testFiles.forEach((file) => mocha.addFile(file)); 139 | 140 | global.contractTypesPath = path.resolve(testDir, "typedContract"); 141 | 142 | try { 143 | await new Promise((resolve, reject) => { 144 | mocha.run((failures) => { 145 | if (failures) { 146 | reject(new Error(`Tests failed. See report: ${reportDir}`)); 147 | } else { 148 | console.log(`All tests passed. See report: ${reportDir}`); 149 | resolve(); 150 | } 151 | }); 152 | }); 153 | } catch (error) { 154 | throw new TestError("Mocha tests failed", { cause: error }); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/commands/contract/tx.ts: -------------------------------------------------------------------------------- 1 | import { Flags } from "@oclif/core"; 2 | import { ContractPromise } from "@polkadot/api-contract/promise"; 3 | import { cryptoWaitReady } from "@polkadot/util-crypto/crypto"; 4 | 5 | import { ContractCall } from "../../lib/contractCall.js"; 6 | 7 | export class Tx extends ContractCall { 8 | static description = "Call a Tx message on smart contract"; 9 | 10 | static flags = { 11 | dry: Flags.boolean({ 12 | char: "d", 13 | description: "Do a dry run, without signing the transaction", 14 | }), 15 | ...ContractCall.callFlags, 16 | }; 17 | 18 | static args = { ...ContractCall.callArgs }; 19 | 20 | public async run(): Promise { 21 | const { flags, args } = await this.parse(Tx); 22 | 23 | const contract = new ContractPromise( 24 | this.api.apiInst, 25 | this.metadata, 26 | this.deploymentInfo.address 27 | ); 28 | 29 | const storageDepositLimit = null; 30 | 31 | const queryResult = await contract.query[args.messageName]( 32 | this.account.pair.address, 33 | { 34 | gasLimit: this.api.apiInst.registry.createType("WeightV2", { 35 | refTime: BigInt(10000000000), 36 | proofSize: BigInt(10000000000), 37 | }), 38 | storageDepositLimit, 39 | }, 40 | ...flags.params 41 | ); 42 | 43 | this.log( 44 | `Gas required: ${queryResult.gasRequired.refTime.toString()} (proofSize: ${queryResult.gasRequired.proofSize.toString()})` 45 | ); 46 | 47 | if (flags.dry) { 48 | console.log(`Dry run result:`); 49 | console.log(queryResult.result.toHuman()); 50 | return; 51 | } 52 | 53 | const customGas = flags.gas ? BigInt(flags.gas) : null; 54 | await cryptoWaitReady(); 55 | const txResult = contract.tx[args.messageName]( 56 | { 57 | storageDepositLimit, 58 | gasLimit: customGas ?? queryResult.gasRequired, 59 | }, 60 | ...flags.params 61 | ); 62 | await txResult.signAndSend(this.account.pair, async (result: any) => { 63 | if (result.status.isFinalized || result.status.isInBlock) { 64 | console.log("Tx result:"); 65 | if (flags.verbose) { 66 | console.log(JSON.stringify(result.toHuman(), null, 2)); 67 | } else { 68 | console.log(result.toHuman()); 69 | } 70 | await this.api.apiInst.disconnect(); 71 | } 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/commands/contract/verify.ts: -------------------------------------------------------------------------------- 1 | import { Args, Flags } from "@oclif/core"; 2 | import { 3 | extractCargoContractVersion, 4 | findContractRecord, 5 | getSwankyConfig, 6 | Spinner, 7 | } from "../../lib/index.js"; 8 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 9 | import { InputError, ProcessError } from "../../lib/errors.js"; 10 | import { spawn } from "node:child_process"; 11 | import { ConfigBuilder } from "../../lib/config-builder.js"; 12 | import { BuildData, SwankyConfig } from "../../index.js"; 13 | import { 14 | ensureContractNameOrAllFlagIsSet, 15 | ensureContractPathExists, 16 | ensureCargoContractVersionCompatibility, 17 | } from "../../lib/checks.js"; 18 | 19 | export class VerifyContract extends SwankyCommand { 20 | static description = "Verify the smart contract(s) in your contracts directory"; 21 | 22 | static flags = { 23 | all: Flags.boolean({ 24 | default: false, 25 | char: "a", 26 | description: "Set all to true to verify all contracts", 27 | }), 28 | }; 29 | 30 | static args = { 31 | contractName: Args.string({ 32 | name: "contractName", 33 | required: false, 34 | default: "", 35 | description: "Name of the contract to verify", 36 | }), 37 | }; 38 | 39 | async run(): Promise { 40 | const { args, flags } = await this.parse(VerifyContract); 41 | 42 | const localConfig = getSwankyConfig("local") as SwankyConfig; 43 | 44 | const cargoContractVersion = extractCargoContractVersion(); 45 | if (cargoContractVersion === null) 46 | throw new InputError( 47 | `Cargo contract tool is required for verifiable mode. Please ensure it is installed.` 48 | ); 49 | 50 | ensureCargoContractVersionCompatibility(cargoContractVersion, "4.0.0", ["4.0.0-alpha"]); 51 | 52 | ensureContractNameOrAllFlagIsSet(args, flags); 53 | 54 | const contractNames = flags.all 55 | ? Object.keys(this.swankyConfig.contracts) 56 | : [args.contractName]; 57 | 58 | const spinner = new Spinner(); 59 | 60 | for (const contractName of contractNames) { 61 | this.logger.info(`Started compiling contract [${contractName}]`); 62 | 63 | const contractRecord = findContractRecord(this.swankyConfig, contractName); 64 | 65 | await ensureContractPathExists(contractName); 66 | 67 | if (!contractRecord.build) { 68 | throw new InputError(`Contract ${contractName} is not compiled. Please compile it first`); 69 | } 70 | 71 | await spinner.runCommand( 72 | async () => { 73 | return new Promise((resolve, reject) => { 74 | if (contractRecord.build!.isVerified) { 75 | this.logger.info(`Contract ${contractName} is already verified`); 76 | resolve(true); 77 | } 78 | const compileArgs = [ 79 | "contract", 80 | "verify", 81 | `artifacts/${contractName}/${contractName}.contract`, 82 | "--manifest-path", 83 | `contracts/${contractName}/Cargo.toml`, 84 | ]; 85 | const compile = spawn("cargo", compileArgs); 86 | this.logger.info(`Running verify command: [${JSON.stringify(compile.spawnargs)}]`); 87 | let outputBuffer = ""; 88 | let errorBuffer = ""; 89 | 90 | compile.stdout.on("data", (data) => { 91 | outputBuffer += data.toString(); 92 | spinner.ora.clear(); 93 | }); 94 | 95 | compile.stderr.on("data", (data) => { 96 | errorBuffer += data; 97 | }); 98 | 99 | compile.on("exit", (code) => { 100 | if (code === 0) { 101 | const regex = /Successfully verified contract (.*) against reference contract (.*)/; 102 | const match = outputBuffer.match(regex); 103 | if (match) { 104 | this.logger.info(`Contract ${contractName} verification done.`); 105 | resolve(true); 106 | } 107 | } else { 108 | reject(new ProcessError(errorBuffer)); 109 | } 110 | }); 111 | }); 112 | }, 113 | `Verifying ${contractName} contract`, 114 | `${contractName} Contract verified successfully` 115 | ); 116 | 117 | await this.spinner.runCommand(async () => { 118 | const buildData = { 119 | ...contractRecord.build, 120 | isVerified: true, 121 | } as BuildData; 122 | 123 | const newLocalConfig = new ConfigBuilder(localConfig) 124 | .addContractBuild(args.contractName, buildData) 125 | .build(); 126 | 127 | await this.storeConfig(newLocalConfig, "local"); 128 | }, "Writing config"); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/commands/env/install.ts: -------------------------------------------------------------------------------- 1 | import { Flags } from "@oclif/core"; 2 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 3 | import { InputError } from "../../lib/errors.js"; 4 | import { installCliDevDeps } from "../../lib/tasks.js"; 5 | import { SUPPORTED_DEPS } from "../../lib/consts.js"; 6 | import { DependencyName, SwankyConfig, getSwankyConfig } from "../../index.js"; 7 | import { ConfigBuilder } from "../../lib/config-builder.js"; 8 | 9 | export class Install extends SwankyCommand { 10 | static description = "Install dev dependencies"; 11 | 12 | static flags = { 13 | deps: Flags.string({ 14 | description: `Install the specified dev dependency name and version in the format . The following options are supported: ${Object.keys( 15 | SUPPORTED_DEPS 16 | ).join(", ")}. For installing rust nightly version run: env install --deps rust@nightly`, 17 | multiple: true, 18 | char: "d", 19 | }), 20 | }; 21 | 22 | async run(): Promise { 23 | const { flags } = await this.parse(Install); 24 | const depsArray = flags.deps ?? []; 25 | 26 | const localConfig = getSwankyConfig("local") as SwankyConfig; 27 | const depsToInstall = depsArray.length > 0 ? this.parseDeps(depsArray) : localConfig.env; 28 | 29 | if (Object.keys(depsToInstall).length === 0) { 30 | this.log("No dependencies to install."); 31 | return; 32 | } 33 | 34 | await this.installDeps(depsToInstall); 35 | 36 | if (depsArray.length > 0) { 37 | await this.updateLocalConfig(depsToInstall); 38 | } 39 | 40 | this.log("Swanky Dev Dependencies Installed successfully"); 41 | } 42 | 43 | parseDeps(deps: string[]): Record { 44 | return deps.reduce( 45 | (acc, dep) => { 46 | const [key, value] = dep.split("@"); 47 | if (!Object.keys(SUPPORTED_DEPS).includes(key)) { 48 | throw new InputError( 49 | `Unsupported dependency '${key}'. Supported: ${Object.keys(SUPPORTED_DEPS).join(", ")}` 50 | ); 51 | } 52 | acc[key] = value || "latest"; 53 | return acc; 54 | }, 55 | {} as Record 56 | ); 57 | } 58 | 59 | async installDeps(dependencies: Record) { 60 | for (const [dep, version] of Object.entries(dependencies)) { 61 | await this.spinner.runCommand( 62 | () => installCliDevDeps(this.spinner, dep as DependencyName, version), 63 | `Installing ${dep}@${version}` 64 | ); 65 | } 66 | } 67 | 68 | async updateLocalConfig(newDeps: Record): Promise { 69 | await this.spinner.runCommand(async () => { 70 | const newLocalConfig = new ConfigBuilder(getSwankyConfig("local")).updateEnv(newDeps).build(); 71 | await this.storeConfig(newLocalConfig, "local"); 72 | }, "Updating Swanky config with new Dev Dependencies..."); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/commands/generate/tests.ts: -------------------------------------------------------------------------------- 1 | import { Args, Flags } from "@oclif/core"; 2 | import { 3 | findContractRecord, 4 | getTemplates, 5 | prepareTestFiles, 6 | processTemplates, 7 | } from "../../lib/index.js"; 8 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 9 | import { ConfigError, InputError } from "../../lib/errors.js"; 10 | import path from "node:path"; 11 | import { existsSync } from "node:fs"; 12 | import inquirer from "inquirer"; 13 | import { kebabCase, pascalCase } from "change-case"; 14 | import { TestType } from "../../index.js"; 15 | import { contractFromRecord, ensureArtifactsExist } from "../../lib/checks.js"; 16 | 17 | export class GenerateTests extends SwankyCommand { 18 | static description = "Generate test files for the specified contract"; 19 | 20 | static args = { 21 | contractName: Args.string({ 22 | name: "contractName", 23 | description: "Name of the contract", 24 | }), 25 | }; 26 | 27 | static flags = { 28 | template: Flags.string({ 29 | options: getTemplates().contractTemplatesList, 30 | }), 31 | mocha: Flags.boolean({ 32 | default: false, 33 | description: "Generate mocha test files", 34 | }), 35 | }; 36 | 37 | async run(): Promise { 38 | const { args, flags } = await this.parse(GenerateTests); 39 | 40 | if (flags.mocha) { 41 | if (!args.contractName) { 42 | throw new InputError("The 'contractName' argument is required to generate mocha tests."); 43 | } 44 | 45 | await this.checkContract(args.contractName); 46 | } 47 | 48 | const testType: TestType = flags.mocha ? "mocha" : "e2e"; 49 | const testsFolderPath = path.resolve("tests"); 50 | const testPath = this.getTestPath(testType, testsFolderPath, args.contractName); 51 | 52 | const templates = getTemplates(); 53 | const templateName = await this.resolveTemplateName(flags, templates.contractTemplatesList); 54 | 55 | const overwrite = await this.checkOverwrite(testPath, testType, args.contractName); 56 | if (!overwrite) return; 57 | 58 | await this.generateTests( 59 | testType, 60 | templates.templatesPath, 61 | process.cwd(), 62 | args.contractName, 63 | templateName 64 | ); 65 | } 66 | 67 | async checkContract(name: string) { 68 | const contractRecord = findContractRecord(this.swankyConfig, name); 69 | 70 | const contract = await contractFromRecord(contractRecord); 71 | 72 | await ensureArtifactsExist(contract); 73 | } 74 | 75 | async checkOverwrite( 76 | testPath: string, 77 | testType: TestType, 78 | contractName?: string 79 | ): Promise { 80 | if (!existsSync(testPath)) return true; // No need to overwrite 81 | const message = 82 | testType === "e2e" 83 | ? "Test helpers already exist. Overwrite?" 84 | : `Mocha tests for ${contractName} already exist. Overwrite?`; 85 | 86 | const { overwrite } = await inquirer.prompt({ 87 | type: "confirm", 88 | name: "overwrite", 89 | message, 90 | default: false, 91 | }); 92 | 93 | return overwrite; 94 | } 95 | 96 | getTestPath(testType: TestType, testsPath: string, contractName?: string): string { 97 | if (testType === "e2e") { 98 | return path.resolve(testsPath, "test_helpers"); 99 | } else if (testType === "mocha" && contractName) { 100 | return path.resolve(testsPath, contractName, "index.test.ts"); 101 | } else { 102 | throw new InputError("The 'contractName' argument is required to generate mocha tests."); 103 | } 104 | } 105 | 106 | async resolveTemplateName(flags: any, templates: any): Promise { 107 | if (flags.mocha && !flags.template) { 108 | if (!templates?.length) throw new ConfigError("Template list is empty!"); 109 | const response = await inquirer.prompt([ 110 | { 111 | type: "list", 112 | name: "template", 113 | message: "Choose a contract template:", 114 | choices: templates, 115 | }, 116 | ]); 117 | return response.template; 118 | } 119 | return flags.template; 120 | } 121 | 122 | async generateTests( 123 | testType: TestType, 124 | templatesPath: string, 125 | projectPath: string, 126 | contractName?: string, 127 | templateName?: string 128 | ): Promise { 129 | if (testType === "e2e") { 130 | await this.spinner.runCommand( 131 | () => prepareTestFiles("e2e", templatesPath, projectPath), 132 | "Generating e2e test helpers" 133 | ); 134 | } else { 135 | await this.spinner.runCommand( 136 | () => prepareTestFiles("mocha", templatesPath, projectPath, templateName, contractName), 137 | `Generating tests for ${contractName} with mocha` 138 | ); 139 | } 140 | await this.spinner.runCommand( 141 | () => 142 | processTemplates(projectPath, { 143 | project_name: kebabCase(this.config.pjson.name), 144 | swanky_version: this.config.pjson.version, 145 | contract_name: contractName ?? "", 146 | contract_name_pascal: contractName ? pascalCase(contractName) : "", 147 | }), 148 | "Processing templates" 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/commands/generate/types.ts: -------------------------------------------------------------------------------- 1 | import { Args } from "@oclif/core"; 2 | import { findContractRecord, generateTypes } from "../../lib/index.js"; 3 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 4 | import { contractFromRecord, ensureArtifactsExist } from "../../lib/checks.js"; 5 | 6 | export class GenerateTypes extends SwankyCommand { 7 | static description = "Generate types from compiled contract metadata"; 8 | 9 | static args = { 10 | contractName: Args.string({ 11 | name: "contractName", 12 | required: true, 13 | description: "Name of the contract", 14 | }), 15 | }; 16 | 17 | async run(): Promise { 18 | const { args } = await this.parse(GenerateTypes); 19 | 20 | const contractRecord = findContractRecord(this.swankyConfig, args.contractName); 21 | 22 | const contract = await contractFromRecord(contractRecord); 23 | 24 | await ensureArtifactsExist(contract); 25 | 26 | await this.spinner.runCommand(async () => { 27 | await generateTypes(contract.name); 28 | }, "Generating types"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/node/chopsticks/init.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { SwankyCommand } from "../../../lib/swankyCommand.js"; 3 | import { copyChopsticksTemplateFile, getSwankyConfig, getTemplates } from "../../../lib/index.js"; 4 | import { ConfigBuilder } from "../../../lib/config-builder.js"; 5 | import { SwankyConfig } from "../../../types/index.js"; 6 | 7 | export const chopsticksConfig = "dev.yml"; 8 | 9 | export class InitChopsticks extends SwankyCommand { 10 | static description = "Initialize chopsticks config"; 11 | 12 | async run(): Promise { 13 | const localConfig = getSwankyConfig("local") as SwankyConfig; 14 | const projectPath = path.resolve(); 15 | 16 | const chopsticksTemplatePath = getTemplates().chopsticksTemplatesPath; 17 | const configPath = path.resolve(projectPath, "node", "config"); 18 | 19 | await this.spinner.runCommand( 20 | () => copyChopsticksTemplateFile(chopsticksTemplatePath, configPath), 21 | "Copying Chopsticks template files..." 22 | ); 23 | 24 | await this.spinner.runCommand(async () => { 25 | const newLocalConfig = new ConfigBuilder(localConfig) 26 | .addChopsticks(path.resolve(projectPath, "node", "config", chopsticksConfig)) 27 | .build(); 28 | await this.storeConfig(newLocalConfig, "local"); 29 | }, "Updating Swanky configuration with Chopsticks settings..."); 30 | 31 | this.log("Chopsticks config initialized successfully"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/node/chopsticks/start.ts: -------------------------------------------------------------------------------- 1 | import { Flags } from "@oclif/core"; 2 | import { execaCommand } from "execa"; 3 | import { SwankyCommand } from "../../../lib/swankyCommand.js"; 4 | import { ensureSwankyNodeInstalled } from "../../../lib/index.js"; 5 | import { pathExists } from "fs-extra/esm"; 6 | import { ConfigError, FileError } from "../../../lib/errors.js"; 7 | export class StartChopsticks extends SwankyCommand { 8 | static description = "Start chopsticks"; 9 | 10 | static flags = { 11 | config: Flags.string({ 12 | description: "Path to the chopsticks config file", 13 | }), 14 | }; 15 | 16 | async run(): Promise { 17 | const { flags } = await this.parse(StartChopsticks); 18 | 19 | ensureSwankyNodeInstalled(this.swankyConfig); 20 | 21 | const chopsticksConfigPath = flags.config ?? this.swankyConfig.node.chopsticks?.configPath; 22 | 23 | if (!chopsticksConfigPath) { 24 | throw new ConfigError( 25 | "Chopsticks config not set in swanky config. Please set it in swanky config or provide the path to the chopsticks config file using --config flag." 26 | ); 27 | } 28 | 29 | if (!(await pathExists(chopsticksConfigPath))) { 30 | throw new FileError(`Chopsticks config file not found at ${flags.config}`); 31 | } 32 | 33 | await execaCommand(`npx @acala-network/chopsticks@latest --config=${chopsticksConfigPath}`, { 34 | stdio: "inherit", 35 | }); 36 | 37 | this.log("Chopsticks started successfully."); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/node/install.ts: -------------------------------------------------------------------------------- 1 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 2 | import { Flags } from "@oclif/core"; 3 | import { downloadNode, getSwankyConfig, swankyNodeVersions } from "../../lib/index.js"; 4 | import path from "node:path"; 5 | import inquirer from "inquirer"; 6 | import { ConfigBuilder } from "../../lib/config-builder.js"; 7 | import { DEFAULT_NODE_INFO } from "../../lib/consts.js"; 8 | import { choice, pickNodeVersion } from "../../lib/prompts.js"; 9 | import { InputError } from "../../lib/errors.js"; 10 | 11 | export class InstallNode extends SwankyCommand { 12 | static description = "Install swanky node binary"; 13 | 14 | static flags = { 15 | "set-version": Flags.string({ 16 | description: 17 | "Specify version of swanky node to install. \n List of supported versions: " + 18 | Array.from(swankyNodeVersions.keys()).join(", "), 19 | required: false, 20 | }), 21 | }; 22 | async run(): Promise { 23 | const { flags } = await this.parse(InstallNode); 24 | if (flags.verbose) { 25 | this.spinner.verbose = true; 26 | } 27 | let nodeVersion = DEFAULT_NODE_INFO.version; 28 | 29 | if (flags["set-version"]) { 30 | nodeVersion = flags["set-version"]; 31 | if (!swankyNodeVersions.has(nodeVersion)) { 32 | throw new InputError( 33 | `Version ${nodeVersion} is not supported.\n List of supported versions: ${Array.from(swankyNodeVersions.keys()).join(", ")}` 34 | ); 35 | } 36 | } else { 37 | const versions = Array.from(swankyNodeVersions.keys()); 38 | await inquirer.prompt([pickNodeVersion(versions)]).then((answers) => { 39 | nodeVersion = answers.version; 40 | }); 41 | } 42 | 43 | const projectPath = path.resolve(); 44 | 45 | if (this.swankyConfig.node.localPath !== "") { 46 | const { overwrite } = await inquirer.prompt([ 47 | choice("overwrite", "Swanky node already installed. Do you want to overwrite it?"), 48 | ]); 49 | if (!overwrite) { 50 | return; 51 | } 52 | } 53 | 54 | const nodeInfo = swankyNodeVersions.get(nodeVersion)!; 55 | 56 | const taskResult = (await this.spinner.runCommand( 57 | () => downloadNode(projectPath, nodeInfo, this.spinner), 58 | "Downloading Swanky node" 59 | )) as string; 60 | const nodePath = path.resolve(projectPath, taskResult); 61 | 62 | await this.spinner.runCommand(async () => { 63 | const newLocalConfig = new ConfigBuilder(getSwankyConfig("local")) 64 | .updateNodeSettings({ 65 | localPath: nodePath, 66 | polkadotPalletVersions: nodeInfo.polkadotPalletVersions, 67 | supportedInk: nodeInfo.supportedInk, 68 | version: nodeInfo.version, 69 | }) 70 | .build(); 71 | await this.storeConfig(newLocalConfig, "local"); 72 | }, "Updating swanky config"); 73 | 74 | this.log("Swanky Node Installed successfully"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/commands/node/purge.ts: -------------------------------------------------------------------------------- 1 | import { execaCommand } from "execa"; 2 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 3 | export class PurgeNode extends SwankyCommand { 4 | static description = "Purge local chain state"; 5 | 6 | async run(): Promise { 7 | await execaCommand(`${this.swankyConfig.node.localPath} purge-chain`, { 8 | stdio: "inherit", 9 | }); 10 | 11 | this.log("Purged chain state"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/node/start.ts: -------------------------------------------------------------------------------- 1 | import { Flags } from "@oclif/core"; 2 | import { execaCommand } from "execa"; 3 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 4 | import semver from "semver"; 5 | import { ensureSwankyNodeInstalled } from "../../lib/index.js"; 6 | export class StartNode extends SwankyCommand { 7 | static description = "Start a local node"; 8 | 9 | static flags = { 10 | tmp: Flags.boolean({ 11 | char: "t", 12 | required: false, 13 | description: "Run node with non-persistent mode", 14 | }), 15 | rpcCors: Flags.string({ 16 | required: false, 17 | default: 18 | "http://localhost:*,http://127.0.0.1:*,https://localhost:*,https://127.0.0.1:*,https://polkadot.js.org,https://contracts-ui.substrate.io/", 19 | description: `RPC CORS origin swanky-node accepts. With '--tmp' flag, node accepts all origins. 20 | Without it, you may need to specify by comma separated string. 21 | By default, 'http://localhost:*,http://127.0.0.1:*,https://localhost:*,https://127.0.0.1:*,https://polkadot.js.org,https://contracts-ui.substrate.io/' is set.`, 22 | }), 23 | finalizeDelaySec: Flags.integer({ 24 | required: false, 25 | default: 0, // 0 means instant finalization 26 | description: "Delay time in seconds after blocks being sealed", 27 | }), 28 | }; 29 | 30 | async run(): Promise { 31 | const { flags } = await this.parse(StartNode); 32 | 33 | ensureSwankyNodeInstalled(this.swankyConfig); 34 | 35 | // Run persistent mode by default. non-persistent mode in case flag is provided. 36 | // Non-Persistent mode (`--dev`) allows all CORS origin, without `--dev`, users need to specify origins by `--rpc-cors`. 37 | await execaCommand( 38 | `${this.swankyConfig.node.localPath} \ 39 | ${semver.gte(this.swankyConfig.node.version, "1.6.0") ? `--finalize-delay-sec ${flags.finalizeDelaySec}` : ""} \ 40 | ${flags.tmp ? "--dev" : `--rpc-cors ${flags.rpcCors}`}`, 41 | { 42 | stdio: "inherit", 43 | } 44 | ); 45 | 46 | this.log("Node started"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/node/version.ts: -------------------------------------------------------------------------------- 1 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 2 | export class NodeVersion extends SwankyCommand { 3 | static description = "Show swanky node version"; 4 | async run(): Promise { 5 | if (this.swankyConfig.node.version === "") { 6 | this.log("Swanky node is not installed"); 7 | } else { 8 | this.log(`Swanky node version: ${this.swankyConfig.node.version}`); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/zombienet/init.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { Flags } from "@oclif/core"; 3 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 4 | import { 5 | buildZombienetConfigFromBinaries, 6 | copyZombienetTemplateFile, 7 | downloadZombienetBinaries, 8 | getSwankyConfig, 9 | getTemplates, 10 | osCheck, 11 | Spinner, 12 | } from "../../lib/index.js"; 13 | import { pathExistsSync } from "fs-extra/esm"; 14 | import { zombienet, zombienetBinariesList } from "../../lib/zombienetInfo.js"; 15 | import { ConfigBuilder } from "../../lib/config-builder.js"; 16 | import { SwankyConfig, ZombienetData } from "../../index.js"; 17 | 18 | export const zombienetConfig = "zombienet.config.toml"; 19 | 20 | export class InitZombienet extends SwankyCommand { 21 | static description = "Initialize Zombienet"; 22 | 23 | static flags = { 24 | binaries: Flags.string({ 25 | char: "b", 26 | multiple: true, 27 | options: zombienetBinariesList, 28 | description: "Binaries to install", 29 | }), 30 | }; 31 | 32 | async run(): Promise { 33 | const { flags } = await this.parse(InitZombienet); 34 | const binArray = flags.deps ?? []; 35 | 36 | const localConfig = getSwankyConfig("local") as SwankyConfig; 37 | 38 | const platform = osCheck().platform; 39 | if (platform === "darwin") { 40 | this.warn(`Note for MacOs users: Polkadot binary is not currently supported for MacOs. 41 | As a result users of MacOS need to clone the Polkadot repo (https://github.com/paritytech/polkadot), create a release and add it in your PATH manually (setup will advice you so as well). Check the official zombienet documentation for manual settings: https://paritytech.github.io/zombienet/.`); 42 | } 43 | 44 | const projectPath = path.resolve(); 45 | if (pathExistsSync(path.resolve(projectPath, "zombienet", "bin", "zombienet"))) { 46 | this.error("Zombienet config already initialized"); 47 | } 48 | 49 | const spinner = new Spinner(flags.verbose); 50 | 51 | const zombienetData: ZombienetData = { 52 | version: zombienet.version, 53 | downloadUrl: zombienet.downloadUrl, 54 | binaries: {}, 55 | }; 56 | 57 | if (!binArray.includes("polkadot")) { 58 | binArray.push("polkadot"); 59 | } 60 | 61 | for (const binaryName of binArray) { 62 | if (platform === "darwin" && binaryName.startsWith("polkadot")) { 63 | continue; 64 | } 65 | if (!Object.keys(zombienet.binaries).includes(binaryName)) { 66 | this.error(`Binary ${binaryName} not found in Zombienet config`); 67 | } 68 | zombienetData.binaries[binaryName] = 69 | zombienet.binaries[binaryName as keyof typeof zombienet.binaries]; 70 | } 71 | 72 | await this.spinner.runCommand(async () => { 73 | const newLocalConfig = new ConfigBuilder(localConfig).addZombienet(zombienetData).build(); 74 | await this.storeConfig(newLocalConfig, "local"); 75 | }, "Writing config"); 76 | 77 | const zombienetTemplatePath = getTemplates().zombienetTemplatesPath; 78 | 79 | const configPath = path.resolve(projectPath, "zombienet", "config"); 80 | 81 | if (binArray.length === 1 && binArray[0] === "polkadot") { 82 | await spinner.runCommand( 83 | () => copyZombienetTemplateFile(zombienetTemplatePath, configPath), 84 | "Copying template files" 85 | ); 86 | } else { 87 | await spinner.runCommand( 88 | () => buildZombienetConfigFromBinaries(binArray, zombienetTemplatePath, configPath), 89 | "Copying template files" 90 | ); 91 | } 92 | 93 | // Install binaries based on zombie config 94 | await this.spinner.runCommand( 95 | () => downloadZombienetBinaries(binArray, projectPath, localConfig, this.spinner), 96 | "Downloading Zombienet binaries" 97 | ); 98 | 99 | this.log("ZombieNet config Installed successfully"); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/commands/zombienet/start.ts: -------------------------------------------------------------------------------- 1 | import { SwankyCommand } from "../../lib/swankyCommand.js"; 2 | import path from "node:path"; 3 | import { pathExistsSync } from "fs-extra/esm"; 4 | import { execaCommand } from "execa"; 5 | import { Flags } from "@oclif/core"; 6 | 7 | export class StartZombienet extends SwankyCommand { 8 | static description = "Start Zombienet"; 9 | 10 | static flags = { 11 | "config-path": Flags.string({ 12 | char: "c", 13 | required: false, 14 | default: "./zombienet/config/zombienet.config.toml", 15 | description: "Path to zombienet config", 16 | }), 17 | }; 18 | 19 | async run(): Promise { 20 | const { flags } = await this.parse(StartZombienet); 21 | const projectPath = path.resolve(); 22 | const binPath = path.resolve(projectPath, "zombienet", "bin"); 23 | if (!pathExistsSync(path.resolve(binPath, "zombienet"))) { 24 | this.error("Zombienet has not initialized. Run `swanky zombienet:init` first"); 25 | } 26 | 27 | await execaCommand( 28 | `./zombienet/bin/zombienet \ 29 | spawn --provider native \ 30 | ${flags["config-path"]} 31 | `, 32 | { 33 | stdio: "inherit", 34 | } 35 | ); 36 | 37 | this.log("ZombieNet started successfully"); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/hooks/command_not_found/command_not_found.ts: -------------------------------------------------------------------------------- 1 | import { Hook } from "@oclif/core"; 2 | import chalk from "chalk"; 3 | import process from "node:process"; 4 | const hook: Hook<"command_not_found"> = async function (opts) { 5 | if (opts.id === "compile" || opts.id === "deploy") { 6 | process.stdout.write( 7 | chalk.redBright(`The "${opts.id}" command is now a subcommand of "contract" \n`) 8 | ); 9 | process.stdout.write( 10 | `You can use it like: ${chalk.greenBright(`swanky contract ${opts.id} contract_name`)}\n` 11 | ); 12 | } else { 13 | process.stdin.write( 14 | `${opts.id} is not known swanky command. Run swanky help to list known commands.\n` 15 | ); 16 | } 17 | }; 18 | 19 | export default hook; 20 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "@polkadot/api-augment"; 2 | 3 | export * from "./lib/index.js"; 4 | export * from "./types/index.js"; 5 | 6 | export { run } from "@oclif/core"; 7 | -------------------------------------------------------------------------------- /src/lib/account.ts: -------------------------------------------------------------------------------- 1 | import { mnemonicGenerate } from "@polkadot/util-crypto"; 2 | import { Keyring } from "@polkadot/keyring"; 3 | import { KeyringPair } from "@polkadot/keyring/types"; 4 | import { AccountData, ChainProperty, KeypairType } from "../types/index.js"; 5 | import { KEYPAIR_TYPE, LOCAL_FAUCET_AMOUNT } from "./consts.js"; 6 | import { Command } from "@oclif/core"; 7 | import { SwankyCommand } from "./swankyCommand.js"; 8 | import { ChainApi } from "./substrate-api.js"; 9 | import { resolveNetworkUrl } from "./command-utils.js"; 10 | import chalk from "chalk"; 11 | import { ApiError } from "./errors.js"; 12 | 13 | interface IChainAccount { 14 | pair: KeyringPair; 15 | keyring: Keyring; 16 | } 17 | 18 | export class ChainAccount implements IChainAccount { 19 | private _keyring: Keyring; 20 | private _mnemonic: string; 21 | private _keyringType: KeypairType; 22 | 23 | public static generate() { 24 | return mnemonicGenerate(); 25 | } 26 | 27 | constructor(mnemonic: string, type: KeypairType = KEYPAIR_TYPE) { 28 | this._keyringType = type; 29 | this._keyring = new Keyring({ type: type }); 30 | this._mnemonic = mnemonic; 31 | } 32 | 33 | public get pair(): KeyringPair { 34 | return this._keyring.addFromUri(this._mnemonic, { name: "Default" }, this._keyringType); 35 | } 36 | 37 | public get keyring(): Keyring { 38 | return this._keyring; 39 | } 40 | 41 | public formatAccount(chainProperty: ChainProperty): void { 42 | this._keyring.setSS58Format(chainProperty.ss58Prefix); 43 | } 44 | } 45 | 46 | export abstract class SwankyAccountCommand extends SwankyCommand { 47 | async performFaucetTransfer(accountData: AccountData, canBeSkipped = false) { 48 | let api: ChainApi | null = null; 49 | try { 50 | api = (await this.spinner.runCommand(async () => { 51 | const networkUrl = resolveNetworkUrl(this.swankyConfig, ""); 52 | const api = await ChainApi.create(networkUrl); 53 | await api.start(); 54 | return api; 55 | }, "Connecting to node")) as ChainApi; 56 | 57 | if (api) 58 | await this.spinner.runCommand( 59 | async () => { 60 | if (api) await api.faucet(accountData); 61 | }, 62 | `Transferring ${LOCAL_FAUCET_AMOUNT} units from faucet account to ${accountData.alias}`, 63 | `Transferred ${LOCAL_FAUCET_AMOUNT} units from faucet account to ${accountData.alias}`, 64 | `Failed to transfer ${LOCAL_FAUCET_AMOUNT} units from faucet account to ${accountData.alias}`, 65 | true 66 | ); 67 | } catch (cause) { 68 | if (cause instanceof Error) { 69 | if (cause.message.includes("ECONNREFUSED") && canBeSkipped) { 70 | this.warn( 71 | `Unable to connect to the node. Skipping faucet transfer for ${chalk.yellowBright(accountData.alias)}.` 72 | ); 73 | } else { 74 | throw new ApiError("Error transferring tokens from faucet account", { cause }); 75 | } 76 | } else { 77 | throw new ApiError("An unknown error occurred during faucet transfer", { 78 | cause: new Error(String(cause)), 79 | }); 80 | } 81 | } finally { 82 | if (api) { 83 | await api.disconnect(); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/cargoContractInfo.ts: -------------------------------------------------------------------------------- 1 | export interface CargoContractInkDependency { 2 | minCargoContractVersion: string; 3 | validInkVersionRange: string; 4 | } 5 | 6 | // Keep cargo-contract versions in descending order 7 | // Ranges are supported by semver 8 | export const CARGO_CONTRACT_INK_DEPS: CargoContractInkDependency[] = [ 9 | { minCargoContractVersion: "4.0.0", validInkVersionRange: "<99.0.0" }, // Non-max version known yet: a very high version is used as fallback in the meantime 10 | { minCargoContractVersion: "2.2.0", validInkVersionRange: "<5.0.0" }, 11 | { minCargoContractVersion: "2.0.2", validInkVersionRange: "<4.2.0" }, 12 | { minCargoContractVersion: "2.0.0", validInkVersionRange: "<4.0.1" }, 13 | ]; 14 | -------------------------------------------------------------------------------- /src/lib/checks.ts: -------------------------------------------------------------------------------- 1 | import { ConfigError, FileError, InputError, ProcessError } from "./errors.js"; 2 | import { pathExists } from "fs-extra"; 3 | import chalk from "chalk"; 4 | import path from "node:path"; 5 | import semver from "semver"; 6 | import { Contract } from "./contract.js"; 7 | import { AccountData, ContractData } from "../types/index.js"; 8 | 9 | export function ensureContractNameOrAllFlagIsSet( 10 | args: any, 11 | flags: any, 12 | errorMessage = "No contracts were selected. Specify a contract name or use the --all flag." 13 | ) { 14 | if (args.contractName === undefined && !flags.all) { 15 | throw new ConfigError(errorMessage); 16 | } 17 | } 18 | 19 | export async function ensureContractPathExists(contractName: string, projectPath = "") { 20 | const contractPath = path.resolve(projectPath, "contracts", contractName); 21 | if (!(await pathExists(contractPath))) { 22 | throw new InputError( 23 | `Contract folder not found ${chalk.yellowBright(contractName)} at path: ${contractPath}` 24 | ); 25 | } 26 | } 27 | 28 | export async function contractFromRecord(contractRecord: ContractData) { 29 | const contract = new Contract(contractRecord); 30 | 31 | if (!(await contract.pathExists())) { 32 | throw new FileError( 33 | `Path to contract ${contractRecord.name} does not exist: ${contract.contractPath}` 34 | ); 35 | } 36 | return contract; 37 | } 38 | 39 | export async function ensureArtifactsExist(contract: Contract) { 40 | const artifactsCheck = await contract.artifactsExist(); 41 | if (!artifactsCheck.result) { 42 | throw new FileError( 43 | `No artifact file found at path: ${artifactsCheck.missingPaths.toString()}` 44 | ); 45 | } 46 | } 47 | 48 | export async function ensureTypedContractExists(contract: Contract) { 49 | const typedContractCheck = await contract.typedContractExists(); 50 | 51 | if (!typedContractCheck.result) { 52 | throw new FileError( 53 | `No typed contract found at path: ${typedContractCheck.missingPaths.toString()}` 54 | ); 55 | } 56 | } 57 | 58 | export function ensureDevAccountNotInProduction(accountData: AccountData, network: string) { 59 | if (accountData.isDev && network !== "local") { 60 | throw new ConfigError( 61 | `Account ${accountData.alias} is a DEV account and can only be used with local network` 62 | ); 63 | } 64 | } 65 | 66 | export function ensureCargoContractVersionCompatibility( 67 | cargoContractVersion: string, 68 | minimalVersion: string, 69 | invalidVersionsList?: string[] 70 | ) { 71 | if (invalidVersionsList?.includes(cargoContractVersion)) { 72 | throw new ProcessError( 73 | `The cargo-contract version ${cargoContractVersion} is not supported. Please update or change the version.` 74 | ); 75 | } 76 | 77 | if (!semver.satisfies(cargoContractVersion.replace(/-.*$/, ""), `>=${minimalVersion}`)) { 78 | throw new ProcessError( 79 | `cargo-contract version >= ${minimalVersion} required, but found version ${cargoContractVersion}. Please update to a compatible version.` 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/lib/command-utils.ts: -------------------------------------------------------------------------------- 1 | import { execaCommand, execaCommandSync } from "execa"; 2 | import { copy, emptyDir, ensureDir, readJSONSync } from "fs-extra/esm"; 3 | import path from "node:path"; 4 | import { 5 | DEFAULT_NETWORK_URL, 6 | ARTIFACTS_PATH, 7 | TYPED_CONTRACTS_PATH, 8 | DEFAULT_SHIBUYA_NETWORK_URL, 9 | DEFAULT_SHIDEN_NETWORK_URL, 10 | DEFAULT_ASTAR_NETWORK_URL, 11 | DEFAULT_ACCOUNT, 12 | DEFAULT_CONFIG_NAME, 13 | DEFAULT_CONFIG_FOLDER_NAME, 14 | DEFAULT_NODE_INFO, 15 | DEFAULT_RUST_DEP_VERSION, 16 | DEFAULT_CARGO_CONTRACT_DEP_VERSION, 17 | DEFAULT_CARGO_DYLINT_DEP_VERSION, 18 | } from "./consts.js"; 19 | import { SwankyConfig, SwankySystemConfig } from "../types/index.js"; 20 | import { ConfigError, FileError, ProcessError } from "./errors.js"; 21 | import { userInfo } from "os"; 22 | import { existsSync } from "fs"; 23 | 24 | export function commandStdoutOrNull(command: string): string | null { 25 | try { 26 | const result = execaCommandSync(command); 27 | return result.stdout; 28 | } catch { 29 | return null; 30 | } 31 | } 32 | 33 | export function getSwankyConfig(configType: "local" | "global"): SwankyConfig | SwankySystemConfig { 34 | let configPath: string; 35 | 36 | if (configType === "global") { 37 | configPath = getSystemConfigDirectoryPath() + `/${DEFAULT_CONFIG_NAME}`; 38 | } else { 39 | configPath = isEnvConfigCheck() ? process.env.SWANKY_CONFIG! : DEFAULT_CONFIG_NAME; 40 | } 41 | 42 | const config = readJSONSync(configPath); 43 | return config; 44 | } 45 | 46 | export function getSystemConfigDirectoryPath(): string { 47 | const homeDir = userInfo().homedir; 48 | const configPath = homeDir + `/${DEFAULT_CONFIG_FOLDER_NAME}`; 49 | return configPath; 50 | } 51 | 52 | export function resolveNetworkUrl(config: SwankyConfig, networkName: string): string { 53 | if (networkName === "") { 54 | return DEFAULT_NETWORK_URL; 55 | } 56 | 57 | try { 58 | return config.networks[networkName].url; 59 | } catch { 60 | throw new ConfigError("Network name not found in SwankyConfig"); 61 | } 62 | } 63 | 64 | export async function storeArtifacts( 65 | artifactsPath: string, 66 | contractAlias: string, 67 | moduleName: string 68 | ): Promise { 69 | const destArtifactsPath = path.resolve(ARTIFACTS_PATH, contractAlias); 70 | const testArtifactsPath = path.resolve("tests", contractAlias, "artifacts"); 71 | 72 | await ensureDir(destArtifactsPath); 73 | await emptyDir(destArtifactsPath); 74 | 75 | await ensureDir(testArtifactsPath); 76 | await emptyDir(testArtifactsPath); 77 | 78 | try { 79 | for (const fileName of [`${moduleName}.contract`, `${moduleName}.json`]) { 80 | const artifactFileToCopy = path.resolve(artifactsPath, fileName); 81 | await copy(artifactFileToCopy, path.resolve(destArtifactsPath, fileName)); 82 | await copy(artifactFileToCopy, path.resolve(testArtifactsPath, fileName)); 83 | } 84 | } catch (cause) { 85 | throw new FileError("Error storing artifacts", { cause }); 86 | } 87 | } 88 | // TODO: Use the Abi type (optionally, support legacy types version) 89 | export function printContractInfo(abi: any) { 90 | // TODO: Use templating, colorize. 91 | console.log(` 92 | 😎 ${abi.contract.name} Contract 😎 93 | 94 | Hash: ${abi.source.hash} 95 | Language: ${abi.source.language} 96 | Compiler: ${abi.source.compiler} 97 | `); 98 | 99 | console.log(` === Constructors ===\n`); 100 | for (const constructor of abi.spec.constructors) { 101 | console.log(` * ${constructor.label}: 102 | Args: ${ 103 | constructor.args.length > 0 104 | ? constructor.args.map((arg: any) => { 105 | return `\n - ${arg.label} (${arg.type.displayName})`; 106 | }) 107 | : "None" 108 | } 109 | Description: ${constructor.docs.map((line: any) => { 110 | if (line != "") { 111 | return `\n ` + line; 112 | } 113 | })} 114 | `); 115 | } 116 | 117 | console.log(` === Messages ===\n`); 118 | for (const message of abi.spec.messages) { 119 | console.log(` * ${message.label}: 120 | Payable: ${message.payable} 121 | Args: ${ 122 | message.args.length > 0 123 | ? message.args.map((arg: any) => { 124 | return `\n - ${arg.label} (${arg.type.displayName})`; 125 | }) 126 | : "None" 127 | } 128 | Description: ${message.docs.map((line: any) => { 129 | if (line != "") { 130 | return `\n ` + line; 131 | } 132 | })} 133 | `); 134 | } 135 | } 136 | 137 | export async function generateTypes(contractName: string) { 138 | const relativeInputPath = `${ARTIFACTS_PATH}/${contractName}`; 139 | const relativeOutputPath = `${TYPED_CONTRACTS_PATH}/${contractName}`; 140 | const outputPath = path.resolve(process.cwd(), relativeOutputPath); 141 | 142 | ensureDir(outputPath); 143 | emptyDir(outputPath); 144 | 145 | await execaCommand( 146 | `npx typechain-polkadot --in ${relativeInputPath} --out ${relativeOutputPath}` 147 | ); 148 | } 149 | export function ensureAccountIsSet(account: string | undefined, config: SwankyConfig) { 150 | if (!account && config.defaultAccount === null) { 151 | throw new ConfigError( 152 | "No default account set. Please set one or provide an account alias with --account" 153 | ); 154 | } 155 | } 156 | 157 | export function buildSwankyConfig() { 158 | return { 159 | node: { 160 | localPath: "", 161 | polkadotPalletVersions: DEFAULT_NODE_INFO.polkadotPalletVersions, 162 | supportedInk: DEFAULT_NODE_INFO.supportedInk, 163 | version: DEFAULT_NODE_INFO.version, 164 | }, 165 | defaultAccount: DEFAULT_ACCOUNT, 166 | accounts: [ 167 | { 168 | alias: "alice", 169 | mnemonic: "//Alice", 170 | isDev: true, 171 | address: "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", 172 | }, 173 | { 174 | alias: "bob", 175 | mnemonic: "//Bob", 176 | isDev: true, 177 | address: "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", 178 | }, 179 | ], 180 | networks: { 181 | local: { url: DEFAULT_NETWORK_URL }, 182 | astar: { url: DEFAULT_ASTAR_NETWORK_URL }, 183 | shiden: { url: DEFAULT_SHIDEN_NETWORK_URL }, 184 | shibuya: { url: DEFAULT_SHIBUYA_NETWORK_URL }, 185 | }, 186 | contracts: {}, 187 | env: { 188 | rust: extractRustVersion() ?? DEFAULT_RUST_DEP_VERSION, 189 | "cargo-dylint": extractCargoDylintVersion() ?? DEFAULT_CARGO_DYLINT_DEP_VERSION, 190 | "cargo-contract": extractCargoContractVersion() ?? DEFAULT_CARGO_CONTRACT_DEP_VERSION, 191 | }, 192 | }; 193 | } 194 | 195 | export function isEnvConfigCheck(): boolean { 196 | if (process.env.SWANKY_CONFIG === undefined) { 197 | return false; 198 | } else if (existsSync(process.env.SWANKY_CONFIG)) { 199 | return true; 200 | } else { 201 | throw new ConfigError(`Provided config path ${process.env.SWANKY_CONFIG} does not exist`); 202 | } 203 | } 204 | 205 | export function isLocalConfigCheck(): boolean { 206 | const defaultLocalConfigPath = process.cwd() + `/${DEFAULT_CONFIG_NAME}`; 207 | return process.env.SWANKY_CONFIG === undefined 208 | ? existsSync(defaultLocalConfigPath) 209 | : existsSync(process.env.SWANKY_CONFIG); 210 | } 211 | 212 | export function configName(): string { 213 | if (!isLocalConfigCheck()) { 214 | return DEFAULT_CONFIG_NAME + " [system config]"; 215 | } 216 | 217 | return process.env.SWANKY_CONFIG?.split("/").pop() ?? DEFAULT_CONFIG_NAME; 218 | } 219 | 220 | export function extractVersion(command: string, regex: RegExp) { 221 | const output = commandStdoutOrNull(command); 222 | if (!output) { 223 | return null; 224 | } 225 | 226 | const match = output.match(regex); 227 | if (!match) { 228 | throw new ProcessError( 229 | `Unable to determine version from command '${command}'. Please verify its installation.` 230 | ); 231 | } 232 | 233 | return match[1]; 234 | } 235 | 236 | export function extractRustVersion() { 237 | return extractVersion("rustc --version", /rustc (.*) \((.*)/); 238 | } 239 | 240 | export function extractCargoVersion() { 241 | return extractVersion("cargo -V", /cargo (.*) \((.*)/); 242 | } 243 | 244 | export function extractCargoNightlyVersion() { 245 | return extractVersion("cargo +nightly -V", /cargo (.*)-nightly \((.*)/); 246 | } 247 | 248 | export function extractCargoDylintVersion() { 249 | return extractVersion("cargo dylint -V", /cargo-dylint (.*)/); 250 | } 251 | 252 | export function extractCargoContractVersion() { 253 | return extractVersion( 254 | "cargo contract -V", 255 | /cargo-contract-contract (\d+\.\d+\.\d+(?:-[\w.]+)?)(?:-unknown-[\w-]+)/ 256 | ); 257 | } 258 | -------------------------------------------------------------------------------- /src/lib/config-builder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountData, 3 | BuildData, 4 | DeploymentData, 5 | SwankyConfig, 6 | SwankySystemConfig, 7 | ZombienetData, 8 | } from "../index.js"; 9 | import { snakeCase } from "change-case"; 10 | 11 | export class ConfigBuilder { 12 | private config: T; 13 | 14 | constructor(existingConfig: T) { 15 | this.config = { ...existingConfig }; 16 | } 17 | 18 | setDefaultAccount(account: string): ConfigBuilder { 19 | this.config.defaultAccount = account; 20 | return this; 21 | } 22 | 23 | addAccount(account: AccountData): ConfigBuilder { 24 | this.config.accounts.push(account); 25 | return this; 26 | } 27 | 28 | updateNetwork(name: string, url: string): ConfigBuilder { 29 | if (this.config.networks?.[name]) { 30 | this.config.networks[name].url = url; 31 | } 32 | return this; 33 | } 34 | 35 | updateNodeSettings(nodeSettings: Partial): ConfigBuilder { 36 | if ("node" in this.config) { 37 | this.config.node = { ...this.config.node, ...nodeSettings }; 38 | } 39 | return this; 40 | } 41 | 42 | updateEnv(env: Record): ConfigBuilder { 43 | if ("env" in this.config) { 44 | this.config.env = { ...this.config.env, ...env }; 45 | } 46 | return this; 47 | } 48 | 49 | updateContracts(contracts: SwankyConfig["contracts"]): ConfigBuilder { 50 | if ("contracts" in this.config) { 51 | this.config.contracts = { ...contracts }; 52 | } 53 | return this; 54 | } 55 | 56 | addContract(name: string, moduleName?: string): ConfigBuilder { 57 | if ("contracts" in this.config) { 58 | this.config.contracts[name] = { 59 | name: name, 60 | moduleName: moduleName ?? snakeCase(name), 61 | deployments: [], 62 | }; 63 | } 64 | return this; 65 | } 66 | 67 | addContractDeployment(name: string, data: DeploymentData): ConfigBuilder { 68 | if ("contracts" in this.config) { 69 | this.config.contracts[name].deployments.push(data); 70 | } 71 | return this; 72 | } 73 | 74 | addContractBuild(name: string, data: BuildData): ConfigBuilder { 75 | if ("contracts" in this.config) { 76 | this.config.contracts[name].build = data; 77 | } 78 | return this; 79 | } 80 | 81 | addZombienet(data: ZombienetData): ConfigBuilder { 82 | if ("zombienet" in this.config) { 83 | this.config.zombienet = data; 84 | } 85 | return this; 86 | } 87 | 88 | addChopsticks(path: string): ConfigBuilder { 89 | if ("node" in this.config) { 90 | this.config.node.chopsticks = { 91 | configPath: path, 92 | }; 93 | } 94 | return this; 95 | } 96 | 97 | build(): T { 98 | return this.config; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | import { configName, SwankyConfig } from "../index.js"; 2 | import { FileError } from "./errors.js"; 3 | 4 | export function ensureSwankyNodeInstalled(config: SwankyConfig) { 5 | if (config.node.localPath === "") { 6 | throw new FileError("Swanky node is not installed. Please run `swanky node:install` first."); 7 | } 8 | } 9 | 10 | // //deploy 11 | // if (!contractRecord) { 12 | // throw new ConfigError( 13 | // `Cannot find a contract named ${args.contractName} in "${configName()}"` 14 | // ); 15 | // } 16 | // 17 | // //explain 18 | // if (!contractRecord) { 19 | // throw new ConfigError( 20 | // `Cannot find a contract named ${args.contractName} in "${configName()}"` 21 | // ); 22 | // } 23 | // 24 | // //test 25 | // if (!contractRecord) { 26 | // throw new ConfigError( 27 | // `Cannot find a contract named ${args.contractName} in "${configName()}"` 28 | // ); 29 | // } 30 | // 31 | // //contractCall 32 | // if (!contractRecord) { 33 | // throw new ConfigError( 34 | // `Cannot find a contract named ${args.contractName} in "${configName()}"`, 35 | // ); 36 | // } 37 | 38 | export function findContractRecord(config: SwankyConfig, contractName: string) { 39 | const contractRecord = config.contracts[contractName]; 40 | if (!contractRecord) { 41 | throw new FileError(`Cannot find a contract named ${contractName} in "${configName()}"`); 42 | } 43 | return contractRecord; 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/consts.ts: -------------------------------------------------------------------------------- 1 | import { swankyNodeVersions } from "./nodeInfo.js"; 2 | 3 | export const DEFAULT_NODE_INFO = swankyNodeVersions.get("1.7.0")!; 4 | 5 | export const DEFAULT_NETWORK_URL = "ws://127.0.0.1:9944"; 6 | export const DEFAULT_ASTAR_NETWORK_URL = "wss://rpc.astar.network"; 7 | export const DEFAULT_SHIDEN_NETWORK_URL = "wss://rpc.shiden.astar.network"; 8 | export const DEFAULT_SHIBUYA_NETWORK_URL = "wss://shibuya.public.blastapi.io"; 9 | 10 | export const DEFAULT_ACCOUNT = "alice"; 11 | export const DEFAULT_CONFIG_FOLDER_NAME = "swanky"; 12 | export const DEFAULT_CONFIG_NAME = "swanky.config.json"; 13 | 14 | export const ARTIFACTS_PATH = "artifacts"; 15 | export const TYPED_CONTRACTS_PATH = "typedContracts"; 16 | 17 | export const DEFAULT_RUST_DEP_VERSION = "1.76.0"; 18 | export const DEFAULT_CARGO_DYLINT_DEP_VERSION = "2.6.1"; 19 | export const DEFAULT_CARGO_CONTRACT_DEP_VERSION = "4.0.0-rc.2"; 20 | 21 | export const SUPPORTED_DEPS = { 22 | rust: DEFAULT_RUST_DEP_VERSION, 23 | "cargo-dylint": DEFAULT_CARGO_DYLINT_DEP_VERSION, 24 | "cargo-contract": DEFAULT_CARGO_CONTRACT_DEP_VERSION, 25 | } as const; 26 | 27 | export const LOCAL_FAUCET_AMOUNT = 100; 28 | export const KEYPAIR_TYPE = "sr25519"; 29 | export const ALICE_URI = "//Alice"; 30 | export const BOB_URI = "//Bob"; 31 | -------------------------------------------------------------------------------- /src/lib/contract.ts: -------------------------------------------------------------------------------- 1 | import { AbiType, consts, printContractInfo } from "./index.js"; 2 | import { BuildMode, ContractData, DeploymentData } from "../types/index.js"; 3 | import { pathExists, readJSON } from "fs-extra/esm"; 4 | import path from "node:path"; 5 | import { FileError } from "./errors.js"; 6 | 7 | export class Contract { 8 | static artifactTypes = [".json", ".contract"]; 9 | name: string; 10 | moduleName: string; 11 | deployments: DeploymentData[]; 12 | contractPath: string; 13 | artifactsPath: string; 14 | buildMode?: BuildMode; 15 | 16 | constructor(contractRecord: ContractData) { 17 | this.name = contractRecord.name; 18 | this.moduleName = contractRecord.moduleName; 19 | this.deployments = contractRecord.deployments; 20 | this.contractPath = path.resolve("contracts", contractRecord.name); 21 | this.artifactsPath = path.resolve(consts.ARTIFACTS_PATH, contractRecord.name); 22 | this.buildMode = contractRecord.build?.buildMode; 23 | } 24 | 25 | async pathExists() { 26 | return pathExists(this.contractPath); 27 | } 28 | 29 | async artifactsExist(): Promise<{ result: boolean; missingPaths: string[] }> { 30 | const missingPaths: string[] = []; 31 | let result = true; 32 | 33 | for (const artifactType of Contract.artifactTypes) { 34 | const artifactPath = path.resolve(this.artifactsPath, `${this.moduleName}${artifactType}`); 35 | if (!(await pathExists(artifactPath))) { 36 | result = false; 37 | missingPaths.push(artifactPath); 38 | } 39 | } 40 | 41 | return { result, missingPaths }; 42 | } 43 | 44 | async typedContractExists() { 45 | const result: { result: boolean; missingPaths: string[] } = { 46 | result: true, 47 | missingPaths: [], 48 | }; 49 | const artifactPath = path.resolve("typedContracts", `${this.name}`); 50 | if (!(await pathExists(artifactPath))) { 51 | result.result = false; 52 | result.missingPaths.push(artifactPath); 53 | } 54 | return result; 55 | } 56 | 57 | async getABI(): Promise { 58 | const jsonArtifactPath = `${this.moduleName}.json`; 59 | await this.ensureArtifactExists(jsonArtifactPath); 60 | return readJSON(path.resolve(this.artifactsPath, jsonArtifactPath)); 61 | } 62 | 63 | async getBundle() { 64 | const contractArtifactPath = `${this.moduleName}.contract`; 65 | await this.ensureArtifactExists(contractArtifactPath); 66 | return readJSON(path.resolve(this.artifactsPath, contractArtifactPath), "utf8"); 67 | } 68 | 69 | async getWasm(): Promise { 70 | const bundle = await this.getBundle(); 71 | if (bundle.source?.wasm) return bundle.source.wasm; 72 | throw new FileError(`Cannot find wasm field in the .contract bundle!`); 73 | } 74 | 75 | async printInfo(): Promise { 76 | const abi = await this.getABI(); 77 | printContractInfo(abi); 78 | } 79 | 80 | private async ensureArtifactExists(artifactFileName: string): Promise { 81 | const artifactPath = path.resolve(this.artifactsPath, artifactFileName); 82 | if (!(await pathExists(artifactPath))) { 83 | throw new FileError(`Artifact file not found at path: ${artifactPath}`); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/lib/contractCall.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbiType, 3 | ChainAccount, 4 | ChainApi, 5 | configName, 6 | ensureAccountIsSet, 7 | decrypt, 8 | resolveNetworkUrl, 9 | findContractRecord, 10 | } from "./index.js"; 11 | import { ContractData, DeploymentData, Encrypted } from "../types/index.js"; 12 | import { Args, Command, Flags, Interfaces } from "@oclif/core"; 13 | import inquirer from "inquirer"; 14 | import chalk from "chalk"; 15 | import { SwankyCommand } from "./swankyCommand.js"; 16 | import { cryptoWaitReady } from "@polkadot/util-crypto/crypto"; 17 | import { NetworkError } from "./errors.js"; 18 | import { 19 | contractFromRecord, 20 | ensureArtifactsExist, 21 | ensureDevAccountNotInProduction, 22 | } from "./checks.js"; 23 | 24 | export type JoinedFlagsType = Interfaces.InferredFlags< 25 | (typeof ContractCall)["baseFlags"] & T["flags"] 26 | >; 27 | 28 | export abstract class ContractCall extends SwankyCommand< 29 | typeof ContractCall 30 | > { 31 | static callArgs = { 32 | contractName: Args.string({ 33 | name: "Contract name", 34 | description: "Contract to call", 35 | required: true, 36 | }), 37 | messageName: Args.string({ 38 | name: "Message name", 39 | required: true, 40 | description: "What message to call", 41 | }), 42 | }; 43 | 44 | static callFlags = { 45 | network: Flags.string({ 46 | char: "n", 47 | default: "local", 48 | description: "Name of network to connect to", 49 | }), 50 | }; 51 | 52 | protected flags!: JoinedFlagsType; 53 | protected args!: Record; 54 | protected contractInfo!: ContractData; 55 | protected deploymentInfo!: DeploymentData; 56 | protected account!: ChainAccount; 57 | protected metadata!: AbiType; 58 | protected api!: ChainApi; 59 | 60 | public async init(): Promise { 61 | await super.init(); 62 | const { flags, args } = await this.parse(this.ctor); 63 | this.args = args; 64 | this.flags = flags as JoinedFlagsType; 65 | 66 | const contractRecord = findContractRecord(this.swankyConfig, args.contractName); 67 | 68 | const contract = await contractFromRecord(contractRecord); 69 | 70 | await ensureArtifactsExist(contract); 71 | 72 | const deploymentData = flags.address 73 | ? contract.deployments.find( 74 | (deployment: DeploymentData) => deployment.address === flags.address 75 | ) 76 | : contract.deployments[0]; 77 | 78 | if (!deploymentData?.address) 79 | throw new NetworkError( 80 | `Cannot find a deployment with address: ${flags.address} in "${configName()}"` 81 | ); 82 | 83 | this.deploymentInfo = deploymentData; 84 | 85 | ensureAccountIsSet(flags.account, this.swankyConfig); 86 | 87 | const accountAlias = flags.account ?? this.swankyConfig.defaultAccount; 88 | const accountData = this.findAccountByAlias(accountAlias); 89 | 90 | ensureDevAccountNotInProduction(accountData, flags.network); 91 | 92 | const networkUrl = resolveNetworkUrl(this.swankyConfig, flags.network ?? ""); 93 | const api = await ChainApi.create(networkUrl); 94 | this.api = api; 95 | await this.api.start(); 96 | 97 | const mnemonic = accountData.isDev 98 | ? (accountData.mnemonic as string) 99 | : decrypt( 100 | accountData.mnemonic as Encrypted, 101 | ( 102 | await inquirer.prompt([ 103 | { 104 | type: "password", 105 | message: `Enter password for ${chalk.yellowBright(accountData.alias)}: `, 106 | name: "password", 107 | }, 108 | ]) 109 | ).password 110 | ); 111 | 112 | const account = (await this.spinner.runCommand(async () => { 113 | await cryptoWaitReady(); 114 | return new ChainAccount(mnemonic); 115 | }, "Initialising")) as ChainAccount; 116 | this.account = account; 117 | 118 | this.metadata = (await this.spinner.runCommand(async () => { 119 | const abi = await contract.getABI(); 120 | return abi; 121 | }, "Getting metadata")) as AbiType; 122 | } 123 | 124 | protected async catch(err: Error & { exitCode?: number }): Promise { 125 | // add any custom logic to handle errors from the command 126 | // or simply return the parent class error handling 127 | return super.catch(err); 128 | } 129 | 130 | protected async finally(_: Error | undefined): Promise { 131 | // called after run and catch regardless of whether or not the command errored 132 | return super.finally(_); 133 | } 134 | } 135 | 136 | // Static property baseFlags needs to be defined like this (for now) because of the way TS transpiles ESNEXT code 137 | // https://github.com/oclif/oclif/issues/1100#issuecomment-1454910926 138 | ContractCall.baseFlags = { 139 | ...SwankyCommand.baseFlags, 140 | params: Flags.string({ 141 | required: false, 142 | description: "Arguments supplied to the message", 143 | multiple: true, 144 | default: [], 145 | char: "p", 146 | }), 147 | gas: Flags.string({ 148 | char: "g", 149 | description: "Manually specify gas limit", 150 | }), 151 | network: Flags.string({ 152 | char: "n", 153 | description: "Network name to connect to", 154 | }), 155 | account: Flags.string({ 156 | char: "a", 157 | description: "Account alias to sign the transaction with", 158 | }), 159 | address: Flags.string({ 160 | required: false, 161 | description: "Target specific address, defaults to last deployed. (--addr, --add)", 162 | aliases: ["addr", "add"], 163 | }), 164 | }; 165 | -------------------------------------------------------------------------------- /src/lib/crypto.ts: -------------------------------------------------------------------------------- 1 | import crypto from "node:crypto"; 2 | const ALGO = "aes-256-cbc"; 3 | 4 | import { Encrypted } from "../types/index.js"; 5 | 6 | export function encrypt(text: string, password: string): Encrypted { 7 | const iv = crypto.randomBytes(16); 8 | const key = crypto.scryptSync(password, "swankySalt", 32); 9 | const cipher = crypto.createCipheriv(ALGO, Buffer.from(key), iv); 10 | const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); 11 | return { iv: iv.toString("hex"), data: encrypted.toString("hex") }; 12 | } 13 | 14 | export function decrypt(encrypted: Encrypted, password: string) { 15 | const iv = Buffer.from(encrypted.iv, "hex"); 16 | const key = crypto.scryptSync(password, "swankySalt", 32); 17 | const encryptedText = Buffer.from(encrypted.data, "hex"); 18 | const decipher = crypto.createDecipheriv(ALGO, Buffer.from(key), iv); 19 | const decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]); 20 | return decrypted.toString(); 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | import ModernError, { InstanceOptions } from "modern-errors"; 2 | import modernErrorsBugs from "modern-errors-bugs"; 3 | import modernErrorsClean from "modern-errors-clean"; 4 | import modernErrorWinston from "modern-errors-winston"; 5 | export const BaseError = ModernError.subclass("BaseError", { 6 | plugins: [modernErrorWinston, modernErrorsBugs, modernErrorsClean], 7 | }); 8 | 9 | export const UnknownError = BaseError.subclass("UnknownError", { 10 | bugs: "https://github.com/swankyhub/swanky-cli/issues", 11 | }); 12 | 13 | export const InputError = BaseError.subclass("InputError"); 14 | 15 | export const RpcError = BaseError.subclass("RpcError"); 16 | 17 | export const ApiError = BaseError.subclass("ApiError"); 18 | 19 | export const TestError = BaseError.subclass("TestError"); 20 | 21 | export const FileError = BaseError.subclass("FileError"); 22 | 23 | export const ConfigError = BaseError.subclass("ConfigError"); 24 | 25 | export const NetworkError = BaseError.subclass("NetworkError"); 26 | 27 | export const ProcessError = BaseError.subclass("ProcessError", { 28 | custom: class extends BaseError { 29 | constructor(message: string, options?: InstanceOptions & ProcessErrorOptions) { 30 | super(message, options); 31 | if (options?.shouldExit) { 32 | console.error(options.cause); 33 | process.exit(1); 34 | } 35 | } 36 | }, 37 | }); 38 | 39 | interface ProcessErrorOptions { 40 | shouldExit?: boolean; 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./account.js"; 2 | export * from "./command-utils.js"; 3 | export * from "./config.js"; 4 | export * as consts from "./consts.js"; 5 | export * from "./crypto.js"; 6 | export * from "./nodeInfo.js"; 7 | export * from "./spinner.js"; 8 | export * from "./substrate-api.js"; 9 | export * from "./tasks.js"; 10 | export * from "./templates.js"; 11 | -------------------------------------------------------------------------------- /src/lib/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from "winston"; 2 | import { BaseError } from "./errors.js"; 3 | import { env } from "node:process"; 4 | 5 | export const swankyLogger = createLogger({ 6 | format: format.combine(BaseError.shortFormat({ stack: true }), format.cli()), 7 | 8 | transports: [new transports.Console({ level: "error" })], 9 | }); 10 | 11 | if (!env.CI) { 12 | swankyLogger.add( 13 | new transports.File({ 14 | format: format.combine( 15 | format.uncolorize(), 16 | format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), 17 | BaseError.fullFormat({ stack: true }), 18 | format.printf( 19 | (info) => `[${info.timestamp}] ${info.level}:\n ${info.message}\n${"=".repeat(80)}` 20 | ) 21 | ), 22 | filename: "swanky.log", 23 | options: { 24 | flags: "w", 25 | }, 26 | level: "info", 27 | }) 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/nodeInfo.ts: -------------------------------------------------------------------------------- 1 | export interface nodeInfo { 2 | version: string; 3 | polkadotPalletVersions: string; 4 | supportedInk: string; 5 | downloadUrl: { 6 | darwin: { 7 | arm64?: string; 8 | x64?: string; 9 | }; 10 | linux: { 11 | arm64?: string; 12 | x64?: string; 13 | }; 14 | }; 15 | } 16 | 17 | export const swankyNodeVersions = new Map([ 18 | [ 19 | "1.7.0", 20 | { 21 | version: "1.7.0", 22 | polkadotPalletVersions: "polkadot-v0.9.43", 23 | supportedInk: "v5.0.0", 24 | downloadUrl: { 25 | darwin: { 26 | arm64: 27 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.7.0/swanky-node-v1.7.0-macOS-universal.tar.gz", 28 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.7.0/swanky-node-v1.7.0-macOS-universal.tar.gz", 29 | }, 30 | linux: { 31 | arm64: 32 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.7.0/swanky-node-v1.7.0-ubuntu-aarch64.tar.gz", 33 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.7.0/swanky-node-v1.7.0-ubuntu-x86_64.tar.gz", 34 | }, 35 | }, 36 | }, 37 | ], 38 | [ 39 | "1.6.0", 40 | { 41 | version: "1.6.0", 42 | polkadotPalletVersions: "polkadot-v0.9.39", 43 | supportedInk: "v5.0.0", 44 | downloadUrl: { 45 | darwin: { 46 | arm64: 47 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.6.0/swanky-node-v1.6.0-macOS-universal.tar.gz", 48 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.6.0/swanky-node-v1.6.0-macOS-universal.tar.gz", 49 | }, 50 | linux: { 51 | arm64: 52 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.6.0/swanky-node-v1.6.0-ubuntu-aarch64.tar.gz", 53 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.6.0/swanky-node-v1.6.0-ubuntu-x86_64.tar.gz", 54 | }, 55 | }, 56 | }, 57 | ], 58 | [ 59 | "1.5.0", 60 | { 61 | version: "1.5.0", 62 | polkadotPalletVersions: "polkadot-v0.9.39", 63 | supportedInk: "v4.0.0", 64 | downloadUrl: { 65 | darwin: { 66 | arm64: 67 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.5.0/swanky-node-v1.5.0-macOS-universal.tar.gz", 68 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.5.0/swanky-node-v1.5.0-macOS-universal.tar.gz", 69 | }, 70 | linux: { 71 | arm64: 72 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.5.0/swanky-node-v1.5.0-ubuntu-aarch64.tar.gz", 73 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.5.0/swanky-node-v1.5.0-ubuntu-x86_64.tar.gz", 74 | }, 75 | }, 76 | }, 77 | ], 78 | [ 79 | "1.4.0", 80 | { 81 | version: "1.4.0", 82 | polkadotPalletVersions: "polkadot-v0.9.37", 83 | supportedInk: "v4.0.0", 84 | downloadUrl: { 85 | darwin: { 86 | arm64: 87 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.4.0/swanky-node-v1.4.0-macOS-universal.tar.gz", 88 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.4.0/swanky-node-v1.4.0-macOS-universal.tar.gz", 89 | }, 90 | linux: { 91 | arm64: 92 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.4.0/swanky-node-v1.4.0-ubuntu-aarch64.tar.gz", 93 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.4.0/swanky-node-v1.4.0-ubuntu-x86_64.tar.gz", 94 | }, 95 | }, 96 | }, 97 | ], 98 | [ 99 | "1.3.0", 100 | { 101 | version: "1.3.0", 102 | polkadotPalletVersions: "polkadot-v0.9.37", 103 | supportedInk: "v4.0.0", 104 | downloadUrl: { 105 | darwin: { 106 | arm64: 107 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.3.0/swanky-node-v1.3.0-macOS-universal.tar.gz", 108 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.3.0/swanky-node-v1.3.0-macOS-universal.tar.gz", 109 | }, 110 | linux: { 111 | arm64: 112 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.3.0/swanky-node-v1.3.0-ubuntu-aarch64.tar.gz", 113 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.3.0/swanky-node-v1.3.0-ubuntu-x86_64.tar.gz", 114 | }, 115 | }, 116 | }, 117 | ], 118 | [ 119 | "1.2.0", 120 | { 121 | version: "1.2.0", 122 | polkadotPalletVersions: "polkadot-v0.9.37", 123 | supportedInk: "v4.0.0", 124 | downloadUrl: { 125 | darwin: { 126 | arm64: 127 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.2.0/swanky-node-v1.2.0-macOS-universal.tar.gz", 128 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.2.0/swanky-node-v1.2.0-macOS-universal.tar.gz", 129 | }, 130 | linux: { 131 | arm64: 132 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.2.0/swanky-node-v1.2.0-ubuntu-aarch64.tar.gz", 133 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.2.0/swanky-node-v1.2.0-ubuntu-x86_64.tar.gz", 134 | }, 135 | }, 136 | }, 137 | ], 138 | [ 139 | "1.1.0", 140 | { 141 | version: "1.1.0", 142 | polkadotPalletVersions: "polkadot-v0.9.37", 143 | supportedInk: "v4.0.0", 144 | downloadUrl: { 145 | darwin: { 146 | arm64: 147 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.1.0/swanky-node-v1.1.0-macOS-x86_64.tar.gz", 148 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.1.0/swanky-node-v1.1.0-macOS-x86_64.tar.gz", 149 | }, 150 | linux: { 151 | arm64: 152 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.1.0/swanky-node-v1.1.0-ubuntu-x86_64.tar.gz", 153 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.1.0/swanky-node-v1.1.0-ubuntu-x86_64.tar.gz", 154 | }, 155 | }, 156 | }, 157 | ], 158 | [ 159 | "1.0.0", 160 | { 161 | version: "1.0.0", 162 | polkadotPalletVersions: "polkadot-v0.9.30", 163 | supportedInk: "v3.4.0", 164 | downloadUrl: { 165 | darwin: { 166 | arm64: 167 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.0.0/swanky-node-v1.0.0-macOS-x86_64.tar.gz", 168 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.0.0/swanky-node-v1.0.0-macOS-x86_64.tar.gz", 169 | }, 170 | linux: { 171 | arm64: 172 | "https://github.com/AstarNetwork/swanky-node/releases/download/v1.0.0/swanky-node-v1.0.0-ubuntu-x86_64.tar.gz", 173 | x64: "https://github.com/AstarNetwork/swanky-node/releases/download/v1.0.0/swanky-node-v1.0.0-ubuntu-x86_64.tar.gz", 174 | }, 175 | }, 176 | }, 177 | ], 178 | ]); 179 | -------------------------------------------------------------------------------- /src/lib/prompts.ts: -------------------------------------------------------------------------------- 1 | import { Answers, ListQuestion, Question } from "inquirer"; 2 | import { ConfigError } from "./errors.js"; 3 | 4 | export function pickTemplate(templateList: string[]): ListQuestion { 5 | if (!templateList?.length) throw new ConfigError("Template list is empty!"); 6 | return { 7 | name: "contractTemplate", 8 | type: "list", 9 | choices: templateList, 10 | message: "Which contract template should we use initially?", 11 | }; 12 | } 13 | 14 | export function pickNodeVersion(nodeVersions: string[]): ListQuestion { 15 | if (!nodeVersions?.length) throw new ConfigError("Node version list is empty!"); 16 | return { 17 | name: "version", 18 | type: "list", 19 | choices: nodeVersions, 20 | message: "Which node version should we use?", 21 | }; 22 | } 23 | 24 | export function name( 25 | subject: string, 26 | initial?: (answers: Answers) => string, 27 | questionText?: string 28 | ): Question { 29 | const question: Question = { 30 | name: `${subject}Name`, 31 | type: "input", 32 | message: questionText ?? `What name should we use for ${subject}?`, 33 | }; 34 | if (initial) question.default = initial; 35 | return question; 36 | } 37 | 38 | export function email(initial?: string, questionText?: string): Question { 39 | const question: Question = { 40 | name: "email", 41 | type: "input", 42 | message: questionText ?? "What is your email?", 43 | }; 44 | if (initial) question.default = initial; 45 | return question; 46 | } 47 | 48 | export function choice(subject: string, questionText: string): Question { 49 | return { 50 | name: subject, 51 | type: "confirm", 52 | message: questionText, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/spinner.ts: -------------------------------------------------------------------------------- 1 | import ora, { Ora } from "ora"; 2 | import { ProcessError } from "./errors.js"; 3 | 4 | export class Spinner { 5 | ora: Ora; 6 | verbose: boolean; 7 | constructor(verbose = false) { 8 | this.ora = ora(); 9 | this.verbose = verbose; 10 | } 11 | 12 | start(text: string) { 13 | this.ora = ora(text).start(); 14 | return Promise.resolve(); 15 | } 16 | 17 | succeed(text: string) { 18 | this.ensureSpinner(); 19 | this.ora.succeed(text); 20 | } 21 | 22 | fail(text: string) { 23 | this.ensureSpinner(); 24 | this.ora.fail(text); 25 | } 26 | 27 | output(text: string) { 28 | this.ora.info(text).start(); 29 | } 30 | 31 | text(text: string) { 32 | this.ora.text = text; 33 | } 34 | 35 | async runCommand( 36 | command: () => Promise, 37 | runningMessage: string, 38 | successMessage?: string, 39 | failMessage?: string, 40 | shouldExitOnError = false 41 | ) { 42 | try { 43 | await this.start(runningMessage); 44 | const res = await command(); 45 | this.succeed(successMessage ?? `${runningMessage} OK`); 46 | return res; 47 | } catch (cause) { 48 | const errorMessage = failMessage ?? `Error ${runningMessage}`; 49 | this.fail(failMessage ?? `Error ${runningMessage}`); 50 | throw new ProcessError(errorMessage, { cause, shouldExit: shouldExitOnError }); 51 | } 52 | } 53 | 54 | ensureSpinner() { 55 | if (!this.ora) { 56 | throw new ProcessError("spinner not started"); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/substrate-api.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise } from "@polkadot/api/promise"; 2 | import { Keyring, WsProvider } from "@polkadot/api"; 3 | import { SignerOptions } from "@polkadot/api/types"; 4 | import { Codec, ITuple } from "@polkadot/types-codec/types"; 5 | import { ISubmittableResult } from "@polkadot/types/types"; 6 | import { TypeRegistry } from "@polkadot/types"; 7 | import { DispatchError, BlockHash } from "@polkadot/types/interfaces"; 8 | import { ChainAccount } from "./account.js"; 9 | import BN from "bn.js"; 10 | import { ChainProperty, ExtrinsicPayload, AccountData } from "../types/index.js"; 11 | 12 | import { KeyringPair } from "@polkadot/keyring/types"; 13 | import { Abi, CodePromise } from "@polkadot/api-contract"; 14 | import { ApiError, UnknownError } from "./errors.js"; 15 | import { ALICE_URI, KEYPAIR_TYPE, LOCAL_FAUCET_AMOUNT } from "./consts.js"; 16 | import { BN_TEN } from "@polkadot/util"; 17 | 18 | export type AbiType = Abi; 19 | // const AUTO_CONNECT_MS = 10_000; // [ms] 20 | const TEN_B = 10_000_000_000; 21 | 22 | function CreateApiPromise(provider: WsProvider): Promise { 23 | return new Promise((resolve, reject) => { 24 | const apiPromise = new ApiPromise({ provider }); 25 | 26 | apiPromise.on("error", (error) => { 27 | reject(new ApiError("Error creating ApiPromise", { cause: error })); 28 | }); 29 | 30 | const disconnectHandler = (cause: Error) => { 31 | reject(new ApiError("Disconnected from the endpoint", { cause })); 32 | }; 33 | 34 | apiPromise.on("disconnected", disconnectHandler); 35 | 36 | apiPromise.on("ready", () => { 37 | apiPromise.off("disconnected", disconnectHandler); 38 | resolve(apiPromise); 39 | }); 40 | }); 41 | } 42 | 43 | function CreateProvider(endpoint: string): Promise { 44 | return new Promise((resolve, reject) => { 45 | const provider = new WsProvider(endpoint, false); 46 | 47 | const unsubscribe = provider.on("error", (cause) => { 48 | unsubscribe(); 49 | 50 | const errorSymbol = Object.getOwnPropertySymbols(cause).find( 51 | (symbol) => symbol.description === "kError" 52 | ); 53 | 54 | const errorObject = errorSymbol 55 | ? cause[errorSymbol] 56 | : new UnknownError("Unknown WsProvider error", { cause }); 57 | 58 | reject(new ApiError("Cannot connect to the WsProvider", { cause: errorObject })); 59 | }); 60 | 61 | provider.on("connected", () => { 62 | resolve(provider); 63 | }); 64 | provider.connect(); 65 | }); 66 | } 67 | export class ChainApi { 68 | private _chainProperty?: ChainProperty; 69 | private _registry: TypeRegistry; 70 | 71 | protected _provider: WsProvider; 72 | protected _api: ApiPromise; 73 | 74 | static async create(endpoint: string) { 75 | const provider = await CreateProvider(endpoint); 76 | const apiPromise = await CreateApiPromise(provider); 77 | return new ChainApi(provider, apiPromise); 78 | } 79 | 80 | constructor(provider: WsProvider, apiPromise: ApiPromise) { 81 | this._provider = provider; 82 | 83 | this._api = apiPromise; 84 | 85 | this._registry = new TypeRegistry(); 86 | 87 | this._chainProperty = undefined; 88 | } 89 | 90 | public get apiInst(): ApiPromise { 91 | if (!this._api) { 92 | throw new ApiError("The ApiPromise has not been initialized"); 93 | } 94 | 95 | return this._api; 96 | } 97 | 98 | public get chainProperty(): ChainProperty { 99 | return this._chainProperty!; 100 | } 101 | 102 | public get typeRegistry(): TypeRegistry { 103 | return this._registry; 104 | } 105 | 106 | public async disconnect(): Promise { 107 | await this._provider.disconnect(); 108 | } 109 | 110 | public async start(): Promise { 111 | const chainProperties = await this._api.rpc.system.properties(); 112 | 113 | const ss58Prefix = Number.parseInt(this._api.consts.system.ss58Prefix.toString() || "0", 10); 114 | 115 | const tokenDecimals = chainProperties.tokenDecimals 116 | .unwrapOrDefault() 117 | .toArray() 118 | .map((i) => i.toNumber()); 119 | 120 | const tokenSymbols = chainProperties.tokenSymbol 121 | .unwrapOrDefault() 122 | .toArray() 123 | .map((i) => i.toString()); 124 | 125 | const chainName = (await this._api.rpc.system.chain()).toString(); 126 | 127 | this._chainProperty = { 128 | tokenSymbols, 129 | tokenDecimals, 130 | chainName, 131 | ss58Prefix, 132 | }; 133 | } 134 | 135 | public async getBlockHash(blockNumber: number): Promise { 136 | return this._api?.rpc.chain.getBlockHash(blockNumber); 137 | } 138 | 139 | public buildTxCall(extrinsic: string, method: string, ...args: any[]): ExtrinsicPayload { 140 | const ext = this._api?.tx[extrinsic][method](...args); 141 | if (ext) return ext; 142 | throw new ApiError(`Undefined extrinsic call ${extrinsic} with method ${method}`); 143 | } 144 | 145 | public async buildStorageQuery( 146 | extrinsic: string, 147 | method: string, 148 | ...args: any[] 149 | ): Promise { 150 | const ext = await this._api?.query[extrinsic][method](...args); 151 | if (ext) return ext; 152 | throw new ApiError(`Undefined storage query ${extrinsic} for method ${method}`); 153 | } 154 | 155 | public wrapBatchAll(txs: ExtrinsicPayload[]): ExtrinsicPayload { 156 | const ext = this._api?.tx.utility.batchAll(txs); 157 | if (ext) return ext; 158 | throw new ApiError("Undefined batch all"); 159 | } 160 | 161 | public wrapSudo(tx: ExtrinsicPayload): ExtrinsicPayload { 162 | const ext = this._api?.tx.sudo.sudo(tx); 163 | if (ext) return ext; 164 | throw new ApiError("Undefined sudo"); 165 | } 166 | 167 | public async nonce(account: ChainAccount): Promise { 168 | return ((await this._api?.query.system.account(account.pair.address)) as any)?.nonce.toNumber(); 169 | } 170 | 171 | public async getBalance(account: ChainAccount): Promise { 172 | return ( 173 | (await this._api?.query.system.account(account.pair.address)) as any 174 | ).data.free.toBn() as BN; 175 | } 176 | 177 | public async signAndSend( 178 | signer: KeyringPair, 179 | tx: ExtrinsicPayload, 180 | options?: Partial, 181 | handler?: (result: ISubmittableResult) => void 182 | ): Promise<() => void> { 183 | // ensure that we automatically increment the nonce per transaction 184 | return tx.signAndSend(signer, { nonce: -1, ...options }, (result) => { 185 | // handle transaction errors 186 | result.events 187 | .filter((record): boolean => Boolean(record.event) && record.event.section !== "democracy") 188 | .forEach(({ event: { data, method, section } }) => { 189 | if (section === "system" && method === "ExtrinsicFailed") { 190 | const [dispatchError] = data as unknown as ITuple<[DispatchError]>; 191 | let message = dispatchError.type.toString(); 192 | 193 | if (dispatchError.isModule) { 194 | try { 195 | const mod = dispatchError.asModule; 196 | const error = dispatchError.registry.findMetaError(mod); 197 | 198 | message = `${error.section}.${error.name}`; 199 | } catch (error) { 200 | console.error(error); 201 | } 202 | } else if (dispatchError.isToken) { 203 | message = `${dispatchError.type}.${dispatchError.asToken.type}`; 204 | } 205 | 206 | const errorMessage = `${section}.${method} ${message}`; 207 | console.error(`error: ${errorMessage}`); 208 | } else if (section === "utility" && method === "BatchInterrupted") { 209 | const anyData = data as any; 210 | const error = anyData[1].registry.findMetaError(anyData[1].asModule); 211 | const message = `${error.section}.${error.name}`; 212 | console.error(`error: ${section}.${method} ${message}`); 213 | } 214 | }); 215 | 216 | if (handler) handler(result); 217 | }); 218 | } 219 | public async deploy( 220 | abi: Abi, 221 | wasm: Buffer, 222 | constructorName: string, 223 | signerPair: KeyringPair, 224 | args: string[], 225 | customGas?: number 226 | ) { 227 | const gasLimit = this.apiInst.registry.createType("WeightV2", { 228 | refTime: BigInt(TEN_B), 229 | proofSize: BigInt(customGas ?? TEN_B), 230 | }); 231 | 232 | const code = new CodePromise(this._api, abi, wasm); 233 | const storageDepositLimit = null; 234 | if (typeof code.tx[constructorName] !== "function") { 235 | throw new ApiError(`Contract has no constructor called ${constructorName}`); 236 | } 237 | const tx = code.tx[constructorName]({ gasLimit, storageDepositLimit }, ...(args || [])); 238 | return new Promise((resolve, reject) => { 239 | this.signAndSend(signerPair, tx, {}, ({ status, events }) => { 240 | if (status.isInBlock || status.isFinalized) { 241 | const instantiateEvent = events.find(({ event }: any) => event.method === "Instantiated"); 242 | 243 | const addresses = instantiateEvent?.event.data.toHuman() as { 244 | contract: string; 245 | deployer: string; 246 | }; 247 | 248 | if (!addresses?.contract) reject(new Error("Unable to get the contract address")); 249 | resolve(addresses.contract); 250 | this._provider.disconnect(); 251 | } 252 | }); 253 | }); 254 | } 255 | 256 | public async faucet(accountData: AccountData): Promise { 257 | const keyring = new Keyring({ type: KEYPAIR_TYPE }); 258 | const alicePair = keyring.addFromUri(ALICE_URI); 259 | 260 | const chainDecimals = this._api.registry.chainDecimals[0]; 261 | const amount = new BN(LOCAL_FAUCET_AMOUNT).mul(BN_TEN.pow(new BN(chainDecimals))); 262 | 263 | const tx = this._api.tx.balances.transfer(accountData.address, amount); 264 | 265 | return new Promise((resolve, reject) => { 266 | this.signAndSend(alicePair, tx, {}, ({ status, events }) => { 267 | if (status.isInBlock || status.isFinalized) { 268 | const transferEvent = events.find(({ event }) => event?.method === "Transfer"); 269 | if (!transferEvent) { 270 | reject(); 271 | return; 272 | } 273 | resolve(); 274 | } 275 | }).catch((error) => reject(error)); 276 | }); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/lib/swankyCommand.ts: -------------------------------------------------------------------------------- 1 | import { Command, Flags, Interfaces } from "@oclif/core"; 2 | import chalk from "chalk"; 3 | import { 4 | buildSwankyConfig, 5 | configName, 6 | getSwankyConfig, 7 | getSystemConfigDirectoryPath, 8 | Spinner, 9 | } from "./index.js"; 10 | import { AccountData, SwankyConfig, SwankySystemConfig } from "../types/index.js"; 11 | import { writeJSON } from "fs-extra/esm"; 12 | import { existsSync, mkdirSync } from "fs"; 13 | import { BaseError, ConfigError, UnknownError } from "./errors.js"; 14 | import { swankyLogger } from "./logger.js"; 15 | import { Logger } from "winston"; 16 | import path from "node:path"; 17 | import { DEFAULT_CONFIG_FOLDER_NAME, DEFAULT_CONFIG_NAME } from "./consts.js"; 18 | 19 | export type Flags = Interfaces.InferredFlags< 20 | (typeof SwankyCommand)["baseFlags"] & T["flags"] 21 | >; 22 | export type Args = Interfaces.InferredArgs; 23 | 24 | export abstract class SwankyCommand extends Command { 25 | static ENSURE_SWANKY_CONFIG = true; 26 | 27 | protected spinner!: Spinner; 28 | protected swankyConfig!: SwankyConfig; 29 | protected logger!: Logger; 30 | 31 | protected flags!: Flags; 32 | protected args!: Args; 33 | 34 | public async init(): Promise { 35 | await super.init(); 36 | this.spinner = new Spinner(); 37 | 38 | const { args, flags } = await this.parse({ 39 | flags: this.ctor.flags, 40 | baseFlags: (super.ctor as typeof SwankyCommand).baseFlags, 41 | args: this.ctor.args, 42 | strict: this.ctor.strict, 43 | }); 44 | 45 | this.flags = flags as Flags; 46 | this.args = args as Args; 47 | 48 | this.logger = swankyLogger; 49 | this.swankyConfig = buildSwankyConfig(); 50 | 51 | await this.loadAndMergeConfig(); 52 | 53 | this.logger.info(`Running command: ${this.ctor.name} 54 | Args: ${JSON.stringify(this.args)} 55 | Flags: ${JSON.stringify(this.flags)} 56 | Full command: ${JSON.stringify(process.argv)}`); 57 | } 58 | 59 | protected async loadAndMergeConfig(): Promise { 60 | try { 61 | const systemConfig = getSwankyConfig("global"); 62 | this.swankyConfig = { ...this.swankyConfig, ...systemConfig }; 63 | } catch (error) { 64 | this.warn( 65 | `No Swanky system config found; creating one in "/${DEFAULT_CONFIG_FOLDER_NAME}/${DEFAULT_CONFIG_NAME}" at home directory` 66 | ); 67 | await this.storeConfig(this.swankyConfig, "global"); 68 | } 69 | 70 | try { 71 | const localConfig = getSwankyConfig("local") as SwankyConfig; 72 | this.mergeAccountsWithExistingConfig(this.swankyConfig, localConfig); 73 | const originalDefaultAccount = this.swankyConfig.defaultAccount; 74 | this.swankyConfig = { ...this.swankyConfig, ...localConfig }; 75 | this.swankyConfig.defaultAccount = localConfig.defaultAccount ?? originalDefaultAccount; 76 | } catch (error) { 77 | this.handleLocalConfigError(error); 78 | } 79 | } 80 | 81 | private handleLocalConfigError(error: unknown): void { 82 | this.logger.warn("No local config found"); 83 | if ( 84 | error instanceof Error && 85 | error.message.includes(configName()) && 86 | (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG 87 | ) { 88 | throw new ConfigError(`Cannot find ${process.env.SWANKY_CONFIG ?? DEFAULT_CONFIG_NAME}`, { 89 | cause: error, 90 | }); 91 | } 92 | } 93 | 94 | protected async storeConfig( 95 | newConfig: SwankyConfig | SwankySystemConfig, 96 | configType: "local" | "global", 97 | projectPath?: string 98 | ) { 99 | let configPath: string; 100 | 101 | if (configType === "local") { 102 | configPath = 103 | process.env.SWANKY_CONFIG ?? 104 | path.resolve(projectPath ?? process.cwd(), DEFAULT_CONFIG_NAME); 105 | } else { 106 | // global 107 | configPath = getSystemConfigDirectoryPath() + `/${DEFAULT_CONFIG_NAME}`; 108 | if ("accounts" in newConfig) { 109 | // If it's a SwankyConfig, extract only the system relevant parts for the global SwankySystemConfig config 110 | newConfig = { 111 | defaultAccount: newConfig.defaultAccount, 112 | accounts: newConfig.accounts, 113 | networks: newConfig.networks, 114 | }; 115 | } 116 | if (existsSync(configPath)) { 117 | const systemConfig = getSwankyConfig("global"); 118 | this.mergeAccountsWithExistingConfig(systemConfig, newConfig); 119 | } 120 | } 121 | 122 | this.ensureDirectoryExists(configPath); 123 | await writeJSON(configPath, newConfig, { spaces: 2 }); 124 | } 125 | 126 | private ensureDirectoryExists(filePath: string) { 127 | const directory = path.dirname(filePath); 128 | if (!existsSync(directory)) { 129 | mkdirSync(directory, { recursive: true }); 130 | } 131 | } 132 | 133 | private mergeAccountsWithExistingConfig( 134 | existingConfig: SwankySystemConfig | SwankyConfig, 135 | newConfig: SwankySystemConfig 136 | ) { 137 | const accountMap = new Map( 138 | [...existingConfig.accounts, ...newConfig.accounts].map((account) => [account.alias, account]) 139 | ); 140 | 141 | newConfig.accounts = Array.from(accountMap.values()); 142 | } 143 | 144 | protected findAccountByAlias(alias: string): AccountData { 145 | const accountData = this.swankyConfig.accounts.find( 146 | (account: AccountData) => account.alias === alias 147 | ); 148 | 149 | if (!accountData) { 150 | throw new ConfigError( 151 | `Provided account alias ${chalk.yellowBright(alias)} not found in swanky.config.json` 152 | ); 153 | } 154 | 155 | return accountData; 156 | } 157 | 158 | protected async catch(err: Error & { exitCode?: number }): Promise { 159 | // add any custom logic to handle errors from the command 160 | // or simply return the parent class error handling 161 | const error = BaseError.normalize(err, UnknownError); 162 | this.logger.error(error); 163 | } 164 | 165 | protected async finally(_: Error | undefined): Promise { 166 | // called after run and catch regardless of whether or not the command errored 167 | return super.finally(_); 168 | } 169 | } 170 | 171 | // Static property baseFlags needs to be defined like this (for now) because of the way TS transpiles ESNEXT code 172 | // https://github.com/oclif/oclif/issues/1100#issuecomment-1454910926 173 | SwankyCommand.baseFlags = { 174 | verbose: Flags.boolean({ 175 | required: false, 176 | description: "Display more info in the result logs", 177 | char: "v", 178 | }), 179 | }; 180 | -------------------------------------------------------------------------------- /src/lib/tasks.ts: -------------------------------------------------------------------------------- 1 | import { execaCommand } from "execa"; 2 | import { copy, ensureDir, remove } from "fs-extra/esm"; 3 | import path from "node:path"; 4 | import { DownloadEndedStats, DownloaderHelper } from "node-downloader-helper"; 5 | import process from "node:process"; 6 | import { nodeInfo } from "./nodeInfo.js"; 7 | import decompress from "decompress"; 8 | import { Spinner } from "./spinner.js"; 9 | import { 10 | DependencyName, 11 | Relaychain, 12 | SupportedArch, 13 | SupportedPlatforms, 14 | SwankyConfig, 15 | TestType, 16 | ZombienetConfig, 17 | } from "../types/index.js"; 18 | import { ConfigError, NetworkError, ProcessError } from "./errors.js"; 19 | import { BinaryNames } from "./zombienetInfo.js"; 20 | import { zombienetConfig } from "../commands/zombienet/init.js"; 21 | import { readFileSync } from "fs"; 22 | import TOML from "@iarna/toml"; 23 | import { writeFileSync } from "node:fs"; 24 | 25 | export async function checkCliDependencies(spinner: Spinner) { 26 | const dependencyList = [ 27 | { dependencyName: "rust", versionCommand: "rustc --version" }, 28 | { dependencyName: "cargo", versionCommand: "cargo -V" }, 29 | { 30 | dependencyName: "cargo contract", 31 | versionCommand: "cargo contract -V", 32 | }, 33 | ]; 34 | 35 | for (const dep of dependencyList) { 36 | spinner.text(` Checking ${dep.dependencyName}`); 37 | await execaCommand(dep.versionCommand); 38 | } 39 | } 40 | 41 | export async function installCliDevDeps(spinner: Spinner, name: DependencyName, version: string) { 42 | switch (name) { 43 | case "rust": { 44 | spinner.text(` Installing rust`); 45 | await execaCommand(`rustup toolchain install ${version}`); 46 | await execaCommand(`rustup default ${version}`); 47 | await execaCommand(`rustup component add rust-src --toolchain ${version}`); 48 | await execaCommand(`rustup target add wasm32-unknown-unknown --toolchain ${version}`); 49 | break; 50 | } 51 | case "cargo-dylint": 52 | case "cargo-contract": { 53 | spinner.text(` Installing ${name}`); 54 | await execaCommand( 55 | `cargo +stable install ${name} ${ 56 | version === "latest" ? "" : ` --force --version ${version}` 57 | }` 58 | ); 59 | break; 60 | } 61 | default: 62 | spinner.fail(`Unsupported dependency. Skipping installation.`); 63 | return; 64 | } 65 | } 66 | 67 | export function osCheck() { 68 | const platform = process.platform; 69 | const arch = process.arch; 70 | 71 | const supportedConfigs = { 72 | darwin: ["x64", "arm64"], 73 | linux: ["x64", "arm64"], 74 | }; 75 | 76 | if (!(platform in supportedConfigs)) { 77 | throw new ConfigError(`Platform '${platform}' is not supported!`); 78 | } 79 | 80 | const supportedArchs = supportedConfigs[platform as keyof typeof supportedConfigs]; 81 | if (!supportedArchs.includes(arch)) { 82 | throw new ConfigError(`Architecture '${arch}' is not supported on platform '${platform}'.`); 83 | } 84 | 85 | return { platform, arch }; 86 | } 87 | 88 | export async function prepareTestFiles( 89 | testType: TestType, 90 | templatePath: string, 91 | projectPath: string, 92 | templateName?: string, 93 | contractName?: string 94 | ) { 95 | switch (testType) { 96 | case "e2e": { 97 | await copy( 98 | path.resolve(templatePath, "test_helpers"), 99 | path.resolve(projectPath, "tests", "test_helpers") 100 | ); 101 | break; 102 | } 103 | case "mocha": { 104 | if (!templateName) { 105 | throw new ProcessError("'templateName' argument is required for mocha tests"); 106 | } 107 | if (!contractName) { 108 | throw new ProcessError("'contractName' argument is required for mocha tests"); 109 | } 110 | await copy( 111 | path.resolve(templatePath, "contracts", templateName, "test"), 112 | path.resolve(projectPath, "tests", contractName) 113 | ); 114 | break; 115 | } 116 | default: { 117 | // This case will make the switch exhaustive 118 | throw new ProcessError("Unhandled test type"); 119 | } 120 | } 121 | } 122 | 123 | export async function downloadNode(projectPath: string, nodeInfo: nodeInfo, spinner: Spinner) { 124 | const binPath = path.resolve(projectPath, "bin"); 125 | await ensureDir(binPath); 126 | 127 | const platformDlUrls = nodeInfo.downloadUrl[process.platform as SupportedPlatforms]; 128 | if (!platformDlUrls) 129 | throw new ConfigError( 130 | `Could not download swanky-node. Platform ${process.platform} not supported!` 131 | ); 132 | 133 | const dlUrl = platformDlUrls[process.arch as SupportedArch]; 134 | if (!dlUrl) 135 | throw new ConfigError( 136 | `Could not download swanky-node. Platform ${process.platform} Arch ${process.arch} not supported!` 137 | ); 138 | 139 | const dlFileDetails = await new Promise((resolve, reject) => { 140 | const dl = new DownloaderHelper(dlUrl, binPath); 141 | 142 | dl.on("progress", (event) => { 143 | spinner.text(`Downloading Swanky node ${event.progress.toFixed(2)}%`); 144 | }); 145 | dl.on("end", (event) => { 146 | resolve(event); 147 | }); 148 | dl.on("error", (error) => { 149 | reject(new Error(`Error downloading node: , ${error.message}`)); 150 | }); 151 | 152 | dl.start().catch((error: Error) => 153 | reject(new Error(`Error downloading node: , ${error.message}`)) 154 | ); 155 | }); 156 | 157 | if (dlFileDetails.incomplete) { 158 | throw new NetworkError("Node download incomplete"); 159 | } 160 | 161 | if (dlFileDetails.filePath.endsWith(".tar.gz")) { 162 | const compressedFilePath = path.resolve(binPath, dlFileDetails.filePath); 163 | const decompressed = await decompress(compressedFilePath, binPath); 164 | const nodePath = path.resolve(binPath, decompressed[0].path); 165 | await remove(compressedFilePath); 166 | await execaCommand(`chmod +x ${nodePath}`); 167 | 168 | return nodePath; 169 | } 170 | 171 | return path.resolve(binPath, dlFileDetails.filePath); 172 | } 173 | 174 | export async function downloadZombienetBinaries( 175 | binaries: string[], 176 | projectPath: string, 177 | swankyConfig: SwankyConfig, 178 | spinner: Spinner 179 | ) { 180 | const binPath = path.resolve(projectPath, "zombienet", "bin"); 181 | await ensureDir(binPath); 182 | 183 | const zombienetInfo = swankyConfig.zombienet; 184 | 185 | if (!zombienetInfo) { 186 | throw new ConfigError("No zombienet config found"); 187 | } 188 | 189 | const dlUrls = new Map(); 190 | if (zombienetInfo.version) { 191 | const version = zombienetInfo.version; 192 | const binaryName = "zombienet"; 193 | const platformDlUrls = zombienetInfo.downloadUrl[process.platform as SupportedPlatforms]; 194 | if (!platformDlUrls) 195 | throw new ConfigError( 196 | `Could not download ${binaryName}. Platform ${process.platform} not supported!` 197 | ); 198 | let dlUrl = platformDlUrls[process.arch as SupportedArch]; 199 | if (!dlUrl) 200 | throw new ConfigError( 201 | `Could not download ${binaryName}. Platform ${process.platform} Arch ${process.arch} not supported!` 202 | ); 203 | dlUrl = dlUrl.replace("${version}", version); 204 | dlUrls.set(binaryName, dlUrl); 205 | } 206 | 207 | for (const binaryName of Object.keys(zombienetInfo.binaries).filter((binaryName) => 208 | binaries.includes(binaryName) 209 | )) { 210 | const binaryInfo = zombienetInfo.binaries[binaryName as BinaryNames]; 211 | const version = binaryInfo.version; 212 | const platformDlUrls = binaryInfo.downloadUrl[process.platform as SupportedPlatforms]; 213 | if (!platformDlUrls) 214 | throw new ConfigError( 215 | `Could not download ${binaryName}. Platform ${process.platform} not supported!` 216 | ); 217 | let dlUrl = platformDlUrls[process.arch as SupportedArch]; 218 | if (!dlUrl) 219 | throw new ConfigError( 220 | `Could not download ${binaryName}. Platform ${process.platform} Arch ${process.arch} not supported!` 221 | ); 222 | dlUrl = dlUrl.replace(/\$\{version}/gi, version); 223 | dlUrls.set(binaryName, dlUrl); 224 | } 225 | 226 | for (const [binaryName, dlUrl] of dlUrls) { 227 | const dlFileDetails = await new Promise((resolve, reject) => { 228 | const dl = new DownloaderHelper(dlUrl, binPath); 229 | 230 | dl.on("progress", (event) => { 231 | spinner.text(`Downloading ${binaryName} ${event.progress.toFixed(2)}%`); 232 | }); 233 | dl.on("end", (event) => { 234 | resolve(event); 235 | }); 236 | dl.on("error", (error) => { 237 | reject(new Error(`Error downloading ${binaryName}: , ${error.message}`)); 238 | }); 239 | 240 | dl.start().catch((error: Error) => 241 | reject(new Error(`Error downloading ${binaryName}: , ${error.message}`)) 242 | ); 243 | }); 244 | 245 | if (dlFileDetails.incomplete) { 246 | throw new NetworkError("${binaryName} download incomplete"); 247 | } 248 | 249 | let fileName = dlFileDetails.fileName; 250 | 251 | if (dlFileDetails.filePath.endsWith(".tar.gz")) { 252 | const compressedFilePath = path.resolve(binPath, dlFileDetails.filePath); 253 | const decompressed = await decompress(compressedFilePath, binPath); 254 | await remove(compressedFilePath); 255 | fileName = decompressed[0].path; 256 | } 257 | 258 | if (fileName !== binaryName) { 259 | await execaCommand(`mv ${binPath}/${fileName} ${binPath}/${binaryName}`); 260 | } 261 | await execaCommand(`chmod +x ${binPath}/${binaryName}`); 262 | } 263 | } 264 | 265 | export async function buildZombienetConfigFromBinaries( 266 | binaries: string[], 267 | templatePath: string, 268 | configPath: string 269 | ) { 270 | await ensureDir(configPath); 271 | const configBuilder = { 272 | settings: { 273 | timeout: 1000, 274 | }, 275 | relaychain: { 276 | default_command: "", 277 | chain: "", 278 | nodes: [], 279 | }, 280 | parachains: [], 281 | } as ZombienetConfig; 282 | 283 | for (const binaryName of binaries) { 284 | const template = TOML.parse( 285 | readFileSync(path.resolve(templatePath, binaryName + ".toml"), "utf8") 286 | ); 287 | if (template.parachains !== undefined) { 288 | (template.parachains as any).forEach((parachain: any) => { 289 | configBuilder.parachains.push(parachain); 290 | }); 291 | } 292 | if (template.hrmp_channels !== undefined) { 293 | configBuilder.hrmp_channels = []; 294 | (template.hrmp_channels as any).forEach((hrmp_channel: any) => { 295 | configBuilder.hrmp_channels!.push(hrmp_channel); 296 | }); 297 | } 298 | if (template.relaychain !== undefined) { 299 | configBuilder.relaychain = template.relaychain as unknown as Relaychain; 300 | } 301 | } 302 | 303 | writeFileSync(path.resolve(configPath, zombienetConfig), TOML.stringify(configBuilder as any)); 304 | } 305 | 306 | export async function installDeps(projectPath: string) { 307 | let installCommand = "npm install"; 308 | 309 | try { 310 | await execaCommand("yarn --version"); 311 | installCommand = "yarn install"; 312 | } catch (_error) { 313 | console.log("\n\t >>Yarn not detected, using NPM"); 314 | } finally { 315 | await execaCommand(installCommand, { cwd: projectPath }); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/lib/templates.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from "fs"; 2 | import { fileURLToPath } from "url"; 3 | import path from "node:path"; 4 | import { globby } from "globby"; 5 | import handlebars from "handlebars"; 6 | import { ensureDir, copy } from "fs-extra"; 7 | import { readFile, rename, rm, writeFile } from "fs/promises"; 8 | import { chopsticksConfig } from "../commands/node/chopsticks/init.js"; 9 | import { zombienetConfig } from "../commands/zombienet/init.js"; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | export function getTemplates() { 15 | const templatesPath = path.resolve(__dirname, "..", "templates"); 16 | const contractTemplatesPath = path.resolve(templatesPath, "contracts"); 17 | const zombienetTemplatesPath = path.resolve(templatesPath, "zombienet"); 18 | const chopsticksTemplatesPath = path.resolve(templatesPath, "chopsticks"); 19 | const fileList = readdirSync(contractTemplatesPath, { 20 | withFileTypes: true, 21 | }); 22 | const contractTemplatesList = fileList 23 | .filter((entry) => entry.isDirectory()) 24 | .map((entry) => entry.name); 25 | 26 | return { 27 | templatesPath, 28 | contractTemplatesPath, 29 | contractTemplatesList, 30 | zombienetTemplatesPath, 31 | chopsticksTemplatesPath, 32 | }; 33 | } 34 | 35 | export async function copyContractTemplateFiles( 36 | contractTemplatePath: string, 37 | contractName: string, 38 | projectPath: string 39 | ) { 40 | await copy( 41 | path.resolve(contractTemplatePath, "contract"), 42 | path.resolve(projectPath, "contracts", contractName) 43 | ); 44 | } 45 | 46 | export async function copyZombienetTemplateFile(templatePath: string, configPath: string) { 47 | await ensureDir(configPath); 48 | await copy( 49 | path.resolve(templatePath, zombienetConfig), 50 | path.resolve(configPath, zombienetConfig) 51 | ); 52 | } 53 | 54 | export async function copyChopsticksTemplateFile(templatePath: string, configPath: string) { 55 | await ensureDir(configPath); 56 | await copy( 57 | path.resolve(templatePath, chopsticksConfig), 58 | path.resolve(configPath, chopsticksConfig) 59 | ); 60 | } 61 | 62 | export async function copyCommonTemplateFiles(templatesPath: string, projectPath: string) { 63 | await ensureDir(projectPath); 64 | const commonFiles = await globby(`*`, { cwd: templatesPath }); 65 | await Promise.all( 66 | commonFiles.map(async (file) => { 67 | await copy(path.resolve(templatesPath, file), path.resolve(projectPath, file)); 68 | }) 69 | ); 70 | await rename(path.resolve(projectPath, "gitignore"), path.resolve(projectPath, ".gitignore")); 71 | await rename( 72 | path.resolve(projectPath, "mocharc.json"), 73 | path.resolve(projectPath, ".mocharc.json") 74 | ); 75 | await copy(path.resolve(templatesPath, "github"), path.resolve(projectPath, ".github")); 76 | } 77 | 78 | export async function processTemplates(projectPath: string, templateData: Record) { 79 | const templateFiles = await globby(projectPath, { 80 | expandDirectories: { extensions: ["hbs"] }, 81 | }); 82 | 83 | for (const tplFilePath of templateFiles) { 84 | const rawTemplate = await readFile(tplFilePath, "utf8"); 85 | const template = handlebars.compile(rawTemplate); 86 | const compiledFile = template(templateData); 87 | await rm(tplFilePath); 88 | await writeFile(tplFilePath.split(".hbs")[0], compiledFile); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/lib/zombienetInfo.ts: -------------------------------------------------------------------------------- 1 | export type zombienetInfo = typeof zombienet; 2 | 3 | export type BinaryNames = "zombienet" | "polkadot" | "polkadot-parachain" | "astar-collator"; 4 | 5 | export const zombienet = { 6 | version: "1.3.89", 7 | downloadUrl: { 8 | darwin: { 9 | arm64: 10 | "https://github.com/paritytech/zombienet/releases/download/v${version}/zombienet-macos", 11 | x64: "https://github.com/paritytech/zombienet/releases/download/v${version}/zombienet-macos", 12 | }, 13 | linux: { 14 | arm64: 15 | "https://github.com/paritytech/zombienet/releases/download/v${version}/zombienet-linux-arm64", 16 | x64: "https://github.com/paritytech/zombienet/releases/download/v${version}/zombienet-linux-x64", 17 | }, 18 | }, 19 | binaries: { 20 | polkadot: { 21 | version: "0.9.43", 22 | downloadUrl: { 23 | linux: { 24 | arm64: "https://github.com/paritytech/polkadot/releases/download/v${version}/polkadot", 25 | x64: "https://github.com/paritytech/polkadot/releases/download/v${version}/polkadot", 26 | }, 27 | }, 28 | }, 29 | "polkadot-parachain": { 30 | version: "0.9.430", 31 | downloadUrl: { 32 | linux: { 33 | arm64: 34 | "https://github.com/paritytech/cumulus/releases/download/v${version}/polkadot-parachain", 35 | x64: "https://github.com/paritytech/cumulus/releases/download/v${version}/polkadot-parachain", 36 | }, 37 | }, 38 | }, 39 | "astar-collator": { 40 | version: "5.28.0", 41 | downloadUrl: { 42 | darwin: { 43 | arm64: 44 | "https://github.com/AstarNetwork/Astar/releases/download/v${version}/astar-collator-v${version}-macOS-x86_64.tar.gz", 45 | x64: "https://github.com/AstarNetwork/Astar/releases/download/v${version}/astar-collator-v${version}-macOS-x86_64.tar.gz", 46 | }, 47 | linux: { 48 | arm64: 49 | "https://github.com/AstarNetwork/Astar/releases/download/v${version}/astar-collator-v${version}-ubuntu-aarch64.tar.gz", 50 | x64: "https://github.com/AstarNetwork/Astar/releases/download/v${version}/astar-collator-v${version}-ubuntu-x86_64.tar.gz", 51 | }, 52 | }, 53 | }, 54 | }, 55 | }; 56 | 57 | export const zombienetBinariesList = Object.keys(zombienet.binaries); 58 | -------------------------------------------------------------------------------- /src/templates/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "contracts/*", 4 | ] 5 | -------------------------------------------------------------------------------- /src/templates/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Salesforce 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/templates/chopsticks/dev.yml: -------------------------------------------------------------------------------- 1 | endpoint: ws://127.0.0.1:9944 2 | mock-signature-host: true 3 | block: 1 4 | db: ./db.sqlite -------------------------------------------------------------------------------- /src/templates/contracts/blank/contract/Cargo.toml.hbs: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{contract_name_snake}}" 3 | version = "0.1.0" 4 | authors = ["{{author_name}}"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | ink = { version = "5", default-features = false } 9 | 10 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 11 | scale-info = { version = "2.11", default-features = false, features = ["derive"], optional = true } 12 | 13 | [lib] 14 | name = "{{contract_name_snake}}" 15 | path = "src/lib.rs" 16 | 17 | [features] 18 | default = ["std"] 19 | std = [ 20 | "ink/std", 21 | "scale/std", 22 | "scale-info/std" 23 | ] 24 | ink-as-dependency = [] 25 | -------------------------------------------------------------------------------- /src/templates/contracts/blank/contract/src/lib.rs.hbs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | #[ink::contract] 4 | mod {{contract_name_snake}} {} 5 | -------------------------------------------------------------------------------- /src/templates/contracts/blank/test/index.test.ts.hbs: -------------------------------------------------------------------------------- 1 | import { expect, use } from "chai"; 2 | import chaiAsPromised from "chai-as-promised"; 3 | import {{contract_name_pascal}}Factory from "./typedContract/constructors/{{contract_name}}"; 4 | import {{contract_name_pascal}} from "./typedContract/contracts/{{contract_name}}"; 5 | import { ApiPromise, WsProvider, Keyring } from "@polkadot/api"; 6 | import { KeyringPair } from "@polkadot/keyring/types"; 7 | 8 | use(chaiAsPromised); 9 | 10 | // Create a new instance of contract 11 | const wsProvider = new WsProvider("ws://127.0.0.1:9944"); 12 | // Create a keyring instance 13 | const keyring = new Keyring({ type: "sr25519" }); 14 | 15 | describe("{{contract_name}} test", () => { 16 | let {{contract_name}}Factory: {{contract_name_pascal}}Factory; 17 | let api: ApiPromise; 18 | let deployer: KeyringPair; 19 | 20 | let contract: {{contract_name_pascal}}; 21 | const initialState = true; 22 | 23 | before(async function setup(): Promise { 24 | api = await ApiPromise.create({ provider: wsProvider }); 25 | deployer = keyring.addFromUri("//Alice"); 26 | 27 | {{contract_name}}Factory = new {{contract_name_pascal}}Factory(api, deployer); 28 | 29 | contract = new {{contract_name_pascal}}( 30 | (await {{contract_name}}Factory.new(initialState)).address, 31 | deployer, 32 | api 33 | ); 34 | }); 35 | 36 | after(async function tearDown() { 37 | await api.disconnect(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/templates/contracts/flipper/contract/Cargo.toml.hbs: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{contract_name_snake}}" 3 | version = "0.1.0" 4 | authors = ["{{author_name}}"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | ink = { version = "5", default-features = false } 9 | 10 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 11 | scale-info = { version = "2.11", default-features = false, features = ["derive"], optional = true } 12 | 13 | [dev-dependencies] 14 | ink_e2e = "5" 15 | 16 | [lib] 17 | name = "{{contract_name_snake}}" 18 | path = "src/lib.rs" 19 | 20 | [features] 21 | default = ["std"] 22 | std = [ 23 | "ink/std", 24 | "scale/std", 25 | "scale-info/std", 26 | ] 27 | ink-as-dependency = [] 28 | e2e-tests = [] 29 | -------------------------------------------------------------------------------- /src/templates/contracts/flipper/contract/src/lib.rs.hbs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | #[ink::contract] 4 | mod {{contract_name_snake}} { 5 | 6 | /// Defines the storage of your contract. 7 | /// Add new fields to the below struct in order 8 | /// to add new static storage fields to your contract. 9 | #[ink(storage)] 10 | pub struct {{contract_name_pascal}} { 11 | /// Stores a single `bool` value on the storage. 12 | value: bool, 13 | } 14 | 15 | impl {{contract_name_pascal}} { 16 | /// Constructor that initializes the `bool` value to the given `init_value`. 17 | #[ink(constructor)] 18 | pub fn new(init_value: bool) -> Self { 19 | Self { value: init_value } 20 | } 21 | 22 | /// Constructor that initializes the `bool` value to `false`. 23 | /// 24 | /// Constructors can delegate to other constructors. 25 | #[ink(constructor)] 26 | pub fn default() -> Self { 27 | Self::new(Default::default()) 28 | } 29 | 30 | /// A message that can be called on instantiated contracts. 31 | /// This one flips the value of the stored `bool` from `true` 32 | /// to `false` and vice versa. 33 | /// 34 | /// To avoid typechain-polkadot [issue](https://github.com/727-Ventures/typechain-polkadot/issues/19) 35 | /// returning bool intentionally until it is resolved. 36 | #[ink(message)] 37 | pub fn flip(&mut self) -> bool { 38 | self.value = !self.value; 39 | self.value 40 | } 41 | 42 | /// Simply returns the current value of our `bool`. 43 | #[ink(message)] 44 | pub fn get(&self) -> bool { 45 | self.value 46 | } 47 | } 48 | 49 | /// Unit tests in Rust are normally defined within such a `#[cfg(test)]` 50 | /// module and test functions are marked with a `#[test]` attribute. 51 | /// The below code is technically just normal Rust code. 52 | #[cfg(test)] 53 | mod tests { 54 | /// Imports all the definitions from the outer scope so we can use them here. 55 | use super::*; 56 | 57 | /// We test if the default constructor does its job. 58 | #[ink::test] 59 | fn default_works() { 60 | let flipper = {{contract_name_pascal}}::default(); 61 | assert_eq!(flipper.get(), false); 62 | } 63 | 64 | /// We test a simple use case of our contract. 65 | #[ink::test] 66 | fn it_works() { 67 | let mut flipper = {{contract_name_pascal}}::new(false); 68 | assert_eq!(flipper.get(), false); 69 | flipper.flip(); 70 | assert_eq!(flipper.get(), true); 71 | } 72 | } 73 | 74 | 75 | /// This is how you'd write end-to-end (E2E) or integration tests for ink! contracts. 76 | /// 77 | /// When running these you need to make sure that you: 78 | /// - Compile the tests with the `e2e-tests` feature flag enabled (`--features e2e-tests`) 79 | /// - Are running a Substrate node which contains `pallet-contracts` in the background 80 | #[cfg(all(test, feature = "e2e-tests"))] 81 | mod e2e_tests { 82 | /// Imports all the definitions from the outer scope so we can use them here. 83 | use super::*; 84 | 85 | /// A helper function used for calling contract messages. 86 | use ink_e2e::build_message; 87 | 88 | /// The End-to-End test `Result` type. 89 | type E2EResult = std::result::Result>; 90 | 91 | /// We test that we can upload and instantiate the contract using its default constructor. 92 | #[ink_e2e::test] 93 | async fn default_works(mut client: ink_e2e::Client) -> E2EResult<()> { 94 | // Given 95 | let constructor = {{contract_name_pascal}}Ref::default(); 96 | 97 | // When 98 | let contract_account_id = client 99 | .instantiate("{{contract_name_snake}}", &ink_e2e::alice(), constructor, 0, None) 100 | .await 101 | .expect("instantiate failed") 102 | .account_id; 103 | 104 | // Then 105 | let get = build_message::<{{contract_name_pascal}}Ref>(contract_account_id.clone()) 106 | .call(|flipper| flipper.get()); 107 | let get_result = client.call_dry_run(&ink_e2e::alice(), &get, 0, None).await; 108 | assert!(matches!(get_result.return_value(), false)); 109 | 110 | Ok(()) 111 | } 112 | 113 | /// We test that we can read and write a value from the on-chain contract contract. 114 | #[ink_e2e::test] 115 | async fn it_works(mut client: ink_e2e::Client) -> E2EResult<()> { 116 | // Given 117 | let constructor = {{contract_name_pascal}}Ref::new(false); 118 | let contract_account_id = client 119 | .instantiate("{{contract_name_snake}}", &ink_e2e::bob(), constructor, 0, None) 120 | .await 121 | .expect("instantiate failed") 122 | .account_id; 123 | 124 | let get = build_message::<{{contract_name_pascal}}Ref>(contract_account_id.clone()) 125 | .call(|flipper| flipper.get()); 126 | let get_result = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; 127 | assert!(matches!(get_result.return_value(), false)); 128 | 129 | // When 130 | let flip = build_message::<{{contract_name_pascal}}Ref>(contract_account_id.clone()) 131 | .call(|flipper| flipper.flip()); 132 | let _flip_result = client 133 | .call(&ink_e2e::bob(), flip, 0, None) 134 | .await 135 | .expect("flip failed"); 136 | 137 | // Then 138 | let get = build_message::<{{contract_name_pascal}}Ref>(contract_account_id.clone()) 139 | .call(|flipper| flipper.get()); 140 | let get_result = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; 141 | assert!(matches!(get_result.return_value(), true)); 142 | 143 | Ok(()) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/templates/contracts/flipper/test/index.test.ts.hbs: -------------------------------------------------------------------------------- 1 | import { expect, use } from "chai"; 2 | import chaiAsPromised from "chai-as-promised"; 3 | import {{contract_name_pascal}}Factory from "../../typedContracts/{{contract_name}}/constructors/{{contract_name}}"; 4 | import {{contract_name_pascal}} from "../../typedContracts/{{contract_name}}/contracts/{{contract_name}}"; 5 | import { ApiPromise, WsProvider, Keyring } from "@polkadot/api"; 6 | import { KeyringPair } from "@polkadot/keyring/types"; 7 | 8 | use(chaiAsPromised); 9 | 10 | // Create a new instance of contract 11 | const wsProvider = new WsProvider("ws://127.0.0.1:9944"); 12 | // Create a keyring instance 13 | const keyring = new Keyring({ type: "sr25519" }); 14 | 15 | describe("{{contract_name}} test", () => { 16 | let {{contract_name}}Factory: {{contract_name_pascal}}Factory; 17 | let api: ApiPromise; 18 | let deployer: KeyringPair; 19 | 20 | let contract: {{contract_name_pascal}}; 21 | const initialState = true; 22 | 23 | before(async function setup(): Promise { 24 | api = await ApiPromise.create({ provider: wsProvider }); 25 | deployer = keyring.addFromUri("//Alice"); 26 | 27 | {{contract_name}}Factory = new {{contract_name_pascal}}Factory(api, deployer); 28 | 29 | contract = new {{contract_name_pascal}}( 30 | (await {{contract_name}}Factory.new(initialState)).address, 31 | deployer, 32 | api 33 | ); 34 | }); 35 | 36 | after(async function tearDown() { 37 | await api.disconnect(); 38 | }); 39 | 40 | it("Sets the initial state", async () => { 41 | expect((await contract.query.get()).value.ok).to.equal(initialState); 42 | }); 43 | 44 | it("Can flip the state", async () => { 45 | const { gasRequired } = await contract.withSigner(deployer).query.flip(); 46 | 47 | await contract.withSigner(deployer).tx.flip({ 48 | gasLimit: gasRequired, 49 | }); 50 | 51 | await expect((await contract.query.get()).value.ok).to.be.equal(!initialState); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/templates/contracts/psp22/contract/Cargo.toml.hbs: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{contract_name_snake}}" 3 | version = "0.1.0" 4 | authors = ["{{author_name}}"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | ink = { version = "5", default-features = false} 9 | 10 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 11 | scale-info = { version = "2.11", default-features = false, features = ["derive"], optional = true } 12 | 13 | openbrush = { git = "https://github.com/Brushfam/openbrush-contracts", tag = "4.0.0", default-features = false, features = ["psp22"] } 14 | 15 | [dev-dependencies] 16 | ink_e2e = "5" 17 | test_helpers = { path = "../../tests/test_helpers", default-features = false } 18 | 19 | [lib] 20 | name = "{{contract_name_snake}}" 21 | path = "src/lib.rs" 22 | 23 | [features] 24 | default = ["std"] 25 | std = [ 26 | "ink/std", 27 | "scale/std", 28 | "scale-info/std", 29 | "openbrush/std", 30 | ] 31 | ink-as-dependency = [] 32 | e2e-tests = [] 33 | 34 | [profile.dev] 35 | codegen-units = 16 36 | -------------------------------------------------------------------------------- /src/templates/contracts/psp22/contract/src/lib.rs.hbs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std, no_main)] 2 | 3 | #[openbrush::implementation(PSP22)] 4 | #[openbrush::contract] 5 | pub mod {{contract_name_snake}} { 6 | use ink::codegen::{Env, EmitEvent}; 7 | use openbrush::traits::Storage; 8 | 9 | #[ink(event)] 10 | pub struct TransferEvent { 11 | #[ink(topic)] 12 | from: Option, 13 | #[ink(topic)] 14 | to: Option, 15 | value: Balance, 16 | } 17 | 18 | #[ink(event)] 19 | pub struct ApprovalEvent { 20 | #[ink(topic)] 21 | owner: AccountId, 22 | #[ink(topic)] 23 | spender: AccountId, 24 | value: Balance, 25 | } 26 | 27 | #[ink(storage)] 28 | #[derive(Storage)] 29 | pub struct {{contract_name_pascal}} { 30 | #[storage_field] 31 | psp22: psp22::Data, 32 | } 33 | 34 | #[overrider(psp22::Internal)] 35 | fn _emit_transfer_event( 36 | &self, 37 | from: Option, 38 | to: Option, 39 | amount: Balance, 40 | ) { 41 | self.env().emit_event(TransferEvent { 42 | from, 43 | to, 44 | value: amount, 45 | }); 46 | } 47 | 48 | #[overrider(psp22::Internal)] 49 | fn _emit_approval_event(&self, owner: AccountId, spender: AccountId, amount: Balance) { 50 | self.env().emit_event(ApprovalEvent { 51 | owner, 52 | spender, 53 | value: amount, 54 | }); 55 | } 56 | 57 | impl {{contract_name_pascal}} { 58 | #[ink(constructor)] 59 | pub fn new(total_supply: Balance) -> Self { 60 | let mut instance = Self { 61 | psp22: Default::default() 62 | }; 63 | Internal::_mint_to(&mut instance, Self::env().caller(), total_supply).expect("Should mint"); 64 | instance 65 | } 66 | 67 | #[ink(message)] 68 | pub fn get_total_supply(&self) -> Balance { 69 | PSP22::total_supply(self) 70 | } 71 | } 72 | 73 | #[cfg(all(test, feature = "e2e-tests"))] 74 | pub mod tests { 75 | use super::*; 76 | use ink_e2e::{ 77 | build_message, 78 | }; 79 | use openbrush::contracts::psp22::psp22_external::PSP22; 80 | use test_helpers::{ 81 | address_of, 82 | balance_of, 83 | }; 84 | 85 | type E2EResult = Result>; 86 | 87 | #[ink_e2e::test] 88 | async fn assigns_initial_balance(mut client: ink_e2e::Client) -> E2EResult<()> { 89 | let constructor = {{contract_name_pascal}}Ref::new(100); 90 | let address = client 91 | .instantiate("{{contract_name_snake}}", &ink_e2e::alice(), constructor, 0, None) 92 | .await 93 | .expect("instantiate failed") 94 | .account_id; 95 | 96 | let result = { 97 | let _msg = build_message::<{{contract_name_pascal}}Ref>(address.clone()) 98 | .call(|contract| contract.balance_of(address_of!(Alice))); 99 | client.call_dry_run(&ink_e2e::alice(), &_msg, 0, None).await 100 | }; 101 | 102 | assert!(matches!(result.return_value(), 100)); 103 | 104 | Ok(()) 105 | } 106 | 107 | #[ink_e2e::test] 108 | async fn transfer_adds_amount_to_destination_account(mut client: ink_e2e::Client) -> E2EResult<()> { 109 | let constructor = {{contract_name_pascal}}Ref::new(100); 110 | let address = client 111 | .instantiate("{{contract_name_snake}}", &ink_e2e::alice(), constructor, 0, None) 112 | .await 113 | .expect("instantiate failed") 114 | .account_id; 115 | 116 | let result = { 117 | let _msg = build_message::<{{contract_name_pascal}}Ref>(address.clone()) 118 | .call(|contract| contract.transfer(address_of!(Bob), 50, vec![])); 119 | client 120 | .call(&ink_e2e::alice(), _msg, 0, None) 121 | .await 122 | .expect("transfer failed") 123 | }; 124 | 125 | assert!(matches!(result.return_value(), Ok(()))); 126 | 127 | let balance_of_alice = balance_of!({{contract_name_pascal}}Ref, client, address, Alice); 128 | 129 | let balance_of_bob = balance_of!({{contract_name_pascal}}Ref, client, address, Bob); 130 | 131 | assert_eq!(balance_of_bob, 50, "Bob should have 50 tokens"); 132 | assert_eq!(balance_of_alice, 50, "Alice should have 50 tokens"); 133 | 134 | Ok(()) 135 | } 136 | 137 | #[ink_e2e::test] 138 | async fn cannot_transfer_above_the_amount(mut client: ink_e2e::Client) -> E2EResult<()> { 139 | let constructor = {{contract_name_pascal}}Ref::new(100); 140 | let address = client 141 | .instantiate("{{contract_name_snake}}", &ink_e2e::alice(), constructor, 0, None) 142 | .await 143 | .expect("instantiate failed") 144 | .account_id; 145 | 146 | let result = { 147 | let _msg = build_message::<{{contract_name_pascal}}Ref>(address.clone()) 148 | .call(|contract| contract.transfer(address_of!(Bob), 101, vec![])); 149 | client.call_dry_run(&ink_e2e::alice(), &_msg, 0, None).await 150 | }; 151 | 152 | assert!(matches!(result.return_value(), Err(PSP22Error::InsufficientBalance))); 153 | 154 | Ok(()) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/templates/contracts/psp22/test/index.test.ts.hbs: -------------------------------------------------------------------------------- 1 | import { expect, use } from "chai"; 2 | import chaiAsPromised from "chai-as-promised"; 3 | import {{contract_name_pascal}}Factory from "../../typedContracts/{{contract_name}}/constructors/{{contract_name}}"; 4 | import {{contract_name_pascal}} from "../../typedContracts/{{contract_name}}/contracts/{{contract_name}}"; 5 | import { ApiPromise, WsProvider, Keyring } from "@polkadot/api"; 6 | import { KeyringPair } from "@polkadot/keyring/types"; 7 | 8 | use(chaiAsPromised); 9 | 10 | // Create a new instance of contract 11 | const wsProvider = new WsProvider("ws://127.0.0.1:9944"); 12 | // Create a keyring instance 13 | const keyring = new Keyring({ type: "sr25519" }); 14 | 15 | const EMPTY_ADDRESS = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"; 16 | describe("{{contract_name}} test", () => { 17 | let {{contract_name}}Factory: {{contract_name_pascal}}Factory; 18 | let api: ApiPromise; 19 | let deployer: KeyringPair; 20 | let wallet1: KeyringPair; 21 | let wallet2: KeyringPair; 22 | let contract: {{contract_name_pascal}}; 23 | const maxSupply = 10000000000; 24 | 25 | before(async function setup(): Promise { 26 | api = await ApiPromise.create({ provider: wsProvider }); 27 | deployer = keyring.addFromUri("//Alice"); 28 | wallet1 = keyring.addFromUri("//Bob"); 29 | wallet2 = keyring.addFromUri("//Charlie"); 30 | 31 | {{contract_name}}Factory = new {{contract_name_pascal}}Factory(api, deployer); 32 | 33 | contract = new {{contract_name_pascal}}( 34 | (await {{contract_name}}Factory.new(maxSupply)).address, 35 | deployer, 36 | api 37 | ); 38 | }); 39 | 40 | after(async function tearDown() { 41 | await api.disconnect(); 42 | }); 43 | 44 | it("Assigns initial balance", async () => { 45 | expect( 46 | (await contract.query.totalSupply()).value.ok?.rawNumber.toNumber() 47 | ).to.equal(maxSupply); 48 | }); 49 | 50 | it("Transfer adds amount to destination account", async () => { 51 | const transferredAmount = 2; 52 | 53 | const { gasRequired } = await contract 54 | .withSigner(deployer) 55 | .query.transfer(wallet1.address, transferredAmount, []); 56 | 57 | await contract.tx.transfer(wallet1.address, transferredAmount, [], { 58 | gasLimit: gasRequired, 59 | }); 60 | 61 | await expect( 62 | (await contract.query.balanceOf(wallet1.address)).value.ok?.toNumber() 63 | ).to.be.equal(transferredAmount); 64 | await expect( 65 | (await contract.query.balanceOf(contract.signer.address)).value.ok?.toNumber() 66 | ).to.be.equal(maxSupply - transferredAmount); 67 | }); 68 | 69 | it("Can not transfer above the amount", async () => { 70 | const transferredAmount = maxSupply + 1; 71 | 72 | const { gasRequired } = await contract 73 | .withSigner(deployer) 74 | .query.transfer(wallet1.address, transferredAmount, []); 75 | 76 | await expect( 77 | contract.tx.transfer(wallet1.address, transferredAmount, [], { 78 | gasLimit: gasRequired, 79 | }) 80 | ).to.eventually.be.rejected; 81 | }); 82 | }); -------------------------------------------------------------------------------- /src/templates/github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | types: [opened, reopened, synchronize, ready_for_review] 5 | workflow_dispatch: 6 | env: 7 | NODE_JS_VER: 18.x 8 | SWANKY_NODE_VER: v1.2.0 9 | jobs: 10 | e2e-tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout the source code 14 | uses: actions/checkout@v3 15 | 16 | - name: Install & display rust toolchain 17 | run: | 18 | rustup toolchain install nightly 19 | rustup default nightly 20 | rustup show 21 | rustup component add rust-src 22 | 23 | - name: Check targets are installed correctly 24 | run: rustup target list --installed 25 | 26 | - name: Cache cargo 27 | uses: actions/cache@v3 28 | with: 29 | path: ~/.cargo 30 | key: ${{ runner.os }}-rust-${{ hashFiles('rust-toolchain.toml') }} 31 | restore-keys: | 32 | ${{ runner.os }}-rust 33 | 34 | - name: Check if cargo-contract exists 35 | id: check-cargo-contract 36 | continue-on-error: true 37 | run: cargo contract --version 38 | 39 | - name: Install cargo contract 40 | if: ${{ steps.check-cargo-contract.outcome == 'failure' }} 41 | run: | 42 | cargo install cargo-dylint dylint-link 43 | cargo install --force --locked cargo-contract 44 | 45 | - name: Use Node.js 46 | uses: actions/setup-node@v3 47 | with: 48 | node-version: ${{ env.NODE_JS_VER }} 49 | 50 | - name: Install swanky-cli 51 | run: npm install && npm install -g @astar-network/swanky-cli 52 | 53 | - name: Compile contracts 54 | run: swanky contract compile --all -v 55 | 56 | - name: Download and start swanky-node 57 | run: | 58 | sudo wget https://github.com/inkdevhub/swanky-node/releases/download/${{ env.SWANKY_NODE_VER }}/swanky-node-${{ env.SWANKY_NODE_VER }}-ubuntu-x86_64.tar.gz 59 | sudo tar -zxvf swanky-node-v1.2.0-ubuntu-x86_64.tar.gz 60 | sudo chmod +x swanky-node 61 | ./swanky-node -lerror,runtime::contracts=debug & 62 | sleep 10 63 | 64 | - name: Test contracts 65 | run: swanky contract test --all 66 | -------------------------------------------------------------------------------- /src/templates/gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node,macos,visualstudiocode 2 | 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,visualstudiocode 4 | 5 | ### macOS 6 | 7 | # General 8 | 9 | .DS_Store 10 | .AppleDouble 11 | .LSOverride 12 | 13 | # Icon must end with two \r 14 | 15 | Icon 16 | 17 | # Thumbnails 18 | 19 | .\_\* 20 | 21 | # Files that might appear in the root of a volume 22 | 23 | .DocumentRevisions-V100 24 | .fseventsd 25 | .Spotlight-V100 26 | .TemporaryItems 27 | .Trashes 28 | .VolumeIcon.icns 29 | .com.apple.timemachine.donotpresent 30 | 31 | # Directories potentially created on remote AFP share 32 | 33 | .AppleDB 34 | .AppleDesktop 35 | Network Trash Folder 36 | Temporary Items 37 | .apdisk 38 | 39 | ### macOS Patch 40 | 41 | # iCloud generated files 42 | 43 | \*.icloud 44 | 45 | ### Node 46 | 47 | # Logs 48 | 49 | logs 50 | _.log 51 | npm-debug.log_ 52 | yarn-debug.log* 53 | yarn-error.log* 54 | lerna-debug.log* 55 | .pnpm-debug.log* 56 | 57 | # Diagnostic reports (https://nodejs.org/api/report.html) 58 | 59 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 60 | 61 | # Runtime data 62 | 63 | pids 64 | _.pid 65 | _.seed 66 | \*.pid.lock 67 | 68 | # Directory for instrumented libs generated by jscoverage/JSCover 69 | 70 | lib-cov 71 | 72 | # Coverage directory used by tools like istanbul 73 | 74 | coverage 75 | \*.lcov 76 | 77 | # nyc test coverage 78 | 79 | .nyc_output 80 | 81 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 82 | 83 | .grunt 84 | 85 | # Bower dependency directory (https://bower.io/) 86 | 87 | bower_components 88 | 89 | # node-waf configuration 90 | 91 | .lock-wscript 92 | 93 | # Compiled binary addons (https://nodejs.org/api/addons.html) 94 | 95 | build/Release 96 | 97 | # Dependency directories 98 | 99 | node_modules/ 100 | jspm_packages/ 101 | 102 | # Snowpack dependency directory (https://snowpack.dev/) 103 | 104 | web_modules/ 105 | 106 | # TypeScript cache 107 | 108 | \*.tsbuildinfo 109 | 110 | # Optional npm cache directory 111 | 112 | .npm 113 | 114 | # Optional eslint cache 115 | 116 | .eslintcache 117 | 118 | # Optional stylelint cache 119 | 120 | .stylelintcache 121 | 122 | # Microbundle cache 123 | 124 | .rpt2_cache/ 125 | .rts2_cache_cjs/ 126 | .rts2_cache_es/ 127 | .rts2_cache_umd/ 128 | 129 | # Optional REPL history 130 | 131 | .node_repl_history 132 | 133 | # Output of 'npm pack' 134 | 135 | \*.tgz 136 | 137 | # Yarn Integrity file 138 | 139 | .yarn-integrity 140 | 141 | # dotenv environment variable files 142 | 143 | .env 144 | .env.development.local 145 | .env.test.local 146 | .env.production.local 147 | .env.local 148 | 149 | # parcel-bundler cache (https://parceljs.org/) 150 | 151 | .cache 152 | .parcel-cache 153 | 154 | # Next.js build output 155 | 156 | .next 157 | out 158 | 159 | # Nuxt.js build / generate output 160 | 161 | .nuxt 162 | dist 163 | 164 | # Gatsby files 165 | 166 | .cache/ 167 | 168 | # Comment in the public line in if your project uses Gatsby and not Next.js 169 | 170 | # https://nextjs.org/blog/next-9-1#public-directory-support 171 | 172 | # public 173 | 174 | # vuepress build output 175 | 176 | .vuepress/dist 177 | 178 | # vuepress v2.x temp and cache directory 179 | 180 | .temp 181 | 182 | # Docusaurus cache and generated files 183 | 184 | .docusaurus 185 | 186 | # Serverless directories 187 | 188 | .serverless/ 189 | 190 | # FuseBox cache 191 | 192 | .fusebox/ 193 | 194 | # DynamoDB Local files 195 | 196 | .dynamodb/ 197 | 198 | # TernJS port file 199 | 200 | .tern-port 201 | 202 | # Stores VSCode versions used for testing VSCode extensions 203 | 204 | .vscode-test 205 | 206 | # yarn v2 207 | 208 | .yarn/cache 209 | .yarn/unplugged 210 | .yarn/build-state.yml 211 | .yarn/install-state.gz 212 | .pnp.\* 213 | 214 | ### Node Patch 215 | 216 | # Serverless Webpack directories 217 | 218 | .webpack/ 219 | 220 | # Optional stylelint cache 221 | 222 | # SvelteKit build / generate output 223 | 224 | .svelte-kit 225 | 226 | ### VisualStudioCode 227 | 228 | .vscode/_ 229 | !.vscode/settings.json 230 | !.vscode/tasks.json 231 | !.vscode/launch.json 232 | !.vscode/extensions.json 233 | !.vscode/_.code-snippets 234 | 235 | # Local History for Visual Studio Code 236 | 237 | .history/ 238 | 239 | # Built Visual Studio Code Extensions 240 | 241 | \*.vsix 242 | 243 | ### VisualStudioCode Patch 244 | 245 | # Ignore all local history of files 246 | 247 | .history 248 | .ionide 249 | 250 | # Support for Project snippet scope 251 | 252 | .vscode/\*.code-snippets 253 | 254 | # Ignore code-workspaces 255 | 256 | \*.code-workspace 257 | 258 | ### Rust 259 | 260 | # Generated by Cargo 261 | 262 | # will have compiled files and executables 263 | 264 | debug/ 265 | target/ 266 | 267 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 268 | 269 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 270 | 271 | Cargo.lock 272 | 273 | # These are backup files generated by rustfmt 274 | 275 | \*_/_.rs.bk 276 | 277 | # MSVC Windows builds of rustc generate these, which store debugging information 278 | 279 | \*.pdb 280 | 281 | # End of https://www.toptal.com/developers/gitignore/api/node,macos,visualstudiocode 282 | 283 | _-debug.log 284 | _-error.log 285 | /.nyc_output 286 | /dist 287 | /lib 288 | /package-lock.json 289 | /tmp 290 | node_modules 291 | oclif.manifest.json 292 | -------------------------------------------------------------------------------- /src/templates/mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "ts-node/register", 3 | "spec": "tests/**/*.test.ts", 4 | "exit": true, 5 | "timeout": 20000 6 | } 7 | -------------------------------------------------------------------------------- /src/templates/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{project_name}}", 3 | "version": "1.0.0", 4 | {{#if author_email}} 5 | "author": "{{author_name}} <{{author_email}}>", 6 | {{else}} 7 | "author": "{{author_name}}", 8 | {{/if}} 9 | "license": "MIT", 10 | "scripts": { 11 | "run-node": "swanky node start", 12 | "test": "mocha --config .mocharc.json" 13 | }, 14 | "engines": { 15 | "node": ">=18.0.0" 16 | }, 17 | "dependencies": { 18 | "@727-ventures/typechain-types": "1.0.0-beta.1", 19 | "@727-ventures/typechain-polkadot": "1.0.0-beta.2", 20 | "@types/node": "^18", 21 | "typescript": "^5.0.4" 22 | }, 23 | "devDependencies": { 24 | "@types/chai": "^4.3.0", 25 | "@types/chai-as-promised": "^7.1.5", 26 | "@types/mocha": "^8.0.3", 27 | "chai": "^4.3.6", 28 | "chai-as-promised": "^7.1.1", 29 | "mocha": "^10.1.0", 30 | "mochawesome": "^7.1.3", 31 | "ts-node": "^10.8.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/templates/test_helpers/Cargo.toml.hbs: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test_helpers" 3 | version= "0.1.0" 4 | authors = ["{{author_name}}"] 5 | edition = "2021" 6 | 7 | [lib] 8 | name = "test_helpers" 9 | path = "lib.rs" 10 | 11 | [profile.dev] 12 | codegen-units = 16 -------------------------------------------------------------------------------- /src/templates/test_helpers/lib.rs.hbs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! address_of { 3 | ($account:ident) => { 4 | ::ink_e2e::account_id(::ink_e2e::AccountKeyring::$account) 5 | }; 6 | } 7 | 8 | #[macro_export] 9 | macro_rules! balance_of { 10 | ($contract_ref:ident, $client:ident, $address:ident, $account:ident) => \{{ 11 | let _msg = 12 | build_message::<$contract_ref>($address.clone()).call(|contract| contract.balance_of(address_of!($account))); 13 | $client 14 | .call_dry_run(&::ink_e2e::alice(), &_msg, 0, None) 15 | .await 16 | .return_value() 17 | }}; 18 | } -------------------------------------------------------------------------------- /src/templates/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "noImplicitAny": false, 9 | "resolveJsonModule": true 10 | }, 11 | "include": ["**/*.ts"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /src/templates/zombienet/astar-collator.toml: -------------------------------------------------------------------------------- 1 | [[parachains]] 2 | id = 2006 3 | chain = "astar-dev" 4 | cumulus_based = true 5 | 6 | [parachains.collator] 7 | name = "astar" 8 | command = "./zombienet/bin/astar-collator" 9 | rpc_port = 8545 10 | args = [ "-l=xcm=trace", "--enable-evm-rpc" ] 11 | 12 | [[parachains]] 13 | id = 2007 14 | chain = "shiden-dev" 15 | cumulus_based = true 16 | 17 | [parachains.collator] 18 | name = "shiden" 19 | command = "./zombienet/bin/astar-collator" 20 | rpc_port = 8546 21 | args = [ "-l=xcm=trace", "--enable-evm-rpc" ] 22 | 23 | [[parachains]] 24 | id = 2008 25 | chain = "shibuya-dev" 26 | cumulus_based = true 27 | 28 | [parachains.collator] 29 | name = "shibuya" 30 | command = "./zombienet/bin/astar-collator" 31 | rpc_port = 8546 32 | args = [ "-l=xcm=trace", "--enable-evm-rpc" ] 33 | 34 | [[hrmp_channels]] 35 | sender = 2006 36 | recipient = 2007 37 | max_capacity = 8 38 | max_message_size = 512 39 | 40 | [[hrmp_channels]] 41 | sender = 2007 42 | recipient = 2006 43 | max_capacity = 8 44 | max_message_size = 512 45 | 46 | [[hrmp_channels]] 47 | sender = 2006 48 | recipient = 2008 49 | max_capacity = 8 50 | max_message_size = 512 51 | 52 | [[hrmp_channels]] 53 | sender = 2008 54 | recipient = 2006 55 | max_capacity = 8 56 | max_message_size = 512 57 | 58 | 59 | [[hrmp_channels]] 60 | sender = 2008 61 | recipient = 2007 62 | max_capacity = 8 63 | max_message_size = 512 64 | 65 | [[hrmp_channels]] 66 | sender = 2007 67 | recipient = 2008 68 | max_capacity = 8 69 | max_message_size = 512 -------------------------------------------------------------------------------- /src/templates/zombienet/polkadot-parachain.toml: -------------------------------------------------------------------------------- 1 | [[parachains]] 2 | id = 100 3 | 4 | [parachains.collator] 5 | name = "collator01" 6 | command = "./zombienet/bin/polkadot-parachain" 7 | args = ["-lparachain=debug"] -------------------------------------------------------------------------------- /src/templates/zombienet/polkadot.toml: -------------------------------------------------------------------------------- 1 | [settings] 2 | timeout = 1000 3 | 4 | [relaychain] 5 | default_command = "./zombienet/bin/polkadot" 6 | chain = "rococo-local" 7 | 8 | [[relaychain.nodes]] 9 | name = "relay01" 10 | 11 | [[relaychain.nodes]] 12 | name = "relay02" 13 | 14 | [[relaychain.nodes]] 15 | name = "relay03" -------------------------------------------------------------------------------- /src/templates/zombienet/zombienet.config.toml: -------------------------------------------------------------------------------- 1 | [settings] 2 | timeout = 1000 3 | 4 | [relaychain] 5 | default_command = "./zombienet/bin/polkadot" 6 | chain = "rococo-local" 7 | 8 | [[relaychain.nodes]] 9 | name = "relay01" 10 | 11 | [[relaychain.nodes]] 12 | name = "relay02" 13 | 14 | [[relaychain.nodes]] 15 | name = "relay03" 16 | 17 | -------------------------------------------------------------------------------- /src/test/helpers/init.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import path from "node:path"; 3 | 4 | import process from "node:process"; 5 | 6 | process.env.TS_NODE_PROJECT = path.resolve("test/tsconfig.json"); 7 | process.env.NODE_ENV = "development"; 8 | 9 | global.oclif = global.oclif || {}; 10 | global.oclif.columns = 80; 11 | -------------------------------------------------------------------------------- /src/test/lib/prompts.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { choice, email, name, pickTemplate } from "../../lib/prompts.js"; 3 | 4 | describe("Prompts", function () { 5 | describe("Template", function () { 6 | it("Throws on no arg", function () { 7 | expect(pickTemplate).to.throw("Template list is empty!"); 8 | }); 9 | it("Throws on empty array", function () { 10 | expect(() => pickTemplate([])).to.throw("Template list is empty!"); 11 | }); 12 | it("Returns a question object", function () { 13 | const templatesList = ["TemplateOne", "TemplateTwo"]; 14 | const result = pickTemplate(templatesList); 15 | expect(result).to.have.nested.property("choices[0]", "TemplateOne"); 16 | expect(result).to.have.nested.property("choices[1]", "TemplateTwo"); 17 | }); 18 | }); 19 | 20 | describe("Name", function () { 21 | describe("Default call", function () { 22 | const result = name("subject"); 23 | it("Uses subject in the name", function () { 24 | expect(result).to.have.property("name", "subjectName"); 25 | }); 26 | it("Uses default question", function () { 27 | expect(result).to.have.property("message", "What name should we use for subject?"); 28 | }); 29 | }); 30 | describe("Optional params", function () { 31 | const result = name("subject", () => "myName", "What's your name?"); 32 | it("Uses passed in initial value", function () { 33 | expect(result.default()).to.eq("myName"); 34 | }); 35 | it("Uses passed in question", function () { 36 | expect(result).to.have.property("message", "What's your name?"); 37 | }); 38 | }); 39 | }); 40 | 41 | describe("Email", function () { 42 | describe("Default call", function () { 43 | const result = email(); 44 | it("Uses default question", function () { 45 | expect(result).to.have.property("message", "What is your email?"); 46 | }); 47 | it("Has no default value", function () { 48 | expect(result).to.not.have.property("default"); 49 | }); 50 | }); 51 | 52 | describe("Optional params", function () { 53 | const question = "What yer email be?"; 54 | const initial = "my@e.mail"; 55 | const result = email(initial, question); 56 | it("Uses passed in intial value", function () { 57 | expect(result).to.have.property("default", initial); 58 | }); 59 | it("Uses passed in question", function () { 60 | expect(result).to.have.property("message", question); 61 | }); 62 | }); 63 | }); 64 | 65 | describe("Choice", function () { 66 | const subject = "Who"; 67 | const questionText = "What?"; 68 | const result = choice(subject, questionText); 69 | 70 | it("Returns a correct question", function () { 71 | expect(result).to.have.property("name", subject); 72 | expect(result).to.have.property("message", questionText); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { SubmittableExtrinsic } from "@polkadot/api/types"; 2 | import { SUPPORTED_DEPS } from "../lib/consts.js"; 3 | 4 | export type KeypairType = "ed25519" | "sr25519" | "ecdsa" | "ethereum"; 5 | 6 | export interface ChainProperty { 7 | tokenSymbols: string[]; 8 | tokenDecimals: number[]; 9 | chainName: string; 10 | ss58Prefix: number; 11 | } 12 | 13 | export type ExtrinsicPayload = SubmittableExtrinsic<"promise">; 14 | 15 | export interface Encrypted { 16 | iv: string; 17 | data: string; 18 | } 19 | 20 | export interface AccountData { 21 | isDev: boolean; 22 | alias: string; 23 | mnemonic: string | Encrypted; 24 | address: string; 25 | } 26 | 27 | export interface ContractData { 28 | name: string; 29 | moduleName: string; 30 | build?: BuildData; 31 | deployments: DeploymentData[]; 32 | } 33 | 34 | export interface BuildData { 35 | timestamp: number; 36 | artifactsPath: string; 37 | buildMode: BuildMode; 38 | isVerified: boolean; 39 | } 40 | 41 | export interface DeploymentData { 42 | timestamp: number; 43 | networkUrl: string; 44 | deployerAlias: string; 45 | address: string; 46 | } 47 | 48 | export interface DownloadUrl { 49 | darwin: { 50 | arm64: string; 51 | x64: string; 52 | }; 53 | linux: { 54 | arm64: string; 55 | x64: string; 56 | }; 57 | } 58 | export interface SwankyConfig extends SwankySystemConfig { 59 | node: { 60 | polkadotPalletVersions: string; 61 | localPath: string; 62 | supportedInk: string; 63 | version: string; 64 | chopsticks?: { 65 | configPath: string; 66 | }; 67 | }; 68 | contracts: Record | Record; 69 | zombienet?: ZombienetData; 70 | env: Record; 71 | } 72 | 73 | export interface SwankySystemConfig { 74 | defaultAccount: string | null; 75 | accounts: AccountData[]; 76 | networks: Record; 77 | } 78 | 79 | export interface ZombienetData { 80 | version: string; 81 | downloadUrl: DownloadUrl; 82 | binaries: Record }>; 83 | } 84 | 85 | export interface ZombienetConfig { 86 | settings: { timeout: number }; 87 | relaychain: Relaychain; 88 | parachains: Parachain[]; 89 | hrmp_channels?: HrmpChannel[]; 90 | } 91 | 92 | export interface Relaychain { 93 | default_command: string; 94 | chain: string; 95 | nodes: Node[]; 96 | } 97 | export interface Node { 98 | name: string; 99 | } 100 | export interface HrmpChannel { 101 | sender: number; 102 | recipient: number; 103 | max_capacity: number; 104 | max_message_size: number; 105 | } 106 | export interface Parachain { 107 | id: number; 108 | chain: string; 109 | cumulus_based: boolean; 110 | collator: Collator; 111 | } 112 | export interface Collator { 113 | name: string; 114 | command: string; 115 | rpc_port: number; 116 | args: string[]; 117 | } 118 | 119 | export enum BuildMode { 120 | Debug = "Debug", 121 | Release = "Release", 122 | Verifiable = "Verifiable", 123 | } 124 | 125 | export type SupportedPlatforms = "darwin" | "linux"; 126 | export type SupportedArch = "arm64" | "x64"; 127 | 128 | export type DependencyName = keyof typeof SUPPORTED_DEPS; 129 | export type TestType = "e2e" | "mocha"; 130 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "importHelpers": true, 5 | "module": "NodeNext", 6 | "strict": true, 7 | "target": "es2020", 8 | "resolveJsonModule": true, 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "outDir": "dist", 12 | "rootDir": "src" 13 | }, 14 | "include": ["src/**/*", "src/test"], 15 | "ts-node": { 16 | "esm": true 17 | } 18 | } 19 | --------------------------------------------------------------------------------