├── .dockerignore ├── .eslintrc.json ├── .github └── pull_request_template.md ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── run └── run.cmd ├── completion ├── _eth ├── completion.json └── eth-completion.bash ├── docs ├── CHANGELOG.md └── COMMANDS.md ├── e2e ├── Dockerfile ├── abi.test.js ├── abis │ ├── ERC20.json │ └── ERC721.json ├── address.test.js ├── block-and-nop.test.js ├── common.js ├── configuration.test.js ├── convert.test.js ├── docker-compose.yml ├── ganache │ ├── Dockerfile │ └── package.json ├── help.test.js ├── method.test.js ├── repl.test.js ├── run-tests.sh └── test-contracts │ ├── Box.abi │ ├── Box.bin │ └── Box.sol ├── example.md ├── img ├── How-to-install-eth-cli.gif ├── contract-call.gif ├── eth-cli-commands.gif ├── fetching-data.gif └── repl.gif ├── package.json ├── scripts └── generate-commands-list.js ├── src ├── base │ ├── help-command.ts │ └── network.ts ├── commands │ ├── abi │ │ ├── add.ts │ │ ├── events.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── methods.ts │ │ ├── remove.ts │ │ ├── show.ts │ │ └── update.ts │ ├── address │ │ ├── add.ts │ │ ├── balance.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── random.ts │ │ ├── remove.ts │ │ └── show.ts │ ├── block │ │ ├── get.ts │ │ ├── index.ts │ │ └── number.ts │ ├── contract │ │ ├── add.ts │ │ ├── address.ts │ │ ├── call.ts │ │ ├── deploy.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── remove.ts │ │ ├── send.ts │ │ └── show.ts │ ├── convert.ts │ ├── event │ │ ├── get.ts │ │ ├── index.ts │ │ └── watch.ts │ ├── method │ │ ├── decode.ts │ │ ├── encode.ts │ │ ├── hash.ts │ │ ├── index.ts │ │ └── search.ts │ ├── network │ │ ├── add.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── remove.ts │ │ └── update.ts │ ├── repl.ts │ └── transaction │ │ ├── get.ts │ │ ├── index.ts │ │ ├── nop.ts │ │ └── send.ts ├── declarations.d.ts ├── flags.ts ├── helpers │ ├── abi │ │ ├── erc20.json │ │ └── erc721.json │ ├── checkCommandInputs.ts │ ├── config-service.ts │ ├── contractCall.ts │ ├── convert.ts │ ├── decodeTxData.ts │ ├── deploy.ts │ ├── encode.ts │ ├── generateNop.ts │ ├── getBalance.ts │ ├── getBlockNumber.ts │ ├── getBlockObject.ts │ ├── getContractAddress.ts │ ├── getEvents.ts │ ├── getNetworkId.ts │ ├── getTransactionObject.ts │ ├── knownAbis.ts │ ├── networks.ts │ ├── randomAddress.ts │ ├── replHistory.ts │ ├── replStarter.ts │ ├── searchSignature.ts │ ├── sendRawTransaction.ts │ ├── sendTransaction.ts │ ├── startRepl.ts │ ├── transactions.ts │ └── utils.ts ├── index.ts └── types.ts ├── test ├── .eslintrc.js ├── commands │ ├── abi │ │ ├── __snapshots__ │ │ │ └── methods.spec.ts.snap │ │ ├── methods.spec.ts │ │ └── show.spec.ts │ ├── address │ │ └── random.spec.ts │ ├── contract │ │ ├── __snapshots__ │ │ │ └── address.spec.ts.snap │ │ ├── address.spec.ts │ │ ├── deploy.spec.ts │ │ └── index.spec.ts │ ├── method │ │ ├── __snapshots__ │ │ │ ├── decode.spec.ts.snap │ │ │ ├── encode.spec.ts.snap │ │ │ └── hash.spec.ts.snap │ │ ├── decode.spec.ts │ │ ├── encode.spec.ts │ │ ├── hash.spec.ts │ │ └── index.spec.ts │ └── transaction │ │ ├── __snapshots__ │ │ └── get.spec.ts.snap │ │ ├── get.spec.ts │ │ ├── index.spec.ts │ │ └── nop.spec.ts └── files │ └── contracts │ ├── Proxy.abi │ ├── Proxy.bin │ └── Proxy.sol ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "extends": "./tsconfig.json", 5 | "include": ["test/**/*.ts"], 6 | "ecmaVersion": 2018, 7 | "sourceType": "module" 8 | }, 9 | "extends": ["plugin:@typescript-eslint/recommended", "prettier", "prettier/@typescript-eslint"], 10 | "plugins": ["@typescript-eslint", "oclif", "implicit-dependencies"], 11 | "rules": { 12 | "no-console": "off", 13 | "no-constant-condition": "off", 14 | "implicit-dependencies/no-implicit": ["error", { "peer": true, "dev": true, "optional": true }], 15 | "no-eval": "error", 16 | "@typescript-eslint/no-explicit-any": "off", 17 | "@typescript-eslint/explicit-function-return-type": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Cute animal picture 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .idea 4 | coverage 5 | .vscode/ 6 | .nyc_outputlib/ 7 | *-debug.log 8 | *-error.log 9 | /lib 10 | tsconfig.tsbuildinfo 11 | oclif.manifest.json 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | completion/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "semi": false, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | 5 | services: 6 | - docker 7 | 8 | script: 9 | - npm run lint 10 | - npm test 11 | - cd e2e && ./run-tests.sh 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to eth-cli 2 | 3 | _Thanks for taking the time to help out and improve eth-cli! :tada:_ 4 | 5 | The following is a set of guidelines for eth-cli contributions and may change 6 | over time. Feel free to suggest improvements to this document in a pull request! 7 | 8 | ## Contents 9 | 10 | [How Can I Contribute?](#how-can-i-contribute) 11 | 12 | [Development](#development) 13 | 14 | - [Overview](#overview) 15 | - [Development Requirements](#development-requirements) 16 | - [Getting Started](#getting-started) 17 | - [Forks, Branches, and Pull Requests](#forks-branches-and-pull-requests) 18 | - [Branching Model](#branching-model) 19 | - [Working on a Branch](#working-on-a-branch) 20 | 21 | [Additional Notes](#additional-notes) 22 | 23 | ## How Can I Contribute? 24 | 25 | All contributions are welcome! 26 | 27 | If you run into an issue, the first step is to report a problem or to suggest a new feature, [open a GitHub Issue](https://github.com/protofire/eth-cli/issues/new). 28 | This will help the eth-cli maintainers become aware of the problem and prioritize a fix. 29 | 30 | For code contributions, for either new features or bug fixes, see [Development](#development). 31 | 32 | If you're looking to make a substantial change, you may want to reach out first to give us a heads up. 33 | 34 | ## Development 35 | 36 | ### Overview 37 | 38 | This repository ([protofire/eth-cli](https://github.com/protofire/eth-cli)) is a collection of CLI tools to help with ethereum learning and development. 39 | 40 | ### Development Requirements 41 | 42 | In order to develop eth-cli, you'll need: 43 | 44 | - [Git](https://git-scm.com/) 45 | - [Node.js](https://nodejs.org) 46 | 47 | ### Getting Started 48 | 49 | First clone this repository and install NPM dependencies: 50 | 51 | $ git clone git@github.com:protofire/eth-cli.git 52 | $ cd eth-cli 53 | $ npm install 54 | 55 | ### Forks, Branches, and Pull Requests 56 | 57 | Community contributions to eth-cli require that you first fork the 58 | repository you are modifying. After your modifications, push changes to your fork and submit a pull request upstream to `eth-cli`'s fork(s). 59 | 60 | See GitHub documentation about [Collaborating with issues and pull requests](https://help.github.com/categories/collaborating-with-issues-and-pull-requests/) for more information. 61 | 62 | > :exclamation: **Note:** _eth-cli development uses a long-lived `master` branch for new (non-hotfix) 63 | > development. Pull Requests should be opened against `master` in all 64 | > repositories._ 65 | 66 | #### Branching Model 67 | 68 | eth-cli project maintains one stable branch: 69 | 70 | - **`master`**, for latest full releases and work targeting a patch release 71 | 72 | #### Working on a Branch 73 | 74 | Use a branch for your modifications, tracking it on your fork: 75 | 76 | $ git checkout -b feature/sweet-feature 77 | $ git push --set-upstream origin feature/sweet-feature 78 | 79 | Then, make changes and commit as usual. 80 | 81 | ## Additional Notes 82 | 83 | Some things that will increase the chance that your pull request is accepted: 84 | 85 | - Write tests. 86 | - Write a [good commit message][commit]. 87 | 88 | [commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 89 | 90 | **Thanks again for all your support, encouragement, and effort! eth-cli would not 91 | be possible without contributors like you. :bow:** 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ProtoFire 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eth-cli 2 | 3 | A CLI swiss army knife for Ethereum developers 4 | 5 | [![Donate with Ethereum](https://en.cryptobadges.io/badge/micro/0xe8cdf02efd8ab0a490d7b2cb13553389c9bc932e)](https://en.cryptobadges.io/donate/0xe8cdf02efd8ab0a490d7b2cb13553389c9bc932e) 6 | 7 | [![Build Status](https://travis-ci.org/protofire/eth-cli.svg?branch=master)](https://travis-ci.org/protofire/eth-cli) 8 | [![NPM version](https://badge.fury.io/js/eth-cli.svg)](https://npmjs.org/package/eth-cli) 9 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/protofire/eth-cli/master/LICENSE) 10 | [![dependencies Status](https://david-dm.org/protofire/eth-cli/status.svg)](https://david-dm.org/protofire/eth-cli) 11 | [![devDependencies Status](https://david-dm.org/protofire/eth-cli/dev-status.svg)](https://david-dm.org/protofire/eth-cli?type=dev) 12 | 13 | ## Why use it? 14 | 15 | `eth-cli` allows you to fetch data from the blockchain, start an interactive REPL connected to some node, call methods on deployed contracts, and more, all at the comfort of your command line. Checkout the [examples](#examples) below for more information or check the [full list of commands](docs/COMMANDS.md). 16 | 17 | 18 | 19 | ## Table of Contents 20 | 21 | - [Installation](#installation) 22 | - [Demo](#demo) 23 | - [Examples](#examples) 24 | - [Fetch data from the blockchain](#fetch-data-from-the-blockchain) 25 | - [Start an interactive REPL connected to some node](#start-an-interactive-repl-connected-to-some-node) 26 | - [Call methods on deployed contracts](#call-methods-on-deployed-contracts) 27 | - [Autocomplete](#autocomplete) 28 | - [Init file](#init-file) 29 | - [Sibling projects](#sibling-projects) 30 | - [Back us](#back-us) 31 | - [Credits](#credits) 32 | 33 | 34 | 35 | ## Installation 36 | 37 | Install it globally: 38 | 39 | ```shell 40 | npm install -g eth-cli 41 | ``` 42 | 43 | You can also try it with `npx`: 44 | 45 | ``` 46 | $ npx eth-cli repl --mainnet erc721@0x06012c8cf97bead5deae237070f9587f8e7a266d 47 | > erc721.methods.name().call() 48 | 'CryptoKitties' 49 | ``` 50 | 51 | ## Demo 52 | 53 | Check [this screencast](https://www.youtube.com/watch?v=7tEUtg9DKTo) to see it in action. 54 | 55 | ## Examples 56 | 57 | There are a lot of things that you can do with `eth-cli`, and we keep adding more. These are some of our favorites: 58 | 59 | ### Fetch data from the blockchain 60 | 61 | Use commands like `block:number`, `tx:get` and `address:balance` to get information from the blockchain. 62 | 63 | ![Fetch data from the blockchain](img/fetching-data.gif) 64 | 65 | 66 | [more examples](/example.md) 67 | 68 | ## Autocomplete 69 | 70 | `eth-cli` supports some basic autocompletion, generated with [`completely`](https://github.com/fvictorio/completely). 71 | 72 | The [completion](completion) directory has a bash completion script (`eth-completion.bash`) and a zsh completion script 73 | (`_eth`). If you use bash, download the script and source it in your bashrc. If you use zsh, download the script and put 74 | it in some directory in your [`fpath`](https://unix.stackexchange.com/questions/33255/how-to-define-and-load-your-own-shell-function-in-zsh). 75 | 76 | ## Init file 77 | 78 | If you want to have some helper variables or functions in your REPL, you can create an init file that will be loaded 79 | every time you use `eth repl`. Just create a file called `.eth_cli_repl_init.js` in your home directory. For example, if 80 | you create it with some content like: 81 | 82 | ```js 83 | module.exports = function(context) { 84 | context.toWei = x => context.web3.utils.toWei(x.toString()) 85 | context.fromWei = x => context.web3.utils.fromWei(x.toString()) 86 | } 87 | ``` 88 | 89 | you will have `toWei` and `fromWei` as global functions in the REPL. 90 | 91 | 92 | ## Sibling projects 93 | 94 | - [Solhint](https://github.com/protofire/solhint): A linter for the Solidity language. 95 | 96 | ## Back us 97 | 98 | eth-cli is free to use and open-sourced. If you value our effort and feel like helping us to keep pushing this tool forward, you can send us a small donation. We'll highly appreciate it :) 99 | 100 | [![Donate with Ethereum](https://en.cryptobadges.io/badge/micro/0xe8cdf02efd8ab0a490d7b2cb13553389c9bc932e)](https://en.cryptobadges.io/donate/0xe8cdf02efd8ab0a490d7b2cb13553389c9bc932e) 101 | 102 | ## Credits 103 | 104 | Table of Contents *generated with [DocToc](https://github.com/thlorenz/doctoc)* 105 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('@oclif/command').run() 4 | .then(require('@oclif/command/flush')) 5 | .catch(require('@oclif/errors/handle')) 6 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /completion/_eth: -------------------------------------------------------------------------------- 1 | #compdef eth 2 | 3 | function _eth { 4 | local _line 5 | 6 | _arguments -C \ 7 | "1: :(abi:add abi:events abi:list abi:methods abi:remove abi:show abi:update address:add address:balance address:list address:random address:remove address:show block:get block:number contract:address contract:call convert event:get event:watch method:decode method:encode method:hash method:search network:add network:list network:remove network:update repl transaction:get transaction:nop)" \ 8 | "*::arg:->args" 9 | 10 | case $line[1] in 11 | abi:add) 12 | __eth_abi:add 13 | ;; 14 | abi:events) 15 | __eth_abi:events 16 | ;; 17 | abi:list) 18 | __eth_abi:list 19 | ;; 20 | abi:methods) 21 | __eth_abi:methods 22 | ;; 23 | abi:remove) 24 | __eth_abi:remove 25 | ;; 26 | abi:show) 27 | __eth_abi:show 28 | ;; 29 | abi:update) 30 | __eth_abi:update 31 | ;; 32 | address:add) 33 | __eth_address:add 34 | ;; 35 | address:balance) 36 | __eth_address:balance 37 | ;; 38 | address:list) 39 | __eth_address:list 40 | ;; 41 | address:random) 42 | __eth_address:random 43 | ;; 44 | address:remove) 45 | __eth_address:remove 46 | ;; 47 | address:show) 48 | __eth_address:show 49 | ;; 50 | block:get) 51 | __eth_block:get 52 | ;; 53 | block:number) 54 | __eth_block:number 55 | ;; 56 | contract:address) 57 | __eth_contract:address 58 | ;; 59 | contract:call) 60 | __eth_contract:call 61 | ;; 62 | convert) 63 | __eth_convert 64 | ;; 65 | event:get) 66 | __eth_event:get 67 | ;; 68 | event:watch) 69 | __eth_event:watch 70 | ;; 71 | method:decode) 72 | __eth_method:decode 73 | ;; 74 | method:encode) 75 | __eth_method:encode 76 | ;; 77 | method:hash) 78 | __eth_method:hash 79 | ;; 80 | method:search) 81 | __eth_method:search 82 | ;; 83 | network:add) 84 | __eth_network:add 85 | ;; 86 | network:list) 87 | __eth_network:list 88 | ;; 89 | network:remove) 90 | __eth_network:remove 91 | ;; 92 | network:update) 93 | __eth_network:update 94 | ;; 95 | repl) 96 | __eth_repl 97 | ;; 98 | transaction:get) 99 | __eth_transaction:get 100 | ;; 101 | transaction:nop) 102 | __eth_transaction:nop 103 | ;; 104 | esac 105 | } 106 | 107 | function __eth_abi:add { 108 | 109 | } 110 | function __eth_abi:events { 111 | 112 | } 113 | function __eth_abi:list { 114 | 115 | } 116 | function __eth_abi:methods { 117 | 118 | } 119 | function __eth_abi:remove { 120 | 121 | } 122 | function __eth_abi:show { 123 | 124 | } 125 | function __eth_abi:update { 126 | 127 | } 128 | function __eth_address:add { 129 | 130 | } 131 | function __eth_address:balance { 132 | _arguments \ 133 | "--network: :_files" \ 134 | "--mainnet" \ 135 | "--rinkeby" \ 136 | "--ropsten" \ 137 | "--kovan" \ 138 | 139 | } 140 | function __eth_address:list { 141 | 142 | } 143 | function __eth_address:random { 144 | _arguments \ 145 | "--prefix: :_files" \ 146 | "--password" \ 147 | 148 | } 149 | function __eth_address:remove { 150 | 151 | } 152 | function __eth_address:show { 153 | 154 | } 155 | function __eth_block:get { 156 | _arguments \ 157 | "--network: :_files" \ 158 | "--pk: :_files" \ 159 | "--mainnet" \ 160 | "--rinkeby" \ 161 | "--ropsten" \ 162 | "--kovan" \ 163 | 164 | } 165 | function __eth_block:number { 166 | _arguments \ 167 | "--network: :_files" \ 168 | "--pk: :_files" \ 169 | "--mainnet" \ 170 | "--rinkeby" \ 171 | "--ropsten" \ 172 | "--kovan" \ 173 | 174 | } 175 | function __eth_contract:address { 176 | 177 | } 178 | function __eth_contract:call { 179 | _arguments \ 180 | "--network: :_files" \ 181 | "--pk: :_files" \ 182 | "--mainnet" \ 183 | "--rinkeby" \ 184 | "--ropsten" \ 185 | "--kovan" \ 186 | 187 | } 188 | function __eth_convert { 189 | _arguments \ 190 | "--from: :(wei gwei eth)" \ 191 | "--to: :(wei gwei eth)" \ 192 | 193 | } 194 | function __eth_event:get { 195 | _arguments \ 196 | "--network: :_files" \ 197 | "--pk: :_files" \ 198 | "--from: :_files" \ 199 | "--to: :_files" \ 200 | "--mainnet" \ 201 | "--rinkeby" \ 202 | "--ropsten" \ 203 | "--kovan" \ 204 | "--json" \ 205 | 206 | } 207 | function __eth_event:watch { 208 | _arguments \ 209 | "--network: :_files" \ 210 | "--pk: :_files" \ 211 | "--mainnet" \ 212 | "--rinkeby" \ 213 | "--ropsten" \ 214 | "--kovan" \ 215 | "--json" \ 216 | 217 | } 218 | function __eth_method:decode { 219 | 220 | } 221 | function __eth_method:encode { 222 | 223 | } 224 | function __eth_method:hash { 225 | 226 | } 227 | function __eth_method:search { 228 | 229 | } 230 | function __eth_network:add { 231 | 232 | } 233 | function __eth_network:list { 234 | 235 | } 236 | function __eth_network:remove { 237 | 238 | } 239 | function __eth_network:update { 240 | 241 | } 242 | function __eth_repl { 243 | _arguments \ 244 | "--network: :_files" \ 245 | "--pk: :_files" \ 246 | "--mainnet" \ 247 | "--rinkeby" \ 248 | "--ropsten" \ 249 | "--kovan" \ 250 | 251 | } 252 | function __eth_transaction:get { 253 | _arguments \ 254 | "--network: :_files" \ 255 | "--pk: :_files" \ 256 | "--mainnet" \ 257 | "--rinkeby" \ 258 | "--ropsten" \ 259 | "--kovan" \ 260 | 261 | } 262 | function __eth_transaction:nop { 263 | _arguments \ 264 | "--network: :_files" \ 265 | "--pk: :_files" \ 266 | "--mainnet" \ 267 | "--rinkeby" \ 268 | "--ropsten" \ 269 | "--kovan" \ 270 | 271 | } 272 | 273 | _eth 274 | 275 | -------------------------------------------------------------------------------- /completion/completion.json: -------------------------------------------------------------------------------- 1 | { 2 | "command": "eth", 3 | "subcommands": [ 4 | { 5 | "command": "abi:add", 6 | "args": [ 7 | { 8 | "name": "name", 9 | "completion": { 10 | "type": "any" 11 | } 12 | }, 13 | { 14 | "name": "abi", 15 | "completion": { 16 | "type": "files" 17 | } 18 | } 19 | ], 20 | "flags": [] 21 | }, 22 | { 23 | "command": "abi:events", 24 | "args": [ 25 | { 26 | "name": "name", 27 | "completion": { 28 | "type": "command", 29 | "command": "eth abi:list" 30 | } 31 | } 32 | ], 33 | "flags": [] 34 | }, 35 | { 36 | "command": "abi:list", 37 | "flags": [], 38 | "args": [] 39 | }, 40 | { 41 | "command": "abi:methods", 42 | "args": [ 43 | { 44 | "name": "name", 45 | "completion": { 46 | "type": "command", 47 | "command": "eth abi:list" 48 | } 49 | } 50 | ], 51 | "flags": [] 52 | }, 53 | { 54 | "command": "abi:remove", 55 | "args": [ 56 | { 57 | "name": "name", 58 | "completion": { 59 | "type": "command", 60 | "command": "eth abi:list" 61 | } 62 | } 63 | ], 64 | "flags": [] 65 | }, 66 | { 67 | "command": "abi:show", 68 | "args": [ 69 | { 70 | "name": "name", 71 | "completion": { 72 | "type": "command", 73 | "command": "eth abi:list" 74 | } 75 | } 76 | ], 77 | "flags": [] 78 | }, 79 | { 80 | "command": "abi:update", 81 | "args": [ 82 | { 83 | "name": "name", 84 | "completion": { 85 | "type": "command", 86 | "command": "eth abi:list" 87 | } 88 | }, 89 | { 90 | "name": "abi", 91 | "completion": { 92 | "type": "files" 93 | } 94 | } 95 | ], 96 | "flags": [] 97 | }, 98 | { 99 | "command": "address:add", 100 | "flags": [], 101 | "args": [] 102 | }, 103 | { 104 | "command": "address:balance", 105 | "args": [ 106 | { 107 | "name": "name", 108 | "completion": { 109 | "type": "command", 110 | "command": "eth address:list --json | jq -r 'keys[]'" 111 | } 112 | } 113 | ], 114 | "flags": [ 115 | { 116 | "name": "mainnet", 117 | "type": "boolean" 118 | }, 119 | { 120 | "name": "rinkeby", 121 | "type": "boolean" 122 | }, 123 | { 124 | "name": "ropsten", 125 | "type": "boolean" 126 | }, 127 | { 128 | "name": "kovan", 129 | "type": "boolean" 130 | }, 131 | { 132 | "name": "network", 133 | "char": "n", 134 | "type": "string", 135 | "completion": { 136 | "type": "command", 137 | "command": "eth networks --json | jq -r 'keys[]'" 138 | } 139 | } 140 | ] 141 | }, 142 | { 143 | "command": "address:list", 144 | "flags": [], 145 | "args": [] 146 | }, 147 | { 148 | "command": "address:random", 149 | "flags": [ 150 | { 151 | "name": "password", 152 | "type": "boolean" 153 | }, 154 | { 155 | "name": "prefix", 156 | "type": "string", 157 | "completion": { 158 | "type": "any" 159 | } 160 | } 161 | ], 162 | "args": [] 163 | }, 164 | { 165 | "command": "address:remove", 166 | "args": [ 167 | { 168 | "name": "name", 169 | "completion": { 170 | "type": "command", 171 | "command": "eth address:list --json | jq -r 'keys[]'", 172 | "requiredCommands": [ 173 | "eth", 174 | "jq" 175 | ] 176 | } 177 | } 178 | ], 179 | "flags": [] 180 | }, 181 | { 182 | "command": "address:show", 183 | "flags": [], 184 | "args": [] 185 | }, 186 | { 187 | "command": "block:get", 188 | "flags": [ 189 | { 190 | "name": "mainnet", 191 | "type": "boolean" 192 | }, 193 | { 194 | "name": "rinkeby", 195 | "type": "boolean" 196 | }, 197 | { 198 | "name": "ropsten", 199 | "type": "boolean" 200 | }, 201 | { 202 | "name": "kovan", 203 | "type": "boolean" 204 | }, 205 | { 206 | "name": "network", 207 | "type": "string", 208 | "completion": { 209 | "type": "any" 210 | } 211 | }, 212 | { 213 | "name": "pk", 214 | "type": "string", 215 | "completion": { 216 | "type": "any" 217 | } 218 | } 219 | ], 220 | "args": [] 221 | }, 222 | { 223 | "command": "block:number", 224 | "flags": [ 225 | { 226 | "name": "mainnet", 227 | "type": "boolean" 228 | }, 229 | { 230 | "name": "rinkeby", 231 | "type": "boolean" 232 | }, 233 | { 234 | "name": "ropsten", 235 | "type": "boolean" 236 | }, 237 | { 238 | "name": "kovan", 239 | "type": "boolean" 240 | }, 241 | { 242 | "name": "network", 243 | "type": "string", 244 | "completion": { 245 | "type": "any" 246 | } 247 | }, 248 | { 249 | "name": "pk", 250 | "type": "string", 251 | "completion": { 252 | "type": "any" 253 | } 254 | } 255 | ], 256 | "args": [] 257 | }, 258 | { 259 | "command": "contract:address", 260 | "flags": [], 261 | "args": [] 262 | }, 263 | { 264 | "command": "contract:call", 265 | "flags": [ 266 | { 267 | "name": "mainnet", 268 | "type": "boolean" 269 | }, 270 | { 271 | "name": "rinkeby", 272 | "type": "boolean" 273 | }, 274 | { 275 | "name": "ropsten", 276 | "type": "boolean" 277 | }, 278 | { 279 | "name": "kovan", 280 | "type": "boolean" 281 | }, 282 | { 283 | "name": "network", 284 | "type": "string", 285 | "completion": { 286 | "type": "any" 287 | } 288 | }, 289 | { 290 | "name": "pk", 291 | "type": "string", 292 | "completion": { 293 | "type": "any" 294 | } 295 | } 296 | ], 297 | "args": [] 298 | }, 299 | { 300 | "command": "convert", 301 | "flags": [ 302 | { 303 | "name": "from", 304 | "type": "string", 305 | "completion": { 306 | "type": "oneOf", 307 | "values": [ 308 | "wei", 309 | "gwei", 310 | "eth" 311 | ] 312 | } 313 | }, 314 | { 315 | "name": "to", 316 | "type": "string", 317 | "completion": { 318 | "type": "oneOf", 319 | "values": [ 320 | "wei", 321 | "gwei", 322 | "eth" 323 | ] 324 | } 325 | } 326 | ], 327 | "args": [] 328 | }, 329 | { 330 | "command": "event:get", 331 | "flags": [ 332 | { 333 | "name": "mainnet", 334 | "type": "boolean" 335 | }, 336 | { 337 | "name": "rinkeby", 338 | "type": "boolean" 339 | }, 340 | { 341 | "name": "ropsten", 342 | "type": "boolean" 343 | }, 344 | { 345 | "name": "kovan", 346 | "type": "boolean" 347 | }, 348 | { 349 | "name": "network", 350 | "type": "string", 351 | "completion": { 352 | "type": "any" 353 | } 354 | }, 355 | { 356 | "name": "pk", 357 | "type": "string", 358 | "completion": { 359 | "type": "any" 360 | } 361 | }, 362 | { 363 | "name": "from", 364 | "type": "string", 365 | "completion": { 366 | "type": "any" 367 | } 368 | }, 369 | { 370 | "name": "to", 371 | "type": "string", 372 | "completion": { 373 | "type": "any" 374 | } 375 | }, 376 | { 377 | "name": "json", 378 | "type": "boolean" 379 | } 380 | ], 381 | "args": [] 382 | }, 383 | { 384 | "command": "event:watch", 385 | "flags": [ 386 | { 387 | "name": "mainnet", 388 | "type": "boolean" 389 | }, 390 | { 391 | "name": "rinkeby", 392 | "type": "boolean" 393 | }, 394 | { 395 | "name": "ropsten", 396 | "type": "boolean" 397 | }, 398 | { 399 | "name": "kovan", 400 | "type": "boolean" 401 | }, 402 | { 403 | "name": "network", 404 | "type": "string", 405 | "completion": { 406 | "type": "any" 407 | } 408 | }, 409 | { 410 | "name": "pk", 411 | "type": "string", 412 | "completion": { 413 | "type": "any" 414 | } 415 | }, 416 | { 417 | "name": "json", 418 | "type": "boolean" 419 | } 420 | ], 421 | "args": [] 422 | }, 423 | { 424 | "command": "method:decode", 425 | "flags": [], 426 | "args": [] 427 | }, 428 | { 429 | "command": "method:encode", 430 | "flags": [], 431 | "args": [] 432 | }, 433 | { 434 | "command": "method:hash", 435 | "flags": [], 436 | "args": [] 437 | }, 438 | { 439 | "command": "method:search", 440 | "flags": [], 441 | "args": [] 442 | }, 443 | { 444 | "command": "network:add", 445 | "flags": [], 446 | "args": [] 447 | }, 448 | { 449 | "command": "network:list", 450 | "flags": [], 451 | "args": [] 452 | }, 453 | { 454 | "command": "network:remove", 455 | "flags": [], 456 | "args": [] 457 | }, 458 | { 459 | "command": "network:update", 460 | "flags": [], 461 | "args": [] 462 | }, 463 | { 464 | "command": "repl", 465 | "flags": [ 466 | { 467 | "name": "mainnet", 468 | "type": "boolean" 469 | }, 470 | { 471 | "name": "rinkeby", 472 | "type": "boolean" 473 | }, 474 | { 475 | "name": "ropsten", 476 | "type": "boolean" 477 | }, 478 | { 479 | "name": "kovan", 480 | "type": "boolean" 481 | }, 482 | { 483 | "name": "network", 484 | "type": "string", 485 | "completion": { 486 | "type": "any" 487 | } 488 | }, 489 | { 490 | "name": "pk", 491 | "type": "string", 492 | "completion": { 493 | "type": "any" 494 | } 495 | } 496 | ], 497 | "args": [] 498 | }, 499 | { 500 | "command": "transaction:get", 501 | "flags": [ 502 | { 503 | "name": "mainnet", 504 | "type": "boolean" 505 | }, 506 | { 507 | "name": "rinkeby", 508 | "type": "boolean" 509 | }, 510 | { 511 | "name": "ropsten", 512 | "type": "boolean" 513 | }, 514 | { 515 | "name": "kovan", 516 | "type": "boolean" 517 | }, 518 | { 519 | "name": "network", 520 | "type": "string", 521 | "completion": { 522 | "type": "any" 523 | } 524 | }, 525 | { 526 | "name": "pk", 527 | "type": "string", 528 | "completion": { 529 | "type": "any" 530 | } 531 | } 532 | ], 533 | "args": [] 534 | }, 535 | { 536 | "command": "transaction:nop", 537 | "flags": [ 538 | { 539 | "name": "mainnet", 540 | "type": "boolean" 541 | }, 542 | { 543 | "name": "rinkeby", 544 | "type": "boolean" 545 | }, 546 | { 547 | "name": "ropsten", 548 | "type": "boolean" 549 | }, 550 | { 551 | "name": "kovan", 552 | "type": "boolean" 553 | }, 554 | { 555 | "name": "network", 556 | "type": "string", 557 | "completion": { 558 | "type": "any" 559 | } 560 | }, 561 | { 562 | "name": "pk", 563 | "type": "string", 564 | "completion": { 565 | "type": "any" 566 | } 567 | } 568 | ], 569 | "args": [] 570 | } 571 | ] 572 | } 573 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.3.0](https://github.com/protofire/eth-cli/compare/v1.2.0...v1.3.0) (2019-08-01) 2 | 3 | 4 | ### Features 5 | 6 | * **method:** add method:search command ([df7ec98](https://github.com/protofire/eth-cli/commit/df7ec98)) 7 | -------------------------------------------------------------------------------- /docs/COMMANDS.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This is the full list of commands supported by `eth-cli`. 4 | 5 | - [`abi`](#abi) 6 | - [`abi:add`](#abiadd) 7 | - [`abi:events`](#abievents) 8 | - [`abi:list`](#abilist) 9 | - [`abi:methods`](#abimethods) 10 | - [`abi:remove`](#abiremove) 11 | - [`abi:show`](#abishow) 12 | - [`abi:update`](#abiupdate) 13 | - [`address`](#address) 14 | - [`address:add`](#addressadd) 15 | - [`address:balance`](#addressbalance) 16 | - [`address:list`](#addresslist) 17 | - [`address:random`](#addressrandom) 18 | - [`address:remove`](#addressremove) 19 | - [`address:show`](#addressshow) 20 | - [`block`](#block) 21 | - [`block:get`](#blockget) 22 | - [`block:number`](#blocknumber) 23 | - [`contract`](#contract) 24 | - [`contract:address`](#contractaddress) 25 | - [`contract:call`](#contractcall) 26 | - [`contract:deploy`](#contractdeploy) 27 | - [`contract:send`](#contractsend) 28 | - [`convert`](#convert) 29 | - [`event`](#event) 30 | - [`event:get`](#eventget) 31 | - [`event:watch`](#eventwatch) 32 | - [`method`](#method) 33 | - [`method:decode`](#methoddecode) 34 | - [`method:encode`](#methodencode) 35 | - [`method:hash`](#methodhash) 36 | - [`method:search`](#methodsearch) 37 | - [`network`](#network) 38 | - [`network:add`](#networkadd) 39 | - [`network:list`](#networklist) 40 | - [`network:remove`](#networkremove) 41 | - [`network:update`](#networkupdate) 42 | - [`repl`](#repl) 43 | - [`transaction`](#transaction) 44 | - [`transaction:get`](#transactionget) 45 | - [`transaction:nop`](#transactionnop) 46 | - [`transaction:send`](#transactionsend) 47 | 48 | 49 | 50 | 51 | 52 | ## `abi` 53 | 54 | Manage known ABIs and obtain their methods and events. 55 | 56 | ### `abi:add` 57 | 58 | Add a known ABI. 59 | 60 | Examples: 61 | - `eth abi:add erc777 ./path/to/erc777.json` 62 | 63 | ### `abi:events` 64 | 65 | Show the list of events in the given ABI. 66 | 67 | Examples: 68 | - `eth abi:events erc20` 69 | 70 | ### `abi:list` 71 | 72 | Display the list of known ABIs. 73 | 74 | Examples: 75 | - `eth abi:list` 76 | 77 | ### `abi:methods` 78 | 79 | Show the list of methods in the given ABI. 80 | 81 | Examples: 82 | - `eth abi:methods ../contracts/proxy.abi` 83 | - `eth abi:methods erc20` 84 | 85 | ### `abi:remove` 86 | 87 | Remove a known ABI. 88 | 89 | Examples: 90 | - `eth abi:rm erc777` 91 | 92 | ### `abi:show` 93 | 94 | Display a known ABI 95 | 96 | Examples: 97 | - `eth abi:show ERC20` 98 | - `eth abi:show ERC721` 99 | 100 | ### `abi:update` 101 | 102 | Update a known ABI. 103 | 104 | Examples: 105 | - `eth abi:update erc777 ./path/to/erc777.json` 106 | 107 | ## `address` 108 | 109 | Manage known addresses, generate random accounts, get balances. 110 | 111 | ### `address:add` 112 | 113 | Add a known address. 114 | 115 | Examples: 116 | - `eth address:add ganache 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d` 117 | - `eth address:add dai 0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 -n 1` 118 | 119 | ### `address:balance` 120 | 121 | Get the balance of the given address. 122 | 123 | Examples: 124 | - `eth address:balance 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1` 125 | 126 | ### `address:list` 127 | 128 | Display the list of known addresses. 129 | 130 | Examples: 131 | - `eth address:list` 132 | 133 | ### `address:random` 134 | 135 | Generate a random Ethereum address with its private key. 136 | 137 | Examples: 138 | - `eth address:random` 139 | - `eth address:random 3` 140 | - `eth address:random --prefix aa` 141 | 142 | ### `address:remove` 143 | 144 | Remove a known address. 145 | 146 | Examples: 147 | - `eth address:rm ganache` 148 | 149 | ### `address:show` 150 | 151 | Display a known address. 152 | 153 | Examples: 154 | - `eth address:get ganache` 155 | 156 | ## `block` 157 | 158 | Get the latest block number of a network or fetch a specific block. 159 | 160 | ### `block:get` 161 | 162 | Get the block object for a given block number. 163 | 164 | Examples: 165 | - `eth block:get --mainnet 12345` 166 | 167 | ### `block:number` 168 | 169 | Get the block number of the chosen network. 170 | 171 | Examples: 172 | - `eth block:number --rinkeby` 173 | - `eth block:number --network 'https://dai.poa.network'` 174 | 175 | ## `contract` 176 | 177 | Deploy contracts or predict their addresses. 178 | 179 | ### `contract:address` 180 | 181 | Get the address for a contract created from the given account. 182 | 183 | Examples: 184 | - `eth contract:address 0x92970dbD5C0Ee6b439422bFd7cD71e1DDA921A03` 185 | - `eth contract:address 0x92970dbD5C0Ee6b439422bFd7cD71e1DDA921A03 --nonce 5` 186 | 187 | ### `contract:call` 188 | 189 | Call a method in the given contract and print the returned value. 190 | 191 | Examples: 192 | - `eth contract:call --rinkeby erc20@0x5592ec0cfb4dbc12d3ab100b257153436a1f0fea 'totalSupply()'` 193 | 194 | ### `contract:deploy` 195 | 196 | Deploy contract with the given binary. 197 | 198 | Examples: 199 | - `eth contract:deploy --ropsten --pk 3daa79a26454a5528a3523f9e6345efdbd636e63f8c24a835204e6ccb5c88f9e ./contracts/proxy.bin` 200 | - `eth contract:deploy --pk knownPK --abi erc20 --args ["MYTKN", 18] ./contracts/erc20.bin` 201 | 202 | ### `contract:send` 203 | 204 | Send a transaction calling a method in the given contract. 205 | 206 | Examples: 207 | - `eth contract:send --rinkeby erc20@0x5592ec0cfb4dbc12d3ab100b257153436a1f0fea 'transfer("0x828DaF877f46fdFB5F1239cd9cB8f0D6E1adfb80", 1000000000)'` 208 | 209 | ## `convert` 210 | 211 | Convert from eth to wei, wei to eth, etc. 212 | 213 | Examples: 214 | - `eth convert 1000000000000000000` 215 | - `eth convert -f eth -t wei 1` 216 | - `echo 1000000000000000000 | eth convert` 217 | 218 | ## `event` 219 | 220 | Get past events and watch for new ones. 221 | 222 | ### `event:get` 223 | 224 | Get the events in the given block range. 225 | 226 | Examples: 227 | - `eth event:get --mainnet erc20@0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 Transfer --from 1` 228 | - `eth event:get --mainnet erc20@0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 Transfer --from 1 --json` 229 | 230 | ### `event:watch` 231 | 232 | Emit new events from the given type in the given contract. 233 | 234 | Examples: 235 | - `eth event:get --mainnet erc20@0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 Transfer` 236 | - `eth event:get --mainnet erc20@0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 Transfer --json` 237 | 238 | ## `method` 239 | 240 | Encode and decode methods, search by signature, etc. 241 | 242 | ### `method:decode` 243 | 244 | Decode the arguments of the given transaction data for the given function signature. 245 | 246 | Examples: 247 | - `eth method:decode 'transfer(address,uint256)' '0xa9059cbb000000000000000000000000697dB915674bAc602F4d6fAfA31c0e45f386416E00000000000000000000000000000000000000000000000000000004ff043b9e'` 248 | 249 | ### `method:encode` 250 | 251 | Encode the ABI for the method and print the ABI byte code. 252 | 253 | Examples: 254 | - `eth method:encode --sokol ./test/files/contracts/Proxy.abi 'updateAppInstance()'` 255 | 256 | ### `method:hash` 257 | 258 | Get the hash of the given method. 259 | 260 | Examples: 261 | - `eth method:hash 'transfer(address,uint256)'` 262 | 263 | ### `method:search` 264 | 265 | Search the given hashed method signature using the 4byte.directory API 266 | 267 | Examples: 268 | - `eth method:search a9059cbb` 269 | 270 | ## `network` 271 | 272 | Manage known networks. 273 | 274 | ### `network:add` 275 | 276 | Add a known network. 277 | 278 | Examples: 279 | - `eth network:add rsk --url https://public-node.rsk.co --id 30 --label RSK` 280 | 281 | ### `network:list` 282 | 283 | Show information for each known network. 284 | 285 | Examples: 286 | - `eth network:list --display json` 287 | - `eth networks` 288 | 289 | ### `network:remove` 290 | 291 | Remove a known network. 292 | 293 | Examples: 294 | - `eth network:remove rsk` 295 | 296 | ### `network:update` 297 | 298 | Update a known network. 299 | 300 | Examples: 301 | - `eth network:update rsk --id 30` 302 | 303 | ## `repl` 304 | 305 | Start a REPL that connects to an RPC node ('localhost:8545' by default). 306 | 307 | The started REPL exposes a `web3` object that you can use to interact with the 308 | node. There's also an `eth` object to save you from typing `web3.eth`. 309 | 310 | You can also indicate some contracts to load in the REPL; see the examples to 311 | learn how to do this. 312 | 313 | Examples: 314 | - `eth repl` 315 | - `eth repl --mainnet` 316 | - `eth repl --url=http://localhost:7545` 317 | - `eth repl ./abis/myContract.json@0xaD2FA57bd95A3dfF0e1728686997F6f2fE67F6f9` 318 | - `eth repl erc20@0x34ee482D419229dAad23f27C44B82075B9418D31 erc721@0xcba140186Fa0436e5155fF6DC909F22Ec4648b12` 319 | 320 | ## `transaction` 321 | 322 | Get information about mined transactions or create empty transaction. 323 | 324 | ### `transaction:get` 325 | 326 | Print the transaction object for the given transaction hash. 327 | 328 | Examples: 329 | - `eth transaction:get --mainnet 0xc83836f1b3acac94a31de8e24c913aceaa9ebc51c93cd374429590596091584a` 330 | - `eth transaction:get --ropsten 0xc83836f1b3acac94a31de8e24c913aceaa9ebc51c93cd374429590596091584a` 331 | - `eth transaction:get --url= http://localhost:8545 0xc83836f1b3acac94a31de8e24c913aceaa9ebc51c93cd374429590596091584a` 332 | 333 | ### `transaction:nop` 334 | 335 | Generates a transaction that does nothing with the given private key. 336 | 337 | Examples: 338 | - `eth transaction:nop --pk 3daa79a26454a5528a3523f9e6345efdbd636e63f8c24a835204e6ccb5c88f9e` 339 | - `ETH_CLI_PRIVATE_KEY=3daa79a26454a5528a3523f9e6345efdbd636e63f8c24a835204e6ccb5c88f9e eth transaction:nop` 340 | 341 | ### `transaction:send` 342 | 343 | Send a raw transaction 344 | 345 | Examples: 346 | - `eth transaction:send --pk 3daa79a26454a5528a3523f9e6345efdbd636e63f8c24a835204e6ccb5c88f9e --to 0x828DaF877f46fdFB5F1239cd9cB8f0D6E1adfb80 --value 1000000000000000000` 347 | 348 | -------------------------------------------------------------------------------- /e2e/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | WORKDIR /eth-cli 4 | 5 | COPY package.json . 6 | COPY yarn.lock . 7 | RUN yarn 8 | 9 | COPY src src 10 | COPY bin bin 11 | COPY README.md .prettierrc.json tsconfig.json .eslintrc.json ./ 12 | 13 | RUN yarn prepack 14 | 15 | COPY e2e e2e 16 | 17 | CMD node e2e/run.js 18 | -------------------------------------------------------------------------------- /e2e/abi.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const shell = require('shelljs') 3 | 4 | const binPath = path.join(__dirname, '..', 'bin', 'run') 5 | const eth = args => shell.exec(`${binPath} ${args}`, { silent: true }) 6 | 7 | const erc20Abi = require('./abis/ERC20.json') 8 | const erc721Abi = require('./abis/ERC721.json') 9 | 10 | describe('abi', () => { 11 | describe('abi:list', () => { 12 | it('should return the list of supported tokens', async () => { 13 | const result = eth(`abi:list`) 14 | expect(result.code).toEqual(0) 15 | expect(result.stdout.trim()).toEqual('erc20\nerc721') 16 | }) 17 | }) 18 | describe('abi:methods', () => { 19 | it('shows the methods of a given abi', () => { 20 | const result = eth(`abi:methods erc20`) 21 | expect(result.code).toEqual(0) 22 | expect(result.stdout.split('\n')).toHaveLength(10) 23 | }) 24 | }) 25 | 26 | describe('abi:show', () => { 27 | it('should return the ERC20 abi', () => { 28 | const result = eth(`abi:show erc20`) 29 | expect(result.code).toEqual(0) 30 | 31 | const resultExpected = JSON.parse(result.stdout) 32 | expect(resultExpected).toMatchObject(erc20Abi) 33 | }) 34 | it('should return the ERC721 abi', () => { 35 | const result = eth(`abi:show erc721`) 36 | 37 | expect(result.code).toEqual(0) 38 | 39 | const resultExpected = JSON.parse(result.stdout) 40 | expect(resultExpected).toMatchObject(erc721Abi) 41 | }) 42 | it('errors when an unknown abi is used', () => { 43 | const result = eth(`abi:show foobar`) 44 | 45 | expect(result.code).not.toEqual(0) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /e2e/abis/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /e2e/abis/ERC721.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [{ "name": "_name", "type": "string" }], 7 | "payable": false, 8 | "stateMutability": "view", 9 | "type": "function" 10 | }, 11 | { 12 | "constant": true, 13 | "inputs": [{ "name": "_tokenId", "type": "uint256" }], 14 | "name": "getApproved", 15 | "outputs": [{ "name": "_approved", "type": "address" }], 16 | "payable": false, 17 | "stateMutability": "view", 18 | "type": "function" 19 | }, 20 | { 21 | "constant": false, 22 | "inputs": [{ "name": "_to", "type": "address" }, { "name": "_tokenId", "type": "uint256" }], 23 | "name": "approve", 24 | "outputs": [], 25 | "payable": false, 26 | "stateMutability": "nonpayable", 27 | "type": "function" 28 | }, 29 | { 30 | "constant": true, 31 | "inputs": [], 32 | "name": "implementsERC721", 33 | "outputs": [{ "name": "_implementsERC721", "type": "bool" }], 34 | "payable": false, 35 | "stateMutability": "view", 36 | "type": "function" 37 | }, 38 | { 39 | "constant": true, 40 | "inputs": [], 41 | "name": "totalSupply", 42 | "outputs": [{ "name": "_totalSupply", "type": "uint256" }], 43 | "payable": false, 44 | "stateMutability": "view", 45 | "type": "function" 46 | }, 47 | { 48 | "constant": false, 49 | "inputs": [ 50 | { "name": "_from", "type": "address" }, 51 | { "name": "_to", "type": "address" }, 52 | { "name": "_tokenId", "type": "uint256" } 53 | ], 54 | "name": "transferFrom", 55 | "outputs": [], 56 | "payable": false, 57 | "stateMutability": "nonpayable", 58 | "type": "function" 59 | }, 60 | { 61 | "constant": true, 62 | "inputs": [{ "name": "_owner", "type": "address" }, { "name": "_index", "type": "uint256" }], 63 | "name": "tokenOfOwnerByIndex", 64 | "outputs": [{ "name": "_tokenId", "type": "uint256" }], 65 | "payable": false, 66 | "stateMutability": "view", 67 | "type": "function" 68 | }, 69 | { 70 | "constant": true, 71 | "inputs": [{ "name": "_tokenId", "type": "uint256" }], 72 | "name": "ownerOf", 73 | "outputs": [{ "name": "_owner", "type": "address" }], 74 | "payable": false, 75 | "stateMutability": "view", 76 | "type": "function" 77 | }, 78 | { 79 | "constant": true, 80 | "inputs": [{ "name": "_tokenId", "type": "uint256" }], 81 | "name": "tokenMetadata", 82 | "outputs": [{ "name": "_infoUrl", "type": "string" }], 83 | "payable": false, 84 | "stateMutability": "view", 85 | "type": "function" 86 | }, 87 | { 88 | "constant": true, 89 | "inputs": [{ "name": "_owner", "type": "address" }], 90 | "name": "balanceOf", 91 | "outputs": [{ "name": "_balance", "type": "uint256" }], 92 | "payable": false, 93 | "stateMutability": "view", 94 | "type": "function" 95 | }, 96 | { 97 | "constant": false, 98 | "inputs": [ 99 | { "name": "_owner", "type": "address" }, 100 | { "name": "_tokenId", "type": "uint256" }, 101 | { "name": "_approvedAddress", "type": "address" }, 102 | { "name": "_metadata", "type": "string" } 103 | ], 104 | "name": "mint", 105 | "outputs": [], 106 | "payable": false, 107 | "stateMutability": "nonpayable", 108 | "type": "function" 109 | }, 110 | { 111 | "constant": true, 112 | "inputs": [], 113 | "name": "symbol", 114 | "outputs": [{ "name": "_symbol", "type": "string" }], 115 | "payable": false, 116 | "stateMutability": "view", 117 | "type": "function" 118 | }, 119 | { 120 | "constant": false, 121 | "inputs": [{ "name": "_to", "type": "address" }, { "name": "_tokenId", "type": "uint256" }], 122 | "name": "transfer", 123 | "outputs": [], 124 | "payable": false, 125 | "stateMutability": "nonpayable", 126 | "type": "function" 127 | }, 128 | { 129 | "constant": true, 130 | "inputs": [], 131 | "name": "numTokensTotal", 132 | "outputs": [{ "name": "", "type": "uint256" }], 133 | "payable": false, 134 | "stateMutability": "view", 135 | "type": "function" 136 | }, 137 | { 138 | "constant": true, 139 | "inputs": [{ "name": "_owner", "type": "address" }], 140 | "name": "getOwnerTokens", 141 | "outputs": [{ "name": "_tokenIds", "type": "uint256[]" }], 142 | "payable": false, 143 | "stateMutability": "view", 144 | "type": "function" 145 | }, 146 | { 147 | "anonymous": false, 148 | "inputs": [ 149 | { "indexed": true, "name": "_to", "type": "address" }, 150 | { "indexed": true, "name": "_tokenId", "type": "uint256" } 151 | ], 152 | "name": "Mint", 153 | "type": "event" 154 | }, 155 | { 156 | "anonymous": false, 157 | "inputs": [ 158 | { "indexed": true, "name": "_from", "type": "address" }, 159 | { "indexed": true, "name": "_to", "type": "address" }, 160 | { "indexed": false, "name": "_tokenId", "type": "uint256" } 161 | ], 162 | "name": "Transfer", 163 | "type": "event" 164 | }, 165 | { 166 | "anonymous": false, 167 | "inputs": [ 168 | { "indexed": true, "name": "_owner", "type": "address" }, 169 | { "indexed": true, "name": "_approved", "type": "address" }, 170 | { "indexed": false, "name": "_tokenId", "type": "uint256" } 171 | ], 172 | "name": "Approval", 173 | "type": "event" 174 | } 175 | ] 176 | -------------------------------------------------------------------------------- /e2e/address.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const shell = require('shelljs') 3 | 4 | const binPath = path.join(__dirname, '..', 'bin', 'run') 5 | const eth = args => shell.exec(`${binPath} ${args}`, { silent: true }) 6 | 7 | describe('address', () => { 8 | describe('address:random', () => { 9 | it('should print a random address with its private key', async () => { 10 | const result = eth('address:random') 11 | 12 | expect(result.code).toEqual(0) 13 | 14 | const output = parse(result.stdout) 15 | 16 | expect(output.length).toEqual(1) 17 | expect(output[0]).toHaveProperty('address') 18 | expect(output[0]).toHaveProperty('privateKey') 19 | }) 20 | 21 | it('should print two random addresses with their private key', async () => { 22 | const result = eth('address:random 2') 23 | 24 | expect(result.code).toEqual(0) 25 | 26 | const output = parse(result.stdout) 27 | 28 | expect(output.length).toEqual(2) 29 | expect(output[0]).toHaveProperty('address') 30 | expect(output[0]).toHaveProperty('privateKey') 31 | expect(output[1]).toHaveProperty('address') 32 | expect(output[1]).toHaveProperty('privateKey') 33 | }) 34 | 35 | it('should print different addresseses with their private key', async () => { 36 | const result = eth('address:random 3') 37 | 38 | expect(result.code).toEqual(0) 39 | 40 | const output = parse(result.stdout) 41 | 42 | expect(output.length).toEqual(3) 43 | expect(output[0].address).not.toEqual(output[1].address) 44 | expect(output[0].address).not.toEqual(output[2].address) 45 | expect(output[1].address).not.toEqual(output[2].address) 46 | expect(output[0].privateKey).not.toEqual(output[1].privateKey) 47 | expect(output[0].privateKey).not.toEqual(output[2].privateKey) 48 | expect(output[1].privateKey).not.toEqual(output[2].privateKey) 49 | }) 50 | }) 51 | 52 | /** 53 | * Convert a stdout consisting in a series of objects to an array: 54 | * 55 | * { "a": 1} 56 | * { "b": 2 } 57 | * 58 | * becomes 59 | * 60 | * [{"a": 1}, {"b": 2}] 61 | */ 62 | function parse(output) { 63 | const outputReplaced = `[${output.replace(/}/g, '},')}]` 64 | const lastBrace = outputReplaced.lastIndexOf('},') 65 | const outputJSON = 66 | outputReplaced.slice(0, lastBrace) + '}' + outputReplaced.slice(lastBrace + 2) 67 | return JSON.parse(outputJSON) 68 | } 69 | }) 70 | -------------------------------------------------------------------------------- /e2e/block-and-nop.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const shell = require('shelljs') 3 | const { PRIVATE_KEYS, RPC } = require('./common') 4 | 5 | const binPath = path.join(__dirname, '..', 'bin', 'run') 6 | const eth = args => shell.exec(`${binPath} ${args}`, { silent: true }) 7 | 8 | describe('block:number and tx:nop', () => { 9 | it('should change the current block number', async () => { 10 | const resultBlockNumber1 = eth(`block:number -n ${RPC}`) 11 | expect(resultBlockNumber1.code).toEqual(0) 12 | const blockNumberBefore = +resultBlockNumber1.stdout.trim() 13 | 14 | const resultTxNop = eth(`tx:nop -n ${RPC} --pk ${PRIVATE_KEYS[0]}`) 15 | expect(resultTxNop.code).toEqual(0) 16 | 17 | const resultBlockNumber2 = eth(`block:number -n ${RPC}`) 18 | expect(resultBlockNumber2.code).toEqual(0) 19 | const blockNumberAfter = +resultBlockNumber2.stdout.trim() 20 | 21 | expect(blockNumberAfter).toEqual(blockNumberBefore + 1) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /e2e/common.js: -------------------------------------------------------------------------------- 1 | const PRIVATE_KEYS = ['0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d'] 2 | const RPC = process.env.RPC_URL 3 | 4 | module.exports = { 5 | PRIVATE_KEYS, 6 | RPC, 7 | } 8 | -------------------------------------------------------------------------------- /e2e/configuration.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const shell = require('shelljs') 3 | const { RPC } = require('./common') 4 | 5 | const binPath = path.join(__dirname, '..', 'bin', 'run') 6 | const eth = args => shell.exec(`${binPath} ${args}`, { silent: true }) 7 | const boxAbiPath = path.join(__dirname, 'test-contracts', 'Box.abi') 8 | 9 | describe('configuration', () => { 10 | it('should allow configuring networks', () => { 11 | // try to use a custom network before adding it 12 | { 13 | const result = eth('block:number -n ganache') 14 | expect(result.code).not.toEqual(0) 15 | } 16 | 17 | // add custom network 18 | { 19 | const result = eth(`network:add ganache --url ${RPC}`) 20 | expect(result.code).toEqual(0) 21 | } 22 | 23 | // use custom network 24 | { 25 | const result = eth('block:number -n ganache') 26 | expect(result.code).toEqual(0) 27 | } 28 | 29 | // check that networks shows up in the list 30 | { 31 | const result = eth('network:list --json') 32 | expect(result.code).toEqual(0) 33 | const networks = JSON.parse(result.stdout) 34 | expect(networks.ganache.url).toEqual(RPC) 35 | expect(networks.ganache.id).toBeUndefined() 36 | } 37 | 38 | // update network 39 | { 40 | const result = eth('network:update ganache --id 50') 41 | expect(result.code).toEqual(0) 42 | } 43 | 44 | // check that network was correctly updated 45 | { 46 | const result = eth('network:list --json') 47 | expect(result.code).toEqual(0) 48 | const networks2 = JSON.parse(result.stdout) 49 | expect(networks2.ganache.url).toEqual(RPC) 50 | expect(networks2.ganache.id).toEqual(50) 51 | } 52 | 53 | // remove network 54 | { 55 | const result = eth('network:remove ganache') 56 | expect(result.code).toEqual(0) 57 | } 58 | 59 | // check that network was correctly removed 60 | { 61 | const result = eth('network:list --json') 62 | expect(result.code).toEqual(0) 63 | const networks3 = JSON.parse(result.stdout) 64 | expect(networks3.ganache).toBeUndefined() 65 | } 66 | }) 67 | it('should allow configuring abis', () => { 68 | // try to use a custom abi before adding it 69 | { 70 | const result = eth('abi:methods box') 71 | expect(result.code).not.toEqual(0) 72 | } 73 | 74 | // add custom abi 75 | { 76 | const result = eth(`abi:add box ${boxAbiPath}`) 77 | expect(result.code).toEqual(0) 78 | } 79 | 80 | // to use a custom abi after adding it 81 | { 82 | const result = eth('abi:methods box') 83 | expect(result.code).toEqual(0) 84 | } 85 | 86 | // remove custom abi 87 | { 88 | const result = eth('abi:remove box') 89 | expect(result.code).toEqual(0) 90 | } 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /e2e/convert.test.js: -------------------------------------------------------------------------------- 1 | const getStream = require('get-stream') 2 | const path = require('path') 3 | const shell = require('shelljs') 4 | 5 | const binPath = path.join(__dirname, '..', 'bin', 'run') 6 | const eth = (args, options = {}) => shell.exec(`${binPath} ${args}`, { ...options, silent: true }) 7 | 8 | describe('convert', () => { 9 | it('should convert from wei to ether by default', async () => { 10 | const result = eth('convert 1000000000000000000') 11 | expect(result.code).toEqual(0) 12 | expect(result.stdout.trim()).toEqual('1') 13 | }) 14 | it('should convert from ether to wei', async () => { 15 | const result = eth('convert --from eth --to wei 1') 16 | expect(result.code).toEqual(0) 17 | expect(result.stdout.trim()).toEqual('1000000000000000000') 18 | }) 19 | it('should convert from gwei to wei', async () => { 20 | const result = eth('convert --from gwei --to wei 5') 21 | expect(result.code).toEqual(0) 22 | expect(result.stdout.trim()).toEqual('5000000000') 23 | }) 24 | it('should accept shorthand flags', async () => { 25 | const result = eth('convert -f gwei -t wei 5') 26 | expect(result.code).toEqual(0) 27 | expect(result.stdout.trim()).toEqual('5000000000') 28 | }) 29 | it('should read from stdin if no amount is specified', async () => { 30 | const child = eth('convert', { async: true }) 31 | const stdout = getStream(child.stdout) 32 | child.stdin.write('1000000000000000000') 33 | child.stdin.end() 34 | 35 | const result = (await stdout).trim() 36 | 37 | expect(result).toEqual('1') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /e2e/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | ganache: 4 | build: ganache 5 | ports: 6 | - "8545:8545" 7 | e2e: 8 | build: 9 | context: .. 10 | dockerfile: e2e/Dockerfile 11 | environment: 12 | RPC_URL: 'http://ganache:8545' 13 | command: 'true' 14 | -------------------------------------------------------------------------------- /e2e/ganache/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | ENV PATH="/usr/src/app/node_modules/.bin:${PATH}" 6 | 7 | COPY package.json ./ 8 | 9 | RUN npm install 10 | 11 | EXPOSE 8545 12 | CMD ganache-cli --noVMErrorsOnRPCResponse --gasLimit 30000000 -h 0.0.0.0 -p 8545 --networkId 50 -d 13 | -------------------------------------------------------------------------------- /e2e/ganache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "ganache-cli": "6.4.2" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /e2e/help.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const shell = require('shelljs') 3 | 4 | const binPath = path.join(__dirname, '..', 'bin', 'run') 5 | const eth = args => shell.exec(`${binPath} ${args}`, { silent: true }) 6 | 7 | describe('running eth without commands', () => { 8 | it('should show the help when running it without arguments', async () => { 9 | const result = eth('') 10 | 11 | expect(result.code).toEqual(0) 12 | expect(result.stdout).toMatch(/USAGE/) 13 | }) 14 | 15 | it('should show the help when running it with -h', async () => { 16 | const result = eth(`-h`) 17 | 18 | expect(result.code).toEqual(0) 19 | expect(result.stdout).toMatch(/USAGE/) 20 | }) 21 | 22 | it('should show the help when running it with --help', async () => { 23 | const result = eth(`--help`) 24 | 25 | expect(result.code).toEqual(0) 26 | expect(result.stdout).toMatch(/USAGE/) 27 | }) 28 | 29 | it('should show the help when running the help command', async () => { 30 | const result = eth(`help`) 31 | 32 | expect(result.code).toEqual(0) 33 | expect(result.stdout).toMatch(/USAGE/) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /e2e/method.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const shell = require('shelljs') 3 | 4 | const binPath = path.join(__dirname, '..', 'bin', 'run') 5 | const eth = args => shell.exec(`${binPath} ${args}`, { silent: true }) 6 | const testAbis = path.join(__dirname, 'abis') 7 | 8 | const erc20Abi = path.join(testAbis, 'ERC20.json') 9 | 10 | describe('method', () => { 11 | describe('hash', () => { 12 | it('should return the hash of the transfer signature', async () => { 13 | const signature = 'transfer(address,uint256)' 14 | const result = eth(`method:hash '${signature}'`) 15 | 16 | expect(result.code).toEqual(0) 17 | 18 | expect(result.stdout.trim()).toEqual('a9059cbb') 19 | }) 20 | }) 21 | 22 | describe('encode', () => { 23 | it('should encode the totalSupply method', () => { 24 | const signature = 'totalSupply()' 25 | const result = eth(`method:encode '${erc20Abi}' '${signature}'`) 26 | 27 | expect(result.code).toEqual(0) 28 | 29 | expect(result.stdout.trim()).toEqual('0x18160ddd') 30 | }) 31 | 32 | it('should encode the balanceOf method', () => { 33 | const signature = 'balanceOf("0xacA5Bfc4beb54f3A8608e22F67e66594F532e8Aa")' 34 | const result = eth(`method:encode '${erc20Abi}' '${signature}'`) 35 | 36 | expect(result.code).toEqual(0) 37 | 38 | expect(result.stdout.trim()).toEqual( 39 | '0x70a08231000000000000000000000000aca5bfc4beb54f3a8608e22f67e66594f532e8aa', 40 | ) 41 | }) 42 | }) 43 | 44 | describe('decode', () => { 45 | it('decode a transfer transaction', async () => { 46 | const signature = 'transfer(address,uint256)' 47 | const data = 48 | '0xa9059cbb000000000000000000000000697dB915674bAc602F4d6fAfA31c0e45f386416E00000000000000000000000000000000000000000000000000000004ff043b9e' 49 | const result = eth(`method:decode '${signature}' '${data}'`) 50 | 51 | expect(result.code).toEqual(0) 52 | 53 | const output = JSON.parse(result.stdout) 54 | 55 | expect(output).toHaveLength(2) 56 | expect(output[0]).toEqual('0x697dB915674bAc602F4d6fAfA31c0e45f386416E') 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /e2e/repl.test.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process') 2 | const path = require('path') 3 | const { RPC } = require('./common') 4 | 5 | const binPath = path.join(__dirname, '..', 'bin', 'run') 6 | 7 | const runRepl = async inputs => { 8 | return new Promise(resolve => { 9 | const child = exec(`${binPath} repl -n ${RPC}`, (error, stdout) => { 10 | if (error) { 11 | console.error(error) 12 | } 13 | 14 | const outputs = stdout 15 | .split(`${RPC}> `) 16 | .map(x => x.trim()) 17 | .filter(Boolean) 18 | 19 | resolve(outputs) 20 | }) 21 | 22 | for (const input of inputs) { 23 | child.stdin.write(`${input}\n`) 24 | } 25 | child.stdin.end() 26 | }) 27 | } 28 | 29 | describe('repl', () => { 30 | it('should start a node repl', async () => { 31 | const outputs = await runRepl(['2+2']) 32 | expect(outputs).toEqual(['4']) 33 | }) 34 | 35 | it('should expose web3', async () => { 36 | const outputs = await runRepl(['typeof web3']) 37 | expect(outputs).toEqual(["'object'"]) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /e2e/run-tests.sh: -------------------------------------------------------------------------------- 1 | docker-compose up -d --build 2 | sleep 3 3 | docker-compose run e2e ./node_modules/.bin/jest --verbose --runInBand e2e 4 | rc=$? 5 | docker-compose down 6 | exit $rc 7 | -------------------------------------------------------------------------------- /e2e/test-contracts/Box.abi: -------------------------------------------------------------------------------- 1 | [{"constant":false,"inputs":[],"name":"inc","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"value","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"increment","type":"uint256"}],"name":"incBy","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"inputs":[{"name":"_value","type":"uint256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[],"name":"ValueIncremented","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"increment","type":"uint256"}],"name":"ValueIncrementedBy","type":"event"}] -------------------------------------------------------------------------------- /e2e/test-contracts/Box.bin: -------------------------------------------------------------------------------- 1 | 608060405234801561001057600080fd5b506040516101ba3803806101ba8339818101604052602081101561003357600080fd5b8101908080519060200190929190505050806000819055505061015f8061005b6000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063371303c0146100465780633fa4f2451461005057806370119d061461006e575b600080fd5b61004e61009c565b005b6100586100db565b6040518082815260200191505060405180910390f35b61009a6004803603602081101561008457600080fd5b81019080803590602001909291905050506100e1565b005b60008081548092919060010191905055507f62e75fdd7e231ab7eb52cf10830208d6c086340368fea7f2d3029ebaac33bd0660405160405180910390a1565b60005481565b8060008082825401925050819055507fea7206ec44645c2a794019d17fc3698e55fae28d2b7237ebea1bc515c315ae56816040518082815260200191505060405180910390a15056fea265627a7a72305820accd569e6cc2c4bea8298c16df0dadec7415019ed542871ce24a5da004e2756764736f6c63430005090032 -------------------------------------------------------------------------------- /e2e/test-contracts/Box.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.9; 2 | 3 | contract Box { 4 | uint256 public value; 5 | 6 | event ValueIncremented(); 7 | event ValueIncrementedBy(uint256 increment); 8 | 9 | constructor(uint256 _value) public { 10 | value = _value; 11 | } 12 | 13 | function inc() public { 14 | value++; 15 | emit ValueIncremented(); 16 | } 17 | 18 | function incBy(uint256 increment) public { 19 | value += increment; 20 | emit ValueIncrementedBy(increment); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | ### Install eth-cli 4 | 5 | Run `npm i eth-cli -g` to install the eth-cli package. 6 | 7 | ![How-to-install-eth-cli.gif](img/How-to-install-eth-cli.gif) 8 | 9 | 10 | ### Call methods on deployed contracts 11 | 12 | Use `contract:call` to call methods on contracts deployed on any network. 13 | 14 | ![Call a contract method](img/contract-call.gif) 15 | 16 | 17 | ### Start an interactive REPL connected to some node 18 | 19 | Use `eth repl` to start an interactive REPL connected to an Ethereum node. 20 | 21 | ![Start a REPL](img/repl.gif) 22 | -------------------------------------------------------------------------------- /img/How-to-install-eth-cli.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protofire/eth-cli/32a355b5beb250f62800745d65f26d577d87c8ef/img/How-to-install-eth-cli.gif -------------------------------------------------------------------------------- /img/contract-call.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protofire/eth-cli/32a355b5beb250f62800745d65f26d577d87c8ef/img/contract-call.gif -------------------------------------------------------------------------------- /img/eth-cli-commands.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protofire/eth-cli/32a355b5beb250f62800745d65f26d577d87c8ef/img/eth-cli-commands.gif -------------------------------------------------------------------------------- /img/fetching-data.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protofire/eth-cli/32a355b5beb250f62800745d65f26d577d87c8ef/img/fetching-data.gif -------------------------------------------------------------------------------- /img/repl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/protofire/eth-cli/32a355b5beb250f62800745d65f26d577d87c8ef/img/repl.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eth-cli", 3 | "description": "A collection of CLI tools to help with ethereum learning and development.", 4 | "version": "2.0.2", 5 | "contributors": [ 6 | "Fernando Greco", 7 | "Franco Victorio", 8 | "Gerardo Nardelli", 9 | "Lisandro Corbalan", 10 | "Mariano Agüero", 11 | "Pablo Fullana" 12 | ], 13 | "bin": { 14 | "eth": "./bin/run" 15 | }, 16 | "bugs": "https://github.com/protofire/eth-cli/issues", 17 | "dependencies": { 18 | "@ethersproject/abi": "^5.0.0-beta.139", 19 | "@oclif/command": "^1", 20 | "@oclif/config": "^1", 21 | "@oclif/plugin-help": "^2", 22 | "big.js": "^5.2.2", 23 | "chalk": "^2.4.2", 24 | "cli-ux": "^5.2.1", 25 | "conf": "^5.0.0", 26 | "ethereumjs-util": "^6.1.0", 27 | "get-stdin": "^7.0.0", 28 | "lodash": "^4.17.15", 29 | "node-fetch": "^2.6.0", 30 | "ora": "^3.4.0", 31 | "repl.history": "^0.1.4", 32 | "rlp": "^2.2.3", 33 | "tslib": "^1", 34 | "web3": "1.2.1", 35 | "web3-eth-accounts": "1.2.1", 36 | "web3-providers-http": "1.2.1" 37 | }, 38 | "devDependencies": { 39 | "@oclif/dev-cli": "^1", 40 | "@oclif/tslint": "^3", 41 | "@types/big.js": "^4.0.5", 42 | "@types/chai": "^4", 43 | "@types/cli-table": "^0.3.0", 44 | "@types/jest": "^24.0.11", 45 | "@types/lodash": "^4.14.136", 46 | "@types/node": "^12.0.7", 47 | "@types/node-fetch": "^2.5.0", 48 | "@types/web3": "^1.0.19", 49 | "@typescript-eslint/eslint-plugin": "^2.4.0", 50 | "@typescript-eslint/parser": "^2.4.0", 51 | "chai": "^4", 52 | "doctoc": "^1.4.0", 53 | "eslint": "^6.5.1", 54 | "eslint-config-prettier": "^6.4.0", 55 | "eslint-plugin-implicit-dependencies": "^1.0.4", 56 | "eslint-plugin-oclif": "^0.1.0", 57 | "get-stream": "^5.1.0", 58 | "husky": "^3.0.4", 59 | "jest": "^24.7.1", 60 | "jest-watch-typeahead": "^0.4.0", 61 | "nyc": "^14.1.1", 62 | "prettier": "^1.16.4", 63 | "shelljs": "^0.8.3", 64 | "strip-ansi": "^5.2.0", 65 | "ts-jest": "^24.0.2", 66 | "ts-node": "^8", 67 | "typescript": "^3.3" 68 | }, 69 | "engines": { 70 | "node": ">=8.0.0" 71 | }, 72 | "files": [ 73 | "/bin", 74 | "/npm-shrinkwrap.json", 75 | "/oclif.manifest.json", 76 | "/lib/**/*" 77 | ], 78 | "homepage": "https://github.com/protofire/eth-cli#readme", 79 | "husky": { 80 | "hooks": { 81 | "pre-commit": "npm run lint && npm run build", 82 | "pre-push": "npm test" 83 | } 84 | }, 85 | "keywords": [ 86 | "ethereum", 87 | "blockchain", 88 | "cli", 89 | "oclif" 90 | ], 91 | "license": "MIT", 92 | "main": "lib/index.js", 93 | "oclif": { 94 | "commands": "./lib/commands", 95 | "bin": "eth", 96 | "plugins": [ 97 | "@oclif/plugin-help" 98 | ] 99 | }, 100 | "repository": { 101 | "type": "git", 102 | "url": "https://github.com/protofire/eth-cli" 103 | }, 104 | "scripts": { 105 | "build": "tsc -b", 106 | "watch": "tsc -b --watch", 107 | "lint": "npm run eslint:fix && npm run prettier-check", 108 | "eslint:fix": "eslint --fix . --ext .ts", 109 | "postpack": "rm -f oclif.manifest.json", 110 | "prepack": "rm -rf lib tsconfig.tsbuildinfo && tsc -b && oclif-dev manifest && oclif-dev readme", 111 | "prettier": "prettier --write \"**/*.{ts,tsx,json}\"", 112 | "prettier-check": "prettier --check \"**/*.{ts,tsx,json}\"", 113 | "test": "jest --coverage --detectOpenHandles ./test", 114 | "version": "oclif-dev readme && git add README.md", 115 | "clearjest": "jest --clearCache", 116 | "generate-docs": "npm run prepack && node scripts/generate-commands-list.js docs/COMMANDS.md && doctoc docs/COMMANDS.md --title 'This is the full list of commands supported by `eth-cli`.'" 117 | }, 118 | "types": "lib/index.d.ts", 119 | "jest": { 120 | "collectCoverageFrom": [ 121 | "src/**/*.{js}" 122 | ], 123 | "transform": { 124 | "^.+\\.tsx?$": "ts-jest" 125 | }, 126 | "testEnvironment": "node", 127 | "notify": true, 128 | "watchPlugins": [ 129 | "jest-watch-typeahead/filename", 130 | "jest-watch-typeahead/testname" 131 | ], 132 | "moduleFileExtensions": [ 133 | "ts", 134 | "js", 135 | "node", 136 | "json" 137 | ] 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /scripts/generate-commands-list.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | if (!process.argv[2]) { 5 | console.error('usage: node generate-commands-list.js path/to/file.md') 6 | process.exit(1) 7 | } 8 | 9 | const commandsDocPath = path.join(process.cwd(), process.argv[2]) 10 | const commandsDocStream = fs.createWriteStream(commandsDocPath) 11 | 12 | const write = s => commandsDocStream.write(s + '\n') 13 | 14 | const commandsDir = path.join(__dirname, '..', 'lib', 'commands') 15 | 16 | const files = fs.readdirSync(commandsDir).map(file => path.join(commandsDir, file)) 17 | 18 | write('') 19 | write('') 20 | 21 | const commandPaths = files.filter(file => path.extname(file) !== '.ts') 22 | 23 | for (const commandPath of commandPaths) { 24 | const commandName = path.basename(commandPath).replace('.js', '') 25 | write(`## \`${commandName}\``) 26 | write('') 27 | const command = Object.entries(require(commandPath))[0][1] 28 | write(command.description) 29 | write('') 30 | 31 | const isTopLevel = fs.statSync(commandPath).isFile() 32 | if (isTopLevel) { 33 | if (command.examples && command.examples.length) { 34 | write('Examples:') 35 | for (const example of command.examples || []) { 36 | write(`- \`${example}\``) 37 | } 38 | write('') 39 | } 40 | } else { 41 | const subcommandPaths = fs.readdirSync(commandPath).filter(file => path.extname(file) !== '.ts').filter(file => path.basename(file) !== 'index.js') 42 | .map(file => path.join(commandPath, file)) 43 | 44 | for (const subcommandPath of subcommandPaths) { 45 | const subcommandName = path.basename(subcommandPath).replace('.js', '') 46 | write(`### \`${commandName}:${subcommandName}\``) 47 | write('') 48 | const subcommand = Object.entries(require(subcommandPath))[0][1] 49 | write(subcommand.description) 50 | write('') 51 | if (subcommand.examples && subcommand.examples.length) { 52 | write('Examples:') 53 | for (const example of subcommand.examples || []) { 54 | write(`- \`${example}\``) 55 | } 56 | write('') 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/base/help-command.ts: -------------------------------------------------------------------------------- 1 | import { Command as BaseCommand } from '@oclif/command' 2 | 3 | import { isEmptyCommand } from '../helpers/checkCommandInputs' 4 | 5 | /** 6 | * Base command for "partial" commands that, when called, only show their help. 7 | */ 8 | export default class HelpCommand extends BaseCommand { 9 | async run() { 10 | const { args, flags } = this.parse(this.constructor as any) 11 | 12 | // Show help on empty sub command 13 | if (isEmptyCommand(flags, args)) { 14 | this._help() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/base/network.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | 3 | import { configService } from '../helpers/config-service' 4 | 5 | /** 6 | * Base command that handles the flags used for specifying the desired network. 7 | */ 8 | export abstract class NetworkCommand extends Command { 9 | static defaultUrl = 'http://localhost:8545' 10 | 11 | static flags = { 12 | network: flags.string({ 13 | char: 'n', 14 | default: NetworkCommand.defaultUrl, 15 | description: 'URL to connect to, or name of a known network', 16 | }), 17 | mainnet: flags.boolean({ hidden: true }), 18 | ropsten: flags.boolean({ hidden: true }), 19 | rinkeby: flags.boolean({ hidden: true }), 20 | goerli: flags.boolean({ hidden: true }), 21 | kovan: flags.boolean({ hidden: true }), 22 | } 23 | 24 | getNetworkUrl(flags: { [key in keyof typeof NetworkCommand.flags]: any }): string { 25 | const [, url] = this.getNetworkUrlAndKind(flags) 26 | return url 27 | } 28 | 29 | getNetworkUrlAndKind( 30 | flags: { [key in keyof typeof NetworkCommand.flags]: any }, 31 | ): ['name' | 'url', string, string?] { 32 | const networks = configService.getNetworks() 33 | const name = flags.mainnet 34 | ? 'mainnet' 35 | : flags.ropsten 36 | ? 'ropsten' 37 | : flags.rinkeby 38 | ? 'rinkeby' 39 | : flags.goerli 40 | ? 'goerli' 41 | : flags.kovan 42 | ? 'kovan' 43 | : flags.network 44 | if (networks[name]) { 45 | return ['name', networks[name].url, name] 46 | } 47 | 48 | return ['url', flags.network] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/commands/abi/add.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | 5 | export class AddCommand extends Command { 6 | static description = 'Add a known ABI.' 7 | 8 | static args = [ 9 | { 10 | name: 'name', 11 | required: true, 12 | description: 'Name of the ABI to add', 13 | }, 14 | { 15 | name: 'abiPath', 16 | required: true, 17 | description: 'Path to the file with the ABI', 18 | }, 19 | ] 20 | 21 | static examples = ['eth abi:add erc777 ./path/to/erc777.json'] 22 | 23 | async run() { 24 | const { args } = this.parse(AddCommand) 25 | 26 | try { 27 | const { name, abiPath } = args 28 | 29 | const abis = configService.getAbis() 30 | const { abi } = configService.loadABI(abiPath) 31 | if (abis[name]) { 32 | this.warn(`ABI '${name}' already exists. Use abi:update if you want to modify it.`) 33 | } else { 34 | abis[name] = abi 35 | configService.updateAbis(abis) 36 | } 37 | } catch (e) { 38 | this.error(e.message, { exit: 1 }) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/abi/events.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | 5 | export default class EventsCommand extends Command { 6 | static description = `Show the list of events in the given ABI.` 7 | 8 | static args = [ 9 | { 10 | name: 'abi', 11 | required: true, 12 | description: 'Contract ABI.', 13 | }, 14 | ] 15 | 16 | static examples = ['eth abi:events erc20'] 17 | 18 | async run() { 19 | const { args } = this.parse(EventsCommand) 20 | 21 | try { 22 | const { abi } = args 23 | 24 | const events = configService.getEvents(abi) 25 | 26 | events.forEach(({ signature, signatureHash }) => { 27 | this.log(`${signatureHash}\t${signature}`) 28 | }) 29 | } catch (e) { 30 | this.error(e.message, { exit: 1 }) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/abi/index.ts: -------------------------------------------------------------------------------- 1 | import HelpCommand from '../../base/help-command' 2 | 3 | export default class AbiCommand extends HelpCommand { 4 | static description = `Manage known ABIs and obtain their methods and events.` 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/abi/list.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | 5 | export default class ListCommand extends Command { 6 | static description = 'Display the list of known ABIs.' 7 | 8 | static flags = { 9 | help: flags.help({ char: 'h' }), 10 | } 11 | 12 | static examples = ['eth abi:list'] 13 | 14 | async run() { 15 | const abiList = configService.getAbiList() 16 | const listFormated = abiList.join('\n') 17 | this.log(listFormated) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/abi/methods.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | 5 | export default class MethodsCommand extends Command { 6 | static description = `Show the list of methods in the given ABI.` 7 | 8 | static args = [ 9 | { 10 | name: 'abi', 11 | required: true, 12 | description: 'Contract ABI.', 13 | }, 14 | ] 15 | 16 | static examples = ['eth abi:methods ../contracts/proxy.abi', 'eth abi:methods erc20'] 17 | 18 | async run() { 19 | const { args } = this.parse(MethodsCommand) 20 | 21 | try { 22 | const { abi } = args 23 | 24 | const methods = configService.getMethods(abi) 25 | 26 | methods.forEach(({ signature, signatureHash }) => { 27 | this.log(`${signatureHash}\t${signature}`) 28 | }) 29 | } catch (e) { 30 | this.error(e.message, { exit: 1 }) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/abi/remove.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | 5 | export class RemoveCommand extends Command { 6 | static description = 'Remove a known ABI.' 7 | 8 | static args = [ 9 | { 10 | name: 'name', 11 | required: true, 12 | description: 'Name of the ABI to remove', 13 | }, 14 | ] 15 | 16 | static aliases = ['abi:rm'] 17 | 18 | static examples = ['eth abi:rm erc777'] 19 | 20 | async run() { 21 | const { args } = this.parse(RemoveCommand) 22 | 23 | const { name } = args 24 | 25 | const abis = configService.getAbis() 26 | if (abis[name]) { 27 | delete abis[name] 28 | configService.updateAbis(abis) 29 | } else { 30 | this.warn(`No ABI found for '${name}'`) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/abi/show.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { cli } from 'cli-ux' 3 | 4 | import { configService } from '../../helpers/config-service' 5 | 6 | export default class ShowCommand extends Command { 7 | static description = 'Display a known ABI' 8 | 9 | static flags = { 10 | help: flags.help({ char: 'h' }), 11 | } 12 | 13 | static args = [ 14 | { 15 | name: 'abi', 16 | required: true, 17 | description: 'The contract name.', 18 | }, 19 | ] 20 | 21 | static examples = ['eth abi:show ERC20', 'eth abi:show ERC721'] 22 | 23 | async run() { 24 | const { args } = this.parse(ShowCommand) 25 | 26 | const { abi: abiArg } = args 27 | const { abi } = configService.loadABI(abiArg) 28 | if (abi) { 29 | cli.styledJSON(abi) 30 | } else { 31 | this.error(`ABI for ${abiArg} not found!`) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/commands/abi/update.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | 5 | export class UpdateCommand extends Command { 6 | static description = 'Update a known ABI.' 7 | 8 | static args = [ 9 | { 10 | name: 'name', 11 | required: true, 12 | description: 'Name of the ABI to add', 13 | }, 14 | { 15 | name: 'abiPath', 16 | required: true, 17 | description: 'Path to the file with the ABI', 18 | }, 19 | ] 20 | 21 | static examples = ['eth abi:update erc777 ./path/to/erc777.json'] 22 | 23 | async run() { 24 | const { args } = this.parse(UpdateCommand) 25 | 26 | try { 27 | const { name, abiPath } = args 28 | 29 | const abis = configService.getAbis() 30 | const { abi } = configService.loadABI(abiPath) 31 | if (abis[name]) { 32 | abis[name] = abi 33 | configService.updateAbis(abis) 34 | } else { 35 | this.warn(`No ABI found for '${name}'`) 36 | } 37 | } catch (e) { 38 | this.error(e.message, { exit: 1 }) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/address/add.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import cli from 'cli-ux' 3 | 4 | import { configService } from '../../helpers/config-service' 5 | import { add0x, isAddress, isPrivateKey } from '../../helpers/utils' 6 | 7 | export class AddCommand extends Command { 8 | static description = 'Add a known address.' 9 | 10 | static args = [ 11 | { 12 | name: 'name', 13 | required: true, 14 | description: 'Name of the address to add', 15 | }, 16 | { 17 | name: 'addressOrPk', 18 | required: true, 19 | description: 'Address or private key of the address', 20 | }, 21 | ] 22 | 23 | static flags = { 24 | 'network-id': flags.string({ 25 | char: 'n', 26 | required: false, 27 | default: '*', 28 | }), 29 | } 30 | 31 | static examples = [ 32 | 'eth address:add ganache 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d', 33 | 'eth address:add dai 0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 -n 1', 34 | ] 35 | 36 | async run() { 37 | const { args, flags } = this.parse(AddCommand) 38 | 39 | try { 40 | const { name, addressOrPk } = args 41 | const { 'network-id': networkId } = flags 42 | 43 | const addresses = configService.getAddresses() 44 | let addressObject = null 45 | if (isPrivateKey(addressOrPk)) { 46 | const Accounts = (await import('web3-eth-accounts')).default 47 | const accounts = new Accounts() 48 | const privateKey = add0x(addressOrPk) 49 | const address = accounts.privateKeyToAccount(privateKey).address 50 | 51 | addressObject = { privateKey, address } 52 | } else if (isAddress(addressOrPk)) { 53 | const address = add0x(addressOrPk) 54 | addressObject = { address } 55 | } else { 56 | this.warn('You have to specify an address or private key') 57 | this.exit(1) 58 | return 59 | } 60 | addresses[name] = addresses[name] || {} 61 | addresses[name][networkId] = addressObject 62 | configService.updateAddresses(addresses) 63 | cli.styledJSON(addressObject) 64 | } catch (e) { 65 | this.error(e.message, { exit: 1 }) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/address/balance.ts: -------------------------------------------------------------------------------- 1 | import { NetworkCommand } from '../../base/network' 2 | import { convert } from '../../helpers/convert' 3 | import { Unit } from '../../types' 4 | 5 | export default class BalanceCommand extends NetworkCommand { 6 | static description = `Get the balance of the given address.` 7 | 8 | static args = [ 9 | { 10 | name: 'address', 11 | required: true, 12 | description: 'Address or name of a known address', 13 | }, 14 | ] 15 | 16 | static flags = { 17 | ...NetworkCommand.flags, 18 | } 19 | 20 | static examples = ['eth address:balance 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1'] 21 | 22 | async run() { 23 | const { args, flags } = this.parse(BalanceCommand) 24 | const { address } = args 25 | 26 | let networkUrl 27 | 28 | try { 29 | networkUrl = this.getNetworkUrl(flags) 30 | 31 | const { getBalance } = await import('../../helpers/getBalance') 32 | const balance = await getBalance(address, networkUrl) 33 | const balanceInEth = convert(balance, Unit.Wei, Unit.Eth) 34 | 35 | this.log(balanceInEth) 36 | } catch (e) { 37 | this.error(e.message, { exit: 1 }) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/address/index.ts: -------------------------------------------------------------------------------- 1 | import HelpCommand from '../../base/help-command' 2 | 3 | export default class AddressCommand extends HelpCommand { 4 | static description = 'Manage known addresses, generate random accounts, get balances.' 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/address/list.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import cli from 'cli-ux' 3 | 4 | import { configService } from '../../helpers/config-service' 5 | 6 | export class ListCommand extends Command { 7 | static description = 'Display the list of known addresses.' 8 | 9 | static examples = ['eth address:list'] 10 | 11 | static flags = { 12 | json: flags.boolean({ 13 | description: 'Display data in a json structure.', 14 | required: false, 15 | exclusive: ['table'], 16 | }), 17 | table: flags.boolean({ 18 | description: 'Display data in a table structure.', 19 | required: false, 20 | exclusive: ['json'], 21 | }), 22 | } 23 | 24 | async run() { 25 | const { flags } = this.parse(ListCommand) 26 | const { table, json } = flags 27 | 28 | const displayAsTable = (!table && !json) || table 29 | const addresses: { [name: string]: object } = configService.getAddresses() 30 | if (displayAsTable) { 31 | const addressesList: any[] = [] 32 | for (const [name, addressPerNetwork] of Object.entries(addresses)) { 33 | for (const [networkId, addressObject] of Object.entries(addressPerNetwork)) { 34 | addressesList.push({ 35 | networkId, 36 | name, 37 | ...addressObject, 38 | }) 39 | } 40 | } 41 | 42 | cli.table( 43 | addressesList, 44 | { 45 | networkId: { 46 | header: 'Network', 47 | }, 48 | name: { 49 | header: 'Name', 50 | }, 51 | address: { 52 | header: 'Address', 53 | }, 54 | privateKey: { 55 | header: 'Private key', 56 | get: row => row.privateKey || '', 57 | }, 58 | }, 59 | { 60 | printLine: this.log, 61 | ...flags, // parsed flags 62 | }, 63 | ) 64 | } else { 65 | cli.styledJSON(addresses) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commands/address/random.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { cli } from 'cli-ux' 3 | 4 | export class RandomCommand extends Command { 5 | static description = `Generate a random Ethereum address with its private key.` 6 | 7 | static args = [ 8 | { 9 | name: 'amount', 10 | required: false, 11 | default: '1', 12 | description: 'Can be specified to generate a list of addresses.', 13 | }, 14 | ] 15 | 16 | static flags = { 17 | prefix: flags.string({ 18 | description: 19 | 'Prefix of the generated address. The more characters used, the longer it will take to generate it.', 20 | }), 21 | password: flags.boolean({ 22 | description: 23 | 'Ask for a password and generate a keystore with it. Only 1 address can be generated when this flags is used.', 24 | }), 25 | } 26 | 27 | static examples = ['eth address:random', 'eth address:random 3', 'eth address:random --prefix aa'] 28 | 29 | async run() { 30 | const { args, flags } = this.parse(RandomCommand) 31 | 32 | const { amount = '1' } = args 33 | const { prefix = '', password: promptForPassword = '' } = flags 34 | 35 | const amountNumber = parseInt(amount, 10) 36 | 37 | if (promptForPassword && amountNumber > 1) { 38 | this.error('Only 1 address can be generated when --password is used') 39 | } 40 | 41 | try { 42 | const { randomAddress, generateKeystore } = await import('../../helpers/randomAddress') 43 | 44 | if (promptForPassword) { 45 | const password = await cli.prompt('Password', { type: 'hide' }) 46 | const [{ privateKey }] = randomAddress(1, prefix) 47 | const keystore = await generateKeystore(privateKey, password) 48 | cli.styledJSON(keystore) 49 | } else { 50 | randomAddress(amountNumber, prefix).forEach(({ address, privateKey }) => 51 | cli.styledJSON({ address, privateKey }), 52 | ) 53 | } 54 | } catch (e) { 55 | this.error(e.message, { exit: 1 }) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/commands/address/remove.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | 5 | export class RemoveCommand extends Command { 6 | static description = 'Remove a known address.' 7 | 8 | static args = [ 9 | { 10 | name: 'name', 11 | required: true, 12 | description: 'Name of the address to remove', 13 | }, 14 | ] 15 | 16 | static flags = { 17 | 'network-id': flags.string({ 18 | char: 'n', 19 | required: false, 20 | default: '*', 21 | }), 22 | } 23 | 24 | static aliases = ['address:rm'] 25 | 26 | static examples = ['eth address:rm ganache'] 27 | 28 | async run() { 29 | const { args, flags } = this.parse(RemoveCommand) 30 | 31 | const { name } = args 32 | const { 'network-id': networkId } = flags 33 | const addresses = configService.getAddresses() 34 | if (addresses[name] && addresses[name][networkId]) { 35 | delete addresses[name][networkId] 36 | configService.updateAddresses(addresses) 37 | } else { 38 | this.warn(`No address found for '${name}'`) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/address/show.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import cli from 'cli-ux' 3 | 4 | import { configService } from '../../helpers/config-service' 5 | 6 | export class ShowCommand extends Command { 7 | static description = 'Display a known address.' 8 | 9 | static args = [ 10 | { 11 | name: 'name', 12 | required: true, 13 | description: 'Name of the address to get', 14 | }, 15 | ] 16 | 17 | static examples = ['eth address:show ganache'] 18 | 19 | async run() { 20 | const { args } = this.parse(ShowCommand) 21 | 22 | const { name } = args 23 | const addresses = configService.getAddresses() 24 | if (addresses[name]) { 25 | cli.styledJSON(addresses[name]) 26 | } else { 27 | this.warn(`No address found for '${name}'`) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/block/get.ts: -------------------------------------------------------------------------------- 1 | import { cli } from 'cli-ux' 2 | 3 | import { NetworkCommand } from '../../base/network' 4 | 5 | export default class GetCommand extends NetworkCommand { 6 | static description = 'Get the block object for a given block number.' 7 | 8 | static flags = { 9 | ...NetworkCommand.flags, 10 | } 11 | 12 | static args = [ 13 | { 14 | name: 'number', 15 | required: true, 16 | description: 'The number of the block', 17 | }, 18 | ] 19 | 20 | static examples = ['eth block:get --mainnet 12345'] 21 | 22 | async run() { 23 | const { args, flags } = this.parse(GetCommand) 24 | 25 | let networkUrl 26 | 27 | try { 28 | networkUrl = this.getNetworkUrl(flags) 29 | 30 | const { number: blockNumber } = args 31 | const { getBlock } = await import('../../helpers/getBlockObject') 32 | const block = await getBlock(+blockNumber, networkUrl) 33 | cli.styledJSON(block) 34 | } catch (e) { 35 | this.error(e.message, { exit: 1 }) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/block/index.ts: -------------------------------------------------------------------------------- 1 | import HelpCommand from '../../base/help-command' 2 | 3 | export default class TxCommand extends HelpCommand { 4 | static description = 'Get the latest block number of a network or fetch a specific block.' 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/block/number.ts: -------------------------------------------------------------------------------- 1 | import { NetworkCommand } from '../../base/network' 2 | 3 | export default class GetCommand extends NetworkCommand { 4 | static description = `Get the block number of the chosen network.` 5 | 6 | static flags = { 7 | ...NetworkCommand.flags, 8 | } 9 | 10 | static examples = [ 11 | 'eth block:number --rinkeby', 12 | `eth block:number --network 'https://dai.poa.network'`, 13 | ] 14 | 15 | async run() { 16 | const { flags } = this.parse(GetCommand) 17 | 18 | let networkUrl 19 | 20 | try { 21 | networkUrl = this.getNetworkUrl(flags) 22 | 23 | const { getBlockNumber } = await import('../../helpers/getBlockNumber') 24 | const blockNumber = await getBlockNumber(networkUrl) 25 | 26 | this.log('' + blockNumber) 27 | } catch (e) { 28 | this.error(e.message, { exit: 1 }) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commands/contract/add.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | 3 | import { ConfigService, configService } from '../../helpers/config-service' 4 | 5 | export class AddCommand extends Command { 6 | static description = 'Add a known contract.' 7 | 8 | static args = [ 9 | { 10 | name: 'name', 11 | required: true, 12 | description: 'Name of the contract to add', 13 | }, 14 | { 15 | name: 'contract', 16 | required: true, 17 | description: `The contract's ABI and address, in abi@address format.`, 18 | }, 19 | ] 20 | 21 | static examples = ['eth contract:add dai erc20@dai'] 22 | 23 | async run() { 24 | const { args } = this.parse(AddCommand) 25 | 26 | try { 27 | const { name, contract } = args 28 | 29 | const [abi, address] = ConfigService.parseAbiAtAddress(contract) 30 | 31 | configService.addContract(name, abi, address) 32 | } catch (e) { 33 | this.error(e.message, { exit: 1 }) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/contract/address.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | 3 | export default class AddressCommand extends Command { 4 | static description = `Get the address for a contract created from the given account.` 5 | 6 | static args = [ 7 | { 8 | name: 'account', 9 | required: true, 10 | description: 'The address that will deploy the contract..', 11 | }, 12 | ] 13 | 14 | static flags = { 15 | nonce: flags.string({ 16 | description: 'The nonce of the address that will deploy the contract.', 17 | }), 18 | } 19 | 20 | static examples = [ 21 | 'eth contract:address 0x92970dbD5C0Ee6b439422bFd7cD71e1DDA921A03', 22 | 'eth contract:address 0x92970dbD5C0Ee6b439422bFd7cD71e1DDA921A03 --nonce 5', 23 | ] 24 | 25 | async run() { 26 | const { args, flags } = this.parse(AddressCommand) 27 | 28 | try { 29 | const { account } = args 30 | const { nonce = '0' } = flags 31 | const { getContractAddress } = await import('../../helpers/getContractAddress') 32 | const contractAddress = getContractAddress(account, nonce) 33 | 34 | this.log(contractAddress) 35 | } catch (e) { 36 | this.error(e.message, { exit: 1 }) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/contract/call.ts: -------------------------------------------------------------------------------- 1 | import { NetworkCommand } from '../../base/network' 2 | import { configService } from '../../helpers/config-service' 3 | 4 | export default class CallCommand extends NetworkCommand { 5 | static description = `Call a method in the given contract and print the returned value.` 6 | 7 | static flags = { 8 | ...NetworkCommand.flags, 9 | } 10 | 11 | static args = [ 12 | { 13 | name: 'contract', 14 | required: true, 15 | description: `The contract's ABI and address, in abi@address format.`, 16 | }, 17 | { 18 | name: 'methodCall', 19 | required: true, 20 | description: `e.g.: 'myMethod(arg1,arg2,["a","b",3,["d","0x123..."]])'`, 21 | }, 22 | ] 23 | 24 | static examples = [ 25 | `eth contract:call --rinkeby erc20@0x5592ec0cfb4dbc12d3ab100b257153436a1f0fea 'totalSupply()'`, 26 | ] 27 | 28 | async run() { 29 | const { args, flags } = this.parse(CallCommand) 30 | 31 | let networkUrl 32 | 33 | try { 34 | networkUrl = this.getNetworkUrl(flags) 35 | 36 | const { contract, methodCall } = args 37 | const { contractCall } = await import('../../helpers/contractCall') 38 | const { getNetworkId } = await import('../../helpers/getNetworkId') 39 | const networkId = await getNetworkId(networkUrl) 40 | const { abi, address } = configService.loadContract(contract, networkId) 41 | const result = await contractCall(abi, methodCall, address, networkUrl) 42 | 43 | this.log(result) 44 | } catch (e) { 45 | this.error(e.message, { exit: 1 }) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/contract/deploy.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { cli } from 'cli-ux' 3 | 4 | import { NetworkCommand } from '../../base/network' 5 | import { confirmationBlocksFlag, privateKeyFlag } from '../../flags' 6 | import { awaitTransactionMined } from '../../helpers/transactions' 7 | import { configService } from '../../helpers/config-service' 8 | 9 | export class DeployCommand extends NetworkCommand { 10 | static description = `Deploy contract with the given binary.` 11 | 12 | static flags = { 13 | ...NetworkCommand.flags, 14 | pk: privateKeyFlag, 15 | 'confirmation-blocks': confirmationBlocksFlag, 16 | args: flags.string({ 17 | description: "Arguments for the contract's constructor.", 18 | }), 19 | abi: flags.string({ 20 | description: 'ABI of the contract. Required when using --args.', 21 | }), 22 | } 23 | 24 | static args = [ 25 | { 26 | name: 'bin', 27 | required: true, 28 | description: 'The bin file of the contract.', 29 | }, 30 | ] 31 | 32 | static examples = [ 33 | 'eth contract:deploy --ropsten --pk 3daa79a26454a5528a3523f9e6345efdbd636e63f8c24a835204e6ccb5c88f9e ./contracts/proxy.bin', 34 | 'eth contract:deploy --pk knownPK --abi erc20 --args ["MYTKN", 18] ./contracts/erc20.bin', 35 | ] 36 | 37 | static aliases = ['ct:d', 'ct:deploy'] 38 | 39 | async run() { 40 | const { args, flags } = this.parse(DeployCommand) 41 | 42 | let networkUrl 43 | 44 | try { 45 | networkUrl = this.getNetworkUrl(flags) 46 | 47 | const { bin } = args 48 | const { 49 | pk, 50 | 'confirmation-blocks': confirmationBlocks, 51 | args: constructorArgumentsStr, 52 | abi: abiPath, 53 | } = flags 54 | if (!pk) { 55 | this.error('You have to specify a private key using --pk', { exit: 1 }) 56 | return 57 | } 58 | 59 | if (constructorArgumentsStr && !abiPath) { 60 | this.error('The --abi flags has to be specified when using --args', { exit: 1 }) 61 | return 62 | } 63 | 64 | let constructorArguments = [] 65 | if (constructorArgumentsStr) { 66 | try { 67 | constructorArguments = JSON.parse(constructorArgumentsStr) 68 | if (!Array.isArray(constructorArguments)) { 69 | throw new Error('Given arguments are not an array') 70 | } 71 | } catch (e) { 72 | this.error(`Constructor arguments must be a valid JSON array: ${e.message}`, { exit: 1 }) 73 | return 74 | } 75 | } 76 | 77 | let abi = [] 78 | if (abiPath) { 79 | const { abi: loadedAbi } = configService.loadABI(abiPath) 80 | abi = loadedAbi 81 | } 82 | 83 | const { deploy } = await import('../../helpers/deploy') 84 | const txHash = await deploy(networkUrl, pk, abi, bin, constructorArguments) 85 | 86 | const receipt = await awaitTransactionMined(networkUrl, txHash, confirmationBlocks) 87 | 88 | cli.styledJSON({ address: receipt.contractAddress, receipt }) 89 | } catch (e) { 90 | this.error(e.message, { exit: 1 }) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/commands/contract/index.ts: -------------------------------------------------------------------------------- 1 | import HelpCommand from '../../base/help-command' 2 | 3 | export default class ContractCommand extends HelpCommand { 4 | static description = 'Deploy contracts or predict their addresses.' 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/contract/list.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import cli from 'cli-ux' 3 | 4 | import { configService } from '../../helpers/config-service' 5 | 6 | export class ListCommand extends Command { 7 | static description = 'Display the list of known contracts.' 8 | 9 | static examples = ['eth contract:list'] 10 | 11 | static flags = { 12 | json: flags.boolean({ 13 | description: 'Display data in a json structure.', 14 | required: false, 15 | exclusive: ['table'], 16 | }), 17 | table: flags.boolean({ 18 | description: 'Display data in a table structure.', 19 | required: false, 20 | exclusive: ['json'], 21 | }), 22 | } 23 | 24 | async run() { 25 | const { flags } = this.parse(ListCommand) 26 | const { table, json } = flags 27 | 28 | const displayAsTable = (!table && !json) || table 29 | const contracts: { 30 | [name: string]: { abi: string; address: string } 31 | } = configService.getContracts() 32 | if (displayAsTable) { 33 | const contractsList: any[] = [] 34 | for (const [name, contract] of Object.entries(contracts)) { 35 | contractsList.push({ name, ...contract }) 36 | } 37 | 38 | cli.table( 39 | contractsList, 40 | { 41 | name: { 42 | header: 'Name', 43 | }, 44 | abi: { 45 | header: 'ABI', 46 | }, 47 | address: { 48 | header: 'Address', 49 | }, 50 | }, 51 | { 52 | printLine: this.log, 53 | ...flags, // parsed flags 54 | }, 55 | ) 56 | } else { 57 | cli.styledJSON(contracts) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/contract/remove.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | 5 | export class RemoveCommand extends Command { 6 | static description = 'Remove a known contract.' 7 | 8 | static args = [ 9 | { 10 | name: 'name', 11 | required: true, 12 | description: 'Name of the contract to remove', 13 | }, 14 | ] 15 | 16 | static aliases = ['contract:rm'] 17 | 18 | static examples = ['eth contract:rm usdc'] 19 | 20 | async run() { 21 | const { args } = this.parse(RemoveCommand) 22 | 23 | const { name } = args 24 | const deleted = configService.removeContract(name) 25 | if (!deleted) { 26 | this.warn(`No contract found for '${name}'`) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/contract/send.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { cli } from 'cli-ux' 3 | 4 | import { NetworkCommand } from '../../base/network' 5 | import { confirmationBlocksFlag, privateKeyFlag } from '../../flags' 6 | import { awaitTransactionMined } from '../../helpers/transactions' 7 | import { configService } from '../../helpers/config-service' 8 | 9 | export default class SendCommand extends NetworkCommand { 10 | static description = `Send a transaction calling a method in the given contract.` 11 | 12 | static flags = { 13 | ...NetworkCommand.flags, 14 | pk: { ...privateKeyFlag, required: true }, 15 | 'confirmation-blocks': confirmationBlocksFlag, 16 | gas: flags.string({ 17 | description: 'The gas limit of the transaction. Will be estimated if not specified.', 18 | }), 19 | gasPrice: flags.string({ 20 | description: 'The gas price of the transaction, in wei. Defaults to 1 gwei.', 21 | default: '1000000000', 22 | }), 23 | value: flags.integer({ 24 | description: 'Amount of ether (in wei) to be sent with the transaction', 25 | default: 0, 26 | }), 27 | } 28 | 29 | static args = [ 30 | { 31 | name: 'contract', 32 | required: true, 33 | description: `The contract's ABI and address, in abi@address format.`, 34 | }, 35 | { 36 | name: 'methodCall', 37 | required: true, 38 | description: `e.g.: 'myMethod(arg1,arg2,["a","b",3,["d","0x123..."]])'`, 39 | }, 40 | ] 41 | 42 | static examples = [ 43 | `eth contract:send --rinkeby erc20@0x5592ec0cfb4dbc12d3ab100b257153436a1f0fea 'transfer("0x828DaF877f46fdFB5F1239cd9cB8f0D6E1adfb80", 1000000000)'`, 44 | ] 45 | 46 | async run() { 47 | const { args, flags } = this.parse(SendCommand) 48 | 49 | let networkUrl 50 | 51 | try { 52 | networkUrl = this.getNetworkUrl(flags) 53 | 54 | const { contract: abiAtAddress, methodCall } = args 55 | const { 'confirmation-blocks': confirmationBlocks, pk, value, gas, gasPrice } = flags 56 | const { contractCall } = await import('../../helpers/contractCall') 57 | const { getNetworkId } = await import('../../helpers/getNetworkId') 58 | const networkId = await getNetworkId(networkUrl) 59 | const { abi, address } = configService.loadContract(abiAtAddress, networkId) 60 | const tx = await contractCall(abi, methodCall, address, networkUrl, pk, { 61 | gas, 62 | gasPrice, 63 | value, 64 | }) 65 | 66 | await awaitTransactionMined(networkUrl, tx, confirmationBlocks) 67 | 68 | cli.styledJSON(tx) 69 | } catch (e) { 70 | this.error(e.message, { exit: 1 }) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/commands/contract/show.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import cli from 'cli-ux' 3 | 4 | import { configService } from '../../helpers/config-service' 5 | 6 | export class ShowCommand extends Command { 7 | static description = 'Display a known contract.' 8 | 9 | static args = [ 10 | { 11 | name: 'name', 12 | required: true, 13 | description: 'Name of the contract to show', 14 | }, 15 | ] 16 | 17 | static examples = ['eth contract:show usdc'] 18 | 19 | async run() { 20 | const { args } = this.parse(ShowCommand) 21 | 22 | const { name } = args 23 | const contract = configService.getContract(name) 24 | if (contract) { 25 | cli.styledJSON(contract) 26 | } else { 27 | this.warn(`No contract found for '${name}'`) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/convert.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import getStdin from 'get-stdin' 3 | 4 | import { convert, stringToUnit } from '../helpers/convert' 5 | import { Unit } from '../types' 6 | 7 | export class ConvertCommand extends Command { 8 | static description = 'Convert from eth to wei, wei to eth, etc.' 9 | 10 | static args = [ 11 | { 12 | name: 'amount', 13 | required: false, 14 | description: 'The amount to convert. Can also be specified through stdin.', 15 | }, 16 | ] 17 | 18 | static flags = { 19 | from: flags.string({ 20 | char: 'f', 21 | default: Unit.Wei, 22 | }), 23 | to: flags.string({ 24 | char: 't', 25 | default: Unit.Eth, 26 | }), 27 | } 28 | 29 | static examples = [ 30 | 'eth convert 1000000000000000000', 31 | 'eth convert -f eth -t wei 1', 32 | 'echo 1000000000000000000 | eth convert', 33 | ] 34 | 35 | async run() { 36 | const { args, flags } = this.parse(ConvertCommand) 37 | 38 | let { amount } = args 39 | const { from: fromRaw, to: toRaw } = flags 40 | 41 | if (!amount) { 42 | amount = await getStdin() 43 | } 44 | 45 | const from = stringToUnit(fromRaw) 46 | const to = stringToUnit(toRaw) 47 | 48 | if (!from) { 49 | this.error('--from has to be a valid unit (eth, wei, gwei)') 50 | return 51 | } 52 | if (!to) { 53 | this.error('--to has to be a valid unit (eth, wei, gwei)') 54 | return 55 | } 56 | 57 | try { 58 | const converted = convert(amount.trim(), from, to) 59 | this.log(converted) 60 | } catch (e) { 61 | this.error(e.message, { exit: 1 }) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/commands/event/get.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import cli from 'cli-ux' 3 | 4 | import { NetworkCommand } from '../../base/network' 5 | import { configService } from '../../helpers/config-service' 6 | 7 | export default class GetCommand extends NetworkCommand { 8 | static description = `Get the events in the given block range.` 9 | 10 | static flags = { 11 | ...NetworkCommand.flags, 12 | from: flags.string({ 13 | char: 'f', 14 | required: false, 15 | default: '1', 16 | description: 17 | 'Start of the block number. Can be a positive number, a negative number, or "latest" (1 by default). A negative number is interpreted as substracted from the current block number.', 18 | }), 19 | to: flags.string({ 20 | char: 't', 21 | required: false, 22 | default: 'latest', 23 | description: 24 | 'End of the block range. Can be a positive number, a negative number, or "latest" (default). A negative number is interpreted as substracted from the current block number.', 25 | }), 26 | json: flags.boolean({ 27 | description: 'Print events in JSON format', 28 | }), 29 | } 30 | 31 | static args = [ 32 | { 33 | name: 'contract', 34 | required: true, 35 | description: `The contract's ABI and address, in abi@address format.`, 36 | }, 37 | { 38 | name: 'event', 39 | required: true, 40 | description: `e.g.: 'MyEvent'`, 41 | }, 42 | ] 43 | 44 | static examples = [ 45 | 'eth event:get --mainnet erc20@0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 Transfer --from 1', 46 | 'eth event:get --mainnet erc20@0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 Transfer --from 1 --json', 47 | ] 48 | 49 | async run() { 50 | const { args, flags } = this.parse(GetCommand) 51 | 52 | let networkUrl 53 | 54 | try { 55 | networkUrl = this.getNetworkUrl(flags) 56 | 57 | const { contract: abiAtAddress, event } = args 58 | const { from, to, json } = flags 59 | const { getEvents, parseEvent, processEvent } = await import('../../helpers/getEvents') 60 | 61 | const { getBlockNumber } = await import('../../helpers/getBlockNumber') 62 | const blockNumber = await getBlockNumber(networkUrl) 63 | 64 | let fromBlock = from 65 | if (fromBlock !== 'latest' && +fromBlock < 0) { 66 | fromBlock = String(blockNumber + Number(from)) 67 | } 68 | 69 | let toBlock = to 70 | if (toBlock !== 'latest' && +toBlock < 0) { 71 | toBlock = String(blockNumber + Number(to)) 72 | } 73 | 74 | const { getNetworkId } = await import('../../helpers/getNetworkId') 75 | const networkId = await getNetworkId(networkUrl) 76 | const { abi, address } = configService.loadContract(abiAtAddress, networkId) 77 | 78 | const { events, eventAbi } = await getEvents(abi, event, address, networkUrl, { 79 | from: fromBlock, 80 | to: toBlock, 81 | }) 82 | 83 | if (json) { 84 | events.forEach(event => { 85 | cli.styledJSON(processEvent(event, eventAbi)) 86 | }) 87 | } else { 88 | events.forEach(event => this.log(parseEvent(event, eventAbi))) 89 | } 90 | } catch (e) { 91 | this.error(e.message, { exit: 1 }) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/commands/event/index.ts: -------------------------------------------------------------------------------- 1 | import HelpCommand from '../../base/help-command' 2 | 3 | export default class EventCommand extends HelpCommand { 4 | static description = 'Get past events and watch for new ones.' 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/event/watch.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import cli from 'cli-ux' 3 | 4 | import { NetworkCommand } from '../../base/network' 5 | import { configService } from '../../helpers/config-service' 6 | 7 | export default class GetCommand extends NetworkCommand { 8 | static description = `Emit new events from the given type in the given contract.` 9 | 10 | static flags = { 11 | ...NetworkCommand.flags, 12 | json: flags.boolean({ 13 | description: 'Print events in JSON format', 14 | }), 15 | } 16 | 17 | static args = [ 18 | { 19 | name: 'contract', 20 | required: true, 21 | description: `The contract's ABI and address, in abi@address format.`, 22 | }, 23 | { 24 | name: 'event', 25 | required: true, 26 | description: `e.g.: 'MyEvent'`, 27 | }, 28 | ] 29 | 30 | static examples = [ 31 | 'eth event:get --mainnet erc20@0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 Transfer', 32 | 'eth event:get --mainnet erc20@0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359 Transfer --json', 33 | ] 34 | 35 | async run() { 36 | const { args, flags } = this.parse(GetCommand) 37 | const { json } = flags 38 | 39 | let networkUrl 40 | 41 | try { 42 | networkUrl = this.getNetworkUrl(flags) 43 | 44 | const { contract: abiAtAddress, event } = args 45 | const { getEvents, parseEvent, processEvent } = await import('../../helpers/getEvents') 46 | const { getBlockNumber } = await import('../../helpers/getBlockNumber') 47 | let fromBlock = await getBlockNumber(networkUrl) 48 | 49 | const { getNetworkId } = await import('../../helpers/getNetworkId') 50 | const networkId = await getNetworkId(networkUrl) 51 | const { abi, address } = configService.loadContract(abiAtAddress, networkId) 52 | 53 | while (true) { 54 | const toBlock = await getBlockNumber(networkUrl) 55 | if (fromBlock <= toBlock) { 56 | const { events, eventAbi } = await getEvents(abi, event, address, networkUrl, { 57 | from: fromBlock, 58 | to: toBlock, 59 | }) 60 | 61 | if (json) { 62 | events.forEach(event => cli.styledJSON(processEvent(event, eventAbi))) 63 | } else { 64 | events.forEach(event => this.log(parseEvent(event, eventAbi))) 65 | } 66 | 67 | fromBlock = toBlock + 1 68 | } 69 | await new Promise((res: any) => setTimeout(res, 5000)) 70 | } 71 | } catch (e) { 72 | this.error(e.message, { exit: 1 }) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/commands/method/decode.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { cli } from 'cli-ux' 3 | 4 | export default class DecodeCommand extends Command { 5 | static description = `Decode the arguments of the given transaction data for the given function signature.` 6 | 7 | static args = [ 8 | { 9 | name: 'functionSignature', 10 | required: true, 11 | description: 'The function signature.', 12 | }, 13 | { 14 | name: 'txData', 15 | required: true, 16 | description: 'The given transaction data.', 17 | }, 18 | ] 19 | 20 | static examples = [ 21 | `eth method:decode 'transfer(address,uint256)' '0xa9059cbb000000000000000000000000697dB915674bAc602F4d6fAfA31c0e45f386416E00000000000000000000000000000000000000000000000000000004ff043b9e'`, 22 | ] 23 | 24 | async run() { 25 | const { args } = this.parse(DecodeCommand) 26 | 27 | try { 28 | const { functionSignature, txData } = args 29 | const { decodeTxData } = await import('../../helpers/decodeTxData') 30 | const result = decodeTxData(functionSignature, txData) 31 | 32 | cli.styledJSON(result) 33 | } catch (e) { 34 | this.error(e.message, { exit: 1 }) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commands/method/encode.ts: -------------------------------------------------------------------------------- 1 | import { NetworkCommand } from '../../base/network' 2 | 3 | export default class EncodeCommand extends NetworkCommand { 4 | static description = `Encode the ABI for the method and print the ABI byte code.` 5 | 6 | static flags = { 7 | ...NetworkCommand.flags, 8 | } 9 | 10 | static args = [ 11 | { 12 | name: 'abi', 13 | required: true, 14 | description: 'The abi file.', 15 | }, 16 | { 17 | name: 'methodCall', 18 | required: true, 19 | description: `e.g.: 'myMethod(arg1,arg2,["a","b",3,["d","0x123..."]])'`, 20 | }, 21 | ] 22 | 23 | static examples = [ 24 | `eth method:encode --sokol ./test/files/contracts/Proxy.abi 'updateAppInstance()'`, 25 | ] 26 | 27 | async run() { 28 | const { args, flags } = this.parse(EncodeCommand) 29 | 30 | let networkUrl 31 | 32 | try { 33 | networkUrl = this.getNetworkUrl(flags) 34 | 35 | const { abi, methodCall } = args 36 | const { encode } = await import('../../helpers/encode') 37 | const result = encode(abi, methodCall, networkUrl) 38 | 39 | this.log(result) 40 | } catch (e) { 41 | this.error(e.message, { exit: 1 }) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/method/hash.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | import { keccak } from 'ethereumjs-util' 3 | 4 | export default class HashCommand extends Command { 5 | static description = `Get the hash of the given method.` 6 | 7 | static args = [ 8 | { 9 | name: 'signature', 10 | required: true, 11 | description: 'The given signature.', 12 | }, 13 | ] 14 | 15 | static examples = [`eth method:hash 'transfer(address,uint256)'`] 16 | 17 | async run() { 18 | const { args } = this.parse(HashCommand) 19 | 20 | try { 21 | const { signature } = args 22 | 23 | const hash = keccak(signature) 24 | .toString('hex') 25 | .slice(0, 8) 26 | 27 | this.log(hash) 28 | } catch (e) { 29 | this.error(e.message, { exit: 1 }) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/method/index.ts: -------------------------------------------------------------------------------- 1 | import HelpCommand from '../../base/help-command' 2 | 3 | export default class MethodCommand extends HelpCommand { 4 | static description = 'Encode and decode methods, search by signature, etc.' 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/method/search.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | 3 | export default class SearchCommand extends Command { 4 | static description = `Search the given hashed method signature using the 4byte.directory API` 5 | 6 | static args = [ 7 | { 8 | name: 'hexSignature', 9 | required: true, 10 | description: 'The hash of the function signature.', 11 | }, 12 | ] 13 | 14 | static examples = [`eth method:search a9059cbb`] 15 | 16 | async run() { 17 | const { args } = this.parse(SearchCommand) 18 | 19 | try { 20 | const { hexSignature } = args 21 | const { searchSignature } = await import('../../helpers/searchSignature') 22 | const result = await searchSignature(hexSignature) 23 | 24 | result.forEach((signature: string) => this.log(signature)) 25 | } catch (e) { 26 | this.error(e.message, { exit: 1 }) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/commands/network/add.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | import { NetworkInfo } from '../../types' 5 | 6 | export class AddCommand extends Command { 7 | static description = 'Add a known network.' 8 | 9 | static args = [ 10 | { 11 | name: 'name', 12 | required: true, 13 | description: 'Name of the network to add', 14 | }, 15 | ] 16 | 17 | static flags = { 18 | url: flags.string({ 19 | required: true, 20 | }), 21 | id: flags.string({ 22 | required: false, 23 | }), 24 | label: flags.string({ 25 | required: false, 26 | }), 27 | } 28 | 29 | static examples = ['eth network:add rsk --url https://public-node.rsk.co --id 30 --label RSK'] 30 | 31 | async run() { 32 | const { args, flags } = this.parse(AddCommand) 33 | 34 | try { 35 | const { name } = args 36 | const { id, label, url } = flags 37 | 38 | const newNetwork: NetworkInfo = { url } 39 | if (id) { 40 | newNetwork.id = +id 41 | } 42 | if (label) { 43 | newNetwork.label = label 44 | } 45 | 46 | const networks = configService.getNetworks() 47 | if (networks[name]) { 48 | this.warn(`Network '${name}' already exists. Use network:update if you want to modify it.`) 49 | } else { 50 | networks[name] = newNetwork 51 | configService.updateNetworks(networks) 52 | } 53 | } catch (e) { 54 | this.error(e.message, { exit: 1 }) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/commands/network/index.ts: -------------------------------------------------------------------------------- 1 | import HelpCommand from '../../base/help-command' 2 | 3 | export default class NetworkCommand extends HelpCommand { 4 | static description = 'Manage known networks.' 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/network/list.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | import { cli } from 'cli-ux' 3 | 4 | import { configService } from '../../helpers/config-service' 5 | 6 | export default class ListCommand extends Command { 7 | static description = `Show information for each known network.` 8 | 9 | static aliases = ['networks'] 10 | 11 | static flags = { 12 | json: flags.boolean({ 13 | description: 'Display data in a json structure.', 14 | required: false, 15 | exclusive: ['table'], 16 | }), 17 | table: flags.boolean({ 18 | description: 'Display data in a table structure.', 19 | required: false, 20 | exclusive: ['json'], 21 | }), 22 | } 23 | 24 | static examples = ['eth network:list --display json', 'eth networks'] 25 | 26 | async run() { 27 | try { 28 | const { flags } = this.parse(ListCommand) 29 | const { table, json } = flags 30 | 31 | // display as table by default 32 | const displayAsTable = (!table && !json) || table 33 | 34 | if (displayAsTable) { 35 | const networkConstants = Object.entries(configService.getNetworks()).map( 36 | ([name, network]) => ({ 37 | name, 38 | ...network, 39 | }), 40 | ) 41 | 42 | const networks = networkConstants 43 | .sort((network1, network2) => { 44 | if (network1.id !== undefined && network2.id !== undefined) { 45 | return network1.id - network2.id 46 | } else if (network1.id !== undefined) { 47 | return -1 48 | } else { 49 | return 1 50 | } 51 | }) 52 | .map(network => ({ 53 | ...network, 54 | id: network.id || '', 55 | label: network.label || '', 56 | })) 57 | cli.table( 58 | networks, 59 | { 60 | id: { 61 | header: 'Id', 62 | minWidth: 7, 63 | }, 64 | name: { 65 | header: 'Name', 66 | }, 67 | label: { 68 | header: 'Label', 69 | }, 70 | url: { 71 | header: 'Url', 72 | }, 73 | }, 74 | { 75 | printLine: this.log, 76 | ...flags, // parsed flags 77 | }, 78 | ) 79 | } else { 80 | cli.styledJSON(configService.getNetworks()) 81 | } 82 | } catch (e) { 83 | this.error(e.message, { exit: 1 }) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/commands/network/remove.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | 5 | export class RemoveCommand extends Command { 6 | static description = 'Remove a known network.' 7 | 8 | static aliases = ['network:rm'] 9 | 10 | static args = [ 11 | { 12 | name: 'name', 13 | required: true, 14 | description: 'Name of the network to remove', 15 | }, 16 | ] 17 | 18 | static examples = ['eth network:remove rsk'] 19 | 20 | async run() { 21 | const { args } = this.parse(RemoveCommand) 22 | 23 | const { name } = args 24 | const networks = configService.getNetworks() 25 | if (networks[name]) { 26 | delete networks[name] 27 | configService.updateNetworks(networks) 28 | } else { 29 | this.warn(`No network found for '${name}'`) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/network/update.ts: -------------------------------------------------------------------------------- 1 | import { Command, flags } from '@oclif/command' 2 | 3 | import { configService } from '../../helpers/config-service' 4 | 5 | export class UpdateCommand extends Command { 6 | static description = 'Update a known network.' 7 | 8 | static args = [ 9 | { 10 | name: 'name', 11 | required: true, 12 | description: 'Name of the network to update', 13 | }, 14 | ] 15 | 16 | static flags = { 17 | url: flags.string({ 18 | required: false, 19 | }), 20 | id: flags.string({ 21 | required: false, 22 | }), 23 | label: flags.string({ 24 | required: false, 25 | }), 26 | } 27 | 28 | static examples = ['eth network:update rsk --id 30'] 29 | 30 | async run() { 31 | const { args, flags } = this.parse(UpdateCommand) 32 | 33 | try { 34 | const { name } = args 35 | const { id, label, url } = flags 36 | 37 | const networks = configService.getNetworks() 38 | if (networks[name]) { 39 | const network = networks[name] 40 | if (url) { 41 | network.url = url 42 | } 43 | if (id) { 44 | network.id = +id 45 | } 46 | if (label) { 47 | network.label = label 48 | } 49 | networks[name] = network 50 | configService.updateNetworks(networks) 51 | } else { 52 | this.warn(`No network found for '${name}'`) 53 | } 54 | } catch (e) { 55 | this.error(e.message, { exit: 1 }) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/commands/repl.ts: -------------------------------------------------------------------------------- 1 | import { NetworkCommand } from '../base/network' 2 | import { privateKeyFlag } from '../flags' 3 | import { configService } from '../helpers/config-service' 4 | 5 | export default class ReplCommand extends NetworkCommand { 6 | static description = `Start a REPL that connects to an RPC node ('localhost:8545' by default). 7 | 8 | The started REPL exposes a \`web3\` object that you can use to interact with the 9 | node. There's also an \`eth\` object to save you from typing \`web3.eth\`. 10 | 11 | You can also indicate some contracts to load in the REPL; see the examples to 12 | learn how to do this.` 13 | 14 | static strict = false 15 | 16 | static flags = { 17 | ...NetworkCommand.flags, 18 | pk: privateKeyFlag, 19 | } 20 | 21 | static args = [ 22 | { 23 | name: 'contracts...', 24 | description: './path/to/contractABI@address', 25 | }, 26 | ] 27 | 28 | static examples = [ 29 | 'eth repl', 30 | 'eth repl --mainnet', 31 | 'eth repl --url=http://localhost:7545', 32 | 'eth repl ./abis/myContract.json@0xaD2FA57bd95A3dfF0e1728686997F6f2fE67F6f9', 33 | 'eth repl erc20@0x34ee482D419229dAad23f27C44B82075B9418D31 erc721@0xcba140186Fa0436e5155fF6DC909F22Ec4648b12', 34 | ] 35 | 36 | async run() { 37 | const { flags, argv } = this.parse(ReplCommand) 38 | 39 | try { 40 | const [networkKind, networkUrl, networkName] = this.getNetworkUrlAndKind(flags) 41 | const { getNetworkId } = await import('../helpers/getNetworkId') 42 | const networkId = await getNetworkId(networkUrl) 43 | const prompt = 44 | networkKind === 'url' 45 | ? networkUrl === NetworkCommand.defaultUrl 46 | ? '> ' 47 | : `${networkUrl}> ` 48 | : `${networkName || flags.network}> ` 49 | 50 | const contracts = argv.map(contract => configService.loadContract(contract, networkId)) 51 | const privateKey = flags.pk 52 | 53 | const { startRepl } = await import('../helpers/startRepl') 54 | 55 | await startRepl(networkUrl, prompt, contracts, privateKey) 56 | } catch (e) { 57 | this.error(e.message, { exit: 1 }) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/transaction/get.ts: -------------------------------------------------------------------------------- 1 | import { cli } from 'cli-ux' 2 | import { Transaction } from 'web3/eth/types' 3 | import { TransactionReceipt } from 'web3/types' 4 | 5 | import { NetworkCommand } from '../../base/network' 6 | 7 | export default class GetCommand extends NetworkCommand { 8 | static description = `Print the transaction object for the given transaction hash.` 9 | 10 | static flags = { 11 | ...NetworkCommand.flags, 12 | } 13 | 14 | static args = [ 15 | { 16 | name: 'txHash', 17 | required: true, 18 | description: 'The transaction hash.', 19 | }, 20 | ] 21 | 22 | static examples = [ 23 | 'eth transaction:get --mainnet 0xc83836f1b3acac94a31de8e24c913aceaa9ebc51c93cd374429590596091584a', 24 | 'eth transaction:get --ropsten 0xc83836f1b3acac94a31de8e24c913aceaa9ebc51c93cd374429590596091584a', 25 | 'eth transaction:get --url= http://localhost:8545 0xc83836f1b3acac94a31de8e24c913aceaa9ebc51c93cd374429590596091584a', 26 | ] 27 | 28 | static aliases = ['tx:get'] 29 | 30 | async run() { 31 | const { args, flags } = this.parse(GetCommand) 32 | 33 | let networkUrl 34 | 35 | try { 36 | networkUrl = this.getNetworkUrl(flags) 37 | 38 | const { txHash } = args 39 | const { getTransaction, getReceipt } = await import('../../helpers/getTransactionObject') 40 | const promises: [Promise, Promise] = [ 41 | getTransaction(txHash, networkUrl), 42 | getReceipt(txHash, networkUrl), 43 | ] 44 | const [transaction, receipt] = await Promise.all(promises) 45 | 46 | let output 47 | if (transaction) { 48 | output = { 49 | ...transaction, 50 | receipt, 51 | } 52 | } else { 53 | output = null 54 | } 55 | cli.styledJSON(output) 56 | } catch (e) { 57 | this.error(e.message, { exit: 1 }) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/transaction/index.ts: -------------------------------------------------------------------------------- 1 | import HelpCommand from '../../base/help-command' 2 | 3 | export default class TxCommand extends HelpCommand { 4 | static aliases = ['tx'] 5 | 6 | static description = 'Get information about mined transactions or create empty transaction.' 7 | } 8 | -------------------------------------------------------------------------------- /src/commands/transaction/nop.ts: -------------------------------------------------------------------------------- 1 | import { cli } from 'cli-ux' 2 | 3 | import { NetworkCommand } from '../../base/network' 4 | import { confirmationBlocksFlag, privateKeyFlag } from '../../flags' 5 | import { awaitTransactionMined } from '../../helpers/transactions' 6 | 7 | export default class NopCommand extends NetworkCommand { 8 | static description = `Generates a transaction that does nothing with the given private key.` 9 | 10 | static flags = { 11 | ...NetworkCommand.flags, 12 | pk: { ...privateKeyFlag, required: true }, 13 | 'confirmation-blocks': confirmationBlocksFlag, 14 | } 15 | 16 | static examples = [ 17 | 'eth transaction:nop --pk 3daa79a26454a5528a3523f9e6345efdbd636e63f8c24a835204e6ccb5c88f9e', 18 | 'ETH_CLI_PRIVATE_KEY=3daa79a26454a5528a3523f9e6345efdbd636e63f8c24a835204e6ccb5c88f9e eth transaction:nop', 19 | ] 20 | 21 | static aliases = ['tx:nop'] 22 | 23 | async run() { 24 | const { flags } = this.parse(NopCommand) 25 | 26 | let networkUrl 27 | 28 | try { 29 | networkUrl = this.getNetworkUrl(flags) 30 | 31 | const { 'confirmation-blocks': confirmationBlocks, pk } = flags 32 | 33 | if (!pk) { 34 | this.error('Please specify a private key using --pk', { exit: 1 }) 35 | return 36 | } 37 | 38 | const { generateNop } = await import('../../helpers/generateNop') 39 | const tx = await generateNop(networkUrl, pk) 40 | 41 | await awaitTransactionMined(networkUrl, tx, confirmationBlocks) 42 | 43 | cli.styledJSON(tx) 44 | } catch (e) { 45 | this.error(e.message, { exit: 1 }) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/transaction/send.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | import { cli } from 'cli-ux' 3 | 4 | import { NetworkCommand } from '../../base/network' 5 | import { confirmationBlocksFlag, privateKeyFlag } from '../../flags' 6 | import { awaitTransactionMined } from '../../helpers/transactions' 7 | 8 | export default class SendCommand extends NetworkCommand { 9 | static description = `Send a raw transaction` 10 | 11 | static flags = { 12 | ...NetworkCommand.flags, 13 | pk: { ...privateKeyFlag, required: true }, 14 | 'confirmation-blocks': confirmationBlocksFlag, 15 | to: flags.string({ 16 | description: 'The recipient address of the transaction', 17 | required: true, 18 | }), 19 | gas: flags.string({ 20 | description: 'The gas limit of the transaction. Will be estimated if not specified.', 21 | }), 22 | gasPrice: flags.string({ 23 | description: 'The gas price of the transaction, in wei. Defaults to 1 gwei.', 24 | }), 25 | value: flags.string({ 26 | description: 'The amount of eth to send with the transaciton, in wei.', 27 | }), 28 | data: flags.string({ 29 | description: 30 | 'The raw data field of the transaction. Consider using contract:send instead of this.', 31 | }), 32 | } 33 | 34 | static examples = [ 35 | 'eth transaction:send --pk 3daa79a26454a5528a3523f9e6345efdbd636e63f8c24a835204e6ccb5c88f9e --to 0x828DaF877f46fdFB5F1239cd9cB8f0D6E1adfb80 --value 1000000000000000000', 36 | ] 37 | 38 | static aliases = ['tx:send'] 39 | 40 | async run() { 41 | const { flags } = this.parse(SendCommand) 42 | 43 | let networkUrl 44 | 45 | try { 46 | networkUrl = this.getNetworkUrl(flags) 47 | 48 | const { 49 | 'confirmation-blocks': confirmationBlocks, 50 | pk, 51 | to, 52 | gas, 53 | gasPrice = '1000000000', 54 | value = '0', 55 | data = '', 56 | } = flags 57 | 58 | if (!pk) { 59 | throw new Error('Specify the private key using --pk') 60 | } 61 | 62 | const { sendRawTransaction } = await import('../../helpers/sendRawTransaction') 63 | const tx = await sendRawTransaction(networkUrl, pk, to, { 64 | gas, 65 | gasPrice, 66 | value, 67 | data, 68 | }) 69 | 70 | await awaitTransactionMined(networkUrl, tx, confirmationBlocks) 71 | 72 | cli.styledJSON(tx) 73 | } catch (e) { 74 | this.error(e.message, { exit: 1 }) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'repl.history' 2 | declare module 'web3-providers-http' 3 | declare module 'web3-eth-accounts' 4 | 5 | declare type Maybe = T | null 6 | -------------------------------------------------------------------------------- /src/flags.ts: -------------------------------------------------------------------------------- 1 | import { flags } from '@oclif/command' 2 | 3 | export const privateKeyFlag = flags.string({ 4 | description: 5 | 'Private key to unlock. Can also be specified using the ETH_CLI_PRIVATE_KEY environment variable.', 6 | env: 'ETH_CLI_PRIVATE_KEY', 7 | }) 8 | 9 | export const confirmationBlocksFlag = flags.integer({ 10 | default: 0, 11 | description: 'Number of confirmation blocks to wait for before the command returns.', 12 | }) 13 | -------------------------------------------------------------------------------- /src/helpers/abi/erc20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /src/helpers/abi/erc721.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [{ "name": "_name", "type": "string" }], 7 | "payable": false, 8 | "stateMutability": "view", 9 | "type": "function" 10 | }, 11 | { 12 | "constant": true, 13 | "inputs": [{ "name": "_tokenId", "type": "uint256" }], 14 | "name": "getApproved", 15 | "outputs": [{ "name": "_approved", "type": "address" }], 16 | "payable": false, 17 | "stateMutability": "view", 18 | "type": "function" 19 | }, 20 | { 21 | "constant": false, 22 | "inputs": [{ "name": "_to", "type": "address" }, { "name": "_tokenId", "type": "uint256" }], 23 | "name": "approve", 24 | "outputs": [], 25 | "payable": false, 26 | "stateMutability": "nonpayable", 27 | "type": "function" 28 | }, 29 | { 30 | "constant": true, 31 | "inputs": [], 32 | "name": "implementsERC721", 33 | "outputs": [{ "name": "_implementsERC721", "type": "bool" }], 34 | "payable": false, 35 | "stateMutability": "view", 36 | "type": "function" 37 | }, 38 | { 39 | "constant": true, 40 | "inputs": [], 41 | "name": "totalSupply", 42 | "outputs": [{ "name": "_totalSupply", "type": "uint256" }], 43 | "payable": false, 44 | "stateMutability": "view", 45 | "type": "function" 46 | }, 47 | { 48 | "constant": false, 49 | "inputs": [ 50 | { "name": "_from", "type": "address" }, 51 | { "name": "_to", "type": "address" }, 52 | { "name": "_tokenId", "type": "uint256" } 53 | ], 54 | "name": "transferFrom", 55 | "outputs": [], 56 | "payable": false, 57 | "stateMutability": "nonpayable", 58 | "type": "function" 59 | }, 60 | { 61 | "constant": true, 62 | "inputs": [{ "name": "_owner", "type": "address" }, { "name": "_index", "type": "uint256" }], 63 | "name": "tokenOfOwnerByIndex", 64 | "outputs": [{ "name": "_tokenId", "type": "uint256" }], 65 | "payable": false, 66 | "stateMutability": "view", 67 | "type": "function" 68 | }, 69 | { 70 | "constant": true, 71 | "inputs": [{ "name": "_tokenId", "type": "uint256" }], 72 | "name": "ownerOf", 73 | "outputs": [{ "name": "_owner", "type": "address" }], 74 | "payable": false, 75 | "stateMutability": "view", 76 | "type": "function" 77 | }, 78 | { 79 | "constant": true, 80 | "inputs": [{ "name": "_tokenId", "type": "uint256" }], 81 | "name": "tokenMetadata", 82 | "outputs": [{ "name": "_infoUrl", "type": "string" }], 83 | "payable": false, 84 | "stateMutability": "view", 85 | "type": "function" 86 | }, 87 | { 88 | "constant": true, 89 | "inputs": [{ "name": "_owner", "type": "address" }], 90 | "name": "balanceOf", 91 | "outputs": [{ "name": "_balance", "type": "uint256" }], 92 | "payable": false, 93 | "stateMutability": "view", 94 | "type": "function" 95 | }, 96 | { 97 | "constant": false, 98 | "inputs": [ 99 | { "name": "_owner", "type": "address" }, 100 | { "name": "_tokenId", "type": "uint256" }, 101 | { "name": "_approvedAddress", "type": "address" }, 102 | { "name": "_metadata", "type": "string" } 103 | ], 104 | "name": "mint", 105 | "outputs": [], 106 | "payable": false, 107 | "stateMutability": "nonpayable", 108 | "type": "function" 109 | }, 110 | { 111 | "constant": true, 112 | "inputs": [], 113 | "name": "symbol", 114 | "outputs": [{ "name": "_symbol", "type": "string" }], 115 | "payable": false, 116 | "stateMutability": "view", 117 | "type": "function" 118 | }, 119 | { 120 | "constant": false, 121 | "inputs": [{ "name": "_to", "type": "address" }, { "name": "_tokenId", "type": "uint256" }], 122 | "name": "transfer", 123 | "outputs": [], 124 | "payable": false, 125 | "stateMutability": "nonpayable", 126 | "type": "function" 127 | }, 128 | { 129 | "constant": true, 130 | "inputs": [], 131 | "name": "numTokensTotal", 132 | "outputs": [{ "name": "", "type": "uint256" }], 133 | "payable": false, 134 | "stateMutability": "view", 135 | "type": "function" 136 | }, 137 | { 138 | "constant": true, 139 | "inputs": [{ "name": "_owner", "type": "address" }], 140 | "name": "getOwnerTokens", 141 | "outputs": [{ "name": "_tokenIds", "type": "uint256[]" }], 142 | "payable": false, 143 | "stateMutability": "view", 144 | "type": "function" 145 | }, 146 | { 147 | "anonymous": false, 148 | "inputs": [ 149 | { "indexed": true, "name": "_to", "type": "address" }, 150 | { "indexed": true, "name": "_tokenId", "type": "uint256" } 151 | ], 152 | "name": "Mint", 153 | "type": "event" 154 | }, 155 | { 156 | "anonymous": false, 157 | "inputs": [ 158 | { "indexed": true, "name": "_from", "type": "address" }, 159 | { "indexed": true, "name": "_to", "type": "address" }, 160 | { "indexed": false, "name": "_tokenId", "type": "uint256" } 161 | ], 162 | "name": "Transfer", 163 | "type": "event" 164 | }, 165 | { 166 | "anonymous": false, 167 | "inputs": [ 168 | { "indexed": true, "name": "_owner", "type": "address" }, 169 | { "indexed": true, "name": "_approved", "type": "address" }, 170 | { "indexed": false, "name": "_tokenId", "type": "uint256" } 171 | ], 172 | "name": "Approval", 173 | "type": "event" 174 | } 175 | ] 176 | -------------------------------------------------------------------------------- /src/helpers/checkCommandInputs.ts: -------------------------------------------------------------------------------- 1 | export const hasFlags = (flags: object) => { 2 | return Object.keys(flags).length 3 | } 4 | 5 | export const hasArgs = (args: object) => { 6 | return Object.values(args).filter(v => v !== null).length 7 | } 8 | 9 | // Checks for args or flags supplied to command 10 | export const isEmptyCommand = (flags: object, args: object) => { 11 | if (!hasFlags(flags) && !hasArgs(args)) { 12 | return true 13 | } 14 | return false 15 | } 16 | -------------------------------------------------------------------------------- /src/helpers/config-service.ts: -------------------------------------------------------------------------------- 1 | import { Interface } from '@ethersproject/abi' 2 | import Conf from 'conf' 3 | import * as fs from 'fs' 4 | import _ from 'lodash' 5 | import * as path from 'path' 6 | import { keccak } from 'ethereumjs-util' 7 | 8 | import { Networks, defaultNetworks } from './networks' 9 | import { ABI, ABIInput, ABIItem, Contract } from '../types' 10 | import { Abis, knownAbis } from './knownAbis' 11 | import { add0x } from './utils' 12 | 13 | export class ConfigService { 14 | config: Conf 15 | 16 | constructor(config: Conf) { 17 | this.config = config 18 | } 19 | 20 | getPrivateKey = (pk: string, networkId: number) => { 21 | const addresses = this.config.get('addresses', {}) 22 | 23 | // if it's a known address, use its private key; throw error if it doesn't have one 24 | // otherwise, interpret the parameter as a private key 25 | const knownAddress = addresses[pk] 26 | 27 | if (!knownAddress) { 28 | return add0x(pk) 29 | } 30 | 31 | const knownAddressForNetwork = knownAddress[networkId] || knownAddress['*'] 32 | 33 | if (!knownAddressForNetwork) { 34 | throw new Error(`Selected address doesn't exist on network ${networkId}`) 35 | } 36 | 37 | if (knownAddressForNetwork.privateKey) { 38 | return add0x(knownAddressForNetwork.privateKey) 39 | } else { 40 | throw new Error("Selected address doesn't have a known private key") 41 | } 42 | } 43 | 44 | updateAbis = (abis: any): void => { 45 | this.config.set('abis', abis) 46 | } 47 | 48 | getAddresses = () => { 49 | return this.config.get('addresses', {}) 50 | } 51 | 52 | updateAddresses = (addresses: any) => { 53 | this.config.set('addresses', addresses) 54 | } 55 | 56 | getContracts = () => { 57 | return this.config.get('contracts', {}) 58 | } 59 | 60 | updateContracts = (contracts: any) => { 61 | this.config.set('contracts', contracts) 62 | } 63 | 64 | addContract = (name: string, abi: string, address: string) => { 65 | const contracts = this.getContracts() 66 | 67 | this.updateContracts({ 68 | ...contracts, 69 | [name]: { abi, address }, 70 | }) 71 | } 72 | 73 | removeContract = (name: string): boolean => { 74 | const contracts = this.getContracts() 75 | 76 | if (!contracts[name]) { 77 | return false 78 | } 79 | 80 | delete contracts[name] 81 | 82 | this.updateContracts(contracts) 83 | 84 | return true 85 | } 86 | 87 | getContract = (name: string): Maybe<{ abi: string; address: string }> => { 88 | const contracts = this.getContracts() 89 | 90 | return contracts[name] || null 91 | } 92 | 93 | getAddress = (name: string, networkId: number) => { 94 | const addresses = this.getAddresses() 95 | 96 | if (addresses[name]) { 97 | if (networkId && addresses[name][networkId]) { 98 | return add0x(addresses[name][networkId].address) 99 | } else if (addresses[name]['*']) { 100 | return add0x(addresses[name]['*'].address) 101 | } else { 102 | throw new Error(`No known address named ${name}`) 103 | } 104 | } else { 105 | return add0x(name) 106 | } 107 | } 108 | 109 | getNetworks = (): Networks => { 110 | const addedNetworks = this.config.get('networks', {}) 111 | 112 | return { ...defaultNetworks, ...addedNetworks } 113 | } 114 | 115 | getAbis = () => { 116 | const addedAbis: Abis = this.config.get('abis', {}) 117 | return { ...knownAbis, ...addedAbis } 118 | } 119 | 120 | getAbiByName = (name: string): object | null => { 121 | const knownAbis = this.getAbis() 122 | if (knownAbis[name]) { 123 | return knownAbis[name] 124 | } 125 | return null 126 | } 127 | 128 | getStringAbiByName = (abiName: string): string | null => { 129 | const abiObj = this.getAbiByName(abiName) 130 | if (abiObj) { 131 | return JSON.stringify(abiObj) 132 | } 133 | return abiObj 134 | } 135 | 136 | getAbiList = (): string[] => { 137 | const knownAbis = this.getAbis() 138 | return Object.keys(knownAbis) 139 | } 140 | 141 | updateNetworks = (networks: Networks): void => { 142 | this.config.set('networks', networks) 143 | } 144 | 145 | loadContract = (contract: string, networkId: number): Contract => { 146 | let abiArg: string 147 | let addressArg: string 148 | let name: Maybe = null 149 | if (contract.indexOf('@') !== -1) { 150 | ;[abiArg, addressArg] = ConfigService.parseAbiAtAddress(contract) 151 | } else { 152 | const knownContract = this.getContract(contract) 153 | name = _.camelCase(contract) 154 | if (!knownContract) { 155 | throw new Error( 156 | `Unknown contract ${contract}, add one with contract:add or use abi@address syntax`, 157 | ) 158 | } 159 | 160 | abiArg = knownContract.abi 161 | addressArg = knownContract.address 162 | } 163 | 164 | const { abi, name: defaultName } = this.loadABI(abiArg) 165 | const address = this.getAddress(addressArg, networkId) 166 | 167 | return { 168 | abi, 169 | address, 170 | name: name || defaultName, 171 | } 172 | } 173 | 174 | static parseAbiAtAddress = (abiAtAddress: string): [string, string] => { 175 | const [abi, address] = abiAtAddress.split('@') 176 | if (!abi || !address) { 177 | throw new Error(`Invalid argument '${abiAtAddress}', expected @`) 178 | } 179 | 180 | return [abi, address] 181 | } 182 | 183 | loadABI = (abiPath: string): { abi: any; name: string } => { 184 | // Try to get the abi from the default list of supported abi's 185 | let abiStr: string | null = this.getStringAbiByName(abiPath) 186 | let name: Maybe = null 187 | 188 | if (abiStr) { 189 | name = _.camelCase(abiPath) 190 | } else { 191 | if (fs.existsSync(abiPath)) { 192 | // If not found, check if the given string is a path to a file 193 | const [filename] = path.basename(abiPath).split('.') 194 | name = _.camelCase(filename) 195 | abiStr = fs.readFileSync(abiPath).toString() 196 | } else { 197 | // if it's not a file, check if it's a human readable ABI 198 | name = 'contract' 199 | const iface: any = new Interface([abiPath]) 200 | 201 | // fix for null components 202 | const fragment = JSON.parse(JSON.stringify(iface.fragments[0])) 203 | const inputs = fragment.inputs.map((x: any) => { 204 | if (x.components === null) { 205 | delete x.components 206 | } 207 | return x 208 | }) 209 | const outputs = fragment.outputs 210 | ? fragment.outputs.map((x: any) => { 211 | if (x.components === null) { 212 | delete x.components 213 | } 214 | return x 215 | }) 216 | : [] 217 | const fixedFragment = { 218 | ...fragment, 219 | inputs, 220 | outputs, 221 | } 222 | abiStr = JSON.stringify([fixedFragment]) 223 | } 224 | } 225 | let abi = null 226 | 227 | try { 228 | abi = JSON.parse(abiStr) 229 | 230 | // Allow using truffle artifacts files too. 231 | // If abi variable it's an object and it has an abi property, interpret it as a truffle artifact 232 | if (abi.abi) { 233 | abi = abi.abi 234 | } 235 | } catch (e) { 236 | console.error('Error parsing abi', e) 237 | process.exit(1) 238 | } 239 | 240 | return { abi, name } 241 | } 242 | 243 | getMethodsAndEvents = (abiPath: string) => { 244 | const { abi } = this.loadABI(abiPath) 245 | 246 | const methods = ConfigService.extractMethodsAndEventsFromABI(abi).map( 247 | ({ name, inputs, kind }) => { 248 | const params = inputs.map((x: ABIInput) => x.type).join(',') 249 | const signature = `${name}(${params})` 250 | const signatureHash = keccak(signature) 251 | .toString('hex') 252 | .slice(0, 8) 253 | 254 | return { signature, signatureHash, kind } 255 | }, 256 | ) 257 | 258 | return methods 259 | } 260 | 261 | getMethods = (abiPath: string) => { 262 | return this.getMethodsAndEvents(abiPath).filter(x => x.kind === 'function') 263 | } 264 | 265 | getEvents = (abiPath: string) => { 266 | return this.getMethodsAndEvents(abiPath).filter(x => x.kind === 'event') 267 | } 268 | 269 | static extractMethodsAndEventsFromABI = ( 270 | abi: ABI, 271 | ): Array<{ name: string | undefined; inputs: any | undefined; kind: 'function' | 'event' }> => { 272 | return abi 273 | .filter((x: ABIItem) => x.type === 'function' || (x.type === 'event' && 'name' in x)) 274 | .map(({ name, inputs, type }) => ({ 275 | name, 276 | inputs, 277 | kind: type === 'function' ? 'function' : 'event', 278 | })) 279 | } 280 | } 281 | 282 | export const configService = new ConfigService(new Conf({ projectName: 'eth-cli' })) 283 | -------------------------------------------------------------------------------- /src/helpers/contractCall.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | import { ConfigService, configService } from './config-service' 4 | import { evaluateMethodCallStructure } from './utils' 5 | 6 | export async function contractCall( 7 | abi: any, 8 | methodCall: string, 9 | name: string, 10 | url: string, 11 | privateKeyOrKnownAddress?: string, 12 | extraParams?: { 13 | gas: string | undefined 14 | gasPrice: string 15 | value: number 16 | }, 17 | ) { 18 | const { methodValid, methodName } = evaluateMethodCallStructure(methodCall) 19 | 20 | if (!methodValid) { 21 | throw new Error('[contractCall] methodCall invalid structure') 22 | } 23 | 24 | const matchingMethods = ConfigService.extractMethodsAndEventsFromABI(abi).filter( 25 | x => x.name === methodName, 26 | ) 27 | 28 | if (matchingMethods.length > 1) { 29 | throw new Error('[contractCall] function overloading is not supported in the current version') 30 | } 31 | 32 | if (!matchingMethods.length) { 33 | throw new Error('[contractCall] method specified does not exist in the ABI file provided') 34 | } 35 | 36 | const web3 = new Web3(url) 37 | let address: string | null = null 38 | if (privateKeyOrKnownAddress) { 39 | const networkId = await web3.eth.net.getId() 40 | const privateKey = configService.getPrivateKey(privateKeyOrKnownAddress, networkId) 41 | const account = web3.eth.accounts.wallet.add(privateKey) 42 | address = account.address 43 | } 44 | const networkId = await web3.eth.net.getId() 45 | 46 | const contractAddress = configService.getAddress(name, networkId) 47 | 48 | // `contract` is being used as part of the eval call 49 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 50 | const contract = new web3.eth.Contract(abi, contractAddress) 51 | // eslint-disable-next-line no-eval 52 | const methodObject = eval(`contract.methods.${methodCall}`) 53 | 54 | if (address) { 55 | const gasPrice = extraParams ? extraParams.gasPrice : '1000000000' 56 | const txParams: any = { from: address, gasPrice } 57 | 58 | if (extraParams && extraParams.gas) { 59 | txParams.gas = extraParams.gas 60 | } else { 61 | const gas = await methodObject.estimateGas(txParams) 62 | txParams.gas = gas 63 | } 64 | if (extraParams && extraParams.value) { 65 | txParams.value = extraParams.value 66 | } 67 | 68 | return new Promise(resolve => { 69 | methodObject.send(txParams).once('transactionHash', resolve) 70 | }) 71 | } else { 72 | return methodObject.call() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/helpers/convert.ts: -------------------------------------------------------------------------------- 1 | import Big from 'big.js' 2 | 3 | import { Unit } from '../types' 4 | 5 | // avoid exponential notation in most cases 6 | Big.NE = -19 7 | 8 | export const stringToUnit = (unit: string): Maybe => { 9 | const unitLowerCased = unit.toLowerCase() 10 | if (unitLowerCased === Unit.Eth) { 11 | return Unit.Eth 12 | } else if (unitLowerCased === Unit.Gwei) { 13 | return Unit.Gwei 14 | } else if (unitLowerCased === Unit.Wei) { 15 | return Unit.Wei 16 | } 17 | 18 | return null 19 | } 20 | 21 | export const convert = (amount: string, from: Unit, to: Unit): string => { 22 | const amountBN = new Big(amount) 23 | 24 | let exp = 0 25 | if (from === Unit.Gwei) { 26 | exp -= 9 27 | } else if (from === Unit.Wei) { 28 | exp -= 18 29 | } 30 | if (to === Unit.Gwei) { 31 | exp += 9 32 | } else if (to === Unit.Wei) { 33 | exp += 18 34 | } 35 | 36 | const scale = new Big(10).pow(exp) 37 | 38 | if (to === Unit.Eth) { 39 | return amountBN.mul(scale).toString() 40 | } 41 | return amountBN.mul(scale).toFixed(0) 42 | } 43 | -------------------------------------------------------------------------------- /src/helpers/decodeTxData.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | import { add0x, isBN } from './utils' 4 | 5 | export function decodeTxData(functionSignature: string, txData: string) { 6 | const paramsRegex = /^[^(].*\((.*)\)$/ 7 | 8 | if (!paramsRegex.test(functionSignature)) { 9 | throw new Error('Invalid function signature') 10 | } 11 | 12 | const paramsMatch = functionSignature.match(paramsRegex) 13 | 14 | if (!paramsMatch || !paramsMatch[1]) { 15 | return [] 16 | } 17 | 18 | const web3 = new Web3('') 19 | const abi = web3.eth.abi 20 | const encodedFunctionSignature = abi.encodeFunctionSignature(functionSignature) 21 | 22 | txData = add0x(txData) 23 | 24 | const txDataRegex = new RegExp('^' + encodedFunctionSignature) 25 | 26 | if (!txDataRegex.test(txData)) { 27 | throw new Error('Function signature does not match the given transaction data') 28 | } 29 | 30 | const paramsStr = paramsMatch[1] 31 | const params = paramsStr.split(',') 32 | 33 | const args = abi.decodeParameters(params, add0x(txData.slice(10))) 34 | 35 | // args returns a 'EthAbiDecodeParametersResultArray', which is not exactly an array, so we have to do this 36 | const result = [] 37 | for (let i = 0; i < Object.keys(args).length; i++) { 38 | if (!args[i]) continue 39 | const value = isBN(args[i]) ? args[i].toString() : args[i] 40 | result.push(value) 41 | } 42 | 43 | return result 44 | } 45 | -------------------------------------------------------------------------------- /src/helpers/deploy.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import Web3 from 'web3' 3 | 4 | import { configService } from './config-service' 5 | import { add0x } from './utils' 6 | 7 | export async function deploy( 8 | url: string, 9 | privateKeyOrKnownAddress: string, 10 | abi: any[], 11 | binPath: string, 12 | constructorArguments: any[], 13 | ): Promise { 14 | const web3 = new Web3(url) 15 | const networkId = await web3.eth.net.getId() 16 | const privateKey = configService.getPrivateKey(privateKeyOrKnownAddress, networkId) 17 | 18 | const { address } = web3.eth.accounts.wallet.add(privateKey) 19 | 20 | const data = add0x(fs.readFileSync(binPath).toString()) 21 | 22 | const Contract: any = web3.eth.Contract // ts hack: transactionConfirmationBlocks is not a valid option 23 | const contract = new Contract(abi) 24 | 25 | const deploy = contract.deploy({ data, arguments: constructorArguments }) 26 | 27 | const gas = await deploy.estimateGas({ 28 | from: address, 29 | }) 30 | 31 | return new Promise((resolve, reject) => { 32 | deploy.send( 33 | { 34 | from: address, 35 | gas, 36 | }, 37 | (err: Error, txHash: string) => { 38 | if (err) { 39 | return reject(err) 40 | } 41 | return resolve(txHash) 42 | }, 43 | ) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /src/helpers/encode.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | import { evaluateMethodCallStructure } from './utils' 4 | import { ConfigService, configService } from './config-service' 5 | 6 | export function encode(abiPath: string, methodCall: string, url: string) { 7 | if (!methodCall) { 8 | throw new Error('[encode] methodCall required') 9 | } 10 | 11 | const { methodValid, methodName } = evaluateMethodCallStructure(methodCall) 12 | 13 | if (!methodValid) { 14 | throw new Error('[encode] methodCall invalid structure') 15 | } 16 | 17 | const { abi } = configService.loadABI(abiPath) 18 | const matchingMethods = ConfigService.extractMethodsAndEventsFromABI(abi).filter( 19 | (x: any) => x.name === methodName, 20 | ) 21 | 22 | if (matchingMethods.length > 1) { 23 | throw new Error('[encode] function overloading is not supported in the current version') 24 | } 25 | 26 | if (!matchingMethods.length) { 27 | throw new Error('[encode] method specified does not exist in the ABI file provided') 28 | } 29 | 30 | const web3 = new Web3(url) 31 | // `contract` is being used as part of the eval call 32 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 33 | const contract = new web3.eth.Contract(abi) 34 | // eslint-disable-next-line no-eval 35 | return eval(`contract.methods.${methodCall}.encodeABI()`) 36 | } 37 | -------------------------------------------------------------------------------- /src/helpers/generateNop.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | import { configService } from './config-service' 4 | 5 | export async function generateNop(url: string, privateKeyOrKnownAddress: string): Promise { 6 | const web3 = new Web3(url) 7 | const networkId = await web3.eth.net.getId() 8 | const privateKey = configService.getPrivateKey(privateKeyOrKnownAddress, networkId) 9 | 10 | const { address } = web3.eth.accounts.wallet.add(privateKey) 11 | 12 | return new Promise((resolve, reject) => { 13 | web3.eth 14 | .sendTransaction({ 15 | from: address, 16 | to: address, 17 | gas: 21000, 18 | }) 19 | .once('transactionHash', transactionHash => { 20 | resolve(transactionHash) 21 | }) 22 | .once('error', e => { 23 | reject(e) 24 | }) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/helpers/getBalance.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | import { configService } from './config-service' 4 | 5 | export const getBalance = async function(addressOrName: string, url: string) { 6 | // Connect web3 7 | const web3 = new Web3(url) 8 | const networkId = await web3.eth.net.getId() 9 | const address = configService.getAddress(addressOrName, networkId) 10 | 11 | return web3.eth.getBalance(address) 12 | } 13 | -------------------------------------------------------------------------------- /src/helpers/getBlockNumber.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | export const getBlockNumber = function(url: string) { 4 | if (!url) { 5 | throw new Error('[getBlockNumber] URL required') 6 | } 7 | 8 | // Connect web3 9 | const web3 = new Web3(url) 10 | 11 | return web3.eth.getBlockNumber() 12 | } 13 | -------------------------------------------------------------------------------- /src/helpers/getBlockObject.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | export const getBlock = function(blockNumber: number, url: string) { 4 | // Connect web3 5 | const web3 = new Web3(url) 6 | 7 | return web3.eth.getBlock(blockNumber) 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/getContractAddress.ts: -------------------------------------------------------------------------------- 1 | import { keccak } from 'ethereumjs-util' 2 | import * as rlp from 'rlp' 3 | 4 | import { add0x } from './utils' 5 | 6 | export function getContractAddress(_address: string, _nonce: string) { 7 | if (!_address) { 8 | throw new Error('address is required') 9 | } 10 | 11 | const address = add0x(_address) 12 | const nonce = Number(_nonce || 0) 13 | 14 | const contractAddress = keccak(rlp.encode([address, nonce])) 15 | .toString('hex') 16 | .slice(24) 17 | 18 | return add0x(contractAddress) 19 | } 20 | -------------------------------------------------------------------------------- /src/helpers/getEvents.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | import { EventLog } from 'web3/types' 3 | 4 | import { ConfigService, configService } from './config-service' 5 | 6 | export async function getEvents( 7 | abi: any, 8 | eventName: string, 9 | name: string, 10 | url: string, 11 | { from, to }: any, 12 | ) { 13 | const matchingEvents = ConfigService.extractMethodsAndEventsFromABI(abi).filter( 14 | x => x.name === eventName, 15 | ) 16 | 17 | if (!matchingEvents.length) { 18 | throw new Error('[getEvents] event specified does not exist in the ABI provided') 19 | } 20 | 21 | const eventAbi = matchingEvents[0] 22 | 23 | const web3 = new Web3(url) 24 | const networkId = await web3.eth.net.getId() 25 | 26 | const address = configService.getAddress(name, networkId) 27 | 28 | const contract = new web3.eth.Contract(abi, address) 29 | const events = await contract.getPastEvents(eventName, { fromBlock: from, toBlock: to }) 30 | 31 | return { eventAbi, events } 32 | } 33 | 34 | export const parseEvent = (event: EventLog, eventAbi: any) => { 35 | const returnValues = eventAbi.inputs.map((x: any, i: number) => event.returnValues[i]).join(',') 36 | const parsedEvent = `${eventAbi.name}(${returnValues})` 37 | const block = event.blockNumber 38 | return `#${block}: ${parsedEvent}` 39 | } 40 | 41 | export const processEvent = (event: EventLog, eventAbi: any) => { 42 | const eventJson = { 43 | blockNumber: event.blockNumber, 44 | transactionHash: event.transactionHash, 45 | name: eventAbi.name, 46 | arguments: {} as any, 47 | } 48 | 49 | Object.entries(event.returnValues).forEach(([key, value]) => { 50 | if (/\d+/.test(key)) { 51 | return 52 | } 53 | 54 | eventJson.arguments[key] = value 55 | }) 56 | 57 | return eventJson 58 | } 59 | -------------------------------------------------------------------------------- /src/helpers/getNetworkId.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | export const getNetworkId = function(url: string) { 4 | // Connect web3 5 | const web3 = new Web3(url) 6 | 7 | return web3.eth.net.getId() 8 | } 9 | -------------------------------------------------------------------------------- /src/helpers/getTransactionObject.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | 3 | export const getTransaction = function(transactionHash: string, url: string) { 4 | if (!transactionHash) { 5 | throw new Error('[getTransactionObject] txHash required') 6 | } 7 | 8 | if (!url) { 9 | throw new Error('[getTransactionObject] URL required') 10 | } 11 | 12 | // Connect web3 13 | const web3 = new Web3(url) 14 | 15 | return web3.eth.getTransaction(transactionHash) 16 | } 17 | 18 | export const getReceipt = function(transactionHash: string, url: string) { 19 | if (!transactionHash) { 20 | throw new Error('[getTransactionObject] txHash required') 21 | } 22 | 23 | if (!url) { 24 | throw new Error('[getTransactionObject] URL required') 25 | } 26 | // Connect web3 27 | const web3 = new Web3(url) 28 | 29 | return web3.eth.getTransactionReceipt(transactionHash) 30 | } 31 | -------------------------------------------------------------------------------- /src/helpers/knownAbis.ts: -------------------------------------------------------------------------------- 1 | import erc20Abi from './abi/erc20.json' 2 | import erc721Abi from './abi/erc721.json' 3 | 4 | export type Abis = { [name: string]: object } 5 | 6 | export const knownAbis: Abis = { 7 | erc20: erc20Abi, 8 | erc721: erc721Abi, 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/networks.ts: -------------------------------------------------------------------------------- 1 | import { NetworkInfo } from '../types' 2 | 3 | export type Networks = { [name: string]: NetworkInfo } 4 | 5 | export const defaultNetworks: Networks = { 6 | mainnet: { 7 | url: 'https://mainnet.infura.io/v3/76fb6c10f1584483a45a0a28e91b07ad', 8 | id: 1, 9 | label: 'Mainnet', 10 | }, 11 | ropsten: { 12 | url: 'https://ropsten.infura.io/v3/76fb6c10f1584483a45a0a28e91b07ad', 13 | id: 3, 14 | label: 'Ropsten', 15 | }, 16 | rinkeby: { 17 | url: 'https://rinkeby.infura.io/v3/76fb6c10f1584483a45a0a28e91b07ad', 18 | id: 4, 19 | label: 'Rinkeby', 20 | }, 21 | goerli: { 22 | url: 'https://goerli.infura.io/v3/76fb6c10f1584483a45a0a28e91b07ad', 23 | id: 5, 24 | label: 'Görli', 25 | }, 26 | kovan: { 27 | url: 'https://kovan.infura.io/v3/76fb6c10f1584483a45a0a28e91b07ad', 28 | id: 42, 29 | label: 'Kovan', 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /src/helpers/randomAddress.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | import { evaluatePrefix, generateAccount, range } from './utils' 3 | 4 | export const randomAddress = (amount: number, prefix: string) => { 5 | if (isNaN(amount) || amount === 0) { 6 | throw new Error('[random-address] amount must be an integer number and greater than 0') 7 | } 8 | 9 | const maybePrefix = evaluatePrefix(prefix) 10 | 11 | if (maybePrefix === null) { 12 | throw new Error('[random-address] prefix must be a valid hex value') 13 | } 14 | 15 | const findAccount = generateAccount(maybePrefix) 16 | 17 | return range(amount) 18 | .map(() => findAccount()) 19 | .map(({ address, privateKey }) => ({ address, privateKey })) 20 | } 21 | 22 | export const generateKeystore = async (privateKey: string, password: string) => { 23 | const web3 = new Web3() 24 | return web3.eth.accounts.encrypt(privateKey, password) 25 | } 26 | -------------------------------------------------------------------------------- /src/helpers/replHistory.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | export default function(repl: any, file: string) { 3 | try { 4 | repl.history = fs 5 | .readFileSync(file, 'utf-8') 6 | .split('\n') 7 | .reverse() 8 | repl.history.shift() 9 | repl.historyIndex = -1 // will be incremented before pop 10 | } catch (e) {} 11 | 12 | const fd = fs.openSync(file, 'a') 13 | const wstream = fs.createWriteStream(file, { 14 | fd: fd, 15 | }) 16 | wstream.on('error', function(err: any) { 17 | throw err 18 | }) 19 | 20 | repl.addListener('line', function(code: string) { 21 | if (code && code !== '.history') { 22 | wstream.write(code + '\n') 23 | } else { 24 | repl.historyIndex++ 25 | repl.history.pop() 26 | } 27 | }) 28 | 29 | process.on('exit', function() { 30 | fs.closeSync(fd) 31 | }) 32 | 33 | repl.commands['history'] = { 34 | help: 'Show the history', 35 | action: function() { 36 | const out: string[] = [] 37 | repl.history.forEach(function(v: string) { 38 | out.push(v) 39 | }) 40 | repl.outputStream.write(out.reverse().join('\n') + '\n') 41 | repl.displayPrompt() 42 | }, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/helpers/replStarter.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os' 2 | import * as path from 'path' 3 | import * as repl from 'repl' 4 | import replHistory from './replHistory' 5 | import * as vm from 'vm' 6 | import Web3 from 'web3' 7 | 8 | import { isBN } from './utils' 9 | 10 | const historyFile = path.join(os.homedir(), '.eth_cli_history') 11 | 12 | function isRecoverableError(error: Error) { 13 | if (error.name === 'SyntaxError') { 14 | return /^(Unexpected end of input|Unexpected token)/.test(error.message) 15 | } 16 | return false 17 | } 18 | 19 | function handleResult(result: any, callback: any) { 20 | if (result && result.then) { 21 | result 22 | .then((x: any) => { 23 | handleResult(x, callback) 24 | }) 25 | .catch((e: Error) => callback(e, null)) 26 | } else if (result && isBN(result)) { 27 | callback(null, result.toString()) 28 | } else if (result && result._method && result.call) { 29 | handleResult(result.call(), callback) 30 | } else { 31 | callback(null, result) 32 | } 33 | } 34 | 35 | export function replStarter(context: { [key: string]: any }, prompt: string): repl.REPLServer { 36 | const r = repl.start({ 37 | prompt, 38 | eval: (cmd, context, _, callback) => { 39 | try { 40 | const result = vm.runInContext(cmd, context, { 41 | displayErrors: false, 42 | }) 43 | 44 | handleResult(result, callback) 45 | } catch (e) { 46 | if (isRecoverableError(e)) { 47 | return callback(new repl.Recoverable(e), null) 48 | } 49 | 50 | callback(e, null) 51 | } 52 | }, 53 | }) 54 | 55 | r.context.Web3 = Web3 56 | 57 | for (const expose of Object.keys(context)) { 58 | r.context[expose] = context[expose] 59 | } 60 | 61 | replHistory(r, historyFile) 62 | 63 | return r 64 | } 65 | -------------------------------------------------------------------------------- /src/helpers/searchSignature.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | 3 | const baseUrl = 'https://www.4byte.directory/api/v1' 4 | 5 | export async function searchSignature(hexSignature: string) { 6 | const response = await fetch(`${baseUrl}/signatures/?hex_signature=${hexSignature}`, { 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | }) 11 | const data = await response.json() 12 | 13 | return data.results 14 | .sort((a: any, b: any) => a.id - b.id) 15 | .map((result: any) => result.text_signature) 16 | } 17 | -------------------------------------------------------------------------------- /src/helpers/sendRawTransaction.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | import { Tx } from 'web3/eth/types' 3 | 4 | import { configService } from './config-service' 5 | 6 | interface ExtraParams { 7 | gas?: string 8 | gasPrice: string 9 | value: string 10 | data: string 11 | } 12 | 13 | export async function sendRawTransaction( 14 | url: string, 15 | privateKeyOrKnownAddress: string, 16 | to: string, 17 | { gas, ...extraParams }: ExtraParams, 18 | ): Promise { 19 | const web3 = new Web3(url) 20 | const networkId = await web3.eth.net.getId() 21 | 22 | const privateKey = configService.getPrivateKey(privateKeyOrKnownAddress, networkId) 23 | 24 | const { address } = web3.eth.accounts.wallet.add(privateKey) 25 | 26 | const recipient = configService.getAddress(to, networkId) 27 | const tx: Tx = { from: address, to: recipient, ...extraParams } 28 | 29 | if (gas) { 30 | tx.gas = gas 31 | } else { 32 | const estimatedGas = await web3.eth.estimateGas(tx) 33 | tx.gas = estimatedGas 34 | } 35 | 36 | return new Promise((resolve, reject) => { 37 | web3.eth.sendTransaction(tx, (err: Error, txHash: string) => { 38 | if (err) { 39 | return reject(err) 40 | } 41 | return resolve(txHash) 42 | }) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/helpers/sendTransaction.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | import { Tx } from 'web3/eth/types' 3 | 4 | import { configService } from './config-service' 5 | 6 | export async function sendTransaction( 7 | data: string, 8 | contractAddress: string, 9 | privateKeyOrKnownAddress: string, 10 | url: string, 11 | ): Promise { 12 | const web3 = new Web3(url) 13 | const networkId = await web3.eth.net.getId() 14 | 15 | const privateKey = configService.getPrivateKey(privateKeyOrKnownAddress, networkId) 16 | contractAddress = configService.getAddress(contractAddress, networkId) 17 | 18 | const { address } = web3.eth.accounts.wallet.add(privateKey) 19 | 20 | const tx: Tx = { from: address, data, to: contractAddress } 21 | 22 | const gas = await web3.eth.estimateGas(tx) 23 | 24 | return new Promise((resolve, reject) => { 25 | tx.gas = gas 26 | web3.eth.sendTransaction(tx, (err: Error, txHash: string) => { 27 | if (err) { 28 | return reject(err) 29 | } 30 | return resolve(txHash) 31 | }) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/helpers/startRepl.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as os from 'os' 3 | import * as path from 'path' 4 | import Web3 from 'web3' 5 | 6 | import { configService } from './config-service' 7 | import { replStarter } from './replStarter' 8 | import { Contract } from '../types' 9 | 10 | interface ReplContext { 11 | [key: string]: any 12 | } 13 | 14 | export async function startRepl( 15 | url: string, 16 | prompt: string, 17 | contracts: Array, 18 | privateKeyOrKnownAddress: string | undefined, 19 | ) { 20 | if (!url) { 21 | throw new Error('[startRepl] URL require') 22 | } 23 | 24 | // Connect web3 25 | const web3 = new Web3(url) 26 | const networkId = await web3.eth.net.getId() 27 | 28 | // Default context 29 | const replContext: ReplContext = { 30 | web3, 31 | eth: web3.eth, 32 | } 33 | 34 | const accounts = await web3.eth.getAccounts() 35 | if (privateKeyOrKnownAddress) { 36 | const privateKey = configService.getPrivateKey(privateKeyOrKnownAddress, networkId) 37 | const account = web3.eth.accounts.wallet.add(privateKey) 38 | if (!accounts.includes(account.address)) { 39 | accounts.push(account.address) 40 | } 41 | } 42 | replContext.accounts = accounts 43 | 44 | const loadedContracts: { [name: string]: string } = {} 45 | 46 | const addContract = (contract: Contract, replContext: any) => { 47 | const transactionConfirmationBlocks = 3 48 | const options = { 49 | transactionConfirmationBlocks, 50 | } 51 | const Contract: any = web3.eth.Contract // ts hack: transactionConfirmationBlocks is not a valid option 52 | 53 | const contractInstance = new Contract(contract.abi, contract.address, options) 54 | 55 | let contractName = contract.name 56 | if (replContext[contractName]) { 57 | const suffix = Object.keys(replContext).filter(function(key) { 58 | return key.includes(contractName) 59 | }).length 60 | 61 | contractName = [contractName, '_', suffix].join('') 62 | } 63 | 64 | replContext[contractName] = contractInstance 65 | loadedContracts[contractName] = contract.address 66 | } 67 | 68 | // Add contracts into context 69 | for (const contract of contracts) { 70 | addContract(contract, replContext) 71 | } 72 | 73 | // Start REPL 74 | const r = replStarter(replContext, prompt) 75 | 76 | // run init file 77 | const replInitFile = path.join(os.homedir(), '.eth_cli_repl_init.js') 78 | 79 | if (fs.existsSync(replInitFile)) { 80 | const replInit = (await import(replInitFile)).default 81 | replInit(r.context) 82 | } 83 | 84 | r.defineCommand('contracts', { 85 | help: 'Show loaded contracts', 86 | action() { 87 | this.clearBufferedCommand() 88 | for (const [loadedContract, address] of Object.entries(loadedContracts)) { 89 | console.log(`${loadedContract} (${address})`) 90 | } 91 | this.displayPrompt() 92 | }, 93 | }) 94 | 95 | r.defineCommand('loadc', { 96 | help: 'Load a contract using the specified ABI and address', 97 | action(abiAndAddress: string) { 98 | this.clearBufferedCommand() 99 | const contract = configService.loadContract(abiAndAddress, networkId) 100 | addContract(contract, r.context) 101 | this.displayPrompt() 102 | }, 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /src/helpers/transactions.ts: -------------------------------------------------------------------------------- 1 | import ora from 'ora' 2 | import Web3 from 'web3' 3 | import { TransactionReceipt } from 'web3/types' 4 | 5 | import { sleep } from './utils' 6 | 7 | export const awaitTransactionMined = async ( 8 | networkUrl: string, 9 | txHash: string, 10 | confirmationBlocks: number, 11 | ): Promise => { 12 | const web3 = new Web3(networkUrl) 13 | 14 | const spinner = ora('Waiting for transaction to be mined').start() 15 | 16 | // wait until mined 17 | let receipt: TransactionReceipt | null = null 18 | while (true) { 19 | receipt = await web3.eth.getTransactionReceipt(txHash) 20 | 21 | if (receipt) { 22 | break 23 | } 24 | 25 | await sleep(1000) 26 | } 27 | 28 | // wait until the specified number of blocks has passed 29 | while (true) { 30 | const blockNumber = await web3.eth.getBlockNumber() 31 | 32 | const blocksSinceMined = blockNumber - receipt.blockNumber 33 | 34 | if (blocksSinceMined >= 0) { 35 | spinner.text = `${blocksSinceMined}/${confirmationBlocks} confirmations` 36 | 37 | if (blocksSinceMined >= confirmationBlocks) { 38 | break 39 | } 40 | } 41 | 42 | await sleep(1000) 43 | } 44 | 45 | spinner.stop() 46 | 47 | return receipt 48 | } 49 | -------------------------------------------------------------------------------- /src/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto' 2 | import Accounts from 'web3-eth-accounts' 3 | import HttpProvider from 'web3-providers-http' 4 | 5 | export const add0x = (hex: string) => { 6 | return hex.indexOf('0x') === 0 ? hex : `0x${hex}` 7 | } 8 | 9 | /** 10 | * Evaluates a method call structure and returns an object with the information validated 11 | */ 12 | export const evaluateMethodCallStructure = (methodCall: string) => { 13 | const isValidMethod = /^(\w+)\((.*)\)$/ 14 | const method = isValidMethod.exec(methodCall) 15 | 16 | return { 17 | methodCall, 18 | methodName: method ? method[1] : null, 19 | methodArgs: method ? method[2] : null, 20 | methodValid: !!method, 21 | } 22 | } 23 | 24 | const createAccount = () => { 25 | const { wallet } = new Accounts(new HttpProvider('')) 26 | 27 | return wallet.create(1, randomBytes(32).toString('hex'))[0] 28 | } 29 | 30 | export const generateAccount = (prefix: string) => () => { 31 | let account = createAccount() 32 | 33 | while (account.address.slice(2, 2 + prefix.length) !== prefix) { 34 | account = createAccount() 35 | } 36 | 37 | return account 38 | } 39 | 40 | export const range = (amount: number) => new Array(amount).fill(true) 41 | 42 | export const evaluatePrefix = (prefix: string) => { 43 | const isValidPrefix = /(^$|^[a-fA-F0-9]+)$/ 44 | const match = isValidPrefix.exec(prefix) 45 | 46 | return match ? match[1] : null 47 | } 48 | 49 | export const isBN = (x: any) => x._hex !== undefined 50 | 51 | export const isPrivateKey = (s: string) => { 52 | return /^(0x)?[0-9a-fA-F]{64}$/.test(s) 53 | } 54 | 55 | export const isAddress = (s: string) => { 56 | return /^(0x)?[0-9a-fA-F]{40}$/.test(s) 57 | } 58 | 59 | export const sleep = (timeout: number) => new Promise(res => setTimeout(res, timeout)) 60 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { run } from '@oclif/command' 2 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type ABI = ABIItem[] 2 | 3 | export interface ABIItem { 4 | type: string 5 | name?: string 6 | inputs: ABIInput[] 7 | } 8 | 9 | export interface ABIInput { 10 | name: string 11 | type: string 12 | components: any 13 | } 14 | 15 | export type Contract = { 16 | abi: any 17 | address: string 18 | name: string 19 | } 20 | 21 | export interface NetworkInfo { 22 | url: string 23 | id?: number 24 | label?: string 25 | } 26 | 27 | export enum Unit { 28 | Eth = 'eth', 29 | Gwei = 'gwei', 30 | Wei = 'wei', 31 | } 32 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | "node": true, 4 | "jest": true 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /test/commands/abi/__snapshots__/methods.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`methods Should run 'methods ./test/files/contracts/Proxy.abi' and success. 1`] = ` 4 | Array [ 5 | "06fdde03 name() 6 | ", 7 | "085d4883 provider() 8 | ", 9 | "095ea7b3 approve(address,uint256) 10 | ", 11 | "18160ddd totalSupply() 12 | ", 13 | "23b872dd transferFrom(address,address,uint256) 14 | ", 15 | "2cd18565 proxy_admin() 16 | ", 17 | "2d37e6dc getCrowdsaleInfo() 18 | ", 19 | "313ce567 decimals() 20 | ", 21 | "40556191 getCrowdsaleWhitelist() 22 | ", 23 | "4bc091a3 getCrowdsaleStartAndEndTimes() 24 | ", 25 | "56f7cafe registry_exec_id() 26 | ", 27 | "5cad7cfb getCrowdsaleStatus() 28 | ", 29 | "66188463 decreaseApproval(address,uint256) 30 | ", 31 | "6a73820e app_name() 32 | ", 33 | "6e9960c3 getAdmin() 34 | ", 35 | "70a08231 balanceOf(address) 36 | ", 37 | "792651b0 app_storage() 38 | ", 39 | "95d89b41 symbol() 40 | ", 41 | "a6f2ae3a buy() 42 | ", 43 | "a9059cbb transfer(address,uint256) 44 | ", 45 | "d5d09021 isCrowdsaleFull() 46 | ", 47 | "d73dd623 increaseApproval(address,uint256) 48 | ", 49 | "d76ecb87 app_exec_id() 50 | ", 51 | "d9ba32fc getWhitelistStatus(address) 52 | ", 53 | "d9cb8735 app_index() 54 | ", 55 | "db2e1741 app_version() 56 | ", 57 | "dd62ed3e allowance(address,address) 58 | ", 59 | "ee7c0db0 getTokensSold() 60 | ", 61 | "f716c400 getCrowdsaleUniqueBuyers() 62 | ", 63 | "cfcd8c2d init(address,uint256,uint256,uint256,uint256,uint256,uint256,bool,address,bool) 64 | ", 65 | "7185393c updateAppExec(address) 66 | ", 67 | "3e7f54a9 updateAppInstance() 68 | ", 69 | "55f86501 exec(bytes) 70 | ", 71 | ] 72 | `; 73 | -------------------------------------------------------------------------------- /test/commands/abi/methods.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import MethodsCommand from '../../../src/commands/abi/methods' 4 | 5 | describe('methods', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'methods' with empty args and flags and throw an error.`, async () => { 18 | await expect(MethodsCommand.run()).rejects.toThrow() 19 | }) 20 | 21 | it(`Should run 'methods --help' and throw an error.`, async () => { 22 | await expect(MethodsCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 23 | }) 24 | 25 | it(`Should run 'methods ./test/files/contracts/Proxy.abi' and success.`, async () => { 26 | await MethodsCommand.run(['./test/files/contracts/Proxy.abi']) 27 | 28 | expect(stdoutResult).toMatchSnapshot() 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/commands/abi/show.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import ShowCommand from '../../../src/commands/abi/show' 4 | import erc20Abi from '../../../src/helpers/abi/erc20.json' 5 | import erc721Abi from '../../../src/helpers/abi/erc721.json' 6 | 7 | describe('abi:show', () => { 8 | let stdoutResult: any 9 | 10 | beforeEach(() => { 11 | stdoutResult = [] 12 | jest 13 | .spyOn(process.stdout, 'write') 14 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 15 | }) 16 | 17 | afterEach(() => jest.restoreAllMocks()) 18 | 19 | it(`Should run 'abi:show --help' and throw an error.`, async () => { 20 | await expect(ShowCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 21 | }) 22 | 23 | it(`Should run 'abi:show ERC20' and receive the ERC20 abi.`, async () => { 24 | await ShowCommand.run(['erc20']) 25 | const result = JSON.parse(stdoutResult[0]) 26 | expect(result).toMatchObject(erc20Abi) 27 | }) 28 | 29 | it(`Should run 'abi:show ERC721' and receive the ERC721 abi.`, async () => { 30 | await ShowCommand.run(['erc721']) 31 | const result = JSON.parse(stdoutResult[0]) 32 | expect(result).toMatchObject(erc721Abi) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/commands/address/random.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import { RandomCommand } from '../../../src/commands/address/random' 4 | 5 | describe('address:random', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'randomAddress' and success.`, async () => { 18 | await RandomCommand.run([]) 19 | expect(stdoutResult[0]).toMatch(/address/) 20 | expect(stdoutResult[0]).toMatch(/privateKey/) 21 | expect(stdoutResult.length).toBe(1) 22 | }) 23 | 24 | it(`Should run 'randomAddress 10' and success.`, async () => { 25 | await RandomCommand.run(['10']) 26 | 27 | expect(stdoutResult.length).toBe(10) 28 | }) 29 | 30 | it(`Should run 'randomAddress 2 1' and success.`, async () => { 31 | await RandomCommand.run(['2', '--prefix', '1']) 32 | 33 | expect(stdoutResult.length).toBe(2) 34 | expect(stdoutResult[0]).toMatch(/0x1/) 35 | }, 60000) 36 | }) 37 | -------------------------------------------------------------------------------- /test/commands/contract/__snapshots__/address.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`contract:address Should run 'contract:address --bar' and be get some value. 1`] = ` 4 | Array [ 5 | "0xe01c10fd900939d1eab56ee373ea5e2bd4e2cfb3 6 | ", 7 | ] 8 | `; 9 | 10 | exports[`contract:address Should run 'contract:address 0x92970dbD5C0Ee6b439422bFd7cD71e1DDA921A03' and get success. 1`] = ` 11 | Array [ 12 | "0x68ba6833b22f414e97a7aa23908428163be96e9b 13 | ", 14 | ] 15 | `; 16 | -------------------------------------------------------------------------------- /test/commands/contract/address.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import AddressCommand from '../../../src/commands/contract/address' 4 | 5 | describe('contract:address', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'contract:address' with empty args and flags and throw an error.`, async () => { 18 | await expect(AddressCommand.run()).rejects.toThrow() 19 | }) 20 | 21 | it(`Should run 'contract:address' and throw an error.`, async () => { 22 | await expect(AddressCommand.run([])).rejects.toThrow() 23 | }) 24 | 25 | it(`Should run 'contract:address --help' and throw an error.`, async () => { 26 | await expect(AddressCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 27 | }) 28 | 29 | it(`Should run 'contract:address --bar' and be get some value.`, async () => { 30 | await AddressCommand.run(['--bar']) 31 | expect(stdoutResult).toMatchSnapshot() 32 | }) 33 | 34 | it(`Should run 'contract:address 0x92970dbD5C0Ee6b439422bFd7cD71e1DDA921A03' and get success.`, async () => { 35 | await AddressCommand.run(['0x92970dbD5C0Ee6b439422bFd7cD71e1DDA921A03']) 36 | expect(stdoutResult).toMatchSnapshot() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/commands/contract/deploy.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import { DeployCommand } from '../../../src/commands/contract/deploy' 4 | // const Web3 = require('web3') 5 | 6 | describe('contract:deploy', () => { 7 | let stdoutResult: any 8 | 9 | beforeEach(() => { 10 | stdoutResult = [] 11 | jest 12 | .spyOn(process.stdout, 'write') 13 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 14 | }) 15 | 16 | afterEach(() => jest.restoreAllMocks()) 17 | 18 | it(`Should run 'contract:deploy --help' and throw an error.`, async () => { 19 | await expect(DeployCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/commands/contract/index.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import ContractIndexCommand from '../../../src/commands/contract/index' 4 | 5 | describe('contract', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'contract' with empty args and flags and throw an error.`, async () => { 18 | await expect(ContractIndexCommand.run()).rejects.toThrow() 19 | }) 20 | 21 | it(`Should run 'contract' and throw an error.`, async () => { 22 | await expect(ContractIndexCommand.run([])).rejects.toThrow('EEXIT: 0') 23 | }) 24 | 25 | it(`Should run 'contract --help' and throw an error.`, async () => { 26 | await expect(ContractIndexCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 27 | }) 28 | 29 | it(`Should run 'contract --bar' and throw an error.`, async () => { 30 | await expect(ContractIndexCommand.run(['--bar'])).rejects.toThrow( 31 | 'Unexpected argument: --bar\n' + 'See more help with --help', 32 | ) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/commands/method/__snapshots__/decode.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`decode Should run 'decode 'transfer(address,uint256)' 4 | '0xa9059cbb000000000000000000000000697dB915674bAc602F4d6fAfA31c0e45f386416E00000000000000000000000000000000000000000000000000000004ff043b9e'. 1`] = ` 5 | Array [ 6 | "[ 7 | \\"0x697dB915674bAc602F4d6fAfA31c0e45f386416E\\", 8 | \\"21458336670\\" 9 | ] 10 | ", 11 | ] 12 | `; 13 | -------------------------------------------------------------------------------- /test/commands/method/__snapshots__/encode.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`encode Should run 'encode' and get success 1`] = ` 4 | Array [ 5 | "0x3e7f54a9 6 | ", 7 | ] 8 | `; 9 | -------------------------------------------------------------------------------- /test/commands/method/__snapshots__/hash.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`hash Should run 'hash -- 'updateAppInstance'' and get success 1`] = ` 4 | Array [ 5 | "2ab3e7c9 6 | ", 7 | ] 8 | `; 9 | -------------------------------------------------------------------------------- /test/commands/method/decode.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import DecodeCommand from '../../../src/commands/method/decode' 4 | 5 | describe('decode', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'decode' with empty args and flags and throw an error.`, async () => { 18 | await expect(DecodeCommand.run()).rejects.toThrow() 19 | }) 20 | 21 | it(`Should run 'decode --help' and throw an error.`, async () => { 22 | await expect(DecodeCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 23 | }) 24 | 25 | it(`Should run 'decode 'transfer(address,uint256)' 26 | '0xa9059cbb000000000000000000000000697dB915674bAc602F4d6fAfA31c0e45f386416E00000000000000000000000000000000000000000000000000000004ff043b9e'.`, async () => { 27 | await DecodeCommand.run([ 28 | 'transfer(address,uint256)', 29 | '0xa9059cbb000000000000000000000000697dB915674bAc602F4d6fAfA31c0e45f386416E00000000000000000000000000000000000000000000000000000004ff043b9e', 30 | ]) 31 | 32 | expect(stdoutResult).toMatchSnapshot() 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/commands/method/encode.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import MethodEncodeCommand from '../../../src/commands/method/encode' 4 | 5 | describe('encode', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'encode' with empty args and flags and throw an error.`, async () => { 18 | await expect(MethodEncodeCommand.run()).rejects.toThrow() 19 | }) 20 | 21 | it(`Should run 'encode --help' and throw an error.`, async () => { 22 | await expect(MethodEncodeCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 23 | }) 24 | 25 | it(`Should run 'encode' and get success`, async () => { 26 | await MethodEncodeCommand.run(['./test/files/contracts/Proxy.abi', 'updateAppInstance()']) 27 | expect(stdoutResult).toMatchSnapshot() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/commands/method/hash.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import MethodHashCommand from '../../../src/commands/method/hash' 4 | 5 | describe('hash', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'hash' with empty args and flags and throw an error.`, async () => { 18 | await expect(MethodHashCommand.run()).rejects.toThrow() 19 | }) 20 | 21 | it(`Should run 'hash --help' and throw an error.`, async () => { 22 | await expect(MethodHashCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 23 | }) 24 | 25 | it(`Should run 'hash -- 'updateAppInstance'' and get success`, async () => { 26 | await MethodHashCommand.run(['updateAppInstance']) 27 | expect(stdoutResult).toMatchSnapshot() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/commands/method/index.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import MethodIndexCommand from '../../../src/commands/method/index' 4 | 5 | describe('method', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'method' with empty args and flags and throw an error.`, async () => { 18 | await expect(MethodIndexCommand.run()).rejects.toThrow() 19 | }) 20 | 21 | it(`Should run 'method' and throw an error.`, async () => { 22 | await expect(MethodIndexCommand.run([])).rejects.toThrow('EEXIT: 0') 23 | }) 24 | 25 | it(`Should run 'method --help' and throw an error.`, async () => { 26 | await expect(MethodIndexCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 27 | }) 28 | 29 | it(`Should run 'method --bar' and throw an error.`, async () => { 30 | await expect(MethodIndexCommand.run(['--bar'])).rejects.toThrow( 31 | 'Unexpected argument: --bar\n' + 'See more help with --help', 32 | ) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/commands/transaction/get.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import TransactionGetCommand from '../../../src/commands/transaction/get' 4 | 5 | describe('transaction:get', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'transaction:get' with empty args and flags and throw an error.`, async () => { 18 | await expect(TransactionGetCommand.run()).rejects.toThrow() 19 | }) 20 | 21 | it(`Should run 'transaction:get --help' and throw an error.`, async () => { 22 | await expect(TransactionGetCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 23 | }) 24 | 25 | it(`Should run 'transaction:get -n ropsten 0xc83836f1b3acac94a31de8e24c913aceaa9ebc51c93cd374429590596091584a' and match snapshot.`, async () => { 26 | await TransactionGetCommand.run([ 27 | '-n', 28 | 'ropsten', 29 | '0xc83836f1b3acac94a31de8e24c913aceaa9ebc51c93cd374429590596091584a', 30 | ]) 31 | expect(stdoutResult).toMatchSnapshot() 32 | }) 33 | 34 | it(`Should run 'transaction:get -n ropsten 83836f1b3acac94a31de8e24c913aceaa9ebc51c93cd374429590596091584a' and match snapshot.`, async () => { 35 | await expect( 36 | TransactionGetCommand.run([ 37 | '-n', 38 | 'ropsten', 39 | '83836f1b3acac94a31de8e24c913aceaa9ebc51c93cd374429590596091584a', 40 | ]), 41 | ).rejects.toThrow() 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/commands/transaction/index.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import TransactionindexCommand from '../../../src/commands/transaction/index' 4 | 5 | describe('transaction', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'transaction' with empty args and flags and throw an error.`, async () => { 18 | await expect(TransactionindexCommand.run()).rejects.toThrow() 19 | }) 20 | 21 | it(`Should run 'transaction' and throw an error.`, async () => { 22 | await expect(TransactionindexCommand.run([])).rejects.toThrow('EEXIT: 0') 23 | }) 24 | 25 | it(`Should run 'transaction --help' and throw an error.`, async () => { 26 | await expect(TransactionindexCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 27 | }) 28 | 29 | it(`Should run 'transaction --bar' and throw an error.`, async () => { 30 | await expect(TransactionindexCommand.run(['--bar'])).rejects.toThrow( 31 | 'Unexpected argument: --bar', 32 | ) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /test/commands/transaction/nop.spec.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi' 2 | 3 | import TransactionNopCommand from '../../../src/commands/transaction/nop' 4 | 5 | describe('transaction:nop', () => { 6 | let stdoutResult: any 7 | 8 | beforeEach(() => { 9 | stdoutResult = [] 10 | jest 11 | .spyOn(process.stdout, 'write') 12 | .mockImplementation(val => stdoutResult.push(stripAnsi(val.toString()))) 13 | }) 14 | 15 | afterEach(() => jest.restoreAllMocks()) 16 | 17 | it(`Should run 'transaction:nop' with empty args and flags and throw an error.`, async () => { 18 | await expect(TransactionNopCommand.run()).rejects.toThrow() 19 | }) 20 | 21 | it(`Should run 'transaction:nop --help' and throw an error.`, async () => { 22 | await expect(TransactionNopCommand.run(['--help'])).rejects.toThrow('EEXIT: 0') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/files/contracts/Proxy.abi: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"provider","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amt","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_amt","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"proxy_admin","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCrowdsaleInfo","outputs":[{"name":"","type":"uint256"},{"name":"","type":"address"},{"name":"","type":"uint256"},{"name":"","type":"bool"},{"name":"","type":"bool"},{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCrowdsaleWhitelist","outputs":[{"name":"","type":"uint256"},{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCrowdsaleStartAndEndTimes","outputs":[{"name":"","type":"uint256"},{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"registry_exec_id","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCrowdsaleStatus","outputs":[{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amt","type":"uint256"}],"name":"decreaseApproval","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"app_name","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getAdmin","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"app_storage","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"buy","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_amt","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"isCrowdsaleFull","outputs":[{"name":"","type":"bool"},{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amt","type":"uint256"}],"name":"increaseApproval","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"app_exec_id","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_buyer","type":"address"}],"name":"getWhitelistStatus","outputs":[{"name":"","type":"uint256"},{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"app_index","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"app_version","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getTokensSold","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"getCrowdsaleUniqueBuyers","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[{"name":"_storage","type":"address"},{"name":"_registry_exec_id","type":"bytes32"},{"name":"_provider","type":"address"},{"name":"_app_name","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"execution_id","type":"bytes32"},{"indexed":false,"name":"message","type":"string"}],"name":"StorageException","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"amt","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"amt","type":"uint256"}],"name":"Approval","type":"event"},{"constant":false,"inputs":[{"name":"","type":"address"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"uint256"},{"name":"","type":"bool"},{"name":"","type":"address"},{"name":"","type":"bool"}],"name":"init","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_new_exec_addr","type":"address"}],"name":"updateAppExec","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"updateAppInstance","outputs":[{"name":"success","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"_calldata","type":"bytes"}],"name":"exec","outputs":[{"name":"success","type":"bool"}],"payable":true,"stateMutability":"payable","type":"function"}] -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "importHelpers": true, 6 | "module": "commonjs", 7 | "outDir": "lib", 8 | "rootDir": "src", 9 | "strict": true, 10 | "target": "es2017", 11 | "composite": true, 12 | "resolveJsonModule": true 13 | }, 14 | "include": ["src/**/*", "src/**/*.json"] 15 | } 16 | --------------------------------------------------------------------------------