-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fcffb04
commit 6f7c2d5
Showing
8 changed files
with
236 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters