Skip to content

Commit

Permalink
Add Blockscout verification
Browse files Browse the repository at this point in the history
  • Loading branch information
ScreamingHawk committed Aug 5, 2024
1 parent fcffb04 commit 6f7c2d5
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 8 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ 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'
import type { Logger } from './types/logger'

export { ContractVerifier, deployers, DeploymentFlow, verifiers }
export type {
BlockscoutVerificationRequest,
ContractVerificationRequest,
Deployer,
EtherscanVerificationRequest,
Expand Down
106 changes: 106 additions & 0 deletions src/verifiers/BlockscoutVerifier.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
//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<boolean> => {
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)
}
}
4 changes: 2 additions & 2 deletions src/verifiers/TenderlyVerifier.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -14,7 +14,7 @@ export class TenderlyVerifier {
verifyContract = async (
address: string,
contractAlias: string,
tenderVerificationRequest: VerificationRequest,
tenderVerificationRequest: TenderlyVerificationRequest,
): Promise<void> => {
const addr = address.toLowerCase()

Expand Down
17 changes: 13 additions & 4 deletions src/verifiers/index.ts
Original file line number Diff line number Diff line change
@@ -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 }
2 changes: 2 additions & 0 deletions tests/utils/counter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
101 changes: 101 additions & 0 deletions tests/verifiers/BlockscoutVerify.spec.ts
Original file line number Diff line number Diff line change
@@ -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
})
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit 6f7c2d5

Please sign in to comment.