Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/beige-tomatoes-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nomicfoundation/hardhat-verify": patch
---

Expose an `Etherscan` instance through `network.connect()` for advanced use cases. This version also adds a `customApiCall` method to the Etherscan instance, allowing custom requests to the Etherscan API ([#7644](https://github.com/NomicFoundation/hardhat/issues/7644))
72 changes: 72 additions & 0 deletions v-next/hardhat-verify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,78 @@ await verifyContract(

> Note: The `verifyContract` function is not re-exported from the Hardhat toolboxes, so you need to install the plugin and import it directly from `@nomicfoundation/hardhat-verify/verify`.

## Advanced Usage for Plugin Authors

If you're building a Hardhat plugin that needs direct access to the Etherscan API (for example, to verify proxy contracts or make custom API calls), you can access the Etherscan instance through `network.connect()`.

### Accessing the Etherscan Instance

```typescript
import type { HardhatRuntimeEnvironment } from "hardhat/types";

export async function myCustomVerificationTask(hre: HardhatRuntimeEnvironment) {
const { verifier } = await hre.network.connect();

// Access Etherscan instance
const etherscan = verifier.etherscan;

// Check if a contract is already verified
const isVerified = await etherscan.isVerified("0x1234...");

// Get the contract URL on the block explorer
const url = await etherscan.getContractUrl("0x1234...");

// Submit a contract for verification
const guid = await etherscan.verify({
contractAddress: "0x1234...",
compilerInput: {
/* compiler input JSON */
},
contractName: "contracts/MyContract.sol:MyContract",
compilerVersion: "v0.8.19+commit.7dd6d404",
constructorArguments: "0x...",
});

// Poll for verification status
const result = await etherscan.pollVerificationStatus(
guid,
"0x1234...",
"MyContract",
);
}
```

### Making Custom API Calls

For API endpoints not covered by the standard methods, use `customApiCall()`:

```typescript
const { verifier } = await hre.network.connect();

// Make a custom API call (apikey and chainid are added automatically)
const response = await verifier.etherscan.customApiCall({
module: "contract",
action: "getsourcecode",
address: "0x1234...",
});

// Check the response
if (response.status === "1") {
console.log("Contract source:", response.result);
} else {
console.error("Error:", response.message);
}
```

### API Reference

For complete type definitions and available methods, see the exported types:

- `Etherscan` - The main interface for Etherscan API access
- `EtherscanResponseBody` - Structure of API response bodies
- `EtherscanCustomApiCallOptions` - Options for custom API calls
- `EtherscanVerifyArgs` - Arguments for contract verification

### Build profiles and verification

When no build profile is specified, this plugin defaults to `production`. However, tasks like `build` and `run` default to the `default` build profile. If your contracts are compiled with a different profile than the one used for verification, the compiled bytecode may not match the deployed bytecode, causing verification to fail.
Expand Down
3 changes: 2 additions & 1 deletion v-next/hardhat-verify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"types": "dist/src/index.d.ts",
"exports": {
".": "./dist/src/index.js",
"./verify": "./dist/src/verify.js"
"./verify": "./dist/src/verify.js",
"./types": "./dist/src/types.js"
},
"keywords": [
"ethereum",
Expand Down
1 change: 1 addition & 0 deletions v-next/hardhat-verify/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const hardhatPlugin: HardhatPlugin = {
id: "hardhat-verify",
hookHandlers: {
config: () => import("./internal/hook-handlers/config.js"),
network: () => import("./internal/hook-handlers/network.js"),
},
tasks: [
verifyTask,
Expand Down
179 changes: 173 additions & 6 deletions v-next/hardhat-verify/src/internal/etherscan.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type {
EtherscanCustomApiCallOptions,
EtherscanChainListResponse,
EtherscanGetSourceCodeResponse,
EtherscanResponse,
EtherscanResponseBody,
EtherscanVerifyArgs,
LazyEtherscan,
} from "./etherscan.types.js";
import type {
VerificationProvider,
VerificationResponse,
VerificationStatusResponse,
BaseVerifyFunctionArgs,
CreateEtherscanOptions,
ResolveConfigOptions,
} from "./types.js";
Expand All @@ -20,6 +23,7 @@ import type {
ChainDescriptorsConfig,
VerificationProvidersConfig,
} from "hardhat/types/config";
import type { EthereumProvider } from "hardhat/types/providers";

import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { toBigInt } from "@nomicfoundation/hardhat-utils/bigint";
Expand All @@ -42,10 +46,6 @@ const VERIFICATION_STATUS_POLLING_SECONDS = 3;

export const ETHERSCAN_API_URL = "https://api.etherscan.io/v2/api";

export interface EtherscanVerifyFunctionArgs extends BaseVerifyFunctionArgs {
constructorArguments: string;
}

let supportedChainsCache: ChainDescriptorsConfig | undefined;

export class Etherscan implements VerificationProvider {
Expand Down Expand Up @@ -273,7 +273,7 @@ export class Etherscan implements VerificationProvider {
contractName,
compilerVersion,
constructorArguments,
}: EtherscanVerifyFunctionArgs): Promise<string> {
}: EtherscanVerifyArgs): Promise<string> {
const body = {
contractaddress: contractAddress,
sourceCode: JSON.stringify(compilerInput),
Expand Down Expand Up @@ -462,6 +462,67 @@ export class Etherscan implements VerificationProvider {
{ message: etherscanResponse.message },
);
}

public async customApiCall(
params: Record<string, unknown>,
options: EtherscanCustomApiCallOptions = { method: "GET" },
): Promise<EtherscanResponseBody> {
const queryParams = {
chainid: this.chainId,
apikey: this.apiKey,
...params,
};

let response: HttpResponse;
try {
if (options.method === "GET") {
response = await getRequest(
this.apiUrl,
{ queryParams },
this.dispatcherOrDispatcherOptions,
);
} else {
response = await postFormRequest(
this.apiUrl,
options.body,
{ queryParams },
this.dispatcherOrDispatcherOptions,
);
}
} catch (error) {
ensureError(error);
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_REQUEST_FAILED,
{
name: this.name,
url: this.apiUrl,
errorMessage:
error.cause instanceof Error ? error.cause.message : error.message,
},
);
}

const responseBody =
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- Cast to EtherscanResponseBody because that's what we expect from the API */
(await response.body.json()) as EtherscanResponseBody;

const isSuccessStatusCode =
response.statusCode >= 200 && response.statusCode <= 299;
if (!isSuccessStatusCode) {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.EXPLORER_REQUEST_STATUS_CODE_ERROR,
{
name: this.name,
url: this.apiUrl,
statusCode: response.statusCode,
errorMessage: String(responseBody.result),
},
);
}

return responseBody;
}
}

class EtherscanVerificationResponse implements VerificationResponse {
Expand Down Expand Up @@ -523,3 +584,109 @@ class EtherscanVerificationStatusResponse
return this.status === 1;
}
}

export class LazyEtherscanImpl implements LazyEtherscan {
readonly #provider: EthereumProvider;
readonly #networkName: string;
readonly #chainDescriptors: ChainDescriptorsConfig;
readonly #verificationProvidersConfig: VerificationProvidersConfig;

#etherscan: Etherscan | undefined;

constructor(
provider: EthereumProvider,
networkName: string,
chainDescriptors: ChainDescriptorsConfig,
verificationProvidersConfig: VerificationProvidersConfig,
) {
this.#provider = provider;
this.#networkName = networkName;
this.#chainDescriptors = chainDescriptors;
this.#verificationProvidersConfig = verificationProvidersConfig;
}

/**
* Lazily initializes the underlying Etherscan verification provider and caches
* the created instance so that subsequent calls reuse the same object.
*/
async #getEtherscan(): Promise<Etherscan> {
if (this.#etherscan === undefined) {
const { createVerificationProviderInstance } = await import(
"./verification.js"
);

this.#etherscan =
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- Cast to Etherscan because we know the provider name is "etherscan" */
(await createVerificationProviderInstance({
provider: this.#provider,
networkName: this.#networkName,
chainDescriptors: this.#chainDescriptors,
verificationProviderName: "etherscan",
verificationProvidersConfig: this.#verificationProvidersConfig,
})) as Etherscan;
}
return this.#etherscan;
}

public async getChainId(): Promise<string> {
const etherscan = await this.#getEtherscan();
return etherscan.chainId;
}

public async getName(): Promise<string> {
const etherscan = await this.#getEtherscan();
return etherscan.name;
}

public async getUrl(): Promise<string> {
const etherscan = await this.#getEtherscan();
return etherscan.url;
}

public async getApiUrl(): Promise<string> {
const etherscan = await this.#getEtherscan();
return etherscan.apiUrl;
}

public async getApiKey(): Promise<string> {
const etherscan = await this.#getEtherscan();
return etherscan.apiKey;
}

public async getContractUrl(address: string): Promise<string> {
const etherscan = await this.#getEtherscan();
return etherscan.getContractUrl(address);
}

public async isVerified(address: string): Promise<boolean> {
const etherscan = await this.#getEtherscan();
return etherscan.isVerified(address);
}

public async verify(args: EtherscanVerifyArgs): Promise<string> {
const etherscan = await this.#getEtherscan();
return etherscan.verify(args);
}

public async pollVerificationStatus(
guid: string,
contractAddress: string,
contractName: string,
): Promise<{ success: boolean; message: string }> {
const etherscan = await this.#getEtherscan();
return etherscan.pollVerificationStatus(
guid,
contractAddress,
contractName,
);
}

public async customApiCall(
params: Record<string, unknown>,
options?: EtherscanCustomApiCallOptions,
): Promise<EtherscanResponseBody> {
const etherscan = await this.#getEtherscan();
return etherscan.customApiCall(params, options);
}
}
Loading