├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── README.md ├── bin └── main.js ├── package-lock.json ├── package.json ├── scripts ├── getBuiltIns.js └── getCompilerList.js └── src ├── blockchain.js ├── cli └── utils.js ├── compiler ├── autogenerated │ ├── builtIns.js │ └── solcVersions.js ├── remoteCompiler.js └── utils.js ├── handler.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | scripts -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes will be documented in this file. 3 | 4 | ## v0.2.4 5 | - new: configure an etherscan.io apiKey `.config set etherscanApiKey ` (default: `YourApiKeyToken` demo key) #25 #24 6 | 7 | ## v0.2.3 8 | - update: dependencies 9 | 10 | ## v0.2.2 11 | - new: inspection commands: `.inspect` contract raw storage, show generated bytecode, opcodes, storageLayout - #23 12 | 13 | image 14 | 15 | ``` 16 | .inspect 17 | bytecode ... show bytecode of underlying contract 18 | opcodes ... show disassembled opcodes of underlying contract 19 | storageLayout ... show variable to storage slot mapping for underlying contract 20 | storage [
] ... show raw storage at slot of underlying deployed contract 21 | deployed ... debug: show internal contract object 22 | ``` 23 | 24 | ## v0.2.1 25 | - fix: feed current compiler version into abi-to-sol; strip attribution and other code #20 #21 26 | - update: compiler list 27 | - update: built-in solc -> 0.8.16 28 | 29 | ## v0.2.0 30 | - new: new command to fetch & load interface declaration from etherscan.io #19 31 | 32 | ![shell-fetch-interface](https://user-images.githubusercontent.com/2865694/183062446-c952b308-9fc7-49f9-8308-3eac09ca3b4a.gif) 33 | 34 | 35 | ## v0.1.2 36 | 37 | - fix: support require(), type, abstract, library 38 | - update: compiler list 39 | - update: built-in solc -> 0.8.15 40 | - update: dependencies 41 | 42 | ## v0.1.1 43 | 44 | - fix: return appropriate error message when trying to return an uninit storage pointer - #17 45 | - fix: support enums 46 | 47 | ``` 48 | 🚀 Entering interactive Solidity ^0.8.13 shell (🧁:Ganache built-in). '.help' and '.exit' are your friends. 49 | » enum FreshJuiceSize{ SMALL, MEDIUM, LARGE } 50 | » uint8(FreshJuiceSize.LARGE) 51 | 2 52 | ``` 53 | 54 | - fix: case insensitive bool match for `.config set True|False|true|false` 55 | - update: dependencies 56 | 57 | ## v0.1.0 58 | 59 | ⚠️ pot. breaking changes: `solidity-shell` now ships with ganache. use `.chain set-provider` to switch chain providers. the `built-in` ganache provider is used by default. 60 | 61 | - new: built in ganache provider 62 | - new: `.chain` subcommand 63 | - `.chain restart` - restarts the service (formerly known as `.restartblockchain`) 64 | - `.chain set-provider [fork-url]` - switch between the internal or an external `ganache-cli` command or url-provider. Optionally specify a ganache fork-url. 65 | - ` .chain set-provider internal https://mainnet.infura.io/v3/yourApiKey ` 66 | - `.chain accounts` - show ganache accounts 67 | - `.chain eth_ [args...]` - arbitrary eth JSONrpc method calls to blockchain provider. 68 | - e.g. `.chain eth_accounts` returns the blockchain providers response to the `eth_accounts` JSONrpc call. 69 | - new: command line switches: 70 | - `--fork` overrides fork-url option for internal ganache provider `solidity-shell --fork https://mainnet.infura.io/v3/yourApiKey`. 71 | - `--reset-config` resets the config file. 72 | - `--show-config-file` prints the path to the config file. 73 | - fix: better error handling. prevent vicious cycles where broken config trashes the app 🤦‍♂️ 74 | - update: dependencies and solc references updated 75 | 76 | ## v0.0.11 77 | - new: configurable call and deploy gas 78 | - new: `.restartblockchain` command to restart ganache e.g. after config changes 79 | - fix: fixed returnval for some keywords 80 | - fix: show result for functions declaring multiple return vals 81 | - fix: naive fix to resolve function declarations for multi returnval function invocations. 82 | 83 | image 84 | 85 | ## v0.0.10 86 | - new: update to solc@0.8.11 87 | - new: basic autocomplete for built-ins (configurable via `.config`) - #11 88 | - fix: return value of unit constants (e.g. `2 ether`) - #12 89 | - fix: distinguish between/ autoguess const signed and unsigned int return values - #12 90 | - update: minor refactoring - #11 91 | 92 | ## v0.0.9 93 | - new: support the `import` directive - #8 94 | - new: experimental support for `https` imports, i.e. `import "https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC721/IERC721.sol"`. This can be disabled by setting ` » .config set resolveHttpImports false`. 95 | - fix: `localhost` alias may not be available on some systems - #9 96 | 97 | ## v0.0.8 98 | - new: Passthru ganache-cli settings as options to solidity-shell #7 99 | ```shell 100 | solidity-shell -- -fork https://mainnet.infura.io/v3/yourToken 101 | ``` 102 | 103 | Query a live contracts `ERC20.name()`: 104 | ```solidity 105 | 106 | » interface ERC20 { 107 | multi> function name() external view returns (string memory); 108 | multi> } 109 | 110 | » ERC20(0xB8c77482e45F1F44dE1745F52C74426C631bDD52).name() 111 | BNB 112 | 113 | ``` 114 | - fix: `.config set` handling of strings and multi-word arguments 115 | - fix: `exit` exits solidity-shell completely 116 | 117 | ## v0.0.7 118 | - fix: rework remote compiler 119 | - added a remoteCompiler wrapper 120 | - fix: always use latest compiler shipped with this package by default 121 | - new: ship with solc 0.8.10 122 | - preference: use solc shipped with package by default, else check static solcVersions list and fetch remote compiler, else update solcVersions list and fetch remote compiler. 123 | - fix: better error handling when changing compiler version 124 | - new: support `error` keyword and fix memory/storage type declarations 125 | 126 | ## v0.0.6 127 | - fix: handle interface declarations 128 | 129 | ## v0.0.5 130 | - fix: support blocks/loops - #2 131 | - fix: better ganache error handling and minor refactoring 132 | 133 | ## v0.0.4 134 | - new: dynamic compiler selection via pragma directive 135 | - changing the solidity version pragma attempts to load the selected compiler version remotely. e.g. type `pragma solidity 0.8.4` to switch to solidity v0.8.4. 136 | 137 | ## v0.0.1 - 0.0.3 138 | 139 | - first alpha 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [get in touch with Consensys Diligence](https://diligence.consensys.net)
2 | 3 | [[ 🌐 ](https://diligence.consensys.net) [ 📩 ](https://github.com/ConsenSys/vscode-solidity-doppelganger/blob/master/mailto:diligence@consensys.net) [ 🔥 ](https://consensys.github.io/diligence/)] 4 |

5 | 6 | 7 | ## Solidity Shell 8 | 9 | An interactive Solidity shell with lightweight session recording and remote compiler support. 10 | 11 | [💾](https://www.npmjs.com/package/solidity-shell) `npm install -g solidity-shell` 12 | 13 | ```javascript 14 | ⇒ solidity-shell 15 | 16 | 🚀 Entering interactive Solidity shell. '.help' and '.exit' are your friends. 17 | » ℹ️ ganache-mgr: starting temp. ganache instance ... 18 | » 19 | » uint a = 100 20 | » uint b = 200 21 | » a + b + 2 + uint8(50) 22 | 352 23 | » $_ 24 | 352 25 | ``` 26 | 27 | Oh, did you know that we automatically fetch a matching remote compiler when you change the solidity pragma? It is as easy as typing `pragma solidity 0.5.0` and solidity-shell will do the rest 🙌. 28 | 29 | 30 | 31 | ### Hints 32 | 33 | * `pragma solidity ` attempts to dynamically load the selected compiler version (remote compiler, may take a couple of seconds). 34 | * use `{ ; }` to ignore a calls return value. 35 | * Sessions can be saved and restored using the `.session` command. Your previous session is always stored and can be loaded via `.session load previous` (not safe when running concurrent shells). 36 | * `.reset` completely removes all statements. `.undo` removes the last statement. 37 | * See what's been generated under the hood? call `.dump`. 38 | * Settings are saved on exit (not safe when running concurrent shells). call `config set ` to change settings like ganache port, ganache autostart, etc. 39 | * `$_` is a placeholder for the last known result. Feel free to use that placeholder in your scripts :) 40 | * Special commands are dot-prefixed. Everything else is evaluated as Solidity code. 41 | * `import ""` assumes that `path` is relative to the current working-dir (CWD) or `{CWD}/node_modules/`. There's experimental support for HTTPs URL's. You can disable https resolving by setting ` » .config set resolveHttpImports false`. 42 | 43 | ```solidity 44 | » import "https://raw.githubusercontent.com/OpenZeppelin/openzeppelin-contracts/master/contracts/token/ERC721/IERC721.sol" 45 | ``` 46 | 47 | 48 | ### Usage 49 | 50 | #### Cmdline Passthru 51 | 52 | Any arguments provided after an empty `--` are directly passed to `ganacheCmd` (default: `ganache-cli`). This way, for example, you can start a solidity shell on a ganache fork of mainnet via infura. Check `ganache-cli --help` for a list of available options. 53 | 54 | ```shell 55 | ⇒ solidity-shell -- --fork https://mainnet.infura.io/v3/yourApiToken 56 | 57 | 🚀 Entering interactive Solidity shell. Type '.help' for help, '.exit' to exit. 58 | » ℹ️ ganache-mgr: starting temp. ganache instance ... 59 | » 60 | » interface ERC20 { 61 | multi> function name() external view returns (string memory); 62 | multi> } 63 | 64 | » ERC20(0xB8c77482e45F1F44dE1745F52C74426C631bDD52).name() 65 | BNB 66 | 67 | ``` 68 | 69 | #### Repl 70 | 71 | ```shell 72 | 🚀 Entering interactive Solidity ^0.8.11 shell. '.help' and '.exit' are your friends. 73 | » ℹ️ ganache-mgr: starting temp. ganache instance ... 74 | » 75 | » .help 76 | 77 | 📚 Help: 78 | ----- 79 | 80 | $_ is a placeholder holding the most recent evaluation result. 81 | pragma solidity to change the compiler version. 82 | 83 | 84 | General: 85 | .help ... this help :) 86 | .exit ... exit the shell 87 | 88 | 89 | Source: 90 | .fetch 91 | interface
[chain=mainnet] ... fetch and load an interface declaration from an ABI spec on etherscan.io 92 | .inspect 93 | bytecode ... show bytecode of underlying contract 94 | opcodes ... show disassembled opcodes of underlying contract 95 | storageLayout ... show variable to storage slot mapping for underlying contract 96 | storage [
] ... show raw storage at slot of underlying deployed contract 97 | deployed ... debug: show internal contract object 98 | 99 | 100 | Blockchain: 101 | .chain 102 | restart ... restart the blockchain service 103 | set-provider ... "internal" | | 104 | - fork url e.g. https://mainnet.infura.io/v3/yourApiKey 105 | accounts ... return eth_getAccounts 106 | eth_ [...args] ... initiate an arbitrary eth JSONrpc method call to blockchain provider. 107 | 108 | Settings: 109 | .config ... show settings 110 | set ... set setting 111 | unset ... unset setting 112 | Session: 113 | .session ... list sessions 114 | load ... load session 115 | save ... save session 116 | .undo ... undo last command 117 | .reset ... reset cmd history. start from scratch. 118 | 119 | Debug: 120 | .proc ... show processes managed by solidity-shell (ganache) 121 | .dump ... show template contract 122 | .echo ... every shell needs an echo command 123 | 124 | 125 | cheers 🙌 126 | @tintinweb 127 | ConsenSys Diligence @ https://consensys.net/diligence/ 128 | https://github.com/tintinweb/solidity-shell/ 129 | 130 | ``` 131 | 132 | ## Examples 133 | 134 | 135 | ![solidity-shell](https://user-images.githubusercontent.com/2865694/131328119-e363f20a-f627-43fc-8801-8d6613ad740f.gif) 136 | 137 | 138 | ### Transaction vars: `msg.sender` etc. 139 | 140 | ```javascript 141 | » msg.sender 142 | 0x70e9B09abd6A13D2F5083CD5814076b77427199F 143 | » address(uint160(address(msg.sender))) 144 | 0x70e9B09abd6A13D2F5083CD5814076b77427199F 145 | ``` 146 | 147 | ### Contracts, Structs, Functions 148 | 149 | ```javascript 150 | ⇒ solidity-shell 151 | 152 | 🚀 Entering interactive Solidity shell. Type '.help' for help, '.exit' to exit. 153 | » ℹ️ ganache-mgr: starting temp. ganache instance ... 154 | » 155 | » contract TestContract {} 156 | » new TestContract() 157 | 0xFBC1B2e79D816E36a1E1e923dd6c6fad463F4368 158 | » msg.sender 159 | 0x363830C6aee2F0c43922bcB785C570a7cca613b5 160 | » block.timestamp 161 | 1630339581 162 | » struct yolo {uint8 x; uint8 y;} 163 | » function mytest(uint x) public pure returns(uint) { 164 | multi> return x -5; 165 | multi> } 166 | » mytest(100) 167 | 95 168 | ``` 169 | 170 | ![solidity-shell2](https://user-images.githubusercontent.com/2865694/131328490-e211e89b-ac59-4729-972b-3e3b19b75cfc.gif) 171 | 172 | ### Advanced usage 173 | 174 | ```javascript 175 | » struct yolo {uint8 x; uint8 y;} 176 | » .dump 177 | // SPDX-License-Identifier: GPL-2.0-or-later 178 | pragma solidity ^0.8.7; 179 | 180 | contract TestContract {} 181 | 182 | struct yolo {uint8 x; uint8 y;} 183 | 184 | contract MainContract { 185 | 186 | 187 | 188 | function main() public { 189 | uint a = 100; 190 | uint b = 200; 191 | a + b + 2 + uint8(50); 192 | new TestContract(); 193 | msg.sender; 194 | block.timestamp; 195 | return ; 196 | } 197 | } 198 | ``` 199 | 200 | ### Fetch Interface Declaration from Etherscan 201 | 202 | ![shell-fetch-interface](https://user-images.githubusercontent.com/2865694/183062446-c952b308-9fc7-49f9-8308-3eac09ca3b4a.gif) 203 | 204 | 205 | `.fetch interface
[optional: chain=mainnet]` 206 | 207 | ``` 208 | ⇒ solidity-shell --fork https://mainnet.infura.io/v3/ 209 | 210 | 🚀 Entering interactive Solidity ^0.8.16 shell (🧁:Ganache built-in). '.help' and '.exit' are your friends. 211 | » 212 | » .fetch interface 0x40cfEe8D71D67108Db46F772B7e2CD55813Bf2FB Test 213 | » interface Test { 214 | 215 | ... omitted ... 216 | 217 | function symbol() external view returns (string memory); 218 | 219 | function tokenURI(uint256 tokenId) external view returns (string memory); 220 | 221 | function totalSupply() external view returns (uint256); 222 | 223 | function transferFrom( 224 | address from, 225 | address to, 226 | uint256 tokenId 227 | ) external; 228 | 229 | function transferOwnership(address newOwner) external; 230 | 231 | function withdraw() external; 232 | } 233 | 234 | » Test t = Test(0x40cfEe8D71D67108Db46F772B7e2CD55813Bf2FB) 235 | » t.symbol() 236 | MGX 237 | ``` 238 | 239 | ### Inspect Contract Storage on Ganache Fork 240 | 241 | 1. Run solidity shell in **fork-mode**. 242 | 2. Display contract storage at latest block. 243 | 244 | 245 | ``` 246 | ⇒ solidity-shell --fork https://mainnet.infura.io/v3/ 247 | 248 | 🚀 Entering interactive Solidity ^0.8.16 shell (🧁:Ganache built-in, ⇉ fork-mode). '.help' and '.exit' are your friends. 249 | » .inspect storage 0 10 0x40cfEe8D71D67108Db46F772B7e2CD55813Bf2FB 250 | » 251 | 📚 Contract: 0x40cfee8d71d67108db46f772b7e2cd55813bf2fb @ latest block 252 | 253 | slot 1f 1e 1d 1c 1b 1a 19 18 17 16 15 14 13 12 11 10 0f 0e 0d 0c 0b 0a 09 08 07 06 05 04 03 02 01 00 254 | -------------------------------------------------------------------------------------------------------------------- 255 | 0x000000 ( 0) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1d c7 ................................ 256 | 0x000001 ( 1) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 257 | 0x000002 ( 2) 54 68 65 20 4d 61 67 69 78 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 12 The Magix....................... 258 | 0x000003 ( 3) 4d 47 58 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 06 MGX............................. 259 | 0x000004 ( 4) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 260 | 0x000005 ( 5) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 261 | 0x000006 ( 6) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 262 | 0x000007 ( 7) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 263 | 0x000008 ( 8) 00 00 00 00 00 00 00 00 00 00 00 00 d7 4e 84 57 2f 5f 7b 5d 41 47 4e be d9 b3 02 0a 2e 52 6f c6 .............N.W/_{]AGN......Ro. 264 | 0x000009 ( 9) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 27 0f ..............................'. 265 | ``` 266 | 267 | ### Inspect Generated Contract 268 | 269 | ``` 270 | solidity-shell 271 | 272 | 🚀 Entering interactive Solidity ^0.8.16 shell (🧁:Ganache built-in, ⇉ fork-mode). '.help' and '.exit' are your friends. 273 | » 1+1 274 | 2 275 | » .inspect bytecode 276 | 6080604052348015610010576000 ... 03a7bab64736f6c63430008100033 277 | » .inspect opcodes 278 | PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE ... SLOAD 0xDA POP GASPRICE PUSH28 0xAB64736F6C6343000810003300000000000000000000000000000000 279 | » .inspect storageLayout 280 | { storage: [], types: null } 281 | » .inspect storage 0 4 282 | » 283 | 📚 Contract: 0xCa1061046396daF801dEB0D848FcfeA055fAfBFC @ latest block 284 | 285 | slot 1f 1e 1d 1c 1b 1a 19 18 17 16 15 14 13 12 11 10 0f 0e 0d 0c 0b 0a 09 08 07 06 05 04 03 02 01 00 286 | -------------------------------------------------------------------------------------------------------------------- 287 | 0x000000 ( 0) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 288 | 0x000001 ( 1) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 289 | 0x000002 ( 2) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 290 | 0x000003 ( 3) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................................ 291 | ``` 292 | 293 | ____ 294 | 295 | 296 | ## Acknowledgements 297 | 298 | * Inspired by the great but unfortunately unmaintained [solidity-repl](https://github.com/raineorshine/solidity-repl). 299 | * Fetch interfaces from Etherscan is powered by [abi-to-sol](https://github.com/gnidan/abi-to-sol). 300 | -------------------------------------------------------------------------------- /bin/main.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | /** 4 | * @author github.com/tintinweb 5 | * @license MIT 6 | * */ 7 | const Vorpal = require('vorpal'); 8 | const c = require('chalk'); 9 | const fs = require('fs'); 10 | const os = require('os'); 11 | const path = require('path'); 12 | 13 | const { InteractiveSolidityShell, SolidityStatement } = require('../src/handler'); 14 | const { convert, multilineInput } = require('../src/cli/utils'); 15 | const { builtIns } = require('../src/compiler/autogenerated/builtIns'); 16 | 17 | /** GLobals */ 18 | const CONFIG_HOME = path.join(os.homedir(), '.solidity-shell'); 19 | const CONFIG_FILE = '.config'; 20 | 21 | const REX_PLACEHOLDER = /(\$_)/ig /* LAST_KNOWN_RESULT placeholder */ 22 | 23 | var LAST_KNOWN_RESULT = 'ss'; 24 | var SESSION = 'previous.session'; 25 | 26 | 27 | /** static funcs */ 28 | function loadFile(name) { 29 | let cfgFile = path.join(CONFIG_HOME, name); 30 | 31 | if (fs.existsSync(cfgFile)) { 32 | return JSON.parse(fs.readFileSync(cfgFile)); 33 | } 34 | return {}; 35 | } 36 | 37 | function saveFile(name, data) { 38 | let cfgFile = path.join(CONFIG_HOME, name); 39 | 40 | if (!fs.existsSync(CONFIG_HOME)) { 41 | fs.mkdirSync(CONFIG_HOME); 42 | } 43 | fs.writeFileSync(cfgFile, JSON.stringify(data)); 44 | } 45 | 46 | /** MAIN */ 47 | const argv = require('minimist')(process.argv, { '--': true }); 48 | var config = loadFile(CONFIG_FILE); 49 | 50 | const oldConf = { 51 | ganacheArgs: config.ganacheArgs, 52 | ganacheOptions: config.ganacheOptions 53 | } 54 | 55 | if (argv['--'].length) { // temporarily override ganache args 56 | config.ganacheArgs = argv['--']; 57 | } 58 | if (argv['fork']) { 59 | config.ganacheOptions.fork = { url: argv['fork'] } 60 | } 61 | if (argv['reset-config']) { 62 | config = {}; 63 | } 64 | if (argv['show-config-file']) { 65 | console.log(path.join(CONFIG_HOME, CONFIG_FILE)); 66 | process.exit(0); 67 | } 68 | 69 | const shell = new InteractiveSolidityShell(config); 70 | 71 | process.on('exit', () => { 72 | shell.blockchain.stopService(); 73 | if (argv['--'].length) { //restore old ganache args 74 | shell.settings.ganacheArgs = oldConf.ganacheArgs; 75 | } 76 | if (argv['fork']) { 77 | shell.settings.ganacheOptions.fork = oldConf.ganacheOptions.fork; 78 | } 79 | saveFile(SESSION, shell.dumpSession()) 80 | 81 | // exit if dirty exit detected 82 | if (process.exitCode != 0) { 83 | console.log("🧨 not saving config due to dirty shutdown.") 84 | return; 85 | } 86 | saveFile(CONFIG_FILE, shell.settings) 87 | }); 88 | 89 | const vorpal = new Vorpal() 90 | .delimiter('') 91 | .show() 92 | .parse(argv._); 93 | 94 | vorpal.on('client_prompt_submit', (cmd) => { 95 | if (cmd.trim() === 'exit') { 96 | process.exit(0); // exit completely from repl. otherwise, would return to main vorpal loop 97 | } 98 | }); 99 | 100 | function handleRepl(input, cb) { 101 | let command = multilineInput(input); 102 | 103 | /* substitute placeholder: $_ */ 104 | command = command.replace(REX_PLACEHOLDER, ' (' + LAST_KNOWN_RESULT + ') '); 105 | 106 | if (command.startsWith('.')) { 107 | let commandParts = command.split(' '); 108 | let ret = undefined; 109 | switch (commandParts[0]) { 110 | case '.help': 111 | cb(` 112 | 📚 Help: 113 | ----- 114 | 115 | ${c.bold('$_')} is a placeholder holding the most recent evaluation result. 116 | ${c.bold('pragma solidity ')} to change the compiler version. 117 | 118 | 119 | ${c.bold('General:')} 120 | .help ... this help :) 121 | .exit ... exit the shell 122 | 123 | ${c.bold('Source:')} 124 | .fetch 125 | interface
[chain=mainnet] ... fetch and load an interface declaration from an ABI spec on etherscan.io 126 | .inspect 127 | bytecode ... show bytecode of underlying contract 128 | opcodes ... show disassembled opcodes of underlying contract 129 | storageLayout ... show variable to storage slot mapping for underlying contract 130 | storage [
] ... show raw storage at slot of underlying deployed contract 131 | deployed ... debug: show internal contract object 132 | 133 | 134 | ${c.bold('Blockchain:')} 135 | .chain 136 | restart ... restart the blockchain service 137 | set-provider ... "internal" | | 138 | - fork url e.g. https://mainnet.infura.io/v3/yourApiKey 139 | accounts ... return eth_getAccounts 140 | eth_ [...args] ... initiate an arbitrary eth JSONrpc method call to blockchain provider. 141 | 142 | ${c.bold('Settings:')} 143 | .config ... show settings 144 | set ... set setting 145 | unset ... unset setting 146 | ${c.bold('Session:')} 147 | .session ... list sessions 148 | load ... load session 149 | save ... save session 150 | .undo ... undo last command 151 | .reset ... reset cmd history. start from scratch. 152 | 153 | ${c.bold('Debug:')} 154 | .proc ... show processes managed by solidity-shell (ganache) 155 | .dump ... show template contract 156 | .echo ... every shell needs an echo command 157 | 158 | 159 | cheers 🙌 160 | ${c.bold('@tintinweb')} 161 | ConsenSys Diligence @ https://consensys.net/diligence/ 162 | https://github.com/tintinweb/solidity-shell/ 163 | `); 164 | 165 | break; //show usage 166 | case '.exit': process.exit(); break; //exit -> no more cb() 167 | case '.chain': 168 | if (!commandParts[1]) { 169 | break; 170 | } 171 | switch (commandParts[1]) { 172 | case 'restart': 173 | shell.blockchain.restartService(); 174 | this.log(` ✨ '${shell.blockchain.name}' blockchain provider restarted.`) 175 | break; 176 | case 'set-provider': 177 | shell.settings.blockchainProvider = commandParts[2]; 178 | if (commandParts.length > 3) { 179 | //fork-url 180 | shell.settings.ganacheOptions.fork = { url: commandParts[3] } 181 | } else { 182 | delete shell.settings.ganacheOptions.fork 183 | } 184 | shell.initBlockchain(); 185 | this.log(` ✨ '${shell.blockchain.name}' initialized.`) 186 | break; 187 | case 'accounts': 188 | shell.blockchain.getAccounts().then(acc => { 189 | this.log(`\n 🧝‍ ${acc.join('\n 🧝 ')}\n`) 190 | 191 | }) 192 | break; 193 | default: 194 | if (commandParts[1].startsWith("eth_")) { 195 | shell.blockchain.rpcCall(commandParts[1], commandParts.slice(2)).then(res => this.log(res)).catch(e => this.log(e)) 196 | } 197 | 198 | break; 199 | } 200 | 201 | break; //restart ganache 202 | case '.reset': shell.reset(); break; //reset complete state 203 | case '.undo': shell.revert(); break; //revert last action 204 | case '.config': 205 | switch (commandParts[1]) { 206 | case 'set': shell.setSetting(commandParts[2], convert(commandParts.slice(3).join(' '))); break; 207 | case 'unset': delete shell.settings[commandParts[2]]; break; 208 | default: return cb(shell.settings); 209 | } break; 210 | case '.session': 211 | switch (commandParts[1]) { 212 | default: 213 | let sessions = fs.readdirSync(CONFIG_HOME).filter(file => file.endsWith('.session')); 214 | return cb(' - ' + sessions.map(s => c.bold(s.replace('.session', ''))).join('\n - ')); 215 | case 'load': 216 | shell.loadSession(loadFile(`${commandParts[2]}.session`)) 217 | break; 218 | case 'save': 219 | SESSION = `${commandParts[2]}.session`; 220 | saveFile(SESSION, shell.dumpSession()) 221 | break; 222 | }; 223 | break; 224 | case '//DISABLED-.play': 225 | let path = `./${commandParts[1]}` 226 | if (!fs.existsSync(path)) { 227 | this.log(`file not found: ${path}`); 228 | return cb(); 229 | } 230 | this.log(`⏯️ playing '${path}'`) 231 | let lines = fs.readFileSync(path, 'utf-8') 232 | lines.split('\n').map(l => l.trim()).filter(l => l && l.length).forEach(l => { 233 | this.log(l) 234 | this.parent.exec(l, function (err, data) { 235 | if (!err && data) { 236 | return cb(data) 237 | } 238 | return 239 | }) 240 | }) 241 | break; 242 | case '.dump': return cb(c.yellow(shell.template())); 243 | case '.echo': return cb(c.bold(c.yellow(commandParts.slice(1).join(' ')))) 244 | case '.proc': 245 | if (!shell.blockchain.proc) { 246 | return cb(); 247 | } 248 | return cb(`${c.bold(c.yellow(shell.blockchain.proc.pid))} - ${shell.blockchain.proc.spawnargs.join(', ')}`) 249 | case '.inspect': 250 | let deployed = shell.blockchain.getDeployed(); 251 | switch (commandParts[1]) { 252 | case 'storage': 253 | 254 | function getStorageAt(target, start, num, atBlock) { 255 | 256 | atBlock = atBlock || "latest"; 257 | start = start || 0; 258 | num = num || 1; 259 | let slots = [...Array(num).keys()].map((idx) => (start + idx)) 260 | 261 | return slots.map(slot => shell.blockchain.web3.eth.getStorageAt(target, slot)); 262 | } 263 | let start = commandParts.length > 2 ? parseInt(commandParts[2]) : 0; 264 | let num = commandParts.length > 3 ? parseInt(commandParts[3]) : 10; 265 | 266 | if (isNaN(start) || isNaN(num)) { 267 | console.error("start and num must be numbers!") 268 | break; 269 | } 270 | 271 | var target; 272 | if (commandParts.length > 4) { 273 | target = commandParts[4].trim().toLowerCase(); 274 | } else if (deployed) { 275 | target = deployed.instance.options.address; 276 | } else { 277 | console.error("not yet ready. execute repl command first."); 278 | break; 279 | } 280 | 281 | Promise.all(getStorageAt(target, start, num)) 282 | .then(r => { 283 | 284 | let head = ` 285 | 📚 Contract: ${target} @ latest block 286 | 287 | slot 1f 1e 1d 1c 1b 1a 19 18 17 16 15 14 13 12 11 10 0f 0e 0d 0c 0b 0a 09 08 07 06 05 04 03 02 01 00 288 | -------------------------------------------------------------------------------------------------------------------- 289 | `; 290 | 291 | let lines = r.map((v, i) => { 292 | let datawideBytes = v.replace("0x", "").padStart(256 / 8 * 2, '0').match(/.{2}/g); 293 | let strline = datawideBytes.map(b => { 294 | let bint = parseInt(b, 16); 295 | if (bint >= 32 && bint <= 126) { 296 | return String.fromCharCode(bint); 297 | } else { 298 | return '.'; 299 | } 300 | }).join(""); 301 | return ` 0x${(start + i).toString(16).padStart(6, '0')} (${(start + i).toString().padStart(4, ' ')}) ${datawideBytes.map(b => b == "00" ? b : c.bold(c.bgYellowBright(b))).join(" ")} ${strline}`; 302 | }).join('\n'); 303 | 304 | console.log(head + lines); 305 | }); 306 | 307 | break; 308 | case 'bytecode': 309 | deployed && cb(c.yellow(deployed.bytecode)); 310 | break; 311 | case 'deployed': 312 | cb(deployed); 313 | break; 314 | case 'storageLayout': 315 | deployed && cb(deployed.storageLayout); 316 | break; 317 | case 'opcodes': 318 | deployed && cb(deployed.opcodes); 319 | break; 320 | } 321 | break; 322 | case '.fetch': 323 | if (commandParts.length < 4) { 324 | cb("Invalid params: .fetch interface
[chain=mainnet] ... fetch and load an interface declaration from an ABI spec on etherscan.io") 325 | break; 326 | } 327 | switch (commandParts[1]) { 328 | case 'interface': 329 | const { getRemoteInterfaceFromEtherscan } = require('../src/compiler/remoteCompiler'); 330 | 331 | getRemoteInterfaceFromEtherscan( 332 | commandParts[2], 333 | commandParts[3], 334 | commandParts.length >= 4 ? commandParts[4] : undefined, 335 | shell.settings.installedSolidityVersion, 336 | shell.settings.etherscanApiKey 337 | ).then(interfaceSource => { 338 | console.log(interfaceSource); 339 | return cb(handleRepl(interfaceSource, cb)); // recursively call 340 | }).catch(e => { 341 | console.error(e); 342 | console.log("let's try once more 🤷‍♂️") 343 | // try once more? 344 | getRemoteInterfaceFromEtherscan( 345 | commandParts[2], 346 | commandParts[3], 347 | commandParts.length >= 4 ? commandParts[4] : undefined, 348 | shell.settings.installedSolidityVersion, 349 | shell.settings.etherscanApiKey 350 | ).then(interfaceSource => { 351 | console.log(interfaceSource); 352 | return cb(handleRepl(interfaceSource, cb)); // recursively call 353 | }).catch(e => { 354 | console.error(`Error trying to fetch remote interface: ${JSON.stringify(e)}`) 355 | }) 356 | }) 357 | break; 358 | default: 359 | cb("Invalid params: .fetch interface
[chain=mainnet] ... fetch and load an interface declaration from an ABI spec on etherscan.io") 360 | break; 361 | } 362 | 363 | break; 364 | default: 365 | console.error(`Unknown Command: '${command}'. Type '${c.bold('.help')}' for a list of commands.`); 366 | } 367 | // meta commands 368 | return cb(ret); 369 | } 370 | 371 | const statement = new SolidityStatement(command); 372 | 373 | /* REPL cmd */ 374 | shell.run(statement).then(res => { 375 | if (!Array.isArray(res) && typeof res === 'object') { 376 | if (Object.keys(res).length === 0) { 377 | // empty response, hide 378 | return cb(); 379 | } 380 | res = JSON.stringify(res); //stringify the result 381 | } 382 | LAST_KNOWN_RESULT = res; // can only store last result for simple types 383 | cb(c.bold(c.yellow(res))); 384 | 385 | }).catch(errors => { 386 | console.error(errors) 387 | cb() 388 | }) 389 | } 390 | 391 | vorpal 392 | .mode('repl', 'Enters Solidity Shell Mode') 393 | .delimiter(c.bold('» ')) 394 | .init(function (args, cb) { 395 | this.log(`🚀 Entering interactive Solidity ${c.bold(shell.settings.installedSolidityVersion)} shell (🧁:${c.bold(shell.blockchain.name)}${shell.settings.ganacheOptions && shell.settings.ganacheOptions.fork ? c.bold(', ⇉ fork-mode'):''}). '${c.bold('.help')}' and '${c.bold('.exit')}' are your friends.`); 396 | return cb(); 397 | }) 398 | .action(handleRepl); 399 | 400 | 401 | 402 | /*** make autocomplete happy. this is hacky, i know 🙄 */ 403 | 404 | vorpal 405 | .command(".help") 406 | vorpal 407 | .command(".exit") 408 | .alias("exit") 409 | vorpal 410 | .command(".chain") 411 | .autocomplete(["restart", "set-provider", "accounts", "getAccounts"]) 412 | vorpal 413 | .command(".config") 414 | .autocomplete(["set", "unset"]) 415 | 416 | vorpal 417 | .command(".session") 418 | .autocomplete(["load", "save"]) 419 | vorpal 420 | .command(".undo") 421 | vorpal 422 | .command(".redo") 423 | vorpal 424 | .command(".reset") 425 | vorpal 426 | .command(".proc") 427 | vorpal 428 | .command(".dump") 429 | vorpal 430 | .command(".fetch") 431 | .autocomplete(["interface"]) 432 | vorpal 433 | .command(".inspect") 434 | .autocomplete(["bytecode", "opcodes", "storage", "storageLayout", "deployed"]) 435 | vorpal 436 | .command(".echo ") 437 | vorpal 438 | .command("$_") 439 | 440 | /** autocomplate built-ins (not context sensitive) */ 441 | if (config.enableAutoComplete) { 442 | for (let builtin of builtIns) { 443 | vorpal //register built-in as command (1st level autocomplete, with 2nd level autocomplete for params) 444 | .command(`${builtin}`) 445 | .autocomplete(builtIns); 446 | } 447 | } 448 | 449 | /** start in repl mode */ 450 | vorpal.execSync("repl") 451 | 452 | //vorpal.execSync("uint a = 2") /* debug */ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidity-shell", 3 | "version": "0.2.4", 4 | "description": "An interactive Solidity shell with lightweight session recording and remote compiler support", 5 | "main": "src/index.js", 6 | "bin": { 7 | "solidity-shell": "bin/main.js" 8 | }, 9 | "scripts": { 10 | "updateCompilerList": "node scripts/getCompilerList.js > src/compiler/autogenerated/solcVersions.js", 11 | "updateBuiltIns": "node scripts/getBuiltIns.js > src/compiler/autogenerated/builtIns.js" 12 | }, 13 | "author": { 14 | "name": "tintinweb", 15 | "url": "https://github.com/tintinweb/" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/tintinweb/solidity-shell.git" 20 | }, 21 | "keywords": [ 22 | "solidity", 23 | "repl", 24 | "shell", 25 | "interactive" 26 | ], 27 | "license": "MIT", 28 | "dependencies": { 29 | "abi-to-sol": "^0.6.6", 30 | "ganache": "^7.0.4", 31 | "minimist": "^1.2.5", 32 | "readline-sync": "^1.4.10", 33 | "request": "^2.88.2", 34 | "solc": "^0.8.17", 35 | "sync-request": "^6.1.0", 36 | "vorpal": "^1.12.0", 37 | "web3": "^1.5.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripts/getBuiltIns.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | /** 4 | * @author github.com/tintinweb 5 | * @license MIT 6 | * */ 7 | const request = require('request'); 8 | 9 | const rexQuoted = /'(.*?)'/g; 10 | const rexValid = /^[a-z0-9]{3,}$/i; 11 | 12 | function getBuiltInsFromAntlrGrammar() { 13 | return new Promise((resolve, reject) => { 14 | request.get('https://raw.githubusercontent.com/solidity-parser/antlr/master/Solidity.g4', (err, res, body) => { 15 | if (err) { 16 | return reject(err) 17 | } else { 18 | const array = Array.from(new Set([...body.matchAll(rexQuoted)].map(m => m[1].trim()))).filter(fi => rexValid.test(fi) ); 19 | return resolve(array); 20 | } 21 | }) 22 | }); 23 | } 24 | 25 | 26 | getBuiltInsFromAntlrGrammar().then(b => { 27 | console.log(`'use strict' 28 | /** 29 | * @author github.com/tintinweb 30 | * @license MIT 31 | * */ 32 | 33 | //autogenerated with # npm run updateBuiltIns 34 | module.exports.builtIns = ${JSON.stringify(b, null, 4)}`); 35 | }) 36 | 37 | -------------------------------------------------------------------------------- /scripts/getCompilerList.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | /** 4 | * @author github.com/tintinweb 5 | * @license MIT 6 | * */ 7 | const { getSolcJsCompilerList } = require('../src/compiler/remoteCompiler'); 8 | 9 | 10 | getSolcJsCompilerList({nightly:false}).then(compilers => { 11 | console.log(`'use strict' 12 | /** 13 | * @author github.com/tintinweb 14 | * @license MIT 15 | * */ 16 | 17 | //autogenerated with # npm run updateCompilerList 18 | module.exports.solcVersions = ${JSON.stringify(compilers, null, 4)}`); 19 | }) 20 | 21 | -------------------------------------------------------------------------------- /src/blockchain.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @author github.com/tintinweb 4 | * @license MIT 5 | * */ 6 | /** IMPORT */ 7 | const Web3 = require('web3'); 8 | const ganache = require("ganache"); 9 | 10 | class AbsBlockchainBase { 11 | constructor(shell, name) { 12 | this.log = shell.log; 13 | this.shell = shell 14 | 15 | this.provider = undefined 16 | this.web3 = undefined 17 | this.deployed = {} 18 | 19 | this.proc; 20 | this.name = name; 21 | } 22 | 23 | connect() { 24 | this.provider = new Web3.providers.HttpProvider(this.shell.settings.providerUrl); 25 | this.web3 = new Web3(this.provider); 26 | 27 | this.web3.eth.getAccounts().then().catch(err => { 28 | if (!this.shell.settings.autostartGanache) { 29 | console.warn("⚠️ ganache autostart is disabled") 30 | return; 31 | } 32 | this.startService() 33 | this.provider = new Web3.providers.HttpProvider(this.shell.settings.providerUrl); 34 | this.web3 = new Web3(this.provider); 35 | }) 36 | } 37 | 38 | startService() { 39 | throw Error("Not Implemented"); 40 | } 41 | 42 | stopService() { 43 | throw Error("Not Implemented"); 44 | } 45 | 46 | restartService() { 47 | this.stopService(); 48 | this.startService(); 49 | } 50 | 51 | getAccounts() { 52 | return new Promise((resolve, reject) => { 53 | this.web3.eth.getAccounts((err, result) => { 54 | if (err) return reject(new Error(err)); 55 | return resolve(result); 56 | }) 57 | }); 58 | } 59 | 60 | methodCall(cmd, args) { 61 | return new Promise((resolve, reject) => { 62 | let func = this.web3.eth[cmd]; 63 | if (func === undefined) { 64 | return reject(" 🧨 Unsupported Method"); 65 | } 66 | if (typeof func === "function") { 67 | func((err, result) => { 68 | if (err) return reject(new Error(err)); 69 | return resolve(result); 70 | }); 71 | } else { 72 | return resolve(func); 73 | } 74 | }); 75 | } 76 | 77 | rpcCall(method, params) { 78 | return new Promise((resolve, reject) => { 79 | let payload = { 80 | "jsonrpc": "2.0", 81 | "method": method, 82 | "params": params === undefined ? [] : params, 83 | "id": 1 84 | } 85 | this.provider.send(payload, (error, result) => { 86 | if (error) 87 | return reject(error); 88 | return resolve(result); 89 | }); 90 | }); 91 | } 92 | 93 | getDeployed(){ 94 | return this.deployed[this.shell.settings.templateContractName]; 95 | } 96 | 97 | async deploy(contracts, callback) { 98 | //sort deploy other contracts first 99 | Object.entries(contracts).sort((a, b) => a[1].main ? 10 : -1).forEach(([templateContractName, o]) => { 100 | if (o.evm.bytecode.object.length === 0) { 101 | return; //no bytecode, probably an interface 102 | } 103 | 104 | let thisContract = { 105 | bytecode: o.evm.bytecode.object, 106 | opcodes: o.evm.bytecode.opcodes, 107 | abi: o.abi, 108 | proxy: new this.web3.eth.Contract(o.abi, null), 109 | instance: undefined, 110 | main: o.main, 111 | storageLayout: o.storageLayout, 112 | accounts: undefined 113 | } 114 | 115 | this.deployed[templateContractName] = thisContract; 116 | this.getAccounts() 117 | .then(accounts => { 118 | thisContract.accounts = accounts; 119 | let instance = thisContract.proxy.deploy({ data: thisContract.bytecode }).send({ from: accounts[0], gas: this.shell.settings.deployGas }) 120 | return instance; 121 | }) 122 | .then(contract => { 123 | thisContract.instance = contract; 124 | if (thisContract.main) { 125 | contract.methods[thisContract.main]().call({ from: thisContract.accounts[0], gas: this.shell.settings.callGas }, callback); 126 | } 127 | return; 128 | }) 129 | .catch(err => { 130 | callback(`💥 ganache not yet ready. Please try again. (👉 ${err} 👈)`) 131 | }) 132 | 133 | }, this); 134 | } 135 | } 136 | 137 | 138 | class BuiltinGanacheBlockchain extends AbsBlockchainBase { 139 | 140 | constructor(shell) { 141 | super(shell, "Ganache built-in"); 142 | 143 | /* 144 | const options = { chain: ChainConfig, 145 | database: DatabaseConfig, 146 | logging: LoggingConfig, 147 | miner: MinerConfig, 148 | wallet: WalletConfig, 149 | fork: ForkConfig 150 | } 151 | */ 152 | const defaultOptions = { 153 | logging: { quiet: true }, 154 | }; 155 | this.options = { ...defaultOptions, ...shell.settings.ganacheOptions }; 156 | } 157 | 158 | connect() { 159 | this.startService(); 160 | 161 | this.web3 = new Web3(this.provider); 162 | 163 | this.web3.eth.getAccounts().then().catch(err => { 164 | if (!this.shell.settings.autostartGanache) { 165 | console.warn("⚠️ ganache autostart is disabled") 166 | return; 167 | } 168 | this.startService() 169 | this.provider = new Web3.providers.HttpProvider(this.shell.settings.providerUrl); 170 | this.web3 = new Web3(this.provider); 171 | }); 172 | 173 | 174 | } 175 | 176 | startService() { 177 | if (this.provider !== undefined) { 178 | return this.provider; 179 | } 180 | 181 | this.provider = ganache.provider(this.options); 182 | } 183 | stopService() { 184 | this.provider = undefined; 185 | } 186 | } 187 | 188 | 189 | class ExternalUrlBlockchain extends AbsBlockchainBase { 190 | 191 | constructor(shell, providerUrl) { 192 | super(shell, "Ganache url-provider"); 193 | this.providerUrl = providerUrl; 194 | } 195 | 196 | connect() { 197 | this.provider = new Web3.providers.HttpProvider(this.providerUrl); 198 | this.web3 = new Web3(this.provider); 199 | 200 | this.web3.eth.getAccounts().then().catch(err => { 201 | if (!this.shell.settings.autostartGanache) { 202 | console.warn("⚠️ ganache autostart is disabled") 203 | return; 204 | } 205 | this.startService() 206 | this.provider = new Web3.providers.HttpProvider(this.providerUrl); 207 | this.web3 = new Web3(this.provider); 208 | }) 209 | } 210 | 211 | startService() { 212 | // NOP 213 | } 214 | stopService() { 215 | // NOP 216 | } 217 | } 218 | 219 | class ExternalProcessBlockchain extends AbsBlockchainBase { 220 | 221 | constructor(shell) { 222 | super(shell, "Ganache ext-proc"); 223 | } 224 | 225 | startService() { 226 | if (this.proc) { 227 | return this.proc; 228 | } 229 | this.log("ℹ️ ganache-mgr: starting temp. ganache instance ...\n »"); 230 | 231 | this.proc = require('child_process').spawn(this.shell.settings.ganacheCmd, this.shell.settings.ganacheArgs); 232 | this.proc.on('error', function (err) { 233 | console.error(` 234 | 🧨 Unable to launch blockchain serivce: ➜ ℹ️ ${err} 235 | 236 | Please verify that 'ganache-cli' (or similar service) is installed and available in your PATH. 237 | Otherwise, you can disable autostart by setting 'autostartGanache' to false in your settings or configure a different service and '.restartblockchain'. 238 | `); 239 | }); 240 | } 241 | 242 | stopService() { 243 | this.log("💀 ganache-mgr: stopping temp. ganache instance"); 244 | if (this.proc) { 245 | this.proc.kill('SIGINT'); 246 | this.proc = undefined; 247 | } 248 | } 249 | } 250 | 251 | 252 | 253 | module.exports = { 254 | ExternalProcessBlockchain, 255 | ExternalUrlBlockchain, 256 | BuiltinGanacheBlockchain 257 | } -------------------------------------------------------------------------------- /src/cli/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @author github.com/tintinweb 4 | * @license MIT 5 | * */ 6 | 7 | function convert(str){ 8 | switch(str.toLowerCase()){ 9 | case '': return undefined; 10 | case 'true': return true; 11 | case 'false': return false; 12 | } 13 | try { 14 | let num = parseInt(str); 15 | if(!isNaN(num)) return num; 16 | } catch {} 17 | 18 | return str; 19 | } 20 | 21 | function multilineInput(command){ 22 | while (true) { 23 | 24 | let numBrOpen = command.split('{').length - 1; 25 | let numBrClose = command.split('}').length - 1; 26 | 27 | if (numBrOpen === numBrClose) { 28 | break; 29 | } 30 | 31 | const rl = require('readline-sync'); 32 | command += '\n' + rl.question("... ").trim() 33 | } 34 | return command; 35 | } 36 | 37 | module.exports = { 38 | convert, 39 | multilineInput, 40 | } -------------------------------------------------------------------------------- /src/compiler/autogenerated/builtIns.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @author github.com/tintinweb 4 | * @license MIT 5 | * */ 6 | 7 | //autogenerated with # npm run updateBuiltIns 8 | module.exports.builtIns = [ 9 | "pragma", 10 | "import", 11 | "from", 12 | "abstract", 13 | "contract", 14 | "interface", 15 | "library", 16 | "error", 17 | "type", 18 | "using", 19 | "for", 20 | "struct", 21 | "modifier", 22 | "function", 23 | "returns", 24 | "event", 25 | "enum", 26 | "address", 27 | "payable", 28 | "mapping", 29 | "memory", 30 | "storage", 31 | "calldata", 32 | "else", 33 | "try", 34 | "catch", 35 | "while", 36 | "unchecked", 37 | "assembly", 38 | "continue", 39 | "break", 40 | "return", 41 | "throw", 42 | "emit", 43 | "revert", 44 | "var", 45 | "bool", 46 | "string", 47 | "byte", 48 | "int", 49 | "int8", 50 | "int16", 51 | "int24", 52 | "int32", 53 | "int40", 54 | "int48", 55 | "int56", 56 | "int64", 57 | "int72", 58 | "int80", 59 | "int88", 60 | "int96", 61 | "int104", 62 | "int112", 63 | "int120", 64 | "int128", 65 | "int136", 66 | "int144", 67 | "int152", 68 | "int160", 69 | "int168", 70 | "int176", 71 | "int184", 72 | "int192", 73 | "int200", 74 | "int208", 75 | "int216", 76 | "int224", 77 | "int232", 78 | "int240", 79 | "int248", 80 | "int256", 81 | "uint", 82 | "uint8", 83 | "uint16", 84 | "uint24", 85 | "uint32", 86 | "uint40", 87 | "uint48", 88 | "uint56", 89 | "uint64", 90 | "uint72", 91 | "uint80", 92 | "uint88", 93 | "uint96", 94 | "uint104", 95 | "uint112", 96 | "uint120", 97 | "uint128", 98 | "uint136", 99 | "uint144", 100 | "uint152", 101 | "uint160", 102 | "uint168", 103 | "uint176", 104 | "uint184", 105 | "uint192", 106 | "uint200", 107 | "uint208", 108 | "uint216", 109 | "uint224", 110 | "uint232", 111 | "uint240", 112 | "uint248", 113 | "uint256", 114 | "bytes", 115 | "bytes1", 116 | "bytes2", 117 | "bytes3", 118 | "bytes4", 119 | "bytes5", 120 | "bytes6", 121 | "bytes7", 122 | "bytes8", 123 | "bytes9", 124 | "bytes10", 125 | "bytes11", 126 | "bytes12", 127 | "bytes13", 128 | "bytes14", 129 | "bytes15", 130 | "bytes16", 131 | "bytes17", 132 | "bytes18", 133 | "bytes19", 134 | "bytes20", 135 | "bytes21", 136 | "bytes22", 137 | "bytes23", 138 | "bytes24", 139 | "bytes25", 140 | "bytes26", 141 | "bytes27", 142 | "bytes28", 143 | "bytes29", 144 | "bytes30", 145 | "bytes31", 146 | "bytes32", 147 | "fixed", 148 | "ufixed", 149 | "new", 150 | "after", 151 | "delete", 152 | "let", 153 | "switch", 154 | "case", 155 | "default", 156 | "receive", 157 | "callback", 158 | "true", 159 | "false", 160 | "wei", 161 | "gwei", 162 | "szabo", 163 | "finney", 164 | "ether", 165 | "seconds", 166 | "minutes", 167 | "hours", 168 | "days", 169 | "weeks", 170 | "years", 171 | "hex", 172 | "final", 173 | "inline", 174 | "match", 175 | "null", 176 | "relocatable", 177 | "static", 178 | "typeof", 179 | "anonymous", 180 | "constant", 181 | "immutable", 182 | "leave", 183 | "external", 184 | "indexed", 185 | "internal", 186 | "private", 187 | "public", 188 | "virtual", 189 | "pure", 190 | "view", 191 | "global", 192 | "constructor", 193 | "fallback", 194 | "override", 195 | "unicode" 196 | ] 197 | -------------------------------------------------------------------------------- /src/compiler/autogenerated/solcVersions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @author github.com/tintinweb 4 | * @license MIT 5 | * */ 6 | 7 | //autogenerated with # npm run updateCompilerList 8 | module.exports.solcVersions = [ 9 | "v0.8.16+commit.07a7930e", 10 | "v0.8.15+commit.e14f2714", 11 | "v0.8.14+commit.80d49f37", 12 | "v0.8.13+commit.abaa5c0e", 13 | "v0.8.12+commit.f00d7308", 14 | "v0.8.11+commit.d7f03943", 15 | "v0.8.10+commit.fc410830", 16 | "v0.8.9+commit.e5eed63a", 17 | "v0.8.8+commit.dddeac2f", 18 | "v0.8.7+commit.e28d00a7", 19 | "v0.8.6+commit.11564f7e", 20 | "v0.8.5+commit.a4f2e591", 21 | "v0.8.4+commit.c7e474f2", 22 | "v0.8.3+commit.8d00100c", 23 | "v0.8.2+commit.661d1103", 24 | "v0.8.1+commit.df193b15", 25 | "v0.8.0+commit.c7dfd78e", 26 | "v0.7.6+commit.7338295f", 27 | "v0.7.5+commit.eb77ed08", 28 | "v0.7.4+commit.3f05b770", 29 | "v0.7.3+commit.9bfce1f6", 30 | "v0.7.2+commit.51b20bc0", 31 | "v0.7.1+commit.f4a555be", 32 | "v0.7.0+commit.9e61f92b", 33 | "v0.6.12+commit.27d51765", 34 | "v0.6.11+commit.5ef660b1", 35 | "v0.6.10+commit.00c0fcaf", 36 | "v0.6.9+commit.3e3065ac", 37 | "v0.6.8+commit.0bbfe453", 38 | "v0.6.7+commit.b8d736ae", 39 | "v0.6.6+commit.6c089d02", 40 | "v0.6.5+commit.f956cc89", 41 | "v0.6.4+commit.1dca32f3", 42 | "v0.6.3+commit.8dda9521", 43 | "v0.6.2+commit.bacdbe57", 44 | "v0.6.1+commit.e6f7d5a4", 45 | "v0.6.0+commit.26b70077", 46 | "v0.5.17+commit.d19bba13", 47 | "v0.5.16+commit.9c3226ce", 48 | "v0.5.15+commit.6a57276f", 49 | "v0.5.14+commit.01f1aaa4", 50 | "v0.5.13+commit.5b0b510c", 51 | "v0.5.12+commit.7709ece9", 52 | "v0.5.11+commit.c082d0b4", 53 | "v0.5.10+commit.5a6ea5b1", 54 | "v0.5.9+commit.e560f70d", 55 | "v0.5.8+commit.23d335f2", 56 | "v0.5.7+commit.6da8b019", 57 | "v0.5.6+commit.b259423e", 58 | "v0.5.5+commit.47a71e8f", 59 | "v0.5.4+commit.9549d8ff", 60 | "v0.5.3+commit.10d17f24", 61 | "v0.5.2+commit.1df8f40c", 62 | "v0.5.1+commit.c8a2cb62", 63 | "v0.5.0+commit.1d4f565a", 64 | "v0.4.26+commit.4563c3fc", 65 | "v0.4.25+commit.59dbf8f1", 66 | "v0.4.24+commit.e67f0147", 67 | "v0.4.23+commit.124ca40d", 68 | "v0.4.22+commit.4cb486ee", 69 | "v0.4.21+commit.dfe3193c", 70 | "v0.4.20+commit.3155dd80", 71 | "v0.4.19+commit.c4cbbb05", 72 | "v0.4.18+commit.9cf6e910", 73 | "v0.4.17+commit.bdeb9e52", 74 | "v0.4.16+commit.d7661dd9", 75 | "v0.4.15+commit.bbb8e64f", 76 | "v0.4.14+commit.c2215d46", 77 | "v0.4.13+commit.0fb4cb1a", 78 | "v0.4.12+commit.194ff033", 79 | "v0.4.11+commit.68ef5810", 80 | "v0.4.10+commit.f0d539ae", 81 | "v0.4.9+commit.364da425", 82 | "v0.4.8+commit.60cc1668", 83 | "v0.4.7+commit.822622cf", 84 | "v0.4.6+commit.2dabbdf0", 85 | "v0.4.5+commit.b318366e", 86 | "v0.4.4+commit.4633f3de", 87 | "v0.4.3+commit.2353da71", 88 | "v0.4.2+commit.af6afb04", 89 | "v0.4.1+commit.4fc6fc2c", 90 | "v0.4.0+commit.acd334c9", 91 | "v0.3.6+commit.3fc68da5", 92 | "v0.3.5+commit.5f97274a", 93 | "v0.3.4+commit.7dab8902", 94 | "v0.3.3+commit.4dc1cb14", 95 | "v0.3.2+commit.81ae2a78", 96 | "v0.3.1+commit.c492d9be", 97 | "v0.3.0+commit.11d67369", 98 | "v0.2.2+commit.ef92f566", 99 | "v0.2.1+commit.91a6b35f", 100 | "v0.2.0+commit.4dc2445e", 101 | "v0.1.7+commit.b4e666cc", 102 | "v0.1.6+commit.d41f8b7c", 103 | "v0.1.5+commit.23865e39", 104 | "v0.1.4+commit.5f6c3cdf", 105 | "v0.1.3+commit.028f561d", 106 | "v0.1.2+commit.d0d36e3", 107 | "v0.1.1+commit.6ff4cd6" 108 | ] 109 | -------------------------------------------------------------------------------- /src/compiler/remoteCompiler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @author github.com/tintinweb 4 | * @license MIT 5 | * */ 6 | /** IMPORT */ 7 | const { solcVersions } = require('./autogenerated/solcVersions.js') 8 | const { generateSolidity } = require('abi-to-sol') 9 | const request = require('request'); 10 | 11 | function normalizeSolcVersion(version) { 12 | return version.replace('soljson-', '').replace('.js', ''); 13 | } 14 | 15 | function getSolcJsCompilerList(options) { 16 | options = options || {}; 17 | return new Promise((resolve, reject) => { 18 | request.get('https://solc-bin.ethereum.org/bin/list.json', (err, res, body) => { 19 | if (err) { 20 | return reject(err) 21 | } else { 22 | let data = JSON.parse(body); 23 | let releases = Object.values(data.releases) 24 | 25 | if (options.nightly) { 26 | releases = Array.from(new Set([...releases, ...data.builds.map(b => b.path)])); 27 | } 28 | return resolve(releases.map(normalizeSolcVersion)) 29 | } 30 | }) 31 | }); 32 | } 33 | 34 | function getRemoteCompiler(solidityVersion) { 35 | return new Promise((resolve, reject) => { 36 | 37 | //check if version is in static list (avoid http requests) 38 | let remoteSolidityVersion = solcVersions.find( 39 | (e) => !e.includes('nightly') && e.includes(`v${solidityVersion}`) 40 | ) 41 | 42 | if (remoteSolidityVersion) { 43 | return resolve(remoteSolidityVersion); 44 | } 45 | //download remote compiler list and check again. 46 | getSolcJsCompilerList().then(solcJsCompilerList => { 47 | let found = solcJsCompilerList.find( 48 | (e) => !e.includes('nightly') && e.includes(`v${solidityVersion}`) 49 | ) 50 | if (found) { 51 | return resolve(found); 52 | } 53 | return reject(new Error(`No compiler found for version ${solidityVersion}`)); 54 | }) 55 | 56 | }); 57 | } 58 | 59 | //.import interface 0x40cfee8d71d67108db46f772b7e2cd55813bf2fb test2 60 | function getRemoteInterfaceFromEtherscan(address, name, chain, solidityVersion, apikey) { 61 | return new Promise((resolve, reject) => { 62 | 63 | let provider = `https://api${(!chain || chain == "mainnet") ? "" : `-${chain}`}.etherscan.io` 64 | let url = `${provider}/api?module=contract&action=getabi&address=${address}&apikey=${apikey || 'YourApiKeyToken'}`; 65 | request.get(url, (err, res, body) => { 66 | if (err) { 67 | return reject(err) 68 | } else { 69 | let data = JSON.parse(body); 70 | if (!data.status || data.status != "1" || !data.result) { 71 | return reject(data) 72 | } 73 | let abi = JSON.parse(data.result); 74 | let src = generateSolidity({ 75 | name: name, 76 | solidityVersion: solidityVersion, 77 | abi, 78 | outputSource: false, 79 | }); 80 | src = src.substring(src.indexOf("\n\n") + 2); // strip license/pragma 81 | return resolve(src) 82 | } 83 | }) 84 | }); 85 | } 86 | 87 | module.exports = { 88 | getRemoteCompiler, 89 | getSolcJsCompilerList, 90 | getRemoteInterfaceFromEtherscan 91 | } 92 | 93 | -------------------------------------------------------------------------------- /src/compiler/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @author github.com/tintinweb 4 | * @license MIT 5 | * */ 6 | 7 | const fs = require('fs'); 8 | 9 | function readFileCallback(sourcePath, options) { 10 | options = options || {}; 11 | if (sourcePath.startsWith("https://") && options.allowHttp) { 12 | //allow https! imports; not yet implemented 13 | const res = require('sync-request')('GET', sourcePath); //@todo: this is super buggy and might freeze the app. needs async/promises. 14 | return { contents: res.getBody('utf8') }; 15 | } 16 | else { 17 | const prefixes = [options.basePath ? options.basePath : ""].concat( 18 | options.includePath ? options.includePath : [] 19 | ); 20 | for (const prefix of prefixes) { 21 | const prefixedSourcePath = (prefix ? prefix + '/' : "") + sourcePath; 22 | if (fs.existsSync(prefixedSourcePath)) { 23 | try { 24 | return { contents: fs.readFileSync(prefixedSourcePath).toString('utf8') } 25 | } catch (e) { 26 | return { error: 'Error reading ' + prefixedSourcePath + ': ' + e }; 27 | } 28 | } 29 | } 30 | } 31 | return { error: 'File not found inside the base path or any of the include paths.' } 32 | } 33 | 34 | 35 | 36 | module.exports = { 37 | readFileCallback 38 | } -------------------------------------------------------------------------------- /src/handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @author github.com/tintinweb 4 | * @license MIT 5 | * */ 6 | /** IMPORT */ 7 | const path = require('path'); 8 | const solc = require('solc'); 9 | const { getRemoteCompiler } = require('./compiler/remoteCompiler.js'); 10 | const { readFileCallback } = require('./compiler/utils.js'); 11 | const { ExternalProcessBlockchain, ExternalUrlBlockchain, BuiltinGanacheBlockchain } = require('./blockchain.js'); 12 | 13 | 14 | /** CONST */ 15 | const rexTypeErrorReturnArgumentX = /Return argument type (.*) is not implicitly convertible to expected type \(type of first return variable\)/; 16 | const rexAssign = /[^=]=[^=];?/; 17 | const rexTypeDecl = /^([\w\[\]]+\s(memory|storage)?\s*\w+);?$/; 18 | const rexUnits = /^(\d+\s*(wei|gwei|szabo|finney|ether|seconds|minutes|hours|days|weeks|years))\s*;?$/; 19 | const IGNORE_WARNINGS = [ 20 | "Statement has no effect.", 21 | "Function state mutability can be restricted to ", 22 | "Unused local variable." 23 | ]; 24 | const TYPE_ERROR_DETECT_RETURNS = 'Different number of arguments in return statement than in returns declaration.'; 25 | 26 | const SCOPE = { 27 | CONTRACT: 1, /* statement in contract scope */ 28 | SOURCE_UNIT: 2, /* statement in source unit scope */ 29 | MAIN: 4, /* statement in main function scope */ 30 | VERSION_PRAGMA: 5 /* statement is a solidity version pragma */ 31 | } 32 | 33 | /** STATIC FUNC */ 34 | 35 | function getBestSolidityVersion(source) { 36 | var rx = /^pragma solidity (\^?[^;]+);$/gm; 37 | let allVersions = source.match(rx).map(e => { 38 | try { 39 | return e.match(/(\d+)\.(\d+)\.(\d+)/).splice(1, 3).map(a => parseInt(a)) 40 | } catch { } 41 | }) 42 | let lastVersion = allVersions[allVersions.length - 1]; 43 | if (!lastVersion) { 44 | return undefined; 45 | } 46 | return `^${lastVersion.join('.')}`; 47 | } 48 | 49 | 50 | /** CLASS */ 51 | class SolidityStatement { 52 | 53 | constructor(rawCommand, scope) { 54 | this.rawCommand = rawCommand ? rawCommand.trim() : ""; 55 | this.hasNoReturnValue = (rexAssign.test(this.rawCommand)) 56 | || (this.rawCommand.startsWith('delete ')) 57 | || (this.rawCommand.startsWith('assembly')) 58 | || (this.rawCommand.startsWith('revert')) 59 | || (this.rawCommand.startsWith('require(')) 60 | || (this.rawCommand.startsWith('unchecked ')) 61 | || (this.rawCommand.startsWith('{')) 62 | || (rexTypeDecl.test(this.rawCommand) && !rexUnits.test(this.rawCommand)) /* looks like type decl but is not special builtin like "2 ether" */ 63 | 64 | if (scope) { 65 | this.scope = scope; 66 | } else { 67 | if (['function ', 'modifier ', 'mapping ', 'event ', 'error ', 'type '].some(e => this.rawCommand.startsWith(e))) { 68 | this.scope = SCOPE.CONTRACT; 69 | this.hasNoReturnValue = true; 70 | } else if (this.rawCommand.startsWith('pragma solidity ')) { 71 | this.scope = SCOPE.VERSION_PRAGMA; 72 | this.hasNoReturnValue = true; 73 | this.rawCommand = this.fixStatement(this.rawCommand); 74 | } else if (['pragma ', 'import '].some(e => this.rawCommand.startsWith(e))) { 75 | this.scope = SCOPE.SOURCE_UNIT; 76 | this.hasNoReturnValue = true; 77 | this.rawCommand = this.fixStatement(this.rawCommand); 78 | } else if (['contract ', 'interface ', 'abstract', 'library', 'struct ', 'enum '].some(e => this.rawCommand.startsWith(e))) { 79 | this.scope = SCOPE.SOURCE_UNIT; 80 | this.hasNoReturnValue = true; 81 | } else { 82 | this.scope = SCOPE.MAIN; 83 | this.rawCommand = this.fixStatement(this.rawCommand); 84 | if (this.rawCommand === ';') { 85 | this.hasNoReturnValue = true; 86 | } 87 | } 88 | } 89 | 90 | 91 | if (this.hasNoReturnValue) { 92 | // expression 93 | this.returnExpression = ';'; 94 | this.returnType = ''; 95 | } else { 96 | // not an expression 97 | this.returnExpression = this.rawCommand; 98 | this.returnType = 'bool' 99 | } 100 | } 101 | 102 | fixStatement(stm) { 103 | return (stm.endsWith(';') || stm.endsWith('}')) ? stm : `${stm};` 104 | } 105 | 106 | toString() { 107 | return this.rawCommand; 108 | } 109 | 110 | toList() { 111 | return [this.rawCommand, this.scope] 112 | } 113 | } 114 | 115 | 116 | class InteractiveSolidityShell { 117 | 118 | constructor(settings, log) { 119 | this.log = log || console.log; 120 | 121 | const defaults = { 122 | templateContractName: 'MainContract', 123 | templateFuncMain: 'main', 124 | installedSolidityVersion: null, // overridden after merging settings; never use configured value 125 | providerUrl: 'http://127.0.0.1:8545', 126 | autostartGanache: true, 127 | blockchainProvider: 'internal', 128 | ganacheOptions: {}, 129 | ganacheCmd: 'ganache-cli', 130 | ganacheArgs: [/*'--gasLimit=999000000'*/], //optionally increase default gas limit 131 | debugShowContract: false, 132 | resolveHttpImports: true, 133 | enableAutoComplete: true, 134 | callGas: 3e6, 135 | deployGas: 3e6, 136 | etherscanApiKey: 'YourApiKeyToken' 137 | } 138 | 139 | this.settings = { 140 | ...defaults, ... (settings || {}) 141 | }; 142 | 143 | this.settings.installedSolidityVersion = require('../package.json').dependencies.solc.split("-", 1)[0]; 144 | 145 | this.cache = { 146 | compiler: {} /** compilerVersion:object */ 147 | }; 148 | 149 | this.cache.compiler[this.settings.installedSolidityVersion.startsWith("^") ? this.settings.installedSolidityVersion.substring(1) : this.settings.installedSolidityVersion] = solc; 150 | this.reset(); 151 | 152 | this.initBlockchain(); 153 | } 154 | 155 | initBlockchain() { 156 | if (this.blockchain) { 157 | this.blockchain.stopService(); 158 | } 159 | 160 | if (!this.settings.blockchainProvider || this.settings.blockchainProvider === "internal") { 161 | this.blockchain = new BuiltinGanacheBlockchain(this); 162 | } else if (this.settings.blockchainProvider.startsWith("https://") || this.settings.blockchainProvider.startsWith("http://")) { 163 | this.blockchain = new ExternalUrlBlockchain(this, this.settings.blockchainProvider); 164 | } else if (this.settings.blockchainProvider.length > 0) { 165 | this.settings.ganacheCmd = this.settings.blockchainProvider; 166 | this.blockchain = new ExternalProcessBlockchain(this); 167 | } else { 168 | this.log(" 🧨 unknown blockchain provider. falling back to built-in ganache.") 169 | this.blockchain = new BuiltinGanacheBlockchain(this); 170 | } 171 | this.blockchain.connect(); 172 | } 173 | 174 | loadSession(stmts) { 175 | if (!stmts) { 176 | this.session.statements = [] 177 | } else { 178 | this.session.statements = stmts.map(s => new SolidityStatement(s[0], s[1])); 179 | } 180 | } 181 | 182 | dumpSession() { 183 | return this.session.statements.map(s => s.toList()); 184 | } 185 | 186 | setSetting(key, value) { 187 | switch (key) { 188 | case 'installedSolidityVersion': return; 189 | case 'ganacheArgs': 190 | if (!value) { 191 | value = []; 192 | } 193 | else if (!Array.isArray(value)) { 194 | value = value.split(' '); 195 | } 196 | break; 197 | case 'ganacheCmd': 198 | value = value.trim(); 199 | } 200 | this.settings[key] = value; 201 | } 202 | 203 | reset() { 204 | this.session = { 205 | statements: [], 206 | } 207 | } 208 | 209 | revert() { 210 | this.session.statements.pop(); 211 | } 212 | 213 | prepareNextStatement(stm /* SolidityStatement */) { 214 | this.session.statements.push(stm); 215 | } 216 | 217 | template() { 218 | const prologue = this.session.statements.filter(stm => stm.scope === SCOPE.SOURCE_UNIT); 219 | const contractState = this.session.statements.filter(stm => stm.scope === SCOPE.CONTRACT); 220 | const mainStatements = this.session.statements.filter(stm => stm.scope === SCOPE.MAIN); 221 | 222 | /* figure out which compiler version to use */ 223 | const lastVersionPragma = this.session.statements.filter(stm => stm.scope === SCOPE.VERSION_PRAGMA).pop(); 224 | 225 | /* prepare body and return statement */ 226 | var lastStatement = this.session.statements[this.session.statements.length - 1] || {} 227 | if (lastStatement.scope !== SCOPE.MAIN || lastStatement.hasNoReturnValue === true) { 228 | /* not a main statement, put everything in the body and use a dummy as returnexpression */ 229 | var mainBody = mainStatements; 230 | lastStatement = new SolidityStatement() // add dummy w/o return value 231 | } else { 232 | var mainBody = mainStatements.slice(0, mainStatements.length - 1) 233 | } 234 | 235 | const ret = ` 236 | // SPDX-License-Identifier: GPL-2.0-or-later 237 | ${lastVersionPragma ? lastVersionPragma.rawCommand : `pragma solidity ${this.settings.installedSolidityVersion};`} 238 | 239 | ${prologue.join('\n\n')} 240 | 241 | contract ${this.settings.templateContractName} { 242 | 243 | ${contractState.join(' \n\n')} 244 | 245 | function ${this.settings.templateFuncMain}() public ${lastStatement.returnType ? `returns (${lastStatement.returnType})` : ''} { 246 | ${mainBody.join('\n ')} 247 | return ${lastStatement.returnExpression} 248 | } 249 | }`.trim(); 250 | if (this.settings.debugShowContract) this.log(ret) 251 | return ret; 252 | } 253 | 254 | 255 | loadCachedCompiler(solidityVersion) { 256 | 257 | solidityVersion = solidityVersion.startsWith("^") ? solidityVersion.substring(1) : solidityVersion; //strip leading ^ 258 | var that = this; 259 | /** load remote version - (maybe cache?) */ 260 | 261 | return new Promise((resolve, reject) => { 262 | if (that.cache.compiler[solidityVersion]) { 263 | return resolve(that.cache.compiler[solidityVersion]); 264 | } 265 | 266 | getRemoteCompiler(solidityVersion) 267 | .then(remoteSolidityVersion => { 268 | solc.loadRemoteVersion(remoteSolidityVersion, function (err, solcSnapshot) { 269 | that.cache.compiler[solidityVersion] = solcSnapshot; 270 | return resolve(solcSnapshot) 271 | }) 272 | }) 273 | .catch(err => { 274 | return reject(err) 275 | }) 276 | }); 277 | 278 | } 279 | 280 | compile(source, cbWarning) { 281 | let solidityVersion = getBestSolidityVersion(source); 282 | return new Promise((resolve, reject) => { 283 | 284 | if (!solidityVersion) { 285 | return reject(new Error(`No valid solidity version found in source code (e.g. pragma solidity 0.8.10).`)); 286 | } 287 | this.loadCachedCompiler(solidityVersion).then(solcSelected => { 288 | 289 | let input = { 290 | language: 'Solidity', 291 | sources: { 292 | '': { 293 | content: source, 294 | }, 295 | }, 296 | settings: { 297 | outputSelection: { 298 | '*': { 299 | // 300 | }, 301 | }, 302 | }, 303 | } 304 | input.settings.outputSelection['*']['*'] = ['abi', 'evm.bytecode', 'storageLayout'] 305 | 306 | const callbacks = { 307 | 'import': (sourcePath) => readFileCallback( 308 | sourcePath, { 309 | basePath: process.cwd(), 310 | includePath: [ 311 | path.join(process.cwd(), "node_modules") 312 | ], 313 | allowHttp: this.settings.resolveHttpImports 314 | } 315 | ) 316 | }; 317 | 318 | let ret = JSON.parse(solcSelected.compile(JSON.stringify(input), callbacks)) 319 | if (ret.errors) { 320 | let realErrors = ret.errors.filter(err => err.type !== 'Warning'); 321 | if (realErrors.length) { 322 | return reject(realErrors); 323 | } 324 | // print handle warnings 325 | let warnings = ret.errors.filter(err => err.type === 'Warning' && !IGNORE_WARNINGS.some(target => err.message.includes(target))); 326 | if (warnings.length) cbWarning(warnings); 327 | 328 | } 329 | return resolve(ret); 330 | }) 331 | .catch(err => { 332 | return reject(err); 333 | }); 334 | }); 335 | } 336 | 337 | run(statement) { 338 | return new Promise((resolve, reject) => { 339 | this.prepareNextStatement(statement) 340 | 341 | const sourceCode = this.template(); 342 | // 1st. pass 343 | this.compile(sourceCode, console.warn).then((res) => { 344 | // happy path; types are correct 345 | //console.log("first happy path") 346 | 347 | let contractData = res.contracts['']; 348 | contractData[this.settings.templateContractName]['main'] = this.settings.templateFuncMain; 349 | 350 | this.blockchain.deploy(contractData, (err, retval) => { 351 | if (err) { 352 | this.revert(); 353 | return reject(err) 354 | } 355 | return resolve(retval) // return value 356 | }) 357 | }).catch(errors => { 358 | // frownie face 359 | if (!Array.isArray(errors)) { //handle single error 360 | this.revert(); 361 | return reject(errors); 362 | } 363 | //get last typeError to detect return type: 364 | let lastTypeError = errors.slice().reverse().find(err => err.type === "TypeError"); 365 | if (!lastTypeError) { 366 | this.revert(); 367 | return reject(errors); 368 | } 369 | let retType = "" 370 | let matches = lastTypeError.message.match(rexTypeErrorReturnArgumentX); 371 | if (matches) { 372 | //console.log("2nd pass - detect return type") 373 | retType = matches[1].trim(); 374 | if (retType.startsWith('int_const -')) { 375 | retType = 'int'; 376 | } else if (retType.startsWith('int_const ')) { 377 | retType = 'uint'; 378 | } else if (retType.startsWith('contract ')) { 379 | retType = retType.split("contract ", 2)[1] 380 | } else if (retType.endsWith(' pointer')) { 381 | let fragments = retType.split(' '); //address[] storage pointer 382 | fragments.pop() // pop 'pointer' 383 | console.log(fragments) 384 | if (fragments[1] == "storage") { 385 | fragments[1] = "memory"; 386 | } 387 | retType = fragments.join(' '); 388 | } 389 | } else if (lastTypeError.message.includes(TYPE_ERROR_DETECT_RETURNS)) { 390 | console.error("WARNING: cannot auto-resolve type for complex function yet ://\n If this is a function call, try unpacking the function return values into local variables explicitly!\n e.g. `(uint a, address b, address c) = myContract.doSomething(1,2,3);`") 391 | // lets give it a low-effort try to resolve return types. this will not always work. 392 | let rexFunctionName = new RegExp(`([a-zA-Z0-9_\\.]+)\\s*\\(.*?\\)`); 393 | let matchedFunctionNames = statement.rawCommand.match(rexFunctionName); 394 | if (matchedFunctionNames.length >= 1) { 395 | let funcNameParts = matchedFunctionNames[1].split("."); 396 | let funcName = funcNameParts[funcNameParts.length - 1]; //get last 397 | let rexReturns = new RegExp(`function ${funcName}\\s*\\(.* returns\\s*\\(([^\\)]+)\\)`) 398 | 399 | let returnDecl = sourceCode.match(rexReturns); 400 | if (returnDecl.length > 1) { 401 | retType = returnDecl[1]; 402 | } 403 | } 404 | 405 | if (retType === "") { 406 | this.revert(); 407 | return reject(errors); 408 | } 409 | } else { 410 | console.error("BUG: cannot resolve type ://") 411 | this.revert(); 412 | return reject(errors); 413 | } 414 | 415 | this.session.statements[this.session.statements.length - 1].returnType = retType; 416 | 417 | //try again! 418 | this.compile(this.template(), console.warn).then((res) => { 419 | // happy path 420 | //console.log(res) 421 | 422 | let contractData = res.contracts['']; 423 | contractData[this.settings.templateContractName]['main'] = this.settings.templateFuncMain; 424 | 425 | this.blockchain.deploy(contractData, (err, retval) => { 426 | if (err) { 427 | this.revert(); 428 | return reject(err) 429 | } 430 | return resolve(retval) // return value 431 | }) 432 | }).catch(errors => { 433 | // error here 434 | this.revert(); 435 | return reject(errors); 436 | }) 437 | }) 438 | }); 439 | } 440 | } 441 | 442 | module.exports = { 443 | InteractiveSolidityShell, 444 | SolidityStatement, 445 | SCOPE 446 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @author github.com/tintinweb 4 | * @license MIT 5 | * */ 6 | const { InteractiveSolidityShell } = require('./handler'); 7 | 8 | module.exports = { 9 | InteractiveSolidityShell 10 | } --------------------------------------------------------------------------------