Merge pull request #2 from embark-framework/feat/add-verifier
Add verify command
This commit is contained in:
commit
1e473f18ca
26
index.js
26
index.js
|
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
217
lib/Flattener.js
217
lib/Flattener.js
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
37
yarn.lock
37
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue