From 6f7c2d5d0443ecba776c81325278456e0b0fe868 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 5 Aug 2024 15:08:21 +1200 Subject: [PATCH] Add Blockscout verification --- package.json | 3 +- src/index.ts | 6 +- src/verifiers/BlockscoutVerifier.ts | 106 +++++++++++++++++++++++ src/verifiers/TenderlyVerifier.ts | 4 +- src/verifiers/index.ts | 17 +++- tests/utils/counter.ts | 2 + tests/verifiers/BlockscoutVerify.spec.ts | 101 +++++++++++++++++++++ yarn.lock | 5 ++ 8 files changed, 236 insertions(+), 8 deletions(-) create mode 100644 src/verifiers/BlockscoutVerifier.ts create mode 100644 tests/verifiers/BlockscoutVerify.spec.ts diff --git a/package.json b/package.json index 4d838f3..e81438f 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "dependencies": { "@tenderly/sdk": "^0.1.14", "axios": "^1.3.5", - "ethers": "^5.7.2" + "ethers": "^5.7.2", + "formdata-node": "^6.0.3" }, "devDependencies": { "@types/jest": "^29.5.0", diff --git a/src/index.ts b/src/index.ts index 8f15935..0968198 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,10 @@ import { ContractVerifier } from './ContractVerifier' import { DeploymentFlow } from './DeploymentFlow' import * as deployers from './deployers' -import type { EtherscanVerificationRequest } from './verifiers' +import type { + BlockscoutVerificationRequest, + EtherscanVerificationRequest, +} from './verifiers' import * as verifiers from './verifiers' import type { Deployer } from './types/deployer' @@ -11,6 +14,7 @@ import type { Logger } from './types/logger' export { ContractVerifier, deployers, DeploymentFlow, verifiers } export type { + BlockscoutVerificationRequest, ContractVerificationRequest, Deployer, EtherscanVerificationRequest, diff --git a/src/verifiers/BlockscoutVerifier.ts b/src/verifiers/BlockscoutVerifier.ts new file mode 100644 index 0000000..6e07190 --- /dev/null +++ b/src/verifiers/BlockscoutVerifier.ts @@ -0,0 +1,106 @@ +import axios from 'axios' +import { File, FormData } from 'formdata-node' +import type { CompilerInput } from 'solc' +import type { Logger } from 'src/types/logger' + +export type BlockscoutVerificationRequest = { + contractToVerify: string + version: string // https://etherscan.io/solcversions + licenceType: string + compilerInput: CompilerInput + constructorArgs?: string // As a hex string + waitForSuccess?: boolean +} + +type BlockscoutApiResponse = { + message: string +} + +export class BlockscoutVerifier { + constructor( + private readonly blockscoutUrl: string, + private readonly logger?: Logger, + ) {} + + // Throws on failure + verifyContract = async ( + addr: string, + request: BlockscoutVerificationRequest, + ): Promise => { + //TODO Skip already verified contracts + + const contractNameParts = request.contractToVerify.split(':') + const contractName = contractNameParts[ + contractNameParts.length - 1 + ].replace('.sol', '') + + // Create verification body + const verifyBody = new FormData() + verifyBody.append('compiler_version', request.version) + verifyBody.append('license_type', request.licenceType.toLowerCase()) + verifyBody.append('contract_name', contractName) + verifyBody.append('autodetect_constructor_args', 'false') + verifyBody.append('constructor_args', request.constructorArgs ?? '') + const compilerInput = JSON.stringify(request.compilerInput) + const compilerInputFile = new File([compilerInput], 'compiler_input.json', { + type: 'application/json', + }) + verifyBody.append('files[0]', compilerInputFile, 'compiler_input.json') + + //TODO Add linked library information + + // Send the request + this.logger?.log( + `Verifying ${request.contractToVerify} at ${addr} on ${this.blockscoutUrl}`, + ) + try { + let success = false + while (!success) { + success = await this.sendVerifyRequest(addr, verifyBody) + if (!request.waitForSuccess) { + // Don't wait + break + } + if (!success) { + // Waiting for success is just retrying to verify until "verified" result returned + this.logger?.log('Waiting for verification...') + await new Promise(resolve => setTimeout(resolve, 10000)) // Wait 10 sec + } + } + this.logger?.log('Verified') + } catch (err: unknown) { + this.logger?.error('Failed to verify') + this.logger?.error((err as Error).message) + throw err + } + } + + // Throws on failure + sendVerifyRequest = async ( + addr: string, + body: FormData, + ): Promise => { + const verifyUrl = `${this.blockscoutUrl}/api/v2/smart-contracts/${addr}/verification/via/standard-input` + const res = await axios.postForm(verifyUrl, body) + let errMsg: string + if (res.status < 200 || res.status > 299) { + errMsg = `Failed to verify. Code: ${res.status}` + } else { + // Try parse response + const json = res.data as BlockscoutApiResponse + if (json.message === 'Already verified') { + // Success + return true + } + if (json.message === 'Smart-contract verification started') { + // Pending + return false + } + // Else failed + errMsg = `Failed to verify. Message: ${json.message}` + } + // Fail over + this.logger?.error(errMsg) + throw Error(errMsg) + } +} diff --git a/src/verifiers/TenderlyVerifier.ts b/src/verifiers/TenderlyVerifier.ts index 78fd292..252f15e 100644 --- a/src/verifiers/TenderlyVerifier.ts +++ b/src/verifiers/TenderlyVerifier.ts @@ -1,4 +1,4 @@ -import type { TenderlyConfiguration, VerificationRequest } from '@tenderly/sdk' +import type { TenderlyConfiguration, VerificationRequest as TenderlyVerificationRequest } from '@tenderly/sdk' import { Tenderly } from '@tenderly/sdk' export class TenderlyVerifier { @@ -14,7 +14,7 @@ export class TenderlyVerifier { verifyContract = async ( address: string, contractAlias: string, - tenderVerificationRequest: VerificationRequest, + tenderVerificationRequest: TenderlyVerificationRequest, ): Promise => { const addr = address.toLowerCase() diff --git a/src/verifiers/index.ts b/src/verifiers/index.ts index 711fc9f..355d79c 100644 --- a/src/verifiers/index.ts +++ b/src/verifiers/index.ts @@ -1,6 +1,15 @@ -import type { EtherscanVerificationRequest } from './EtherscanVerifier' -import { EtherscanVerifier } from './EtherscanVerifier' +import { + BlockscoutVerifier, + type BlockscoutVerificationRequest, +} from './BlockscoutVerifier' +import { + EtherscanVerifier, + type EtherscanVerificationRequest, +} from './EtherscanVerifier' import { TenderlyVerifier } from './TenderlyVerifier' -export type { EtherscanVerificationRequest } -export { EtherscanVerifier, TenderlyVerifier } +export type VerificationRequest = BlockscoutVerificationRequest & + EtherscanVerificationRequest + +export { BlockscoutVerifier, EtherscanVerifier, TenderlyVerifier } +export type { BlockscoutVerificationRequest, EtherscanVerificationRequest } diff --git a/tests/utils/counter.ts b/tests/utils/counter.ts index bd2d63c..5fc720a 100644 --- a/tests/utils/counter.ts +++ b/tests/utils/counter.ts @@ -7,6 +7,8 @@ export const COUNTER_ADDR_SEPOLIA = '0x3A15CBFa7EF9F817F11638156Af1b49e149c832a' export const COUNTER_BYTECODE = '0x60808060405234610016576101c4908161001c8239f35b600080fdfe60806040818152600436101561001457600080fd5b600091823560e01c90816306661abd1461017457508063371303c0146100fb5780636d4ce63c146100df5763b3bcfa821461004e57600080fd5b346100db57816003193601126100db5781546000198101918183116100c7577fc5802a3758d71d4ea2b77079fad7b332621c089586b2fd70ec9b7e75761a8def91838260c0935192608084526009608085015268111958dc995b595b9d60ba1b60a08501526020840152820152336060820152a1815580f35b634e487b7160e01b84526011600452602484fd5b5080fd5b50346100db57816003193601126100db57602091549051908152f35b50346100db57816003193601126100db57815460018101918282116100c7577fc5802a3758d71d4ea2b77079fad7b332621c089586b2fd70ec9b7e75761a8def91838260c0935192608084526009608085015268125b98dc995b595b9d60ba1b60a08501526020840152820152336060820152a1815580f35b8390346100db57816003193601126100db57602091548152f3fea26469706673582212203e08e2b41ba8a13950dafce027ef2aab8c15793c5bd7a086ce16d17c06d39dc564736f6c63430008120033' +export const COUNTER_LICENCE = "MIT" + export const COUNTER_SOURCE = `// SPDX-License-Identifier: MIT pragma solidity ^0.8.18; contract CounterWithLogs { diff --git a/tests/verifiers/BlockscoutVerify.spec.ts b/tests/verifiers/BlockscoutVerify.spec.ts new file mode 100644 index 0000000..f7a6b18 --- /dev/null +++ b/tests/verifiers/BlockscoutVerify.spec.ts @@ -0,0 +1,101 @@ +import { JsonRpcProvider } from '@ethersproject/providers' +import axios from 'axios' +import { config as dotenvConfig } from 'dotenv' +import { ContractFactory, Wallet } from 'ethers' +import solc from 'solc' +import type { BlockscoutVerificationRequest } from '../../src/verifiers' +import { BlockscoutVerifier } from '../../src/verifiers' +import { + COUNTER_ADDR_SEPOLIA, + COUNTER_COMPILER_INPUT, + COUNTER_LICENCE, +} from '../utils/counter' + +dotenvConfig() + +const solcSnapshot = solc.setupMethods( + // eslint-disable-next-line @typescript-eslint/no-var-requires + require('../solc/soljson-v0.8.18+commit.87f61d96'), +) + +describe('BlockscoutVerifier', () => { + let blockscoutVerifier: BlockscoutVerifier + let axiosPostStub: jest.SpyInstance + let contractAddr = COUNTER_ADDR_SEPOLIA + + beforeAll(async () => { + const { SEPOLIA_PRIVATE_KEY, SEPOLIA_RPC_URL } = process.env + if (SEPOLIA_PRIVATE_KEY === undefined || SEPOLIA_RPC_URL === undefined) { + // Stub fetch + console.log('Required Sepolia env vars not found, using stubs') + axiosPostStub = jest.spyOn(axios, 'postForm') + axiosPostStub + .mockResolvedValueOnce({ + data: { message: 'Smart-contract verification started' }, + }) + .mockResolvedValueOnce({ + data: { message: 'Already verified' }, + }) + } else { + // Do it for real. Requires manual review on Blockscout + console.log('Sepolia env vars found, using real API for tests') + } + + blockscoutVerifier = new BlockscoutVerifier( + 'https://eth-sepolia.blockscout.com', + console, + ) + }) + + afterEach(async () => { + jest.restoreAllMocks() + }) + + it('verifies blockscout source', async () => { + const request: BlockscoutVerificationRequest = { + contractToVerify: 'contracts/Counter.sol:CounterWithLogs', + version: `v${solcSnapshot.version().replace('.Emscripten.clang', '')}`, + compilerInput: COUNTER_COMPILER_INPUT, + licenceType: COUNTER_LICENCE, + waitForSuccess: true, + } + + const { SEPOLIA_PRIVATE_KEY, SEPOLIA_RPC_URL } = process.env + if (SEPOLIA_PRIVATE_KEY !== undefined && SEPOLIA_RPC_URL !== undefined) { + // Blockscout will automatically verify the contract if it's already deployed with the same settings + // So we randomise the number of runs. That'll work most of the time + const randomRuns = Math.floor(Math.random() * 10000) + console.log(`Randomising runs to ${randomRuns}`) + request.compilerInput.settings.optimizer.runs = randomRuns + + // Create the factory from scratch + const compilerOutput = JSON.parse( + solcSnapshot.compile(JSON.stringify(request.compilerInput)), + ) + const contractOutput = + compilerOutput.contracts['contracts/Counter.sol'].CounterWithLogs + + // Deploy something new so we can verify it + const provider = new JsonRpcProvider(SEPOLIA_RPC_URL) + const wallet = new Wallet(SEPOLIA_PRIVATE_KEY, provider) + const factory = new ContractFactory( + contractOutput.abi, + contractOutput.evm.bytecode, + wallet, + ) + const deployed = await factory.deploy() + contractAddr = deployed.address + console.log(`Deployed new contract at ${contractAddr}`) + + // Pause for Blockscout to index contract + console.log('Waiting a bit so Blockscout can index contract') + await new Promise(resolve => setTimeout(resolve, 30000)) // Delay 30s (sometimes it needs longer...) + } + + await blockscoutVerifier.verifyContract(contractAddr, request) + + if (axiosPostStub) { + expect(axiosPostStub).toHaveBeenCalledTimes(2) // Once pending, once complete + } + }, 120000) // Increase the timeout to 120 seconds +}) diff --git a/yarn.lock b/yarn.lock index d7e7f55..3661733 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2271,6 +2271,11 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +formdata-node@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-6.0.3.tgz#48f8e2206ae2befded82af621ef015f08168dc6d" + integrity sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg== + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"