Skip to content

Commit

Permalink
New Command: Fetch interface declaration from etherscan.io (#19)
Browse files Browse the repository at this point in the history
* fetch and autogen interface declrations for contracts on etherscan.io

* add abi-to-sol

* update dependencies

* improve error handling, remove debug log

* prep 0.2.0

* autoformat
  • Loading branch information
tintinweb committed Aug 5, 2022
1 parent ed3c4c4 commit 5d8c4f4
Show file tree
Hide file tree
Showing 10 changed files with 3,211 additions and 928 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Change Log
All notable changes will be documented in this file.

## v0.2.0
- new: new command to fetch & load interface declaration from etherscan.io #19

![shell-fetch-interface](https://user-images.githubusercontent.com/2865694/183062446-c952b308-9fc7-49f9-8308-3eac09ca3b4a.gif)


## v0.1.2

- fix: support require(), type, abstract, library
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ BNB
.help ... this help :)
.exit ... exit the shell

Source:
.fetch
interface <address> <name> [chain=mainnet] ... fetch and load an interface declaration from an ABI spec on etherscan.io

Blockchain:
.chain
restart ... restart the blockchain service
Expand Down
347 changes: 193 additions & 154 deletions bin/main.js

Large diffs are not rendered by default.

3,665 changes: 2,937 additions & 728 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "solidity-shell",
"version": "0.1.2",
"version": "0.2.0",
"description": "An interactive Solidity shell with lightweight session recording and remote compiler support",
"main": "src/index.js",
"bin": {
Expand All @@ -26,6 +26,7 @@
],
"license": "MIT",
"dependencies": {
"abi-to-sol": "^0.6.5",
"ganache": "^7.0.4",
"minimist": "^1.2.5",
"readline-sync": "^1.4.10",
Expand Down
32 changes: 16 additions & 16 deletions src/blockchain.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ class AbsBlockchainBase {
})
});
}

methodCall(cmd, args) {
return new Promise((resolve, reject) => {
let func = this.web3.eth[cmd];
if(func === undefined) {
if (func === undefined) {
return reject(" 🧨 Unsupported Method");
}
if(typeof func === "function"){
if (typeof func === "function") {
func((err, result) => {
if (err) return reject(new Error(err));
return resolve(result);
Expand All @@ -74,21 +74,21 @@ class AbsBlockchainBase {
});
}

rpcCall(method, params){
rpcCall(method, params) {
return new Promise((resolve, reject) => {
let payload = {
"jsonrpc":"2.0",
"method":method,
"params":params === undefined ? [] : params,
"id":1
"jsonrpc": "2.0",
"method": method,
"params": params === undefined ? [] : params,
"id": 1
}
this.provider.send(payload, (error, result) => {
if(error)
if (error)
return reject(error);
return resolve(result);
});
});

}

async deploy(contracts, callback) {
Expand Down Expand Up @@ -147,7 +147,7 @@ class BuiltinGanacheBlockchain extends AbsBlockchainBase {
const defaultOptions = {
logging: { quiet: true },
};
this.options = {...defaultOptions, ...shell.settings.ganacheOptions};
this.options = { ...defaultOptions, ...shell.settings.ganacheOptions };
}

connect() {
Expand All @@ -165,14 +165,14 @@ class BuiltinGanacheBlockchain extends AbsBlockchainBase {
this.web3 = new Web3(this.provider);
});


}

startService() {
if (this.provider !== undefined) {
return this.provider;
}

this.provider = ganache.provider(this.options);
}
stopService() {
Expand Down Expand Up @@ -222,9 +222,9 @@ class ExternalProcessBlockchain extends AbsBlockchainBase {
return this.proc;
}
this.log("ℹ️ ganache-mgr: starting temp. ganache instance ...\n »");

this.proc = require('child_process').spawn(this.shell.settings.ganacheCmd, this.shell.settings.ganacheArgs);
this.proc.on('error', function(err) {
this.proc.on('error', function (err) {
console.error(`
🧨 Unable to launch blockchain serivce: ➜ ℹ️ ${err}
Expand All @@ -236,7 +236,7 @@ class ExternalProcessBlockchain extends AbsBlockchainBase {

stopService() {
this.log("💀 ganache-mgr: stopping temp. ganache instance");
if(this.proc) {
if (this.proc) {
this.proc.kill('SIGINT');
this.proc = undefined;
}
Expand Down
35 changes: 29 additions & 6 deletions src/compiler/remoteCompiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@
* */
/** IMPORT */
const { solcVersions } = require('./autogenerated/solcVersions.js')
const { generateSolidity } = require('abi-to-sol')
const request = require('request');

function normalizeSolcVersion(version) {
return version.replace('soljson-', '').replace('.js', '');
}

function getSolcJsCompilerList(options){
function getSolcJsCompilerList(options) {
options = options || {};
return new Promise((resolve, reject) => {
request.get('https://solc-bin.ethereum.org/bin/list.json', (err, res, body) => {
if(err){
if (err) {
return reject(err)
}else{
} else {
let data = JSON.parse(body);
let releases = Object.values(data.releases)

if(options.nightly){
if (options.nightly) {
releases = Array.from(new Set([...releases, ...data.builds.map(b => b.path)]));
}
return resolve(releases.map(normalizeSolcVersion))
Expand All @@ -38,7 +39,7 @@ function getRemoteCompiler(solidityVersion) {
(e) => !e.includes('nightly') && e.includes(`v${solidityVersion}`)
)

if (remoteSolidityVersion) {
if (remoteSolidityVersion) {
return resolve(remoteSolidityVersion);
}
//download remote compiler list and check again.
Expand All @@ -55,10 +56,32 @@ function getRemoteCompiler(solidityVersion) {
});
}

//.import interface 0x40cfee8d71d67108db46f772b7e2cd55813bf2fb test2
function getRemoteInterfaceFromEtherscan(address, name, chain) {
return new Promise((resolve, reject) => {

let provider = `https://api${(!chain || chain == "mainnet") ? "" : `-${chain}`}.etherscan.io`
let url = `${provider}/api?module=contract&action=getabi&address=${address}`;
request.get(url, (err, res, body) => {
if (err) {
return reject(err)
} else {
let data = JSON.parse(body);
if (!data.status || data.status != "1" || !data.result) {
return reject(data)
}
let abi = JSON.parse(data.result);
let src = generateSolidity({ name: name, solidityVersion: "0.8.9", abi });
src = src.substring(src.indexOf("\n\n") + 2, src.indexOf("// THIS FILE WAS AUTOGENERATED FROM"));
return resolve(src)
}
})
});
}

module.exports = {
getRemoteCompiler,
getSolcJsCompilerList
getSolcJsCompilerList,
getRemoteInterfaceFromEtherscan
}

6 changes: 4 additions & 2 deletions src/compiler/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const fs = require('fs');

function readFileCallback(sourcePath, options) {
options = options || {};
if(sourcePath.startsWith("https://") && options.allowHttp){
if (sourcePath.startsWith("https://") && options.allowHttp) {
//allow https! imports; not yet implemented
const res = require('sync-request')('GET', sourcePath); //@todo: this is super buggy and might freeze the app. needs async/promises.
return { contents: res.getBody('utf8')};
return { contents: res.getBody('utf8') };
}
else {
const prefixes = [options.basePath ? options.basePath : ""].concat(
Expand All @@ -31,6 +31,8 @@ function readFileCallback(sourcePath, options) {
return { error: 'File not found inside the base path or any of the include paths.' }
}



module.exports = {
readFileCallback
}
39 changes: 19 additions & 20 deletions src/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
* */
/** IMPORT */
const path = require('path');
const Web3 = require('web3');
const solc = require('solc');
const { getRemoteCompiler } = require('./compiler/remoteCompiler.js');
const { readFileCallback } = require('./compiler/utils.js');
const { ExternalProcessBlockchain, ExternalUrlBlockchain ,BuiltinGanacheBlockchain } = require('./blockchain.js');
const { ExternalProcessBlockchain, ExternalUrlBlockchain, BuiltinGanacheBlockchain } = require('./blockchain.js');


/** CONST */
Expand Down Expand Up @@ -153,13 +152,13 @@ class InteractiveSolidityShell {
}

initBlockchain() {
if(this.blockchain){
if (this.blockchain) {
this.blockchain.stopService();
}

if(!this.settings.blockchainProvider || this.settings.blockchainProvider === "internal"){
if (!this.settings.blockchainProvider || this.settings.blockchainProvider === "internal") {
this.blockchain = new BuiltinGanacheBlockchain(this);
} else if(this.settings.blockchainProvider.startsWith("https://") || this.settings.blockchainProvider.startsWith("http://")) {
} else if (this.settings.blockchainProvider.startsWith("https://") || this.settings.blockchainProvider.startsWith("http://")) {
this.blockchain = new ExternalUrlBlockchain(this, this.settings.blockchainProvider);
} else if (this.settings.blockchainProvider.length > 0) {
this.settings.ganacheCmd = this.settings.blockchainProvider;
Expand Down Expand Up @@ -306,12 +305,12 @@ contract ${this.settings.templateContractName} {
const callbacks = {
'import': (sourcePath) => readFileCallback(
sourcePath, {
basePath: process.cwd(),
includePath: [
path.join(process.cwd(), "node_modules")
],
allowHttp: this.settings.resolveHttpImports
}
basePath: process.cwd(),
includePath: [
path.join(process.cwd(), "node_modules")
],
allowHttp: this.settings.resolveHttpImports
}
)
};

Expand Down Expand Up @@ -368,7 +367,7 @@ contract ${this.settings.templateContractName} {
}
let retType = ""
let matches = lastTypeError.message.match(rexTypeErrorReturnArgumentX);
if(matches){
if (matches) {
//console.log("2nd pass - detect return type")
retType = matches[1].trim();
if (retType.startsWith('int_const -')) {
Expand All @@ -381,28 +380,28 @@ contract ${this.settings.templateContractName} {
let fragments = retType.split(' '); //address[] storage pointer
fragments.pop() // pop 'pointer'
console.log(fragments)
if (fragments[1] == "storage"){
if (fragments[1] == "storage") {
fragments[1] = "memory";
}
retType = fragments.join(' ');
}
} else if(lastTypeError.message.includes(TYPE_ERROR_DETECT_RETURNS)) {
} else if (lastTypeError.message.includes(TYPE_ERROR_DETECT_RETURNS)) {
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);`")
// lets give it a low-effort try to resolve return types. this will not always work.
let rexFunctionName = new RegExp(`([a-zA-Z0-9_\\.]+)\\s*\\(.*?\\)`);
let matchedFunctionNames = statement.rawCommand.match(rexFunctionName);
if(matchedFunctionNames.length >= 1 ){
if (matchedFunctionNames.length >= 1) {
let funcNameParts = matchedFunctionNames[1].split(".");
let funcName = funcNameParts[funcNameParts.length-1]; //get last
let funcName = funcNameParts[funcNameParts.length - 1]; //get last
let rexReturns = new RegExp(`function ${funcName}\\s*\\(.* returns\\s*\\(([^\\)]+)\\)`)

let returnDecl = sourceCode.match(rexReturns);
if(returnDecl.length >1){
if (returnDecl.length > 1) {
retType = returnDecl[1];
}
}

if(retType === ""){
if (retType === "") {
this.revert();
return reject(errors);
}
Expand All @@ -411,7 +410,7 @@ contract ${this.settings.templateContractName} {
this.revert();
return reject(errors);
}

this.session.statements[this.session.statements.length - 1].returnType = retType;

//try again!
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @author github.com/tintinweb
* @license MIT
* */
const {InteractiveSolidityShell} = require('./handler');
const { InteractiveSolidityShell } = require('./handler');

module.exports = {
InteractiveSolidityShell
Expand Down

0 comments on commit 5d8c4f4

Please sign in to comment.