Merge pull request #2 from embark-framework/feat/add-verifier

Add verify command
This commit is contained in:
Jonathan Rainville 2019-01-23 14:37:56 -05:00 committed by GitHub
commit 1e473f18ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 281 additions and 3 deletions

View File

@ -25,4 +25,30 @@ module.exports = (embark) => {
flattener.flatten(contractNames, callback);
}
});
const etherscanKeyDocsLink = 'https://etherscancom.freshdesk.com/support/solutions/articles/35000022163-i-need-an-api-key';
embark.registerConsoleCommand({
description: `Verifies a contract on Etherscan using you contract configuration\n\t\tRequires an Etherscan API key.
See: ${etherscanKeyDocsLink}`,
matches: (cmd) => {
const [commandName] = cmd.split(' ');
return commandName === 'verify';
},
usage: "verify <apiKey> <contractName>]",
process: (cmd, callback) => {
const [, apiKey, contractName] = cmd.split(' ');
if (!apiKey || !contractName) {
embark.logger.error('Missing argument. Please provide your Etherscan API key and the contract name'.red);
embark.logger.error(`You can get an API key using this tutorial: ${etherscanKeyDocsLink}`.cyan);
return callback();
}
if (!embark.config.embarkConfig.versions.solc) {
return callback(null, 'solc version not present in embarkjs.json. Please add it to versions.solc'.red);
}
flattener.verify(apiKey, contractName, callback);
}
});
};

View File

@ -1,12 +1,35 @@
const fs = require('fs-extra');
const path = require('path');
const async = require('async');
const axios = require('axios');
const querystring = require('querystring');
const OUTPUT_DIR = 'flattenedContracts';
const solcVersionsListLink = 'https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/list.txt';
const API_URL_MAP = {
1: 'https://api.etherscan.io/api',
3: 'https://api-ropsten.etherscan.io/api',
4: 'https://api-rinkeby.etherscan.io/api',
42: 'https://api-kovan.etherscan.io/api'
};
const CONTRACT_URL_MAP = {
1: 'https://etherscan.io/address',
3: 'https://ropsten.etherscan.io/address',
4: 'https://rinkeby.etherscan.io/address',
42: 'https://kovan.etherscan.io/address'
};
const RETRY_COUNT = 5;
const RETRY_SLEEP_TIME = 5000;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class Flattener {
constructor(embark) {
this.embark = embark;
this.events = embark.events;
this.logger = embark.logger;
}
_doFlatten(contracts, callback) {
@ -48,7 +71,7 @@ class Flattener {
if (err) {
eachCb(err);
}
this.embark.logger.info(`Flattened ${path.basename(contract.path)} to ${outputName}`);
this.logger.info(`Flattened ${path.basename(contract.path)} to ${outputName}`);
eachCb();
});
});
@ -56,12 +79,202 @@ class Flattener {
}, callback);
}
_isContractValid(contract, contractName) {
if (!contract) {
this.logger.error(null, `Contract "${contractName}" was not found in contract list`.red);
return false;
}
if (!contract.deployedAddress) {
this.logger.error(null, `Contract "${contractName}" does not have a deployed address. Was the contract deployed?`.red);
return false;
}
return true;
}
_addLibraries(data, contract, callback) {
if (!contract.linkReferences || !Object.values(contract.linkReferences).length) {
return callback();
}
let libNames = [];
Object.values(contract.linkReferences).forEach(fileObject => {
libNames = libNames.concat(Object.keys(fileObject));
});
libNames = Array.from(new Set(libNames));
async.eachOf(libNames, (libName, index, eachCb) => {
this.events.request("contracts:contract", libName, (lib) => {
if (!this._isContractValid(lib, libName)) {
return eachCb('Make sure the library is not set as `deploy: false`');
}
data[`libraryname${index + 1}`] = libName;
data[`libraryaddress${index + 1}`] = lib.deployedAddress;
eachCb();
});
}, callback);
}
_getUrl(network) {
if (!API_URL_MAP[network]) {
return null;
}
return API_URL_MAP[network];
}
_doVerify(contract, flattenedCode, apiKey, solcVersion, networkId, callback) {
const data = {
apikey: apiKey,
module: 'contract',
action: 'verifysourcecode',
contractaddress: contract.deployedAddress,
sourceCode: flattenedCode,
contractname: contract.className,
compilerversion: solcVersion,
optimizationUsed: this.embark.config.embarkConfig.options.solc.optimize ? 1 : 0,
runs: this.embark.config.embarkConfig.options.solc['optimize-runs']
};
this.events.request('deploy:contract:object', contract, (err, deployObject) => {
if (err) {
return callback(err);
}
const encodedABI = deployObject.encodeABI();
let deployArguments = encodedABI.substring(encodedABI.length - 68);
if (deployArguments.substring(0, 3) !== '0029' && deployArguments.substring(deployArguments.length - 4) === '0029') {
// Most likely NOT arguments
deployArguments = '';
} else {
deployArguments = deployArguments.substring(4);
data.constructorArguements = deployArguments;
}
this._addLibraries(data, contract, async (err) => {
if (err) {
return callback(err);
}
const url = this._getUrl(networkId);
const etherscanContractUrl = CONTRACT_URL_MAP[networkId];
if (!url) {
return callback(null, `The current network (id "${networkId}") not supported by Etherscan. Available are: ${Object.keys(API_URL_MAP).join(', ')}`.red);
}
this.logger.info('Sending the request...');
try {
const response = await axios.request({
method: 'POST',
url,
data: querystring.stringify(data),
headers: {
'Content-type': 'application/x-www-form-urlencoded'
}
});
if (response.status !== 200 || response.data.status !== '1') {
return callback(null, `Error while trying to verify contract: ${JSON.stringify(response.data.result, null, 2)}`.red);
}
this.logger.info('Contract verification in process (this usually takes under 30 seconds)...');
await this.checkEtherscanVerificationStatus(response.data.result, url, RETRY_COUNT);
callback(null, `Contract verified successfully. You can check it here: ${etherscanContractUrl}/${contract.deployedAddress}#code`);
} catch(error) {
this.logger.error('Error while trying to verify contract');
callback(null, error.message);
}
});
});
}
async checkEtherscanVerificationStatus(guid, etherscanApiUrl, retries = RETRY_COUNT) {
const queryParams = querystring.stringify({
guid,
action: 'checkverifystatus',
module: 'contract'
});
try {
this.logger.info('Checking the verification status...');
const response = await axios.request({
method: 'GET',
url: `${etherscanApiUrl}?${queryParams}`
});
if (response.data.status !== '1') {
throw new Error(`Error while trying to verify contract: ${JSON.stringify(response.data.result, null, 2)}`);
}
} catch(error) {
if (retries === 0) {
throw new Error(error.message || 'Error while trying to check verification status');
}
this.logger.warn(`Verification not finished. Checking again in ${RETRY_SLEEP_TIME / 1000} seconds...`);
await sleep(RETRY_SLEEP_TIME);
await this.checkEtherscanVerificationStatus(guid, etherscanApiUrl, retries - 1);
}
}
verify(apiKey, contractName, callback) {
this.events.request("contracts:contract", contractName, (contract) => {
if (!this._isContractValid(contract, contractName)) {
return callback(null, 'Please make sure you specify the contract name as the class name. E.g. SimpleStorage instead of simple_storage.sol');
}
const flattenedContractFile = path.join(OUTPUT_DIR, contract.filename);
let flattenedFileExists = false;
if (fs.existsSync(flattenedContractFile)) {
this.logger.info(`Using the already flattened contract (${flattenedContractFile})`);
flattenedFileExists = true;
}
async.waterfall([
(next) => { // Flatten if needed
if (flattenedFileExists) {
return next();
}
const file = this.embark.config.contractsFiles.find(file => path.normalize(file.path) === path.normalize(contract.originalFilename));
this._doFlatten([file], next);
},
(next) => { // Read the flattened contract
fs.readFile(flattenedContractFile, (err, content) => {
if (err) {
return next(err);
}
next(null, content.toString());
});
},
(content, next) => { // Get supported versions list
axios.get(solcVersionsListLink)
.then((response) => {
const solcVersion = response.data.split('\n').find(version => version.indexOf(this.embark.config.embarkConfig.versions.solc) > -1 && version.indexOf('nightly') === -1);
next(null, content, solcVersion.replace('soljson-', '').replace('.js', ''));
})
.catch(next);
},
(content, solcVersion, next) => {
this.events.request("blockchain:networkId", (networkId) => {
next(null, content, solcVersion, networkId);
});
}
], (err, content, solcVersion, networkId) => {
if (err) {
return callback(err);
}
this._doVerify(contract, content, apiKey, solcVersion, networkId, callback);
});
});
}
flatten(contractNames, callback) {
if (!contractNames) {
return this._doFlatten(this.embark.config.contractsFiles, callback);
}
this.embark.events.request('contracts:all', (err, contracts) => {
this.events.request('contracts:all', (err, contracts) => {
if (err) {
return callback(err);
}

View File

@ -25,6 +25,8 @@
},
"dependencies": {
"async": "^2.6.1",
"fs-extra": "^7.0.1"
"axios": "^0.18.0",
"fs-extra": "^7.0.1",
"querystring": "^0.2.0"
}
}

View File

@ -75,6 +75,14 @@ async@^2.6.1:
dependencies:
lodash "^4.17.10"
axios@^0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102"
integrity sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=
dependencies:
follow-redirects "^1.3.0"
is-buffer "^1.1.5"
babel-code-frame@^6.22.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@ -202,6 +210,13 @@ cross-spawn@^5.1.0:
shebang-command "^1.2.0"
which "^1.2.9"
debug@=3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
dependencies:
ms "2.0.0"
debug@^3.1.0:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@ -369,6 +384,13 @@ flat-cache@^1.2.1:
rimraf "~2.6.2"
write "^0.2.1"
follow-redirects@^1.3.0:
version "1.6.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.6.1.tgz#514973c44b5757368bad8bddfe52f81f015c94cb"
integrity sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==
dependencies:
debug "=3.1.0"
fs-extra@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
@ -472,6 +494,11 @@ inquirer@^3.0.6:
strip-ansi "^4.0.0"
through "^2.3.6"
is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
@ -572,6 +599,11 @@ mkdirp@^0.5.1:
dependencies:
minimist "0.0.8"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
ms@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
@ -658,6 +690,11 @@ pseudomap@^1.0.2:
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
querystring@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
readable-stream@^2.2.2:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"