<
<
<
Hardhat 3 is now production ready. Migrate now
>
>
>

#Adding config to your plugin

In this section we'll add a way to configure your plugin using the Config System.

We'll add a new myAccountIndex property to each network config so that the user can specify which account to use.

#Extending the config types

The network config types don't have a myAccountIndex property, so we'll need to extend them using a Type Extension.

First, open packages/plugin/src/type-extensions.ts and replace the code above the one we added in the previous section with the following:

import "hardhat/types/config";
declare module "hardhat/types/config" {
  export interface EdrNetworkUserConfig {
    myAccountIndex?: number;
  }

  export interface EdrNetworkConfig {
    myAccountIndex: number;
  }

  export interface HttpNetworkUserConfig {
    myAccountIndex?: number;
  }

  export interface HttpNetworkConfig {
    myAccountIndex: number;
  }
}

This extends four types:

  • EdrNetworkUserConfig and HttpNetworkUserConfig: Part of the HardhatUserConfig type, which represent the config that the user writes in their hardhat.config.ts file.
  • EdrNetworkConfig and HttpNetworkConfig: Part of the HardhatConfig type, which is the config that Hardhat uses internally.

We replaced an existing config extension with a new one, so you'd get compilation errors if you try to build your plugin now. Let's remove the code that depends on the old extension.

First, delete these files:

rm packages/plugin/src/types.ts
rm packages/plugin/test/config.ts
rm packages/plugin/test/example-tests.ts

Then, replace the contents of packages/plugin/src/tasks/my-task.ts with the following:

import { HardhatRuntimeEnvironment } from "hardhat/types/hre";

interface MyTaskTaskArguments {
  who: string;
}

export default async function (
  taskArguments: MyTaskTaskArguments,
  hre: HardhatRuntimeEnvironment
) {}

#Extending the config validation and resolution

The config that a user provides can be invalid, so we have to extend the validation process to make sure it's valid.

At the same time, we also need to resolve any default value for networks that don't have a myAccountIndex property.

We'll do this by modifying the validatePluginConfig and resolvePluginConfig functions in the packages/plugin/src/config.ts file. They are called by the validateUserConfig and resolveUserConfig Hook Handlers in packages/plugin/src/hooks/config.ts.

We recommend keeping the config extension logic outside the actual Hook Handlers to make testing them easier. To learn how to test them, take a look at this file in the tutorial branch of the template project.

Replace the contents of packages/plugin/src/config.ts with the following:

import { HardhatUserConfig } from "hardhat/config";
import { HardhatConfig } from "hardhat/types/config";
import { HardhatUserConfigValidationError } from "hardhat/types/hooks";

const DEFAULT_MY_ACCOUNT_INDEX = 0;

/**
 * This function validates the parts of the HardhatUserConfig that are relevant
 * to the plugin.
 *
 * This function is called from the `validateUserConfig` Hook Handler.
 *
 * @param userConfig The HardhatUserConfig, as exported in the config file.
 * @returns An array of validation errors, or an empty array if valid.
 */
export async function validatePluginConfig(
  userConfig: HardhatUserConfig
): Promise<HardhatUserConfigValidationError[]> {
  if (
    userConfig.networks === undefined ||
    typeof userConfig.networks !== "object"
  ) {
    // If there's no networks field or it's invalid, we don't validate anything
    // in this plugin
    return [];
  }

  const errors = [];
  for (const [networkName, networkConfig] of Object.entries(
    userConfig.networks
  )) {
    if (networkConfig.myAccountIndex === undefined) {
      continue;
    }

    if (
      typeof networkConfig.myAccountIndex !== "number" ||
      networkConfig.myAccountIndex < 0
    ) {
      errors.push({
        path: ["networks", networkName, "myAccountIndex"],
        message: "Expected a non-negative number.",
      });
    }
  }

  return errors;
}

/**
 * Resolves the plugin config, based on an already validated HardhatUserConfig
 * and a partially resolved HardhatConfig.
 *
 * This function is called from the `resolveUserConfig` Hook Handler.
 *
 * @param userConfig The HardhatUserConfig.
 * @param partiallyResolvedConfig The partially resolved HardhatConfig, which is
 *  generated by calling `next` in the `resolveUserConfig` Hook Handler.
 * @returns The resolved HardhatConfig.
 */
export async function resolvePluginConfig(
  userConfig: HardhatUserConfig,
  partiallyResolvedConfig: HardhatConfig
): Promise<HardhatConfig> {
  const networks: HardhatConfig["networks"] = {};

  for (const [networkName, networkConfig] of Object.entries(
    partiallyResolvedConfig.networks
  )) {
    const myAccountIndex =
      userConfig.networks?.[networkName]?.myAccountIndex ??
      DEFAULT_MY_ACCOUNT_INDEX;

    networks[networkName] = {
      ...networkConfig,
      myAccountIndex,
    };
  }

  return {
    ...partiallyResolvedConfig,
    networks,
  };
}

The validatePluginConfig function is in charge of validating that the fields that we added are correct. If they aren't, it returns an array of errors. We are doing it manually here, but you can use libraries like Zod or ArkType. It shouldn't validate parts of the config that are not relevant to the plugin.

The resolvePluginConfig function is in charge of returning a new HardhatConfig where all the myAccountIndex properties have been added. Note that it uses the networks from the partially resolved config and not the user config, as they have already been resolved by Hardhat by the time this function is called. On the contrary, the myAccountIndex values are read from the user config, because Hardhat doesn't know about them yet.

#Using the plugin config in the network hook

To use the config in the network hook, we just need to modify the implementation of our NetworkHooks#newConnection Hook Handler so that it looks like this:

const connection = await next(context);

// Get the accounts from the connection
const accounts: string[] = await connection.provider.request({
  method: "eth_accounts",
});

const myAccountIndex = connection.networkConfig.myAccountIndex;

if (accounts.length <= myAccountIndex) {
  throw new HardhatPluginError(
    `hardhat-plugin-template`,
    `Invalid index ${myAccountIndex} for myAccount when connecting to network ${connection.networkName}`,
  );
}

connection.myAccount = accounts[myAccountIndex];

return connection;

We access the config in the highlighted line and validate that the index is valid. If it isn't, we throw a HardhatPluginError, which you should import from hardhat/plugins. When you throw a HardhatPluginError, Hardhat will show a nice error message to the user.

Finally, we use the myAccountIndex to set the myAccount property of the NetworkConnection object.

#Trying out the config extension

You can try out the config extension by updating the example project's config and running the my-account-example.ts script again.

Update your configuration in packages/example-project/hardhat.config.ts by removing any myPlugin field and adding this network config:

networks: {
  default: {
    type: "edr-simulated",
    myAccountIndex: 1,
  },
},

Now if you build your plugin and run the script again, you should see a different address:

pnpm hardhat run scripts/my-account-example.ts
Compiling your Solidity contracts...
Compiled 1 Solidity file with solc 0.8.29 (evm target: cancun)

connection.myAccount: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8