hardhat-build
v0.1.5
Published
A Hardhat plugin to automatically generate Solidity interface files from contract implementations with natspec documentation
Maintainers
Readme
Build Interface Documentation
Overview
Hardhat Build is a powerful Hardhat plugin that automatically generates Solidity interface files from contract implementations. It reads special interface directives embedded in contract comments and creates clean, well-documented interfaces with proper natspec documentation.
This plugin is designed for seamless integration with Hardhat projects but also works as a standalone CLI tool for any TypeScript/Node.js environment.
Features
✅ Automatic Interface Generation - Converts contracts to interfaces with proper function signatures
✅ Natspec Documentation - Copies and preserves both /// and /** */ style natspec comments
✅ Copyright Notice Support - Adds copyright information to generated interfaces
✅ Selective Function Inclusion - Control which functions, events, and errors are included
✅ Inheritance Processing - Handles contract inheritance and interface replacement
✅ Getter Function Generation - Creates getter functions for public variables
✅ Batch Processing - Build all contracts at once with the "all" command
✅ Validation - Ensures build directives are present before processing
Installation & Setup
Hardhat Project Integration (Recommended)
If you're working in a Hardhat project, this is the easiest setup:
# Install as a dev dependency
npm install --save-dev hardhat-build
# Add to your hardhat.config.js or hardhat.config.ts
require('hardhat-build');
// or ES6: import 'hardhat-build';No additional setup required! Hardhat projects typically include TypeScript and ts-node dependencies.
📁 Directory Structure Requirement: This plugin expects your Solidity contracts to be in a
contracts/directory at your project root. This follows the standard Hardhat convention and requires no configuration.
Standalone Installation
For use outside of Hardhat projects or global installation:
# Global installation - works anywhere (includes Hardhat as dependency)
npm install -g hardhat-build
# Local installation
npm install hardhat-build
# TypeScript support (if not already available)
npm install --save-dev typescript ts-nodeNote: npx hardhat-build works even outside Hardhat projects since we include Hardhat as a dependency!
Quick Start
⚠️ Important: This plugin requires your Solidity contracts to be in a
contracts/directory at your project root. This follows the standard Hardhat convention.
Option 1: Hardhat Integration (Recommended)
/// @custom:interface build ../interfaces/IMyContract.sol
contract MyContract {
function myFunction() external pure returns (uint256);
}# Complete build pipeline (TypeScript + Hardhat + Interfaces)
npx hardhat build
# Interface generation only
npx hardhat build --interfaces
# Force regeneration
npx hardhat build --interfaces --forceOption 2: Direct CLI Usage
# Using compiled JavaScript (works anywhere)
npx hardhat-build all
# Using TypeScript source (requires ts-node in your project)
npx ts-node node_modules/hardhat-build/src/buildInterface.ts all
# Force regeneration
npx hardhat-build all --forceOption 3: In-Project Development
If you're working within the plugin's source code:
# Development with TypeScript source
npx ts-node src/buildInterface.ts contracts/MyContract.sol
# Production with compiled version
node dist/buildInterface.js contracts/MyContract.solCommand Line Usage
Hardhat Task Commands (Recommended)
# Complete build pipeline (TypeScript + Hardhat + Interfaces)
npx hardhat build
# Interface generation only
npx hardhat build --interfaces
# Force regeneration of all interfaces
npx hardhat build --interfaces --forceCLI Binary Commands (Works Everywhere)
# Using the CLI binary (includes Hardhat as dependency)
npx hardhat-build # Complete build pipeline
npx hardhat-build --interfaces # Interface generation only
npx hardhat-build --force # Force rebuild all
npx hardhat-build --help # Show all optionsDirect Script Commands
For development or when you need TypeScript source access:
# Using TypeScript source (requires ts-node in your project)
npx ts-node node_modules/hardhat-build/src/buildInterface.ts all
npx ts-node node_modules/hardhat-build/src/buildInterface.ts contracts/MyContract.sol --force
# Using compiled JavaScript
node node_modules/hardhat-build/dist/buildInterface.js all --forceBatch Processing
All commands support batch processing with automatic contract discovery:
# These commands will:
# - Recursively search the './contracts' directory
# - Find all .sol files with '/// @custom:interface build' directives
# - Build interfaces for all discovered contracts
# - Report success/failure summary with file counts
npx hardhat build --interfaces
npx hardhat-build
npx ts-node node_modules/hardhat-build/src/buildInterface.ts allBest Practices
📁 Recommended Directory Structure
We recommend using the interfaces/ directory pattern with the I{ContractName}.sol naming convention:
project/
├── contracts/
│ ├── Token.sol // Contract implementations
│ ├── Staking.sol
│ └── Registry.sol
├── interfaces/ // Generated interfaces (add to .gitignore)
│ ├── IToken.sol
│ ├── IStaking.sol
│ └── IRegistry.sol
└── .gitignore🔧 Interface Directive Pattern
Use this pattern in your contracts:
/// @custom:interface build ./interfaces/I{ContractName}.sol
contract MyContract {
// implementation
}Examples:
/// @custom:interface build ./interfaces/IToken.sol
contract Token { }
/// @custom:interface build ../interfaces/IStaking.sol // from subdirectory
contract Staking { }📝 Git Ignore Configuration
Important: Add generated interfaces to your .gitignore since they are build artifacts:
# Build artifacts
artifacts/
cache/
typechain-types/
# Generated interfaces (hardhat-build)
**/interfaces/This prevents generated interface files from being committed to your repository, keeping it clean and avoiding merge conflicts.
🎯 Why This Pattern?
- Consistent: Standard
I{ContractName}naming follows Solidity conventions - Organized: Dedicated
interfaces/directory keeps generated files separate - Clean Repos: Adding to
.gitignoreprevents committing build artifacts - Team Friendly: Everyone generates the same interfaces locally
Interface Directives
Interface directives are special comments that control how the interface is generated. All directives start with /// @custom:interface.
Required Directives
build
Required. Specifies the output path for the generated interface.
/// @custom:interface build ./interfaces/IMyContract.solOptional Directives
copyright
Adds a copyright notice to the generated interface.
/// @custom:interface copyright "Copyright (c) 2024 MyCompany. All rights reserved."import
Adds import statements to the generated interface.
/// @custom:interface import "@openzeppelin/contracts/access/IOwnable.sol";
/// @custom:interface import "./ICustomInterface.sol";replace
Replaces inheritance contracts with interface equivalents.
contract MyContract is Ownable {
/// @custom:interface replace Ownable with IOwnableremove
Removes inheritance contracts from the interface.
contract MyContract is Ownable, ReentrancyGuard {
/// @custom:interface remove ReentrancyGuardis
Adds additional interface inheritance that wasn't in the original contract.
contract MyContract is Ownable {
/// @custom:interface is IDataStorage
/// @custom:interface is IEventEmitter
// OR use comma-separated in single line:
/// @custom:interface is IDataStorage, IEventEmitter
// Results in: interface IMyContract is IOwnable, IDataStorage, IEventEmitterexclude
Excludes specific functions, events, or errors from the interface.
function internalFunction() public {
// implementation
}
/// @custom:interface exclude internalFunction
event DebugEvent(string message);
/// @custom:interface exclude DebugEventinclude
Forces inclusion of internal/private functions in the interface.
function _internalHelper() internal view returns (uint256) {
return someValue;
}
/// @custom:interface include _internalHelpergetter
Generates getter functions for variables (automatic for public variables).
uint256 internal myValue;
/// @custom:interface getter myValueNatspec Documentation Support
The script preserves natspec documentation from the original contract:
Single-line Style (///)
/// @notice Transfer tokens between accounts
/// @param to The recipient address
/// @param amount The amount to transfer
/// @return success Whether the transfer succeeded
function transfer(address to, uint256 amount) external returns (bool success);Block Style (/** */)
/**
* @notice Approve spending allowance
* @dev Sets the allowance for a spender
* @param spender The address to approve
* @param amount The allowance amount
* @return success Whether approval succeeded
*/
function approve(address spender, uint256 amount) external returns (bool success);Complete Example
Input Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @custom:interface build ./interfaces/IExampleToken.sol
/// @custom:interface copyright "Copyright (c) 2024 MyCompany. All rights reserved."
import "@openzeppelin/contracts/access/Ownable.sol";
/// @custom:interface import "@openzeppelin/contracts/access/IOwnable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/// @title Example Token Contract
/// @notice A simple ERC20-like token implementation
/// @dev Demonstrates the build interface system
contract ExampleToken is Ownable, ReentrancyGuard {
/// @custom:interface replace Ownable with IOwnable
/// @custom:interface remove ReentrancyGuard
/// @notice The total supply of tokens
uint256 public totalSupply;
/// @notice Mapping of account balances
mapping(address => uint256) public balances;
/// @notice Emitted when tokens are transferred
/// @param from The sender address
/// @param to The recipient address
/// @param value The amount transferred
event Transfer(address indexed from, address indexed to, uint256 value);
/// @notice Debug event (excluded from interface)
event DebugEvent(string message);
/// @custom:interface exclude DebugEvent
/// @notice Thrown when insufficient balance
/// @param requested The requested amount
/// @param available The available amount
error InsufficientBalance(uint256 requested, uint256 available);
/**
* @notice Transfer tokens to another account
* @dev Validates balances and emits Transfer event
* @param to The recipient address
* @param amount The amount to transfer
* @return success Whether the transfer succeeded
*/
function transfer(address to, uint256 amount) external returns (bool success) {
if (balances[msg.sender] < amount) {
revert InsufficientBalance(amount, balances[msg.sender]);
}
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
/// @notice Internal helper function (forced to include)
function _calculateFee(uint256 amount) internal pure returns (uint256) {
return amount / 100;
}
/// @custom:interface include _calculateFee
}Generated Interface
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// Copyright (c) 2024 MyCompany. All rights reserved.
import "@openzeppelin/contracts/access/IOwnable.sol";
/// @title Example Token Contract
/// @notice A simple ERC20-like token implementation
/// @dev Demonstrates the build interface system
interface IExampleToken is IOwnable {
/// @notice Emitted when tokens are transferred
/// @param from The sender address
/// @param to The recipient address
/// @param value The amount transferred
event Transfer(address indexed from, address indexed to, uint256 value);
/// @notice Thrown when insufficient balance
/// @param requested The requested amount
/// @param available The available amount
error InsufficientBalance(uint256 requested, uint256 available);
/// @notice The total supply of tokens
function totalSupply() external view returns (uint256);
/// @notice Mapping of account balances
function balances(address account) external view returns (uint256);
/**
* @notice Transfer tokens to another account
* @dev Validates balances and emits Transfer event
* @param to The recipient address
* @param amount The amount to transfer
* @return success Whether the transfer succeeded
*/
function transfer(address to, uint256 amount) external returns (bool success);
/// @notice Internal helper function (forced to include)
function _calculateFee(uint256 amount) external pure returns (uint256);
}Function Inclusion Rules
By default, the script includes:
- ✅
externalfunctions - ✅
publicfunctions (converted toexternalin interface) - ✅ Public state variables (as getter functions)
- ✅ All events (unless explicitly excluded)
- ✅ All custom errors (unless explicitly excluded)
By default, the script excludes:
- ❌
internalfunctions (unless explicitly included) - ❌
privatefunctions (unless explicitly included) - ❌ Constructor functions
- ❌ Modifier definitions
Error Handling
Missing Build Directive
Error: No build directive found. Use /// @custom:interface build <path>Solution: Add /// @custom:interface build <output-path> to your contract.
Invalid Solidity Syntax
Error: Contract declaration not foundSolution: Ensure your contract has valid Solidity syntax and proper contract declaration.
File Permission Issues
Error: EACCES: permission deniedSolution: Check file permissions and ensure the output directory is writable.
Best Practices
1. Organize Interface Directives
Place all interface directives near the top of your contract:
/// @custom:interface build ./interfaces/IMyContract.sol
/// @custom:interface copyright "Copyright (c) 2024 Company Name. All rights reserved."
/// @custom:interface import "@openzeppelin/contracts/access/IOwnable.sol";
contract MyContract is Ownable {
/// @custom:interface replace Ownable with IOwnable
// Contract implementation
}2. Use Descriptive Natspec
Write comprehensive documentation that will be copied to interfaces:
/**
* @notice Performs a complex operation
* @dev This function handles multiple edge cases
* @param input The input parameter with specific constraints
* @return result The computed result
* @custom:security This function requires special access control
*/
function complexOperation(uint256 input) external returns (uint256 result);3. Consistent Interface Naming
Use the I prefix for interface names and mirror the contract structure:
contracts/
├── tokens/
│ └── MyToken.sol → interfaces/tokens/IMyToken.sol
└── governance/
└── MyGovernance.sol → interfaces/governance/IMyGovernance.sol4. Group Related Directives
Keep related directives together and add comments for clarity:
// Interface configuration
/// @custom:interface build ./interfaces/IComplexContract.sol
/// @custom:interface copyright "Copyright (c) 2024 MyCompany. All rights reserved."
// Import dependencies
/// @custom:interface import "@openzeppelin/contracts/access/IOwnable.sol";
/// @custom:interface import "./ICustomInterface.sol";
// Inheritance modifications
/// @custom:interface replace Ownable with IOwnable
/// @custom:interface remove ReentrancyGuard5. Batch Processing in CI/CD
Add interface generation to your build pipeline:
# .github/workflows/build.yml
- name: Generate Interfaces
run: npx ts-node src/buildInterface.ts all
- name: Check for changes
run: git diff --exit-code interfaces/Troubleshooting
Common Issues
Q: The script doesn't find my contract
A: Ensure your contract file contains /// @custom:interface build directive and is located in the ./contracts directory (standard Hardhat structure).
Q: "No contracts found with build directives" error
A: Check that:
- Your contracts are in a
contracts/directory at your project root - Your contract files have the
.solextension - Your contracts contain
/// @custom:interface build <path>directives
Q: Natspec comments are missing from the interface
A: Check that natspec comments are placed directly above the function/event/error definition without blank lines.
Q: Interface inheritance is incorrect
A: Use replace and remove directives to properly map contract inheritance to interface inheritance.
Q: Private functions appear in the interface
A: Remove any include directives for private functions, or make them internal/public if they should be in the interface.
Q: Build fails with parsing errors
A: Ensure your Solidity contract compiles successfully before running the interface generator.
Advanced Usage
Custom Directory Processing
Modify the script to process contracts from different directories:
const contractFiles = await findContractsWithBuildDirectives('./src/contracts');Integration with Hardhat Tasks
Create a Hardhat task for interface generation:
// hardhat.config.js
task("build-interfaces", "Generate all contract interfaces")
.setAction(async (taskArgs, hre) => {
const { buildAllInterfaces } = require("hardhat-build");
await buildAllInterfaces();
});Programmatic Usage
Use the script programmatically in other tools:
import { buildInterface, findContractsWithBuildDirectives } from 'hardhat-build';
// Build single contract
await buildInterface('./contracts/MyContract.sol');
// Find all contracts with directives
const contracts = await findContractsWithBuildDirectives('./contracts');
// Build specific contracts
for (const contract of contracts) {
await buildInterface(contract);
}Contributing
When adding new features to the build interface script:
- Update the interface directive parsing logic
- Add comprehensive tests
- Update this documentation
- Ensure backward compatibility
- Add examples demonstrating the new feature
License
This build interface tool is part of the project and follows the same license terms.
Usage Summary
Primary Usage (Hardhat Integration)
# Recommended: Use Hardhat build task for development
npx hardhat build --interfaces --force
npx hardhat build
# Alternative: Use CLI binary anywhere (includes Hardhat)
npx hardhat-build --forceDevelopment Usage (TypeScript Source)
# When you need access to TypeScript source (dev/debugging)
npx ts-node node_modules/hardhat-build/src/buildInterface.ts all --force
# In-project development (if working on the plugin itself)
npx ts-node src/buildInterface.ts contracts/MyContract.sol --forceProduction Usage (Compiled JavaScript)
# Standalone CLI binary (works anywhere - includes Hardhat dependency)
npx hardhat-build --interfaces --force
# Direct compiled script usage
node node_modules/hardhat-build/dist/buildInterface.js all --forcePerformance Optimization
The interface generator automatically skips files that are already up-to-date by comparing timestamps between the source contract and generated interface files. This significantly speeds up builds when only a few contracts have changed.
Force Regeneration
Use the --force flag to bypass timestamp checks and regenerate all interface files:
- When to use: After changing interface directives, updating the tool version, or when you need to ensure all files are regenerated
- Performance: Slower but ensures all files are current
- Default behavior: Only regenerates files when the source contract is newer than the interface file
# Skip up-to-date files (default, faster)
npx ts-node src/buildInterface.ts all
# Force regeneration of all files (slower, comprehensive)
npx ts-node src/buildInterface.ts all --forceThe tool will show clear messages indicating which files were skipped vs. regenerated:
⏭️ Skipping file.sol (up to date, use --force to regenerate)Interface generated: file.sol
Module Flags:
--remove Remove inheritance from specified contract --replace with Replace contract types in inheritance AND function signatures --is Add comma-separated interfaces to inheritance --import "" Add import statement to the generated interface
Module Examples:
// Basic module interface generation
/// @custom:interface module "@openzeppelin/contracts/access/Ownable.sol" to "./interfaces/IOwnable.sol"
// Remove inheritance
/// @custom:interface module "@openzeppelin/contracts/access/Ownable.sol" to "./interfaces/IOwnable.sol" --remove Context
// Replace types and add imports
/// @custom:interface module "@tw3/esp/contracts/DataPointRegistry.sol" to "./interfaces/IDataPointRegistry.sol" --replace DataPointStorage with IDataPointStorage --import "./IDataPointStorage.sol"
// Multiple flags
/// @custom:interface module "@openzeppelin/contracts/access/Ownable.sol" to "./interfaces/IOwnable.sol" --remove Context --replace Ownable with IOwnable --import "./IOwnable.sol" --is IAccessControl