Crear plugin Hardhat


Después de descubrir cómo crear tasks, ahora nos toca adentramos en el mundo de los plugins. Crear plugins para Hardhat te permite extender y personalizar las funcionalidades de esta herramienta. En esta entrada, aprenderemos a construir plugins que podrán mejorar tu flujo de trabajo.

Este post se fundamenta en mi experiencia desarrollando el plugin hardhat-contract-signatures.

Os voy a compartir que tuve que tener en cuenta en el desarrollado, como implementación del mismo.

¿Qué son los plugins en Hardhat?

Los plugins son extensiones modulares que permiten a los desarrolladores personalizar y ampliar significativamente las capacidades del entorno.

A diferencia de las tasks, que se centran en tareas específicas automatizadas como compilar y desplegar contratos, los plugins ofrecen una personalización más profunda al permitir modificar configuraciones.

Extender la configuración de Hardhat

Cuando desarrollé el plugin, para mi era crucial que los usuarios puedieran configurar fácilmente el formato en el que querían visualizar el contenido. Esto se puede lograr modificando el archivo hardhat.config, similar a cómo se configuran las redes.

Hardhat ofrece una función que, al ser invocada desde tu plugin, extiende automáticamente la configuración existente.

import { extendConfig } from 'hardhat/config'
import { type HardhatConfig, type HardhatUserConfig } from 'hardhat/types'

// Types de mi plugin
import { type ContractSignature } from '../types/type-extension'

extendConfig(
 (config: HardhatConfig, userConfig: Readonly<HardhatUserConfig>) => {
  const defaultValues: ContractSignature = {
   functionsColumns: ['selector', 'sign:minimal'],
   eventsColumns: ['topicHash'],
   errorsColumns: ['selector', 'sign:minimal'],
   findColumns: ['type', 'sign:minimal'],
   exclude: [],
  }

  config.contractSignature = Object.assign(
   defaultValues,
   userConfig.contractSignature
  )
 }
)

Para asegurarme de que el plugin funcione correctamente desde el inicio, establecí una configuración predeterminada. Sin embargo, diseñé esta configuración predeterminada de tal manera que los usuarios puedan sobrescribirla fácilmente en su propio archivo hardhat.config.

⚠️
Si estás trabajando con TypeScript tanto en tu plugin o para el proyecto en el que vayas a incluirlo es mejor que añadas este código para que no haya errores de tipado
export interface ContractSignature {
 exclude: string[]
 functionsColumns: FunctionFormatColumns[]
 eventsColumns: EventsFormatColumns[]
 errorsColumns: ErrorFormatColumns[]
 findColumns: FormatColumns[]
}

declare module 'hardhat/types/config' {
 export interface HardhatUserConfig {
  contractSignature?: DeepPartial<ContractSignature>
 }

 export interface HardhatConfig {
  contractSignature: Required<ContractSignature>
 }
}

Al utilizar declare module 'hardhat/types/config', estamos indicando nuestra intención de extender tanto la configuración de HardhatUserConfig como la de HardhatConfig, cada una con sus respectivos tipos. Como mencioné anteriormente, mi objetivo es permitir que los desarrolladores tengan la opción de sobrescribir la configuración predeterminada, por eso uso el ? y DeepPartial para indicar que la configuración puede ser parcialmente definida.

Sin embargo, al ejecutar las tareas del plugin, es necesario que la propiedad contractSignature esté presente y completa. Por ello, utilizamos Required<ContractSignature> para asegurar que esta propiedad no pueda ser nula ni estar ausente, para evitar posibles errores de tipado relacionados con valores nulos.

Tareas del plugin

Todos los plugins se ejecutan de la misma manera que las tasks de Hardhat: utilizando npx hardhat {task|plugin}.

Para asegurar que todas las tasks de mi plugin se distinguieran claramente y para mantener una organización estructurada, opté por utilizar el scope de Hardhat.

import { scope } from 'hardhat/config'

const myScope = scope("mi-alcance", "Descripción del alcance");

myScope.task("mi-tarea", "Haz algo")
  .setAction(async () => { ... });

myScope.task("mi-otra-tarea", "Haz algo más")
  .setAction(async () => { ... });

Esto permitiría que todos los comandos que realicen sean de este estilo: npx hardhat mi-alcance mi-tarea y npx hardhat mi-alcance mi-otra-tarea.

En el punto en el que nos encontramos tenemos tanto la configuración como la función que queremos ejecutar, pero no hemos visto como utilizar esa configuración en nuestra función.

Al igual que las tasks que vimos en la entrada anterior tenemos acceso al Hardhat Runtime Environment (HRE) por lo que nada más tenemos que hacer hre.config.{miconfiguracion}.

Si seguimos el ejemplo anterior:

myScope.task("mi-otra-tarea", "Haz algo más")
 .setAction(async (args,hre) => {
  const excludeContractsConfig = hre.config.contractSignature.exclude
  const functionsColumns = hre.config.contractSignature.functionsColumns
  const eventsColumns = hre.config.contractSignature.eventsColumns
  const errorsColumns = hre.config.contractSignature.errorsColumns
  const findColumns = hre.config.contractSignature.findColumns
 });

Conclusión

Como has podido observar, crear un plugin como tal es sumamente sencillo, no hay mucha diferencia realmente con realizar una task, unicamente el poder dar la opción de configurarlo.

Si quieres que luego aparezca tu plugin en el repositorio de Hardhat, tienes que abrir una MR.