npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

hardhat-blueprints

v1.2.5

Published

A hardhat plugin offering the possibility to generate new contract files (.sol) out of an extensible list of contract blueprints

Downloads

214

Readme

hardhat-blueprints

A hardhat plugin offering the possibility to generate new contract files (.sol) out of an extensible list of contract blueprints.

Installation

Run this command to install it from NPM:

npm install --save-dev hardhat-common-tools@^1.3.0 hardhat-enquirer-plus@^1.4.0 hardhat-blueprints@^1.2.3

Usage

This is a hardhat plugin, so the first thing to do is to install it in your hardhat.config.ts file:

require("hardhat-common-tools");
require("hardhat-enquirer-plus");
require("hardhat-blueprints");

Once there, you can make use of it (this supports both viem-enabled and ethers-enabled projects):

Listing the available blueprints

Write this command just to start:

npx hardhat blueprint list

At first (if you did not register any new blueprint nor installed other packages that do it) you will see this content:

These are the available blueprints you can use in the `apply` command:
- contract: An empty contract
  - Arguments:
    - SOLIDITY_VERSION: The Solidity version for the new file (An X.Y.Z Solidity version in the project)
- interface: An empty interface
  - Arguments:
    - SOLIDITY_VERSION: The Solidity version for the new file (An X.Y.Z Solidity version in the project)
- library: An empty library
  - Arguments:
    - SOLIDITY_VERSION: The Solidity version for the new file (An X.Y.Z Solidity version in the project)
- existing-contract-deployment-module: An ignition module for an existing contract (by artifact ID and contract address)
  - Arguments:
    - CONTRACT_NAME: The type to use for the contract (The ID of an artifact)
    - CONTRACT_ADDRESS: The address where the contract is deployed (A checksum address)
- new-contract-deployment-module: An ignition module for a new contract (by artifact ID)
  - Arguments:
    - CONTRACT_NAME: The type to use for the contract (The ID of an artifact)

Which details many entries (there are 5 built-in entries by default), like "contract" or "interface" and 3 more.

If you expect a blueprint to be available for use, either provided by your own code or a different plugin, ensure that it is listed here (otherwise, it won't be available).

Applying a blueprint

There are certain types of blueprints here to account for:

  1. Source code blueprints. They typically come from .sol files and will end in your sources directory (e.g. your project's contracts/ directory).
  2. Ignition modules. This will be useful if your project uses hardhat-ignition. They are .js files and will end in your ignition/modules/ directory.

But, independently of the type, applying a blueprint has always this workflow:

  1. You tell which blueprint to use.
  2. You tell the name of the result file (be it a module or a contract / interface / library).
  3. You tell whatever arguments are required (and validated).

This can be done in two ways (which can be partially applied):

  1. You add a positional argument with a valid blueprint key to use (e.g. "contract") or it will be prompted for you to manually fill.
  2. You tell all the required arguments, one by one. For example, SOLIDITY_VERSION=0.8.24 (the format is: like environment variables). These are positional arguments. If you don't specify one or more of the required arguments (and with a valid value for it), it/they'll be prompted for you to manually fill them.
    1. This includes the SCRIPT_NAME argument (e.g. SCRIPT_NAME=MySampleContract for the "contract" example).
  3. The file will be immediately generated.

About the parameters

The parameter system is based on enquirer but, in this case, it has an upper level of abstraction (which is, for example, useful for describing the metadata in the list task).

So these are actually not prompts but instead presets or argument types, which are still built on top of prompts. The available argument types are strings:

  • typeName: A PascalCase name, valid for contract/interface/library names.
  • identifier: A camelCase name, valid for variable or function names.
  • contract: A reference to a contract (from compiled artifacts only).
  • numeric-string: A base-10 number. Returns a string.
  • integer-string: A base-10, or 0x-prefixed base-16, number. Returns a string.
  • bigint: A base-10, or 0x-prefixed base-16, number. Returns a bigint.
  • boolean: A boolean value. You'll use true or false there.
  • account: An account index (works both in viem and ethers).
  • address: A checksum-valid address (works both in viem and ethers).
  • smart-address: A checksum-valid address or an account index (works both in viem and ethers).
  • solidity A solidity version, in format X.Y.Z. It must be one of the versions installed in your project.
  • token-amount: A token amount with units, e.g.: "1.5ether", "2 ether", "1gwei", "0.5 gwei" or other standard units.
  • int8 to int256: A BigInt input of that size.
  • uint8 to uint256: A BigInt input of that size.
  • bytes1 to bytes32: An hexadecimal input of that (byte-wise) length.
  • string and bytes: An arbitrary string or arbitrary byte-aligned hex string.
  • Alternatively, an object. The format of this object is the same as the entries used in the prompt method in the enquirer library (but also considering the available types from the hardhat-enquirer-plus package).
  • hashed-text: A text that will be hashed with keccak256. The result is hashed with keccak256, compatible with Solidity's keccak256(abi.encodePacked(x)).
  • hashed: Either a text that will be hashed or an already-computed valid hash (as in bytes32 format). The given value may be a string (which will be hashed), a compound object like {hashed: false, value: string} (whose value will be hashed) or a compound object like {hashed: true, value: string} (whose value must be a valid bytes32 value and will not be hashed but returned as-is).

Registering a new argument type

To create an AAA-9999 argument type, not listed among these (and not wishing to use the custom object as a one-off type), you can call:

// Registering a new "plate-code" type:
hre.blueprints.registerBlueprintArgumentType(
    "plate-code", {
        type: "plus:given-or-valid-input",
        validate: /^[A-Z]{3}-[0-9]{9}$/,
        makeInvalidInputMessage: (v) => `Invalid plate code: ${v}`,
        onInvalidGiven: (v) => console.error(`Invalid given plate code: ${v}`)
    }, "An AAA-9999 code (3 uppercase letters, 4 decimal digits)"
);

It will work as expected when you try to define arguments of "plate-code" type for your new blueprints.

You'll only typically need this when developing your own plugin (on top of this one) which needs to also define new types for some blueprints on its own.

Manually applying / reading arguments

Considering the given types (and new ones) you could manually implement reading arguments by invoking this method, for example:

const prompts = hre.blueprints.prepareArgumentPrompts([
    {
        name: "fromAddress",
        description: "The source address",
        message: "Enter the source address",
        argumentType: "smart-address"
    },
    {
        name: "toAddress",
        description: "The destination address",
        message: "Enter the destination address",
        argumentType: "smart-address"
    },
    {
        name: "id",
        description: "The token id",
        message: "Enter the ID of the token",
        argumentType: "bigint"
    },
    {
        name: "value",
        description: "The token id",
        message: "Enter the ID of the token",
        argumentType: "bigint"
    },
    {
        name: "plate-code",
        description: "A plate code",
        message: "A plate code (it will be hashed and used as data)",
        argumentType: "plate-code"
    }
]);
console.log(await new hre.enquirerPlus.Enquirer().prompt(prompts));

In the end, the result will be a literal object with fields id, value, data, fromAddress, toAddress.

Compound types: arrays and tuples

Using arrays and tuples is done through custom, non-registered, prompts. However, this package offers some helpers to aid for that purpose. For example, to ask for an array of addresses and then a tuple type of (uint8, int16), this would do the full job:

const prompts = hre.blueprints.prepareArgumentPrompts([
	hre.blueprints.arrayArgument({
		message: "Input an array of addresses",
		description: "An addresses array",
		name: "addresses",
		elements: {
			argumentType: "smart-address",
			message: "Element ${index}"
		} 
	}),
	hre.blueprints.tupleArgument({
		message: "Input a tuple",
		description: "A tuple",
		name: "compound",
		elements: [{
			name: "foo",
			argumentType: "uint8"
		}, {
			name: "bar",
			argumentType: "int16"
		}]
	}),
]);

await hre.enquirerPlus.Enquirer.prompt(prompts);
// It'd return something like: {
//     addresses: [
//         '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
//         '0x70997970C51812dc3A010C7d01b50e0d17dc79C8'
//     ],
//     compound: [ 255n, 32767n ]
// }

In order to provide some given values and force the non-interactive mode, this would also work (considering that the non-interactive mode is enabled for any type, not just tuples or arrays):

const prompts = hre.blueprints.prepareArgumentPrompts([
	hre.blueprints.arrayArgument({
		message: "Input an array of addresses",
		description: "An addresses array",
		name: "addresses",
		elements: {
			argumentType: "smart-address",
			message: "Element ${index}"
		} 
	}),
	hre.blueprints.tupleArgument({
		message: "Input a tuple",
		description: "A tuple",
		name: "compound",
		elements: [{
			name: "foo",
			argumentType: "uint8"
		}, {
			name: "bar",
			argumentType: "int16"
		}]
	}),
], true, {"addresses": ["0", "1"], "compound": ["0xff", "0x7fff"]});

Where the third parameter is a given value matching both names.

Also, arrays of tuples are supported (and nesting more stuff would be done in an analogous way):

const prompts = hre.blueprints.prepareArgumentPrompts([
	hre.blueprints.arrayArgument({
		message: "Input an array of addresses",
		description: "An addresses array",
		name: "compounds",
		elements: hre.blueprints.tupleArgument({
			message: "Input the ${index}",
			description: "A tuple",
			name: "compound",
			elements: [{
				name: "foo",
				argumentType: "uint8"
			}, {
				name: "bar",
				argumentType: "int16"
			}]
		})
	}),
]);

await hre.enquirerPlus.Enquirer.prompt(prompts);

Which also supports given values and non-interactive mode:

const prompts = hre.blueprints.prepareArgumentPrompts([
	hre.blueprints.arrayArgument({
		message: "Input an array of addresses",
		description: "An addresses array",
		name: "compounds",
		elements: hre.blueprints.tupleArgument({
			message: "Input the ${index}",
			description: "A tuple",
			name: "compound",
			elements: [{
				name: "foo",
				argumentType: "uint8"
			}, {
				name: "bar",
				argumentType: "int16"
			}]
		})
	}),
], true, {"compounds": [["0xff", "0x7fff"], ["0xff", "0x7fff"]]});

await hre.enquirerPlus.Enquirer.prompt(prompts);

Registering a new blueprint

To create a new blueprint, not listed among the default blueprints described here, you can call:

// Let's define two new argument types as well.
hre.blueprints.registerBlueprintArgumentType(
    "erc20-symbol", {
        type: "plus:given-or-valid-input",
        validate: /^[A-Z][A-Z]{2,}$/,
        makeInvalidInputMessage: (v) => `Invalid ERC20 symbol: ${v}`,
        onInvalidGiven: (v) => console.error(`Invalid given ERC20 symbol: ${v}`)
    }, "An uppercase (letter-starting) short ERC20 symbol name"
);
hre.blueprints.registerBlueprintArgumentType(
    "erc20-token-name", {
        type: "plus:given-or-valid-input",
        validate: /^[ A-Za-z0-9_-]+$/,
        makeInvalidInputMessage: (v) => `Invalid ERC20 token name: ${v}`,
        onInvalidGiven: (v) => console.error(`Invalid given ERC20 token name: ${v}`)
    }, "An ERC20 token title/name"
);

// Let's say it is an OpenZeppelin-based ERC20 contract.
const filePath = path.resolve(__dirname, "path", "to", "my", "ERC20.sol.template");
hre.blueprints.registerBlueprint(
    "erc-20", "MyERC20", "An OpenZeppelin-based ERC20 contract",
    filePath, "solidity", [{
        // You'll typically define this argument for .sol files.
        name: "SOLIDITY_VERSION",
        description: "The Solidity version for the new file",
        message: "Choose the solidity version for this file",
        argumentType: "solidity"
    }, {
        name: "SYMBOL",
        description: "The symbol for this token",
        message: "What's the symbol for your token?",
        argumentType: "erc20-symbol"
    }, {
        name: "TOKEN_NAME",
        description: "The name for this token",
        message: "Give a name/title to your token",
        argumentType: "erc20-token-name"
    }]
);

PLEASE NOTE: You must not define the SCRIPT_NAME argument. It is already defined.

The ERC20.sol.template contents will look like this:

// SPDX-License-Identifier: UNLICENSED
pragma solidity #SOLIDITY_VERSION#;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract #SCRIPT_NAME# is ERC20 {
    constructor() ERC20("#TOKEN_NAME#", "#SYMBOL#") {}
}

Notice how both SCRIPT_NAME and the oter 3 arguments are defined. They'll be properly accounted for when trying the command:

npx hardhat blueprint apply erc20 ... # the arguments here

Manually executing / applying a blueprint

You have two options here:

  1. Invoke the apply scoped task (running children tasks is documented in hardhat's official docs), properly specifying the arguments.

  2. Invoke hre.blueprints.applyBlueprint. For example, to apply the contract blueprint:

    # 1. nonInteractive is being set to false, thus allowing prompts
    #    if something were to be invalid. Pass it as true in your
    #    calls if you want to ensure that no interactions must occur
    #    via prompting (raising an error instead).
    # 2. Provided 0.8.24 is a valid solidity version in your project.
    #    As SCRIPT_NAME is given, also any other expected argument
    #    can also be given.
    await hre.blueprints.applyBlueprint("contract", false, {"SCRIPT_NAME": "MyContract", "SOLIDITY_VERSION": "0.8.24"});

New enquirer-plus types.

There are two extra enquirer-plus types registered here:

  • Registered as "plus:hardhat:given-or-valid-hashed-input" and used in the "hashed-text" argument type, its class is hre.enquirerPlus.Enquirer.HashedInput and takes extra options: given (a string) and nonInteractive.
  • Registered as "plus:hardhat:given-or-valid-smart-hashed-input" and used in the "hashed" argument type, its class is hre.enquirerPlus.Enquirer.SmartHashedInput and takes extra options: given (a string, a {hashed: false, value: string} or a {hashed: true, value: string}) and nonInteractive.

They can be safely used in calls to hre.enquirerPlus.Enquirer.prompt.