From b2ce7c14c8445e5342dae429be9d94ba0658ca7f Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Fri, 4 Jun 2021 16:33:42 +0700 Subject: [PATCH] Add verify command --- .eslintrc | 4 ++ README.md | 46 ++++++++++++---------- package.json | 3 +- src/commands/hello.ts | 31 --------------- src/commands/verify.ts | 81 ++++++++++++++++++++++++++++++++++++++ src/constants.ts | 24 ++++++++++++ src/etherscan.ts | 45 +++++++++++++++++++++ src/sourcify.ts | 63 ++++++++++++++++++++++++++++++ src/utils.ts | 16 ++++++++ test.js | 88 ++++++++++++++++++++++++++++++++++++++++++ yarn.lock | 12 ++++++ 11 files changed, 361 insertions(+), 52 deletions(-) delete mode 100644 src/commands/hello.ts create mode 100644 src/commands/verify.ts create mode 100644 src/constants.ts create mode 100644 src/etherscan.ts create mode 100644 src/sourcify.ts create mode 100644 src/utils.ts create mode 100644 test.js diff --git a/.eslintrc b/.eslintrc index 7b84619..872d78f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,8 @@ { + "rules": { + "no-constant-condition": "off", + "no-await-in-loop": "off" + }, "extends": [ "oclif", "oclif-typescript" diff --git a/README.md b/README.md index 574eef1..7700c50 100644 --- a/README.md +++ b/README.md @@ -28,43 +28,49 @@ USAGE # Commands -* [`sourcify-to-etherscan hello [FILE]`](#sourcify-to-etherscan-hello-file) * [`sourcify-to-etherscan help [COMMAND]`](#sourcify-to-etherscan-help-command) +* [`sourcify-to-etherscan verify CONTRACT`](#sourcify-to-etherscan-verify-contract) -## `sourcify-to-etherscan hello [FILE]` +## `sourcify-to-etherscan help [COMMAND]` -describe the command here +display help for sourcify-to-etherscan ``` USAGE - $ sourcify-to-etherscan hello [FILE] + $ sourcify-to-etherscan help [COMMAND] -OPTIONS - -f, --force - -h, --help show CLI help - -n, --name=name name to print +ARGUMENTS + COMMAND command to show help for -EXAMPLE - $ sourcify-to-etherscan hello - hello world from ./src/hello.ts! +OPTIONS + --all see all commands in CLI ``` -_See code: [src/commands/hello.ts](https://github.com/k1rill-fedoseev/sourcify-to-etherscan/blob/v0.0.1/src/commands/hello.ts)_ +_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.2/src/commands/help.ts)_ -## `sourcify-to-etherscan help [COMMAND]` +## `sourcify-to-etherscan verify CONTRACT` -display help for sourcify-to-etherscan +check contract verification status in Sourcify and Etherscan ``` USAGE - $ sourcify-to-etherscan help [COMMAND] - -ARGUMENTS - COMMAND command to show help for + $ sourcify-to-etherscan verify CONTRACT OPTIONS - --all see all commands in CLI + -a, --args=args abi-encoded constructor + arguments + + -h, --help show CLI help + + -k, --apikey=apikey etherscan api key + + -n, --network=(mainnet|ropsten|rinkeby|goerli|kovan|bsc|bsc_testnet|1|3|4|5|42|56|97) [default: mainnet] network name + or chain id to use + +EXAMPLES + $ sourcify-to-etherscan verify --apikey <...> --network rinkeby 0x94263a20b1Eea751d6C3B207A7A0ba8fF8Db9E90 + $ sourcify-to-etherscan verify -k <...> -n 4 -a <...> 0x94263a20b1Eea751d6C3B207A7A0ba8fF8Db9E90 ``` -_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.2/src/commands/help.ts)_ +_See code: [src/commands/verify.ts](https://github.com/k1rill-fedoseev/sourcify-to-etherscan/blob/v0.0.1/src/commands/verify.ts)_ diff --git a/package.json b/package.json index 3d9a6ed..9350ab6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@oclif/command": "^1", "@oclif/config": "^1", "@oclif/plugin-help": "^3", + "axios": "^0.21.1", "tslib": "^1" }, "devDependencies": { @@ -24,7 +25,7 @@ "typescript": "^3.3" }, "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" }, "files": [ "/bin", diff --git a/src/commands/hello.ts b/src/commands/hello.ts deleted file mode 100644 index 4fe20a0..0000000 --- a/src/commands/hello.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {Command, flags} from '@oclif/command' - -export default class Hello extends Command { - static description = 'describe the command here' - - static examples = [ - `$ sourcify-to-etherscan hello -hello world from ./src/hello.ts! -`, - ] - - static flags = { - help: flags.help({char: 'h'}), - // flag with a value (-n, --name=VALUE) - name: flags.string({char: 'n', description: 'name to print'}), - // flag with no value (-f, --force) - force: flags.boolean({char: 'f'}), - } - - static args = [{name: 'file'}] - - async run() { - const {args, flags} = this.parse(Hello) - - const name = flags.name ?? 'world' - this.log(`hello ${name} from ./src/commands/hello.ts`) - if (args.file && flags.force) { - this.log(`you input --force and --file: ${args.file}`) - } - } -} diff --git a/src/commands/verify.ts b/src/commands/verify.ts new file mode 100644 index 0000000..660d120 --- /dev/null +++ b/src/commands/verify.ts @@ -0,0 +1,81 @@ +import {Command, flags} from '@oclif/command' +import {Network, NETWORKS, VerificationStatus} from '../constants' +import * as sourcify from '../sourcify' +import * as etherscan from '../etherscan' +import {makeStandardJson} from '../utils' + +export default class Verify extends Command { + static description = 'check contract verification status in Sourcify and Etherscan' + + static examples = [ + '$ sourcify-to-etherscan verify --apikey <...> --network rinkeby 0x94263a20b1Eea751d6C3B207A7A0ba8fF8Db9E90', + '$ sourcify-to-etherscan verify -k <...> -n 4 -a <...> 0x94263a20b1Eea751d6C3B207A7A0ba8fF8Db9E90', + ] + + static flags = { + help: flags.help({char: 'h'}), + network: flags.enum({ + char: 'n', + description: 'network name or chain id to use', + options: [...NETWORKS.map(n => n.name), ...NETWORKS.map(n => n.chainId.toString())], + default: 'mainnet', + }), + args: flags.string({char: 'a', description: 'abi-encoded constructor arguments'}), + apikey: flags.string({char: 'k', description: 'etherscan api key', env: 'ETHERSCAN_API_KEY'}), + } + + static args = [{name: 'contract', required: true}] + + async run() { + const {args, flags} = this.parse(Verify) + + const network: Network = NETWORKS.find(n => n.name === flags.network || n.chainId.toString() === flags.network)! + + this.log('Checking verification status on Sourcify') + + const status = await sourcify.check(network.chainId, args.contract) + + if (status !== 'perfect') { + this.error(`contract ${args.contract} is not yet verified on Sourcify for ${network.name}`) + } + + this.log('Fetching source code and metadata files from Sourcify') + + const files = await sourcify.files(network.chainId, args.contract) + + this.log('Parsing source files') + + const {target, version, metadata, sources, constructorArgs} = sourcify.parseFiles(files) + + this.log('Constructing standard JSON input from obtained source files') + + const standardJson = makeStandardJson(metadata, sources) + + this.log('Submitting standard JSON to etherscan') + + const guid = await etherscan.verify({ + api: network.etherscanApiURL, + apiKey: flags.apikey, + contract: args.contract, + source: standardJson, + target, + version, + args: flags.args || constructorArgs, + }) + + if (guid === VerificationStatus.ALREADY_VERIFIED) { + this.log('Contract is already verified on Etherscan') + return + } + + this.log(`Waiting for the verification job ${guid} to complete`) + + const result = await etherscan.waitFor(network.etherscanApiURL, flags.apikey, guid) + + if (result === VerificationStatus.SUCCESS) { + this.log('Successfully verified contract on Etherscan :)') + } else { + this.error('Failed to verify :(') + } + } +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..ff3ecf0 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,24 @@ +export enum VerificationStatus { + FAILED = 'Fail - Unable to verify', + SUCCESS = 'Pass - Verified', + PENDING = 'Pending in queue', + ALREADY_VERIFIED = 'Contract source code already verified', +} + +export type Network = { + name: string; + chainId: number; + etherscanApiURL: string; +} + +export const NETWORKS: Network[] = [ + {name: 'mainnet', chainId: 1, etherscanApiURL: 'https://api.etherscan.io/api'}, + {name: 'ropsten', chainId: 3, etherscanApiURL: 'https://api-ropsten.etherscan.io/api'}, + {name: 'rinkeby', chainId: 4, etherscanApiURL: 'https://api-rinkeby.etherscan.io/api'}, + {name: 'goerli', chainId: 5, etherscanApiURL: 'https://api-goerli.etherscan.io/api'}, + {name: 'kovan', chainId: 42, etherscanApiURL: 'https://api-kovan.etherscan.io/api'}, + {name: 'bsc', chainId: 56, etherscanApiURL: 'https://api.bscscan.com/api'}, + {name: 'bsc_testnet', chainId: 97, etherscanApiURL: 'https://api-testnet.bscscan.com/api'}, +] + +export const SOURCIFY_API = 'https://sourcify.dev/server/' diff --git a/src/etherscan.ts b/src/etherscan.ts new file mode 100644 index 0000000..379fb0f --- /dev/null +++ b/src/etherscan.ts @@ -0,0 +1,45 @@ +import axios from 'axios' +import {VerificationStatus} from './constants' +import {delay} from './utils' + +type VerifyParams = { + api: string; apiKey: string | undefined; contract: string; source: any; target: string; version: string; args: string; +} +export async function verify({api, apiKey, contract, source, target, version, args}: VerifyParams): Promise { + const params = new URLSearchParams() + if (apiKey) { + params.append('apikey', apiKey) + } + params.append('module', 'contract') + params.append('action', 'verifysourcecode') + params.append('contractaddress', contract) + params.append('sourceCode', JSON.stringify(source)) + params.append('codeformat', 'solidity-standard-json-input') + params.append('contractname', target) + params.append('compilerversion', version) + params.append('constructorArguements', args) + + const {data} = await axios.post(api, params, { + headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'}, + }) + + return data.result +} + +export async function waitFor(api: string, apiKey: string | undefined, guid: string): Promise { + while (true) { + await delay(3000) + + const {data} = await axios.get(api, { + params: { + apiKey, + module: 'contract', + action: 'checkverifystatus', + guid, + }, + }) + if (data.result !== VerificationStatus.PENDING) { + return data.result + } + } +} diff --git a/src/sourcify.ts b/src/sourcify.ts new file mode 100644 index 0000000..9f8edf1 --- /dev/null +++ b/src/sourcify.ts @@ -0,0 +1,63 @@ +import axios from 'axios' +import {SOURCIFY_API} from './constants' + +export async function check(chainId: number, contract: string): Promise { + const {data} = await axios.get(`${SOURCIFY_API}check-by-addresses`, { + params: { + addresses: contract, + chainIds: chainId, + }, + }) + if (!data || !data[0]) { + return 'false' + } + return data[0].status +} + +export async function files(chainId: number, contract: string): Promise { + const {data} = await axios.get(`${SOURCIFY_API}files/${chainId}/${contract}`) + + return data +} + +export type Metadata = { + language: string; + settings: any; +} +type SourcifyOutput = { + metadata: Metadata; + version: string; + sources: any; + target: string; + constructorArgs: string; +} +type FileContent = { + content: string; +} +type File = FileContent & { + name: string; + path: string; +} + +export function parseFiles(files: any): SourcifyOutput { + const metadata = JSON.parse(files.find((file: File) => file.name === 'metadata.json').content) + const constructorArgs = files.find((file: File) => file.name === 'constructor-args.txt') + const sourcesArray = files.filter((file: File) => file.name !== 'metadata.json' && file.name !== 'constructor-args.txt') + const version = `v${metadata.compiler.version}` + const target = Object.entries(metadata.settings.compilationTarget)[0].join(':') + const sources: {[key: string]: FileContent} = {} + + const prefix = sourcesArray[0].path.match(/^.*\/sources\//)[0] + for (const file of sourcesArray) { + const path = file.path.replace('sources/_', 'sources/@').slice(prefix.length) + sources[path] = {content: file.content} + } + + return { + metadata, + version, + target, + sources, + constructorArgs: constructorArgs ? constructorArgs.content.slice(2) : '', + } +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..0b1a789 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,16 @@ +import {Metadata} from './sourcify' + +export const delay = (ms: number) => new Promise(res => setTimeout(res, ms)) + +export function makeStandardJson(metadata: Metadata, sources: any) { + return { + language: metadata.language, + sources, + settings: { + optimizer: metadata.settings.optimizer, + evmVersion: metadata.settings.evmVersion, + remappings: metadata.settings.remappings, + libraries: metadata.settings.libraries, + }, + } +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..f4b5f34 --- /dev/null +++ b/test.js @@ -0,0 +1,88 @@ +const fetch = require('node-fetch') +const querystring = require('querystring') +const axios = require('axios') + +const delay = (ms) => new Promise(res => setTimeout(res, ms)) + +async function main() { + const apiUrl = 'https://api-rinkeby.etherscan.io/api' + const address = '0x94263a20b1Eea751d6C3B207A7A0ba8fF8Db9E90' + const res = await fetch(`https://sourcify.dev/server/files/4/${address}`) + const files = await res.json() + + const metadata = JSON.parse(files.find(file => file.name === 'metadata.json').content) + const sources = files.filter(file => file.name !== 'metadata.json' && file.name !== 'constructor-args.txt') + const version = `v${metadata.compiler.version}` + console.log(version) + const target = Object.entries(metadata.settings.compilationTarget)[0].join(':') + console.log(target) + console.log(metadata.settings) + console.log(sources.length) + + const standardJson = { + language: metadata.language, + sources: {}, + settings: { + optimizer: metadata.settings.optimizer, + evmVersion: metadata.settings.evmVersion, + remappings: metadata.settings.remappings, + libraries: metadata.settings.libraries + } + } + const prefix = '/home/data/repository/contracts/full_match/4/0x94263a20b1Eea751d6C3B207A7A0ba8fF8Db9E90/sources/' + for (const file of sources) { + file.path = file.path.replace('sources/_', 'sources/@') + console.log(file.path) + standardJson.sources[file.path.slice(prefix.length)] = { + content: file.content + } + } + // console.log(sources) + console.log(standardJson) + + const args = '00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010666f7220566572696669636174696f6e00000000000000000000000000000000' + const apikey = 'MWBWHZH2CGU1KM4C28BH55IJHN439G41XQ' + const body = { + apikey, + module: 'contract', + action: 'verifysourcecode', + contractaddress: address, + sourceCode: JSON.stringify(standardJson), + codeformat: 'solidity-standard-json-input', + contractname: target, + compilerversion: version, + constructorArguements: '' + } + const str = querystring.stringify(body) + const result = await fetch(apiUrl, { + method: 'POST', + body: str, + headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' } + }).then(res => res.json()) + console.log(result) + + + + const guid = result.result + while (true) { + await delay(5000) + + try { + const qs = querystring.stringify({ + apiKey: apikey, + module: 'contract', + action: 'checkverifystatus', + guid + }) + const verificationResult = await fetch(`${apiUrl}?${qs}`).then(res => res.json()) + if (verificationResult.result !== VerificationStatus.PENDING) { + console.log(verificationResult) + } + } catch (error) { + console.log(error) + throw new Error(`Failed to connect to Etherscan API at url ${apiUrl}`) + } + } +} + +main() diff --git a/yarn.lock b/yarn.lock index 8cf258d..23c889e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -298,6 +298,13 @@ astral-regex@^1.0.0: resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +axios@^0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== + dependencies: + follow-redirects "^1.10.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -891,6 +898,11 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +follow-redirects@^1.10.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43" + integrity sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg== + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"