Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 the `verification` property on `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 { verification } = await hre.network.connect();

// Access Etherscan instance
const etherscan = verification.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 { verification } = await hre.network.connect();

// Make a custom API call (apikey and chainid are added automatically)
const response = await verification.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